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

Commit df7ad2f

Browse files
committed
allow to add existing lessons and examples to a chapter
1 parent 9e1b921 commit df7ad2f

File tree

7 files changed

+163
-3
lines changed

7 files changed

+163
-3
lines changed

src/components/PostForm.tsx

+27-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1-
import { useCallback, useContext, useEffect, useState } from 'react';
1+
import { useCallback, useContext, useEffect, useRef, useState } from 'react';
2+
import { throttle } from 'lodash';
3+
import dynamic from 'next/dynamic';
4+
import { useRouter } from 'next/router';
25
import { Button, Grid, makeStyles, TextField } from '@material-ui/core';
36
import { Post } from '@zoonk/models';
7+
import { searchPost } from '@zoonk/services';
48
import { appLanguage, GlobalContext } from '@zoonk/utils';
59
import FormattingTips from './FormattingTips';
610
import FormBase from './FormBase';
@@ -9,6 +13,8 @@ import LinkFormField from './LinkFormField';
913
import PostPreview from './PostPreview';
1014
import TopicSelector from './TopicSelector';
1115

16+
const PostSelector = dynamic(() => import('./PostSelector'));
17+
1218
const useStyles = makeStyles((theme) => ({
1319
column: {
1420
'& > *': {
@@ -42,6 +48,8 @@ const PostForm = ({
4248
onSubmit,
4349
}: PostFormProps) => {
4450
const { translate } = useContext(GlobalContext);
51+
const { query } = useRouter();
52+
const category = String(query.category) as Post.Category;
4553
const classes = useStyles();
4654
const [preview, setPreview] = useState<boolean>(false);
4755
const [content, setContent] = useState<string>(data?.content || '');
@@ -50,7 +58,23 @@ const PostForm = ({
5058
const [links, setLinks] = useState<string[]>(
5159
data && data.links && data.links.length > 0 ? data.links : [''],
5260
);
61+
const [search, setSearch] = useState<ReadonlyArray<Post.Index>>([]);
5362
const valid = content.length > 0 && title.length > 0 && topics.length > 0;
63+
const lessonCategories = ['examples', 'lessons'];
64+
const isLesson = lessonCategories.includes(category);
65+
66+
const throttled = useRef(
67+
throttle((searchTerm: string) => {
68+
searchPost(searchTerm, category).then(setSearch);
69+
}, 1000),
70+
);
71+
72+
// Search existing posts when creating a new one.
73+
useEffect(() => {
74+
if (!data && isLesson && title.length > 3) {
75+
throttled.current(title);
76+
}
77+
}, [data, isLesson, title]);
5478

5579
// Add the current topicId when adding a new item.
5680
useEffect(() => {
@@ -122,6 +146,8 @@ const PostForm = ({
122146
type="text"
123147
/>
124148

149+
{isLesson && <PostSelector posts={search} />}
150+
125151
<TextField
126152
required
127153
value={content}

src/components/PostSelector.tsx

+93
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
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 { Post, SnackbarAction } from '@zoonk/models';
14+
import { firebaseError, GlobalContext, timestamp } from '@zoonk/utils';
15+
import { addPostToChapter } from '@zoonk/services';
16+
import Snackbar from './Snackbar';
17+
18+
interface PostSelectorProps {
19+
posts: ReadonlyArray<Post.Index>;
20+
}
21+
22+
/**
23+
* Post list with an option to add it to the current chapter.
24+
*/
25+
const PostSelector = ({ posts }: PostSelectorProps) => {
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+
const chapterId = String(query.chapterId);
31+
const category = String(query.category) as Post.Category;
32+
33+
if (posts.length === 0 || !profile || !user) {
34+
return null;
35+
}
36+
37+
const add = (id: string) => {
38+
setSnackbar({ type: 'progress', msg: translate('post_adding') });
39+
const metadata = {
40+
updatedAt: timestamp,
41+
updatedBy: profile,
42+
updatedById: user.uid,
43+
};
44+
45+
addPostToChapter(id, chapterId, category, metadata)
46+
.then(() =>
47+
push(
48+
`/topics/[topicId]/chapters/[chapterId]/${category}`,
49+
`/topics/${topicId}/chapters/${chapterId}/${category}`,
50+
),
51+
)
52+
.catch((e) => setSnackbar(firebaseError(e, 'add_existing_post')));
53+
};
54+
55+
return (
56+
<Card variant="outlined">
57+
<CardHeader
58+
title={translate('post_select_title')}
59+
subheader={translate('post_select_desc')}
60+
/>
61+
<List>
62+
{posts.map((post, index) => (
63+
<ListItem key={post.objectID} divider={posts.length > index + 1}>
64+
<ListItemText primary={post.title} />
65+
<ListItemSecondaryAction>
66+
<NextLink
67+
href="/posts/[id]"
68+
as={`/posts/${post.objectID}`}
69+
passHref
70+
>
71+
<Button
72+
component="a"
73+
target="_blank"
74+
rel="noopener noreferrer"
75+
color="primary"
76+
>
77+
{translate('view')}
78+
</Button>
79+
</NextLink>
80+
<Button color="secondary" onClick={() => add(post.objectID)}>
81+
{translate('add')}
82+
</Button>
83+
</ListItemSecondaryAction>
84+
</ListItem>
85+
))}
86+
</List>
87+
88+
<Snackbar action={snackbar} />
89+
</Card>
90+
);
91+
};
92+
93+
export default PostSelector;

src/locale/en.ts

+3
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,10 @@ const translate: TranslationFn = (key, args) => {
141141
photo_uploaded: 'Uploaded photo',
142142
photo: 'Photo',
143143
post_add: 'Create a post',
144+
post_adding: 'Adding post to chapter...',
144145
post_edit: 'Edit post',
146+
post_select_desc: 'You can add an existing post to this chapter:',
147+
post_select_title: 'Select a post',
145148
posts_links: 'Posts & Links',
146149
posts: 'Posts',
147150
preview: 'Preview',

src/locale/pt.ts

+4
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,11 @@ const translate: TranslationFn = (key, args) => {
143143
photo_uploaded: 'Foto enviada',
144144
photo: 'Foto',
145145
post_add: 'Criar postagem',
146+
post_adding: 'Adicionando postagem ao capítulo...',
146147
post_edit: 'Editar postagem',
148+
post_select_desc:
149+
'Você pode adicionar uma postagem existente ao capítulo clicando no botão "add":',
150+
post_select_title: 'Escolher postagem',
147151
posts_links: 'Postagens & Links',
148152
posts: 'Postagens',
149153
preview: 'Visualizar',

src/models/i18n.ts

+3
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,10 @@ export type TranslationKeys =
134134
| 'photo_uploaded'
135135
| 'photo'
136136
| 'post_add'
137+
| 'post_adding'
137138
| 'post_edit'
139+
| 'post_select_desc'
140+
| 'post_select_title'
138141
| 'posts_links'
139142
| 'posts'
140143
| 'preview'

src/services/posts.ts

+17-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { pickBy } from 'lodash';
2-
import { ChapterProgress, Post, Profile } from '@zoonk/models';
2+
import { ChapterProgress, ContentMetadata, Post, Profile } from '@zoonk/models';
33
import {
44
analytics,
55
appLanguage,
@@ -52,6 +52,22 @@ export const createPost = async (
5252
return slug;
5353
};
5454

55+
/**
56+
* Add an existing post to a chapter.
57+
*/
58+
export const addPostToChapter = (
59+
postId: string,
60+
chapterId: string,
61+
category: Post.Category,
62+
user: ContentMetadata.Update,
63+
): Promise<void> => {
64+
const changes = {
65+
...user,
66+
[category]: arrayUnion(postId),
67+
};
68+
return db.doc(`chapters/${chapterId}`).update(changes);
69+
};
70+
5571
/**
5672
* Update an existing post.
5773
*/

src/services/search.ts

+16-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 { Chapter, SearchResult } from '@zoonk/models';
3+
import { Chapter, Post, SearchResult } from '@zoonk/models';
44
import { analytics, appLanguage, isProduction } from '@zoonk/utils';
55

66
const ALGOLIA_APP_ID = isProduction ? 'CEHDTPZ5VM' : 'J75DV0NKA3';
@@ -41,3 +41,18 @@ export const searchChapter = async (
4141
const req = await index.search<Chapter.Index>(query, { hitsPerPage: 5 });
4242
return req.hits;
4343
};
44+
45+
/**
46+
* Search for an existing post.
47+
*/
48+
export const searchPost = async (
49+
query: string,
50+
category?: Post.Category,
51+
): Promise<ReadonlyArray<Post.Index>> => {
52+
const index = client.initIndex(`posts_${appLanguage}`);
53+
const req = await index.search<Post.Index>(query, {
54+
hitsPerPage: 5,
55+
filters: category ? `category:${category}` : undefined,
56+
});
57+
return req.hits;
58+
};

0 commit comments

Comments
 (0)