feat: Implement a vocabulary search

This commit is contained in:
Alexander Polynomdivision 2018-10-11 17:08:58 +02:00
parent 3261a81b06
commit dcd0a9888c
5 changed files with 137 additions and 1 deletions

View File

@ -266,3 +266,18 @@ export function setVocabVocab(vocab: IVocab[]) {
}; };
}; };
export const VOCAB_SET_SEARCH_OPEN = "VOCAB_SET_SEARCH_OPEN";
export function setVocabSearchOpen(state: boolean) {
return {
type: VOCAB_SET_SEARCH_OPEN,
state,
};
};
export const VOCAB_SET_SEARCH_TERM = "VOCAB_SET_SEARCH_TERM";
export function setVocabSearchTerm(term: string) {
return {
type: VOCAB_SET_SEARCH_TERM,
term,
};
};

View File

@ -1,6 +1,9 @@
import { connect } from "react-redux"; import { connect } from "react-redux";
import { setVocabLoading, setVocabVocab } from "../actions"; import {
setVocabLoading, setVocabVocab, setVocabSearchOpen,
setVocabSearchTerm
} from "../actions";
import { IVocab } from "../models/vocab"; import { IVocab } from "../models/vocab";
@ -10,12 +13,16 @@ const mapStateToProps = state => {
return { return {
loading: state.vocab.loading, loading: state.vocab.loading,
vocab: state.vocab.vocab, vocab: state.vocab.vocab,
searchOpen: state.vocab.searchOpen,
searchTerm: state.vocab.searchTerm,
}; };
}; };
const mapDispatchToProps = dispatch => { const mapDispatchToProps = dispatch => {
return { return {
setLoading: (state: boolean) => dispatch(setVocabLoading(state)), setLoading: (state: boolean) => dispatch(setVocabLoading(state)),
setVocab: (vocab: IVocab[]) => dispatch(setVocabVocab(vocab)), setVocab: (vocab: IVocab[]) => dispatch(setVocabVocab(vocab)),
setSearchOpen: (state: boolean) => dispatch(setVocabSearchOpen(state)),
setSearchTerm: (term: string) => dispatch(setVocabSearchTerm(term)),
}; };
}; };

View File

@ -88,3 +88,22 @@ body {
/* We otherwise go larger than the page */ /* We otherwise go larger than the page */
box-sizing: border-box; box-sizing: border-box;
} }
.vocab-search-fab {
position: fixed !important;
bottom: 12px;
right: 12px;
}
/*
* Desktop screens a big enough so that the FAB won't cover anything.
* That is not neccessarilly true for mobile devices.
*/
.vocab-bottom-spacer {
}
@media only screen and (max-width: 700px) {
.vocab-bottom-spacer {
/* 56px + 12px */
margin-bottom: 68px;
}
}

View File

@ -4,6 +4,15 @@ import Grid from "@material-ui/core/Grid";
import Typography from "@material-ui/core/Typography"; import Typography from "@material-ui/core/Typography";
import Paper from "@material-ui/core/Paper"; import Paper from "@material-ui/core/Paper";
import CircularProgress from "@material-ui/core/CircularProgress"; import CircularProgress from "@material-ui/core/CircularProgress";
import Button from "@material-ui/core/Button";
import TextField from "@material-ui/core/TextField";
import Dialog from "@material-ui/core/Dialog";
import DialogTitle from "@material-ui/core/DialogTitle";
import DialogContent from "@material-ui/core/DialogContent";
/* import DialogContentText from "@material-ui/core/DialogContentText"; */
import DialogActions from "@material-ui/core/DialogActions";
import SearchIcon from "@material-ui/icons/Search";
import { IVocab, VocabType } from "../models/vocab"; import { IVocab, VocabType } from "../models/vocab";
@ -12,14 +21,19 @@ import VocabularyData from "../components/VocabularyData";
interface IProps { interface IProps {
getVocab: () => Promise<IVocab[]>; getVocab: () => Promise<IVocab[]>;
searchOpen: boolean;
searchTerm: string;
loading: boolean; loading: boolean;
vocab: IVocab[]; vocab: IVocab[];
setLoading: (state: boolean) => void; setLoading: (state: boolean) => void;
setVocab: (vocab: IVocab[]) => void; setVocab: (vocab: IVocab[]) => void;
setSearchOpen: (state: boolean) => void;
setSearchTerm: (term: string) => void;
} }
export default class VocabPage extends React.Component<IProps> { export default class VocabPage extends React.Component<IProps> {
// Internal counter for dynmic children
private uid = 0; private uid = 0;
genUID = () => { genUID = () => {
return `VOCABPAGE-${this.uid++}`; return `VOCABPAGE-${this.uid++}`;
@ -34,7 +48,30 @@ export default class VocabPage extends React.Component<IProps> {
}); });
} }
componentWillUnmount() {
// Reset the search term
this.props.setSearchTerm("");
}
openSearch = () => {
this.props.setSearchOpen(true);
}
closeSearch = () => {
this.props.setSearchOpen(false);
}
vocabToCard = (voc: IVocab) => { vocabToCard = (voc: IVocab) => {
// Do we need to apply a search filter?
const { searchTerm } = this.props;
if (searchTerm !== "") {
// Just ignore the vocabulary item if it does
// not match the "query"
const lower = voc.latin.grundform.toLowerCase();
if (!lower.startsWith(searchTerm.toLowerCase())) {
return;
}
}
const vocabTypeToStr = { const vocabTypeToStr = {
[VocabType.NOMEN]: "Nomen", [VocabType.NOMEN]: "Nomen",
[VocabType.VERB]: "Verb", [VocabType.VERB]: "Verb",
@ -50,6 +87,12 @@ export default class VocabPage extends React.Component<IProps> {
</Paper>; </Paper>;
} }
applySearchFilter = (e: any) => {
// Not sure how much this does
e.preventDefault();
this.closeSearch();
}
render() { render() {
if (this.props.loading) { if (this.props.loading) {
return <div> return <div>
@ -71,9 +114,45 @@ export default class VocabPage extends React.Component<IProps> {
</div>; </div>;
} }
// Trigger a search when the user presses the return key
const onEnter = (event: any) => {
if (event.key === "Enter") {
this.applySearchFilter(event);
}
};
const { vocab } = this.props; const { vocab } = this.props;
return <div className="content"> return <div className="content">
{vocab.map(this.vocabToCard)} {vocab.map(this.vocabToCard)}
<div className="vocab-bottom-spacer" />
<Dialog
open={this.props.searchOpen}
onClose={this.closeSearch}>
<DialogTitle>
Suche
</DialogTitle>
<DialogContent>
<TextField
helperText="Suche nach Vokabeln"
onKeyPress={onEnter}
value={this.props.searchTerm}
onChange={(event) => this.props.setSearchTerm(event.target.value)} />
</DialogContent>
<DialogActions>
<Button onClick={this.applySearchFilter} color="primary">
Suchen
</Button>
</DialogActions>
</Dialog>
<Button
className="vocab-search-fab"
variant="fab"
color="primary"
onClick={this.openSearch}>
<SearchIcon />
</Button>
</div>; </div>;
} }
}; };

View File

@ -61,6 +61,8 @@ interface IState {
vocab: { vocab: {
loading: boolean; loading: boolean;
vocab: IVocab[]; vocab: IVocab[];
searchOpen: boolean;
searchTerm: string;
}; };
register: { register: {
@ -134,6 +136,8 @@ const initialState: IState = {
vocab: { vocab: {
loading: true, loading: true,
vocab: [], vocab: [],
searchOpen: false,
searchTerm: "",
}, },
register: { register: {
@ -335,6 +339,18 @@ export function LateinicusApp(state: IState = initialState, action: any) {
vocab: action.vocab, vocab: action.vocab,
}), }),
}); });
case Actions.VOCAB_SET_SEARCH_OPEN:
return Object.assign({}, state, {
vocab: Object.assign({}, state.vocab, {
searchOpen: action.state,
}),
});
case Actions.VOCAB_SET_SEARCH_TERM:
return Object.assign({}, state, {
vocab: Object.assign({}, state.vocab, {
searchTerm: action.term,
}),
});
default: default:
// Ignore the initialization call to the reducer. By that we can // Ignore the initialization call to the reducer. By that we can
// catch all actions that are not implemented // catch all actions that are not implemented