feat: Implement a vocabulary search
This commit is contained in:
parent
3261a81b06
commit
dcd0a9888c
@ -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,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
@ -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)),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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>;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -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
|
||||||
|
Reference in New Issue
Block a user