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:
- searchParams to string
- Object.entries(searchParams)
.map(
([key, value], i, arr) =>
`${key}=${value}${i < arr.length - 1 ? "&" : ""}`
)
.toString();
- SearchBar component:
- Input for Title
- Submit button
- Clear button
- List of results - paginated so we might need a pagination component:
- List of results:
- Title
- Year
- Type (Filter out "series"?) - hidden ?
@ -26,7 +20,7 @@ It contains:
Error Handling component
- A page for individual movie details:
- Movie component:
- Movie Details component:
- Title
- Year
- Rated
@ -41,7 +35,7 @@ It contains:
- Country
- Awards
- Poster
- Ratings: [ { Source } ]
- Ratings: `[ { Source, Value } ]`
- Metascore
- imdbRating
- imdbVotes
@ -53,27 +47,28 @@ It contains:
- Website
- Response
External API Docs: https://www.omdbapi.com/
### External API Docs: [OMDB](https://www.omdbapi.com/)
Built on:
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.
### Built on:
Next.js
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.
We can implement Context in a client component for managing "Favorites" for a user if we'd like
this might look like:
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
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:
* https://github.com/vercel/next.js/issues/65161
* https://github.com/vercel/next.js/issues/54757
### Known Issues:
* [Error when testing components using NextJS Image](https://github.com/vercel/next.js/issues/65161)
* [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";
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 isFavorite =
favorites.filter((f) => f.imdbID == movie.imdbID).length == 1;
@ -13,9 +16,11 @@ export default function FavoriteBtn({ movie }: any) {
<button
onClick={() => {
if (isFavorite) {
setFavorites([...filteredFavorites]);
dispatch({type:'Remove', payload: movie})
// setFavorites([...filteredFavorites]);
} else {
setFavorites([...favorites, movie]);
dispatch({type:'Add', payload: movie})
// setFavorites([...favorites, movie]);
}
}}
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 { IMovieCardItem } from "./MovieCard";
import { PropsWithChildren, createContext, useReducer
// , 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 {
favorites: Array<IMovieCardItem>
setFavorites: (value: Array<IMovieCardItem>) => void
favorites: FavoriteState;
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>({
favorites: [],
setFavorites: (_value: Array<IMovieCardItem>) => { },
favorites: initialState,
dispatch: () => null,
});
// 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<IMovieCardItem>>([])
export function FavoriteProvider({ children }: PropsWithChildren) {
const [favorites, dispatch] = useReducer(FavoriteReducer, initialState);
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 { primaryBtn } from "../classes";
export interface IMovieCardItem {
Title: string;
Year: string;
imdbID: string;
Type: string;
Poster: string;
}
interface IMovieCardProps {
movie: IMovieCardItem;
movie: IMovieSearch;
}
export default function MovieCard({ movie }: IMovieCardProps) {

View File

@ -1,7 +1,7 @@
import MovieCard, { IMovieCardItem } from "./MovieCard";
import MovieCard from "./MovieCard";
export interface IMovieListProps {
movies: Array<IMovieCardItem>;
movies: Array<IMovieSearch>;
}
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;
}