From dcd0a9888c9b616820d59c1197bed80cfbc1f31a Mon Sep 17 00:00:00 2001 From: Alexander Polynomdivision Date: Thu, 11 Oct 2018 17:08:58 +0200 Subject: [PATCH] feat: Implement a vocabulary search --- frontend/src/actions/index.ts | 15 ++++++ frontend/src/containers/VocabPage.ts | 9 +++- frontend/src/index.css | 19 +++++++ frontend/src/pages/vocab.tsx | 79 ++++++++++++++++++++++++++++ frontend/src/reducers/index.ts | 16 ++++++ 5 files changed, 137 insertions(+), 1 deletion(-) diff --git a/frontend/src/actions/index.ts b/frontend/src/actions/index.ts index ffa4999..62331e3 100644 --- a/frontend/src/actions/index.ts +++ b/frontend/src/actions/index.ts @@ -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, + }; +}; diff --git a/frontend/src/containers/VocabPage.ts b/frontend/src/containers/VocabPage.ts index f695161..1765769 100644 --- a/frontend/src/containers/VocabPage.ts +++ b/frontend/src/containers/VocabPage.ts @@ -1,6 +1,9 @@ import { connect } from "react-redux"; -import { setVocabLoading, setVocabVocab } from "../actions"; +import { + setVocabLoading, setVocabVocab, setVocabSearchOpen, + setVocabSearchTerm +} from "../actions"; import { IVocab } from "../models/vocab"; @@ -10,12 +13,16 @@ const mapStateToProps = state => { return { loading: state.vocab.loading, vocab: state.vocab.vocab, + searchOpen: state.vocab.searchOpen, + searchTerm: state.vocab.searchTerm, }; }; const mapDispatchToProps = dispatch => { return { setLoading: (state: boolean) => dispatch(setVocabLoading(state)), setVocab: (vocab: IVocab[]) => dispatch(setVocabVocab(vocab)), + setSearchOpen: (state: boolean) => dispatch(setVocabSearchOpen(state)), + setSearchTerm: (term: string) => dispatch(setVocabSearchTerm(term)), }; }; diff --git a/frontend/src/index.css b/frontend/src/index.css index 594f643..9c33a36 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -88,3 +88,22 @@ body { /* We otherwise go larger than the page */ 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; + } +} diff --git a/frontend/src/pages/vocab.tsx b/frontend/src/pages/vocab.tsx index d9d48fd..367694c 100644 --- a/frontend/src/pages/vocab.tsx +++ b/frontend/src/pages/vocab.tsx @@ -4,6 +4,15 @@ import Grid from "@material-ui/core/Grid"; import Typography from "@material-ui/core/Typography"; import Paper from "@material-ui/core/Paper"; 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"; @@ -12,14 +21,19 @@ import VocabularyData from "../components/VocabularyData"; interface IProps { getVocab: () => Promise; + searchOpen: boolean; + searchTerm: string; loading: boolean; vocab: IVocab[]; setLoading: (state: boolean) => void; setVocab: (vocab: IVocab[]) => void; + setSearchOpen: (state: boolean) => void; + setSearchTerm: (term: string) => void; } export default class VocabPage extends React.Component { + // Internal counter for dynmic children private uid = 0; genUID = () => { return `VOCABPAGE-${this.uid++}`; @@ -34,7 +48,30 @@ export default class VocabPage extends React.Component { }); } + componentWillUnmount() { + // Reset the search term + this.props.setSearchTerm(""); + } + + openSearch = () => { + this.props.setSearchOpen(true); + } + closeSearch = () => { + this.props.setSearchOpen(false); + } + 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 = { [VocabType.NOMEN]: "Nomen", [VocabType.VERB]: "Verb", @@ -50,6 +87,12 @@ export default class VocabPage extends React.Component { ; } + applySearchFilter = (e: any) => { + // Not sure how much this does + e.preventDefault(); + this.closeSearch(); + } + render() { if (this.props.loading) { return
@@ -71,9 +114,45 @@ export default class VocabPage extends React.Component {
; } + // 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; return
{vocab.map(this.vocabToCard)} +
+ + + + Suche + + + this.props.setSearchTerm(event.target.value)} /> + + + + + + +
; } }; diff --git a/frontend/src/reducers/index.ts b/frontend/src/reducers/index.ts index 96a5c47..ddecf71 100644 --- a/frontend/src/reducers/index.ts +++ b/frontend/src/reducers/index.ts @@ -61,6 +61,8 @@ interface IState { vocab: { loading: boolean; vocab: IVocab[]; + searchOpen: boolean; + searchTerm: string; }; register: { @@ -134,6 +136,8 @@ const initialState: IState = { vocab: { loading: true, vocab: [], + searchOpen: false, + searchTerm: "", }, register: { @@ -335,6 +339,18 @@ export function LateinicusApp(state: IState = initialState, action: any) { 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: // Ignore the initialization call to the reducer. By that we can // catch all actions that are not implemented