Skip to content

Commit

Permalink
feat: add a basic search by name
Browse files Browse the repository at this point in the history
  • Loading branch information
lykoffant committed Feb 10, 2023
1 parent ac56831 commit fa7439f
Show file tree
Hide file tree
Showing 10 changed files with 306 additions and 4 deletions.
67 changes: 67 additions & 0 deletions src/components/FoundItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import {
Button,
Card,
CardActions,
CardContent,
CardMedia,
Divider,
Typography,
} from '@mui/material';
import { Link as RouterLink } from 'react-router-dom';

import { FoundItemShortData as FoundItemShortData } from '../types/data.types';

interface FoundItemProps {
itemData: FoundItemShortData;
}

function FoundItem({ itemData }: FoundItemProps) {
return (
<Card
component='li'
sx={{
mb: 1,
height: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'stretch',
}}
>
<CardMedia
component='img'
image={itemData.Poster}
alt={itemData.Title}
sx={{ mb: 'auto' }}
/>

<Divider variant='middle' sx={{ mt: 2 }} />

<CardContent>
<Typography
gutterBottom
variant='h5'
component='div'
sx={{
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
overflow: 'hidden',
}}
>
{itemData.Title}
</Typography>

<Typography variant='body2' color='text.secondary'>
#{itemData.Type}
</Typography>
</CardContent>

<CardActions>
<Button component={RouterLink} to={`/details/${itemData.imdbID}`}>
Details
</Button>
</CardActions>
</Card>
);
}

export { FoundItem };
27 changes: 27 additions & 0 deletions src/components/FoundList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Box } from '@mui/material';

import { FoundItem } from './FoundItem';

import { useAppSelector } from '../hooks/useAppSelector';

function FoundList() {
const foundList = useAppSelector((state) => state.foundList.list);

return (
<Box
component='ul'
sx={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(250px, 1fr))',
gap: '1rem',
}}
>
{foundList &&
foundList.map((foundItem) => (
<FoundItem key={foundItem.imdbID} itemData={foundItem} />
))}
</Box>
);
}

export { FoundList };
65 changes: 65 additions & 0 deletions src/components/SearchForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { Search as SearchIcon, Close as CloseIcon } from '@mui/icons-material';
import {
Box,
Button,
FormControl,
IconButton,
InputAdornment,
InputLabel,
OutlinedInput,
} from '@mui/material';
import { useState, useRef } from 'react';

import { useAppDispatch } from '../hooks/useAppDispatch';
import { searchItems } from '../store/foundListSlice';

function SearchForm() {
const [searchValue, setSearchValue] = useState<string>('');
const searchInputRef = useRef<HTMLDivElement>(null);
const dispatch = useAppDispatch();

return (
<Box
component='form'
sx={{ mb: 4, display: 'flex' }}
onSubmit={(e) => {
e.preventDefault();
dispatch(searchItems(searchValue));
}}
>
<FormControl sx={{ mr: 1, flexGrow: 1 }} variant='outlined'>
<InputLabel htmlFor='search-movies-form'>Search</InputLabel>
<OutlinedInput
ref={searchInputRef}
id='search-movies-form'
type='text'
value={searchValue}
label='Search'
onChange={(e) => setSearchValue(e.target.value)}
endAdornment={
searchValue && (
<InputAdornment position='end'>
<IconButton
aria-label='clear search movie title'
edge='end'
onClick={() => {
setSearchValue('');
searchInputRef.current?.querySelector('input')?.focus();
}}
>
<CloseIcon />
</IconButton>
</InputAdornment>
)
}
/>
</FormControl>

<Button variant='contained' type='submit'>
<SearchIcon />
</Button>
</Box>
);
}

export { SearchForm };
9 changes: 9 additions & 0 deletions src/hooks/useAppDispatch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { useDispatch } from 'react-redux';

import { AppDispatch } from '../store';

function useAppDispatch() {
return useDispatch<AppDispatch>();
}

export { useAppDispatch };
7 changes: 7 additions & 0 deletions src/hooks/useAppSelector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { TypedUseSelectorHook, useSelector } from 'react-redux';

import { RootState } from '../store';

const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

export { useAppSelector };
10 changes: 7 additions & 3 deletions src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,19 @@ import '@fontsource/roboto/700.css';

import React from 'react';
import ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux';
import { BrowserRouter } from 'react-router-dom';

import App from './App';
import './index.scss';
import store from './store';

ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
<Provider store={store}>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>
</React.StrictMode>,
);
11 changes: 10 additions & 1 deletion src/pages/SearchPage.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import { Container, Typography } from '@mui/material';
import { CircularProgress, Container, Typography } from '@mui/material';
import { useOutletContext } from 'react-router-dom';

import { FoundList } from '../components/FoundList';

import { OutletContextType } from '../components/Layout';
import { SearchForm } from '../components/SearchForm';
import { useAppSelector } from '../hooks/useAppSelector';

function SearchPage() {
const { sx } = useOutletContext<OutletContextType>();
const isLoading = useAppSelector((state) => state.foundList.isLoading);

return (
<Container
Expand All @@ -15,6 +20,10 @@ function SearchPage() {
<Typography variant='h1' className='visually-hidden'>
Search
</Typography>

<SearchForm />

{isLoading ? <CircularProgress sx={{ m: 'auto' }} /> : <FoundList />}
</Container>
);
}
Expand Down
65 changes: 65 additions & 0 deletions src/store/foundListSlice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';

import { FoundItemShortData, ResData, ResStatus } from '../types/data.types';

const API_KEY = import.meta.env.VITE_APP_API_KEY;

export const searchItems = createAsyncThunk<
FoundItemShortData[],
string,
{ rejectValue: string }
>('found-list/searchItems', async function (title, { rejectWithValue }) {
const res = await fetch(
`https://www.omdbapi.com/?apikey=${API_KEY}&s=${title}`,
);

if (!res.ok) {
return rejectWithValue('Server Error');
}

const data: ResData = await res.json();

if (data.Response === ResStatus.FALSE) {
return rejectWithValue(data.Error);
}

return data.Search;
});

interface FoundListState {
list: FoundItemShortData[];
isLoading: boolean;
error: string | null;
}

const initialState: FoundListState = {
list: [],
isLoading: false,
error: null,
};

const foundListSlice = createSlice({
name: 'found-list',
initialState,
reducers: {},
extraReducers: (builder) => {
builder
.addCase(searchItems.pending, (state) => {
state.isLoading = true;
state.error = null;
})
.addCase(searchItems.fulfilled, (state, action) => {
state.list = action.payload;
state.isLoading = false;
})
.addCase(searchItems.rejected, (state, action) => {
state.list = [];
state.isLoading = false;
if (action.payload) {
state.error = action.payload;
}
});
},
});

export default foundListSlice.reducer;
14 changes: 14 additions & 0 deletions src/store/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { configureStore } from '@reduxjs/toolkit';

import foundListReducer from './foundListSlice';

const store = configureStore({
reducer: {
foundList: foundListReducer,
},
});

export default store;

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
35 changes: 35 additions & 0 deletions src/types/data.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
enum FoundItemType {
MOVIE = 'movie',
SERIES = 'series',
GAME = 'game',
}

export interface FoundItemShortData {
imdbID: string;
Title: string;
Year: string;
Type: FoundItemType;
Poster: string;
}

export enum ResStatus {
TRUE = 'True',
FALSE = 'False',
}

interface Res {
Response: ResStatus;
}

interface FoundData extends Res {
Response: ResStatus.TRUE;
Search: FoundItemShortData[];
totalResults: number;
}

interface ErrorData extends Res {
Response: ResStatus.FALSE;
Error: string;
}

export type ResData = FoundData | ErrorData;

0 comments on commit fa7439f

Please sign in to comment.