Skip to content

Commit

Permalink
[web] add game grid, game sorting, loading status and fix height of g…
Browse files Browse the repository at this point in the history
…ame cards (#40)

* Add game grid and game sorting

* add loading status and fix search bug

* fixed height game cards
  • Loading branch information
ryanabooth committed Sep 16, 2020
1 parent 3d507f4 commit 80a29d2
Show file tree
Hide file tree
Showing 4 changed files with 117 additions and 53 deletions.
33 changes: 19 additions & 14 deletions web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,8 @@ import {
} from '@material-ui/core/styles';
import Nav from './components/Nav';
import Games from './components/Games';
// use local storage instead of mock
import awsconfig from './aws-exports';
import { fetchGames, createGame, updateGame, duplicateGame, deleteGame } from './lib/games';
import { fetchGames, sortGames, createGame, updateGame, duplicateGame, deleteGame, SORT_TYPES } from './lib/games';
import { Game } from './types';

const filterGame = (game: Game | null, search: string) => {
Expand All @@ -37,21 +36,15 @@ const theme = createMuiTheme({
function App() {
const [startup, setStartup] = useState(true);
const [loading, setLoading] = useState(false);
const [sortType, setSortType] = useState(SORT_TYPES.UPDATED);
const [searchInput, setSearchInput] = useState('');
const [games, setGames] = useState<(Game | null)[]>([]);

const getGames = async () => {
setLoading(true);
const games = await fetchGames();
setGames(games);
setLoading(false);
};

const saveNewGame = async (newGame: { title: string, description?: string }) => {
setLoading(true);
const game = await createGame(newGame);
if (game) {
const games = await fetchGames();
const games = sortGames(await fetchGames(), sortType);
setGames(games);
}
setLoading(false);
Expand All @@ -60,15 +53,15 @@ function App() {
const saveGame = async (game: Game) => {
const result = await updateGame(game);
if (result) {
const games = await fetchGames();
const games = sortGames(await fetchGames(), sortType);
setGames(games);
}
}

const handleDeleteGame = async (id: number) => {
const result = await deleteGame(id);
if (result) {
const games = await fetchGames();
const games = sortGames(await fetchGames(), sortType);
setGames(games);
}
}
Expand All @@ -77,16 +70,28 @@ function App() {
const handleDuplicateGame = async (game) => {
const result = await duplicateGame(game);
if (result) {
const games = await fetchGames();
const games = sortGames(await fetchGames(), sortType);
setGames(games);
}
}

useEffect(() => {
const getGames = async () => {
setLoading(true);
const games = sortGames(await fetchGames(), SORT_TYPES.UPDATED);
setGames(games);
setLoading(false);
};
getGames();
setStartup(false);
}, []);

useEffect(() => {
// @ts-ignore
setGames(sortGames(games, sortType));
// eslint-disable-next-line
}, [sortType]);

// @ts-ignore
const handleSaveQuestion = async (question, gameIndex, questionIndex) => {
const game = { ...games[Number(gameIndex)] };
Expand All @@ -106,7 +111,7 @@ function App() {
<Box>
<Nav />
<Route path="/">
<Games loading={loading} games={filteredGames} saveNewGame={saveNewGame} saveGame={saveGame} saveQuestion={handleSaveQuestion} setSearchInput={setSearchInput} searchInput={searchInput} deleteGame={handleDeleteGame} duplicateGame={handleDuplicateGame} />
<Games loading={loading} games={filteredGames} saveNewGame={saveNewGame} saveGame={saveGame} saveQuestion={handleSaveQuestion} setSearchInput={setSearchInput} searchInput={searchInput} deleteGame={handleDeleteGame} duplicateGame={handleDuplicateGame} sortType={sortType} setSortType={setSortType} />
</Route>
</Box>
</ThemeProvider>
Expand Down
7 changes: 6 additions & 1 deletion web/src/components/GameForm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,12 @@ function GameForm({ loading, game, gameIndex, saveGame }) {
</Button>
{(game.grade || game.domain || game.cluster || game.standard) && (
<Typography className={classes.ccss}>
<strong>CCSS: </strong>{game.grade}.{game.domain}.{game.cluster}.{game.standard}
{game.grade !== 'General' && (
<>
<strong>CCSS: </strong>
{`${game.grade}.${game.domain}.${game.cluster}.${game.standard}`}
</>
)}
</Typography>
)}
<Button disabled={questions.length > 4} className={classes.addQuestion} color="primary" type="button" variant="contained" onClick={addQuestion}>
Expand Down
86 changes: 58 additions & 28 deletions web/src/components/Games.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { useState } from 'react';
import classnames from 'classnames';
import { Route, Switch, useHistory, useRouteMatch } from 'react-router-dom';
import { SORT_TYPES } from '../lib/games';
import { fade, makeStyles } from '@material-ui/core/styles';
import Box from '@material-ui/core/Box';
import Grid from '@material-ui/core/Grid';
Expand All @@ -20,8 +21,9 @@ import EditGameDialogue from './EditGameDialogue';
import Menu from '@material-ui/core/Menu';
import MenuItem from '@material-ui/core/MenuItem';
import MoreVertIcon from '@material-ui/icons/MoreVert';
import Select from '@material-ui/core/Select';

export default function Games({ loading, games, saveGame, saveQuestion, saveNewGame, searchInput, setSearchInput, deleteGame, duplicateGame }) {
export default function Games({ loading, games, saveGame, saveQuestion, saveNewGame, searchInput, setSearchInput, deleteGame, duplicateGame, sortType, setSortType }) {
const classes = useStyles();
const history = useHistory();
const match = useRouteMatch('/games/:gameIndex');
Expand All @@ -36,6 +38,7 @@ export default function Games({ loading, games, saveGame, saveQuestion, saveNewG
const handleClick = (event) => {
setAnchorEl(event.currentTarget);
setActiveIndex(event.currentTarget.dataset.gameIndex);
event.stopPropagation();
};
const handleClose = () => {
setAnchorEl(null);
Expand Down Expand Up @@ -66,16 +69,20 @@ export default function Games({ loading, games, saveGame, saveQuestion, saveNewG
}
handleClose();
};
const handleSortChange = (event) => {
setSortType(event.target.value);
};

const renderGames = () => {
const renderGames = (loading) => {
if (loading) return <Typography gutterBottom>Loading...</Typography>;
if (games.length >= 1) {
return games
.map((game, index) => {
const { GameID, title, grade, q1, q2, q3, q4, q5 } = game;
const { GameID, title, q1, q2, q3, q4, q5 } = game;
const questionCount = [q1, q2, q3, q4, q5].filter(q => !!q).length;
const image = getGameImage(game);
return (
<Card className={classnames(classes.game, match && Number(match.params.gameIndex) === index + 1 && classes.gameSelected)} key={GameID} onClick={() => history.push(`/games/${index + 1}`)}>
<Card className={classnames(classes.game, !match && classes.gameGrid, match && Number(match.params.gameIndex) === index + 1 && classes.gameSelected)} key={GameID} onClick={() => history.push(`/games/${index + 1}`)}>
<CardContent>
<Box className={classes.titleRow}>
<Typography className={classes.title} gutterBottom>
Expand All @@ -92,7 +99,7 @@ export default function Games({ loading, games, saveGame, saveQuestion, saveNewG
open={activeIndex === String(index)}
onClose={handleClose}
>
<MenuItem onClick={(event) => { history.push(`/games/${index + 1}/edit`); event.stopPropagation(); }}>Edit</MenuItem>
<MenuItem onClick={(event) => { history.push(`/games/${index + 1}/edit`); event.stopPropagation(); handleClose(); }}>Edit</MenuItem>
<MenuItem onClick={duplicateHandler(game)}>Duplicate</MenuItem>
<MenuItem onClick={deleteHandler(GameID)}>Delete</MenuItem>
</Menu>
Expand All @@ -111,9 +118,14 @@ export default function Games({ loading, games, saveGame, saveQuestion, saveNewG
<Typography color="textSecondary" gutterBottom>
{questionCount} question{questionCount > 1 || questionCount === 0 ? 's' : ''}
</Typography>
<Typography color="textSecondary" gutterBottom>
{grade && `Grade ${grade}`}
</Typography>
{(game.grade || game.domain || game.cluster || game.standard) && (
<Typography color="textSecondary" gutterBottom>
{game.grade === 'General' ?
game.grade :
`${game.grade}.${game.domain}.${game.cluster}.${game.standard}`
}
</Typography>
)}
</Box>
</CardContent>
</Card>
Expand All @@ -129,10 +141,10 @@ export default function Games({ loading, games, saveGame, saveQuestion, saveNewG

return (
<Grid container className={classes.root} spacing={4}>
<Grid item xs={3} className={classes.sidebar}>
<Grid item xs={match ? 3 : 12} className={classes.sidebar}>
<Box className={classes.actions}>
<Button variant="contained" color="primary" onClick={() => setNewGameOpen(true)}>
Add game
New game
</Button>
<div className={classes.search}>
<div className={classes.searchIcon}>
Expand All @@ -149,26 +161,36 @@ export default function Games({ loading, games, saveGame, saveQuestion, saveNewG
inputProps={{ 'aria-label': 'search' }}
/>
</div>
<Select
id="sort-select"
value={sortType}
onChange={handleSortChange}
>
<MenuItem value={SORT_TYPES.UPDATED}>Last Updated</MenuItem>
<MenuItem value={SORT_TYPES.ALPHABETICAL}>Alphabetical</MenuItem>
</Select>
<NewGameDialogue open={newGameOpen} onClose={() => setNewGameOpen(false)} submit={handleNewGame} />
</Box>
{renderGames()}
</Grid>
<Grid item xs={9} className={classes.content}>
<Switch>
<Route path="/games/:gameIndex/questions/:questionIndex" render={
({ match }) => {
const { questionIndex, gameIndex } = match.params;
return <QuestionForm loading={loading} saveQuestion={saveQuestion} question={games[Number(gameIndex) - 1][`q${Number(questionIndex)}`]} {...match.params} />;
}
} />
<Route path="/games/:gameIndex" render={
({ match }) => {
const { gameIndex } = match.params;
return <GameForm loading={loading} saveGame={saveGame} game={games[Number(gameIndex) - 1]} gameIndex={gameIndex} />;
}
} />
</Switch>
{renderGames(loading)}
</Grid>
{match && games[Number(match.params.gameIndex) - 1] && (
<Grid item xs={9} className={classes.content}>
<Switch>
<Route path="/games/:gameIndex/questions/:questionIndex" render={
({ match }) => {
const { questionIndex, gameIndex } = match.params;
return <QuestionForm loading={loading} saveQuestion={saveQuestion} question={games[Number(gameIndex) - 1][`q${Number(questionIndex)}`]} {...match.params} />;
}
} />
<Route path="/games/:gameIndex" render={
({ match }) => {
const { gameIndex } = match.params;
return <GameForm loading={loading} saveGame={saveGame} game={games[Number(gameIndex) - 1]} gameIndex={gameIndex} />;
}
} />
</Switch>
</Grid>
)}
<Route path="/games/:gameIndex/edit" render={
({ match }) => {
const { gameIndex } = match.params;
Expand All @@ -186,11 +208,18 @@ const useStyles = makeStyles(theme => ({
width: 'calc(100% + 16px) !important',
},
game: {
width: '350px',
marginBottom: theme.spacing(2),
'&:hover': {
backgroundColor: 'rgba(0, 0, 0, 0.05)',
cursor: 'pointer',
}
},
height: '152px',
},
gameGrid: {
display: 'inline-block',
marginRight: theme.spacing(4),
verticalAlign: 'top',
},
gameSelected: {
backgroundColor: '#CAF0F3',
Expand Down Expand Up @@ -251,6 +280,7 @@ const useStyles = makeStyles(theme => ({
},
image: {
width: '80px',
maxHeight: '80px',
marginRight: theme.spacing(2),
},
square: {
Expand Down
44 changes: 34 additions & 10 deletions web/src/lib/games.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,32 @@ import { listGames } from '../graphql/queries';
import { createGames, updateGames, deleteGames } from '../graphql/mutations';
import { Game, Question, APIGame } from '../types';

export enum SORT_TYPES {
ALPHABETICAL,
UPDATED,
}

const sortByUpdated = (a: Game, b: Game) => {
if (a.updated === null) return 1;
if (b.updated === null) return -1;
if (a.updated > b.updated) return -1;
if (b.updated > a.updated) return 1;
return 0;
};

const sortAlphabetically = (a: Game, b: Game) => {
if (a.title === null) return 1;
if (b.title === null) return -1;
if (a.title > b.title) return 1;
if (b.title > a.title) return -1;
return 0;
};

const SORT_TYPE_TO_FUNCTION = {
[SORT_TYPES.ALPHABETICAL]: sortAlphabetically,
[SORT_TYPES.UPDATED]: sortByUpdated,
}

const deserializeQuestion = (question: string | null) => {
return question === null ? question : (JSON.parse(question) as Question);
}
Expand Down Expand Up @@ -40,20 +66,18 @@ const serializeQuestions = (game: Game): APIGame | null => {
}
}

const sortByUpdated = (a: APIGame, b: APIGame) => {
if (a.updated === null) return 1;
if (b.updated === null) return -1;
if (a.updated > b.updated) return -1;
if (b.updated > a.updated) return 1;
return 0;
};

export const fetchGames = async () => {
export const fetchGames = async (sortType: SORT_TYPES = SORT_TYPES.UPDATED): Promise<Game[]> => {
const result = await API.graphql(graphqlOperation(listGames)) as { data: ListGamesQuery };
const games = (result?.data?.listGames?.items || []) as APIGame[];
return games.sort(sortByUpdated).map(deserializeQuestions);
// @ts-ignore
return games.map(deserializeQuestions);
}

export const sortGames = (games: Game[], sortType: SORT_TYPES = SORT_TYPES.UPDATED) => {
const sortFunction = SORT_TYPE_TO_FUNCTION[sortType];
return [...games].sort(sortFunction);
};

export const createGame = async (game: CreateGamesInput) => {
// @ts-ignore
Object.keys(game).forEach((key) => { if (game[key] === '') game[key] = null; });
Expand Down

0 comments on commit 80a29d2

Please sign in to comment.