Skip to content

Commit

Permalink
refactor: new library and login related components
Browse files Browse the repository at this point in the history
•	Extracted login/register modal component from login button.
•	Extracted AddToLibraryButton from GameProfile
•	Moved the game search drop list from components to pages (as the homepage).
•	Moved logged in state to top level.
•	Added functionality to render list of games in Library page.
•	Fixed some async/rerender errors when logging in and out.
•	Pending to lift the state up in Library and AddToLibraryButton (needs some backend refactoring).
  • Loading branch information
ndeamador committed Apr 6, 2021
1 parent c907e0c commit 9cf013d
Show file tree
Hide file tree
Showing 15 changed files with 278 additions and 191 deletions.
26 changes: 21 additions & 5 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,24 @@
import '@reach/dialog/styles.css';
import '@reach/tooltip/styles.css';

import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';
import {
BrowserRouter as Router,
Switch,
Route,
Redirect,
} from 'react-router-dom';

import GameProfile from './pages/GameProfile';
import NavBar from './components/NavBar';
import GameSearchDropList from './components/GameSearchDropList';
import GameSearchDropList from './pages/Home';
import Library from './pages/Library';
import useCurrentUser from './hooks/useCurrentUser';
import FullPageSpinner from './components/FullPageSpinner';

function App() {
const { authenticatedUser: userLoggedIn, loading } = useCurrentUser();
console.log('app user: ', userLoggedIn, ' - loading: ', loading);

return (
<div
className='App'
Expand All @@ -23,15 +33,21 @@ function App() {
}}
>
<Router>
<NavBar />
<NavBar userLoggedIn={userLoggedIn} />

<Switch>
<Route path={'/game/:gameId'}>
<GameProfile />
<GameProfile userLoggedIn={userLoggedIn} />
</Route>

<Route path={'/library'}>
<Library />
{userLoggedIn ? (
<Library />
) : loading ? (
<FullPageSpinner />
) : (
<Redirect to='/' />
)}
</Route>

<Route path='/'>
Expand Down
78 changes: 78 additions & 0 deletions src/components/AddToLibraryButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/** @jsxImportSource @emotion/react */
import { useHistory } from 'react-router-dom';
import { useMutation, useQuery } from '@apollo/client';
import { GET_LIBRARY_IDS } from '../graphql/queries';
import TooltipButton from '../components/TooltipButton';
import { FaPlusCircle, FaBook } from 'react-icons/fa';
import { ADD_TO_LIBRARY } from '../graphql/mutations';
import { GameInUserLibrary, libraryIdsResponse } from '../types';

const AddToLibraryButton = ({ gameId }: { gameId: string }) => {
const history = useHistory();

const { data: library } = useQuery(GET_LIBRARY_IDS, {
onError: (err) => console.log(err),
});

const isGameInLibrary: boolean = library?.getLibraryIds.find(
(game: GameInUserLibrary) => game.igdb_game_id === parseInt(gameId)
)
? true
: false;

const [
addGameToLibrary,
{ loading: addingToLibrary, error: libraryError },
] = useMutation(ADD_TO_LIBRARY, {
variables: { gameId: parseInt(gameId) },
// refetchQueries: [{ query: GET_LIBRARY_IDS }],
// awaitRefetchQueries: true,

// I use update instead of refetchQueries for optimization.
// This way we can manually update the cache instead of refetching the query.
update: (store, response) => {
try {
const dataInStore: libraryIdsResponse | null = store.readQuery({
query: GET_LIBRARY_IDS,
});

store.writeQuery({
query: GET_LIBRARY_IDS,
data: {
...dataInStore,
getLibraryIds: dataInStore
? [...dataInStore.getLibraryIds, response.data.addGameToLibrary]
: [response.data.addGameToLibrary],
},
});
} catch (err) {
console.log(`Error updating the cache after addGameToLibrary query: ${err}`);
}
},
onError: (err) => console.log(`Error adding game to library: ${err}`)
});

return (
<div>
{isGameInLibrary ? (
<TooltipButton
label='In library'
onClick={() => history.push('/library')}
icon={<FaBook />}
isLoading={false}
/>
) : (
<TooltipButton
label='Add to library'
onClick={addGameToLibrary}
icon={<FaPlusCircle />}
isLoading={addingToLibrary}
isError={libraryError ? true : false}
errorMessage={libraryError?.message}
/>
)}
</div>
);
};

export default AddToLibraryButton;
13 changes: 1 addition & 12 deletions src/components/GameListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,15 @@ import { Link } from 'react-router-dom';
import { CgGames } from 'react-icons/cg';

const GameListItem = ({ game }: { game: Game }) => {
const handleClick = () => {
console.log('clicked', game.id);
};

// Setting image resolution from url: https://api-docs.igdb.com/#images
const imageSize = 'thumb';
// const imageLink = game.cover?.id ? (
// `//images.igdb.com/igdb/image/upload/t_${imageSize}/${game.cover?.image_id}.jpg`
// ) : (
// <GrGamepad />
// );
const imageLink = `//images.igdb.com/igdb/image/upload/t_${imageSize}/${game.cover?.image_id}.jpg`;

return (
<Link
to={`/game/${game.id}`}
key={game.id}
onClick={handleClick}
css={{ textDecoration: 'none', }}
css={{ textDecoration: 'none' }}
>
<div
css={{
Expand All @@ -49,7 +39,6 @@ const GameListItem = ({ game }: { game: Game }) => {
className='ImageDiv'
css={{ width: '90px', height: 'auto', maxwidth: '90px' }}
>
{/* <img src={imageLink} width='25' /> */}
{game.cover ? (
<img src={imageLink} css={{ width: '100%', borderRadius: '8px' }} />
) : (
Expand Down
93 changes: 15 additions & 78 deletions src/components/LoginRegisterButton.tsx
Original file line number Diff line number Diff line change
@@ -1,92 +1,29 @@
/** @jsxImportSource @emotion/react */

import { LoginDetails, LoginOrRegisterButtonProps } from '../types';
import { useMutation, useApolloClient } from '@apollo/client';
import LoginForm from '../components/LoginForm';
import { Dialog } from '@reach/dialog';
import { LoginOrRegisterButtonProps } from '../types';
import { capitalizeFirstLetter } from '../utils/misc';

import { LOGIN, REGISTER_NEW_USER } from '../graphql/mutations';
import { Button, CircleButton } from './styledComponentsLibrary';
import { Button } from './styledComponentsLibrary';
import LoginRegisterModal from './LoginRegisterModal';

const LoginRegisterButton = ({
loginOrRegister,
setOpenModal,
openModal,
buttonType,
}: LoginOrRegisterButtonProps) => {
const apolloClient = useApolloClient();

const [registerNewUser, { loading: registerLoading }] = useMutation(
REGISTER_NEW_USER,
{
onCompleted: (result) => {
console.log(result);
setOpenModal('none');
},
onError: (err) => {
console.log(err.message);
},
}
);

const [login, { loading: loginLoading }] = useMutation(LOGIN, {
onCompleted: (result) => {
console.log('login result: ', result);
apolloClient.resetStore();
setOpenModal('none');
},
onError: (err) => {
console.log('login error:', err.message);
},
});

const submitLogin = async (data: LoginDetails) => {
login({ variables: { email: data.email, password: data.password } });
};

const submitRegistration = (data: LoginDetails) => {
registerNewUser({
variables: { email: data.email, password: data.password },
});
};

const ariaLabel =
loginOrRegister === 'login' ? 'Login form' : 'Registration form';
const handleSubmit =
loginOrRegister === 'login' ? submitLogin : submitRegistration;
const loading = loginOrRegister === 'login' ? loginLoading : registerLoading;

return (
<div>
<Button onClick={() => setOpenModal(loginOrRegister)}>
{capitalizeFirstLetter(loginOrRegister)}
</Button>

<Dialog
aria-label={ariaLabel}
isOpen={openModal === loginOrRegister}
css={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
':click': {
transition: 'transform 0.3s',
transitionDuration: '0.3s',
transitionTimingFunction: 'ease',
transitionProperty: 'all',
},
}}
>
<div css={{ alignSelf: 'flex-end' }}>
<CircleButton onClick={() => setOpenModal('none')}>x</CircleButton>
</div>
<h3>{capitalizeFirstLetter(loginOrRegister)}</h3>
<LoginForm
onSubmit={handleSubmit}
buttonLabel={capitalizeFirstLetter(loginOrRegister)}
loading={loading}
/>
</Dialog>
{buttonType === 'regular' && (
<Button onClick={() => setOpenModal(loginOrRegister)}>
{capitalizeFirstLetter(loginOrRegister)}
</Button>
)}

<LoginRegisterModal
loginOrRegister={loginOrRegister}
setOpenModal={setOpenModal}
openModal={openModal}
/>
</div>
);
};
Expand Down
89 changes: 89 additions & 0 deletions src/components/LoginRegisterModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/** @jsxImportSource @emotion/react */

import { useMutation } from '@apollo/client';
import { LOGIN, REGISTER_NEW_USER } from '../graphql/mutations';

import { capitalizeFirstLetter } from '../utils/misc';
import { LoginDetails, LoginOrRegisterModalProps } from '../types';

import { CircleButton } from './styledComponentsLibrary';
import LoginForm from '../components/LoginForm';
import { Dialog } from '@reach/dialog';
import { CURRENT_USER } from '../graphql/queries';

const LoginRegisterModal = ({
loginOrRegister,
setOpenModal,
openModal,
}: LoginOrRegisterModalProps) => {
const [registerNewUser, { loading: registerLoading }] = useMutation(
REGISTER_NEW_USER,
{
onCompleted: (result) => {
console.log(result);
setOpenModal('none');
},
onError: (err) => {
console.log(err.message);
},
refetchQueries: [{ query: CURRENT_USER }],
}
);

const [login, { loading: loginLoading }] = useMutation(LOGIN, {
onCompleted: (result) => {
console.log('login result: ', result);
setOpenModal('none');
},
onError: (err) => {
console.log('login error:', err.message);
},
refetchQueries: [{ query: CURRENT_USER }],
});

const submitLogin = async (data: LoginDetails) => {
login({ variables: { email: data.email, password: data.password } });
};

const submitRegistration = (data: LoginDetails) => {
registerNewUser({
variables: { email: data.email, password: data.password },
});
};

const ariaLabel =
loginOrRegister === 'login' ? 'Login form' : 'Registration form';
const handleSubmit =
loginOrRegister === 'login' ? submitLogin : submitRegistration;
const loading = loginOrRegister === 'login' ? loginLoading : registerLoading;

return (
<Dialog
aria-label={ariaLabel}
isOpen={openModal === loginOrRegister}
css={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
':click': {
transition: 'transform 0.3s',
transitionDuration: '0.3s',
transitionTimingFunction: 'ease',
transitionProperty: 'all',
},
}}
>
<div css={{ alignSelf: 'flex-end' }}>
<CircleButton onClick={() => setOpenModal('none')}>x</CircleButton>
</div>
<h3>{capitalizeFirstLetter(loginOrRegister)}</h3>
<LoginForm
onSubmit={handleSubmit}
buttonLabel={capitalizeFirstLetter(loginOrRegister)}
loading={loading}
/>
</Dialog>
);
};

export default LoginRegisterModal;
14 changes: 11 additions & 3 deletions src/components/LogoutButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,20 @@ import { LOGOUT } from '../graphql/mutations';
import { Button } from './styledComponentsLibrary';

const LogoutButton = () => {
const [logout] = useMutation(LOGOUT);
const [logout] = useMutation(LOGOUT, {
onError: (err) => {
console.log('Logout mutation error: ', err);
},
});
const apolloClient = useApolloClient();

const handleClick = async () => {
await logout();
await apolloClient.resetStore();
try {
await logout();
await apolloClient.resetStore(); // This also refetches all queries
} catch (err) {
console.log('Error logging out: ', err);
}
};

return <Button onClick={handleClick}>Logout</Button>;
Expand Down
Loading

0 comments on commit 9cf013d

Please sign in to comment.