Skip to content
This repository was archived by the owner on Jan 30, 2025. It is now read-only.

Commit 68d9c4e

Browse files
committedMar 5, 2020
allow adding existing chapter to a topic
1 parent 0376f19 commit 68d9c4e

File tree

7 files changed

+158
-3
lines changed

7 files changed

+158
-3
lines changed
 

‎src/components/ChapterForm.tsx

+20-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1-
import { useContext, useState } from 'react';
1+
import { useContext, useEffect, useRef, useState } from 'react';
2+
import { throttle } from 'lodash';
23
import { Grid, TextField } from '@material-ui/core';
34
import { Chapter } from '@zoonk/models';
5+
import { searchChapter } from '@zoonk/services';
46
import { GlobalContext } from '@zoonk/utils';
7+
import ChapterSelector from './ChapterSelector';
58
import FormBase from './FormBase';
69

710
interface ChapterFormProps {
@@ -25,12 +28,26 @@ const ChapterForm = ({
2528
const [description, setDescription] = useState<string>(
2629
data?.description || '',
2730
);
31+
const [search, setSearch] = useState<ReadonlyArray<Chapter.Index>>([]);
32+
const throttled = useRef(
33+
throttle((query: string) => {
34+
searchChapter(query).then(setSearch);
35+
}, 1000),
36+
);
37+
2838
const descriptionMax = 500;
2939
const valid =
3040
title.length > 0 &&
3141
description.length > 0 &&
3242
description.length <= descriptionMax;
3343

44+
// Search existing chapters when creating a new one.
45+
useEffect(() => {
46+
if (!data && title.length > 3) {
47+
throttled.current(title);
48+
}
49+
}, [data, title]);
50+
3451
return (
3552
<FormBase
3653
valid={valid}
@@ -55,6 +72,8 @@ const ChapterForm = ({
5572
/>
5673
</Grid>
5774

75+
<ChapterSelector chapters={search} />
76+
5877
<Grid item xs={12}>
5978
<TextField
6079
multiline

‎src/components/ChapterSelector.tsx

+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { useContext, useState } from 'react';
2+
import NextLink from 'next/link';
3+
import { useRouter } from 'next/router';
4+
import {
5+
Button,
6+
Card,
7+
CardHeader,
8+
List,
9+
ListItem,
10+
ListItemSecondaryAction,
11+
ListItemText,
12+
} from '@material-ui/core';
13+
import { Chapter, SnackbarAction } from '@zoonk/models';
14+
import { firebaseError, GlobalContext, theme, timestamp } from '@zoonk/utils';
15+
import { addChapterToTopic } from '@zoonk/services';
16+
import Snackbar from './Snackbar';
17+
18+
interface ChapterSelectorProps {
19+
chapters: ReadonlyArray<Chapter.Index>;
20+
}
21+
22+
/**
23+
* Chapter list with an option to add it to the current topic.
24+
*/
25+
const ChapterSelector = ({ chapters }: ChapterSelectorProps) => {
26+
const { profile, translate, user } = useContext(GlobalContext);
27+
const [snackbar, setSnackbar] = useState<SnackbarAction | null>(null);
28+
const { query, push } = useRouter();
29+
const topicId = String(query.id);
30+
31+
if (chapters.length === 0 || !profile || !user) {
32+
return null;
33+
}
34+
35+
const add = (id: string) => {
36+
setSnackbar({ type: 'progress', msg: translate('chapter_adding') });
37+
const metadata = {
38+
updatedAt: timestamp,
39+
updatedBy: profile,
40+
updatedById: user.uid,
41+
};
42+
43+
addChapterToTopic(id, topicId, metadata)
44+
.then(() =>
45+
push(
46+
'/topics/[topicId]/chapters/[chapterId]',
47+
`/topics/${topicId}/chapters/${id}`,
48+
),
49+
)
50+
.catch((e) => setSnackbar(firebaseError(e, 'add_existing_chapter')));
51+
};
52+
53+
return (
54+
<Card
55+
variant="outlined"
56+
style={{ width: '100%', margin: theme.spacing(1) }}
57+
>
58+
<CardHeader
59+
title={translate('chapter_select_title')}
60+
subheader={translate('chapter_select_desc')}
61+
/>
62+
<List>
63+
{chapters.map((chapter, index) => (
64+
<ListItem
65+
key={chapter.objectID}
66+
divider={chapters.length > index + 1}
67+
>
68+
<ListItemText primary={chapter.title} />
69+
<ListItemSecondaryAction>
70+
<NextLink
71+
href="/topics/[id]/chapters/[chapterId]"
72+
as={`/topics/${topicId}/chapters/${chapter.objectID}`}
73+
passHref
74+
>
75+
<Button
76+
component="a"
77+
target="_blank"
78+
rel="noopener noreferrer"
79+
color="primary"
80+
>
81+
{translate('view')}
82+
</Button>
83+
</NextLink>
84+
<Button color="secondary" onClick={() => add(chapter.objectID)}>
85+
{translate('add')}
86+
</Button>
87+
</ListItemSecondaryAction>
88+
</ListItem>
89+
))}
90+
</List>
91+
92+
<Snackbar action={snackbar} />
93+
</Card>
94+
);
95+
};
96+
97+
export default ChapterSelector;

‎src/locale/en.ts

+4
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const translate: TranslationFn = (key, args) => {
66
about_me: 'About me',
77
about_list: 'List of pages about how Zoonk works',
88
add_image: 'Add image',
9+
add: 'Add',
910
admin: 'Admin',
1011
advanced: 'Advanced',
1112
after: 'After',
@@ -18,8 +19,11 @@ const translate: TranslationFn = (key, args) => {
1819
by: 'by',
1920
cancel: 'Cancel',
2021
chapter_add: 'Add a new chapter',
22+
chapter_adding: 'Adding a chapter to this topic...',
2123
chapter_edit: 'Edit chapter',
2224
chapter_invalid_id: 'You cannot create a chapter without a topicId.',
25+
chapter_select_desc: 'You can add an existing chapter to this topic:',
26+
chapter_select_title: 'Select a chapter',
2327
chapter: 'Chapter',
2428
chapters: 'Chapters',
2529
choose_language: 'Choose a language',

‎src/locale/pt.ts

+5
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const translate: TranslationFn = (key, args) => {
66
about_list: 'Lista de páginas sobre como o Zoonk funciona',
77
about_me: 'Sobre mim',
88
add_image: 'Inserir imagem',
9+
add: 'Add',
910
admin: 'Admin',
1011
advanced: 'Avançado',
1112
after: 'Depois',
@@ -18,9 +19,13 @@ const translate: TranslationFn = (key, args) => {
1819
by: 'por',
1920
cancel: 'Cancelar',
2021
chapter_add: 'Crie um novo capítulo',
22+
chapter_adding: 'Adicionando capítulo na página deste assunto...',
2123
chapter_edit: 'Editar capítulo',
2224
chapter_invalid_id:
2325
'Você não pode criar um capítulo sem uma ID válida para a trilha de aprendizagem.',
26+
chapter_select_desc:
27+
'Você pode adicionar um capítulo existente neste tópico clicando no botão "add":',
28+
chapter_select_title: 'Escolher capítulo',
2429
chapter: 'Capítulo',
2530
chapters: 'Capítulos',
2631
choose_language: 'Escolha um idioma',

‎src/models/i18n.ts

+4
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export type TranslationKeys =
66
| 'about_list'
77
| 'about_me'
88
| 'add_image'
9+
| 'add'
910
| 'admin'
1011
| 'advanced'
1112
| 'after'
@@ -18,8 +19,11 @@ export type TranslationKeys =
1819
| 'by'
1920
| 'cancel'
2021
| 'chapter_add'
22+
| 'chapter_adding'
2123
| 'chapter_edit'
2224
| 'chapter_invalid_id'
25+
| 'chapter_select_desc'
26+
| 'chapter_select_title'
2327
| 'chapter'
2428
| 'chapters'
2529
| 'choose_language'

‎src/services/chapters.ts

+16-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { pickBy } from 'lodash';
2-
import { Chapter, Profile } from '@zoonk/models';
2+
import { Chapter, ContentMetadata, Profile } from '@zoonk/models';
33
import {
44
analytics,
55
arrayUnion,
@@ -48,6 +48,21 @@ export const createChapter = async (data: Chapter.Create): Promise<string> => {
4848
return slug;
4949
};
5050

51+
/**
52+
* Add an existing chapter to a topic.
53+
*/
54+
export const addChapterToTopic = (
55+
chapterId: string,
56+
topicId: string,
57+
user: ContentMetadata.Update,
58+
): Promise<void> => {
59+
const changes = {
60+
...user,
61+
chapters: arrayUnion(chapterId),
62+
};
63+
return db.doc(`topics/${topicId}`).update(changes);
64+
};
65+
5166
/**
5267
* Update an existing chapter.
5368
*/

‎src/services/search.ts

+12-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import algolia from 'algoliasearch';
22
import { SearchResponse } from '@algolia/client-search';
3-
import { SearchResult } from '@zoonk/models';
3+
import { Chapter, SearchResult } from '@zoonk/models';
44
import { analytics, appLanguage, isProduction } from '@zoonk/utils';
55

66
const ALGOLIA_APP_ID = isProduction ? 'CEHDTPZ5VM' : 'J75DV0NKA3';
@@ -30,3 +30,14 @@ export const search = async (
3030
index: item.index?.slice(0, item.index.length - 3),
3131
}));
3232
};
33+
34+
/**
35+
* Search for an existing chapter.
36+
*/
37+
export const searchChapter = async (
38+
query: string,
39+
): Promise<ReadonlyArray<Chapter.Index>> => {
40+
const index = client.initIndex(`chapters_${appLanguage}`);
41+
const req = await index.search<Chapter.Index>(query, { hitsPerPage: 5 });
42+
return req.hits;
43+
};

0 commit comments

Comments
 (0)
This repository has been archived.