updated README, added favorites reducer, updated types
This commit is contained in:
parent
8afe43cb96
commit
6ca7efc2f6
53
README.md
53
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:
|
- 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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
);
|
||||||
}
|
}
|
|
@ -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) {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
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