Skip to content

Commit

Permalink
feat(search): add search functionality
Browse files Browse the repository at this point in the history
- add new search page
- refactor to use redux hooks
- install package [email protected]
- install package @types/[email protected]
  • Loading branch information
SimonGolms committed Dec 29, 2020
1 parent 85bb3b5 commit 860ac9c
Show file tree
Hide file tree
Showing 7 changed files with 893 additions and 928 deletions.
1,620 changes: 730 additions & 890 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"i18next-browser-languagedetector": "6.0.1",
"immer": "7.0.14",
"ionicons": "5.2.3",
"lunr": "^2.3.9",
"react": "17.0.1",
"react-dom": "17.0.1",
"react-i18next": "11.7.3",
Expand All @@ -54,6 +55,7 @@
"devDependencies": {
"@capacitor/cli": "2.4.2",
"@types/jest": "26.0.15",
"@types/lunr": "^2.3.3",
"@types/node": "14.14.7",
"@types/react": "^16.9.56",
"@types/react-dom": "16.9.9",
Expand Down
22 changes: 10 additions & 12 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import { IonApp, IonRouterOutlet, IonSplitPane } from '@ionic/react';
import { IonReactRouter } from '@ionic/react-router';
import { Redirect, Route } from 'react-router-dom';
import { connect } from 'react-redux';
import Menu from './components/Menu';
import { Menu } from './components/Menu';
import Page from './pages/Page';
import ChapterPage from './pages/Chapter';
import { ChapterPage } from './pages/Chapter';
/* Core CSS required for Ionic components to work properly */
import '@ionic/react/css/core.css';
/* Basic CSS for apps built with Ionic */
Expand All @@ -26,6 +26,7 @@ import './theme/dark.theme.css';
import Tutorial from './pages/Tutorial';
import Settings from './pages/Settings';
import AboutPage from './pages/About';
import { SearchPage } from './pages/Search';
import { Theme } from './utils/theme';

interface ContainerProps {
Expand All @@ -34,16 +35,6 @@ interface ContainerProps {
}

const App: React.FC<ContainerProps> = ({ hasSeenTutorial, selectedTheme }) => {
if (!hasSeenTutorial && false) {
return (
<Suspense fallback="<App Suspense Loading>">
<IonApp>
<Tutorial />
</IonApp>
</Suspense>
);
}

return (
<Suspense fallback="<App Suspense Loading>">
<IonApp className={selectedTheme.className}>
Expand All @@ -65,6 +56,13 @@ const App: React.FC<ContainerProps> = ({ hasSeenTutorial, selectedTheme }) => {
}}
exact={true}
/>
<Route
path="/page/search"
render={(props) => {
return <SearchPage />;
}}
exact={true}
/>
<Route
path="/chapter/:id/:subId"
render={(props) => {
Expand Down
42 changes: 18 additions & 24 deletions src/components/Menu/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,20 @@ import {
} from '@ionic/react';
import React from 'react';
import { RouteComponentProps, withRouter } from 'react-router-dom';
import { settingsOutline, information, homeOutline } from 'ionicons/icons';
import { connect } from 'react-redux';
import {
settingsOutline,
information,
homeOutline,
searchOutline,
} from 'ionicons/icons';
import { useDispatch } from 'react-redux';
import { push } from 'connected-react-router';

import './index.css';
import { useTranslation } from 'react-i18next';
import { CHAPTER_01 } from '../Chapters/01/config';
import { CHAPTER_02 } from '../Chapters/02/config';

interface MenuProps extends RouteComponentProps {
dispatch: any;
}

interface AppPage {
color?: string;
isHeader?: boolean;
Expand All @@ -44,13 +45,20 @@ const appPages: AppPage[] = [
url: '/page/about',
icon: information,
},
{
title: 'SEARCH.TITLE',
url: '/page/search',
icon: searchOutline,
},
...CHAPTER_01,
...CHAPTER_02,
];

const Menu: React.FunctionComponent<MenuProps> = (props) => {
// const { selectedPage, setSelectedPage } = props;
interface MenuProps {}

export const Menu: React.FunctionComponent<MenuProps> = (props) => {
const { t } = useTranslation();
const dispatch = useDispatch();

return (
<IonMenu contentId="main" type="overlay">
Expand Down Expand Up @@ -79,7 +87,7 @@ const Menu: React.FunctionComponent<MenuProps> = (props) => {
routerDirection="none"
lines="none"
detail={false}
onClick={() => props.dispatch(push(appPage.url))}
onClick={() => dispatch(push(appPage.url))}
>
<IonIcon color={color} icon={icon} slot="start" />
<IonLabel>{t(title)}</IonLabel>
Expand All @@ -96,7 +104,7 @@ const Menu: React.FunctionComponent<MenuProps> = (props) => {
routerDirection="none"
lines="none"
detail={false}
onClick={() => props.dispatch(push('/settings'))}
onClick={() => dispatch(push('/settings'))}
>
<IonIcon slot="start" icon={settingsOutline} />
<IonLabel>Settings</IonLabel>
Expand All @@ -106,17 +114,3 @@ const Menu: React.FunctionComponent<MenuProps> = (props) => {
</IonMenu>
);
};

const mapStateToProps = (state: any) => {
return {
// selectedPage: state.getIn(['global', 'page']),
};
};

const mapDispatchToProps = (dispatch: any) => {
return {
dispatch,
};
};

export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Menu));
4 changes: 2 additions & 2 deletions src/data/global/global.reducer.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import produce from 'immer';
import ActionTypes from './global.constants';

interface Action {
interface GlobalAction {
readonly type: string;
readonly payload: any;
}
Expand All @@ -14,7 +14,7 @@ const initState: GlobalState = {
activePage: 'home',
};

const globalReducer = (state: GlobalState = initState, action: Action) =>
const globalReducer = (state: GlobalState = initState, action: GlobalAction) =>
produce(state, (draft) => {
switch (action.type) {
case ActionTypes.SET_ACTIVE_PAGE:
Expand Down
64 changes: 64 additions & 0 deletions src/pages/Search/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import React, { useState } from 'react';
import {
IonButtons,
IonContent,
IonHeader,
IonMenuButton,
IonPage,
IonRouterLink,
IonSearchbar,
IonTitle,
IonToolbar,
} from '@ionic/react';
import { useTranslation } from 'react-i18next';
import { useLunr } from '../../utils/lunr';
import { Chapter, getChapterIdsByUrl } from '../../components/Chapters';

interface ContainerProps {}

export const SearchPage: React.FC<ContainerProps> = (props) => {
const { t } = useTranslation();

const [searchValue, setSearchValue] = useState('');
const { results } = useLunr(searchValue);

return (
<IonPage>
<IonHeader>
<IonToolbar color="primary" style={{ '--opacity': 1 }}>
<IonButtons slot="start">
<IonMenuButton />
</IonButtons>
<IonTitle>{t('SEARCH.TITLE')}</IonTitle>
</IonToolbar>
</IonHeader>

<IonContent fullscreen={true}>
<IonHeader collapse="condense">
<IonToolbar color="primary" style={{ '--opacity': 1 }}>
<IonTitle size="large">{t('SEARCH.TITLE')}</IonTitle>
</IonToolbar>
</IonHeader>
<IonSearchbar
value={searchValue}
onIonChange={(event) => setSearchValue(event.detail.value!)}
></IonSearchbar>
<div style={{ margin: 'auto', width: 'fit-content' }}>
{results.map((result, resultIndex) => {
const chapterUrl = result.id;
const { id, subId } = getChapterIdsByUrl(chapterUrl);
return (
<IonRouterLink
key={resultIndex}
routerLink={chapterUrl}
routerDirection="forward"
>
<Chapter id={id} isCard subId={subId} />
</IonRouterLink>
);
})}
</div>
</IonContent>
</IonPage>
);
};
67 changes: 67 additions & 0 deletions src/utils/lunr.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { useTranslation } from 'react-i18next';
import { CHAPTER_01 } from '../components/Chapters/01/config';
import { useMemo } from 'react';
import lunr from 'lunr';
import { CHAPTER_02 } from '../components/Chapters/02/config';

type LunrDocs = {
[key: string]: {
id: string;
title: string;
body: string;
};
};

const createLunrDocs = () => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const { t } = useTranslation();

const chapters = [...CHAPTER_01, ...CHAPTER_02];

return chapters.reduce<LunrDocs>((docs, chapter) => {
if (chapter.isHeader) {
return docs;
}

const doc = {
[chapter.url]: {
id: chapter.url,
title: t(chapter.title),
body: chapter.body?.map((text) => t(text)).join(' ') || '',
},
};

return { ...docs, ...doc };
}, {});
};

export const useLunr = (query: string) => {
const docs = createLunrDocs();

const idx = useMemo(() => {
return lunr((builder) => {
builder.field('body');
builder.field('title');

Object.values(docs).forEach((doc) => {
builder.add(doc);
});
});
}, [docs]);

const rankings = useMemo(() => {
if (!query || !idx) {
return [];
}
return idx.search(query);
}, [query, idx]);

const results = useMemo(() => {
if (!rankings) {
return [];
}
return rankings.map(({ ref }) => docs[ref]);
}, [rankings, docs]);

return { rankings, results };
};

0 comments on commit 860ac9c

Please sign in to comment.