updated README, added favorites reducer, updated types
This commit is contained in:
parent
8afe43cb96
commit
6ca7efc2f6
55
README.md
55
README.md
|
@ -1,23 +1,17 @@
|
|||
A simple app that allows users to search for movies by title, review the search results and navigate to a movie’s 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 movie’s 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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
44
movie-search/src/common/types.ts
Normal file
44
movie-search/src/common/types.ts
Normal 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;
|
||||
}
|
Loading…
Reference in New Issue
Block a user