Skip to content

Commit 91bee9b

Browse files
committed
fix: enhance TopicSettings component with improved tag handling and search functionality
1 parent 3c336ee commit 91bee9b

1 file changed

Lines changed: 154 additions & 30 deletions

File tree

Lines changed: 154 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,169 @@
1-
import { ChipsSet } from 'src/components/Elements'
1+
import { useMemo, useState } from 'react'
2+
import { AiFillMobile } from 'react-icons/ai'
3+
import { BsChevronDown, BsChevronUp, BsFillGearFill, BsFillShieldLockFill } from 'react-icons/bs'
4+
import { FaDatabase, FaPaintBrush, FaRobot, FaServer } from 'react-icons/fa'
5+
import { IoMdSearch } from 'react-icons/io'
6+
import { RiDeviceFill } from 'react-icons/ri'
7+
import { SiBnbchain } from 'react-icons/si'
8+
import { ChipsSet, SearchBar } from 'src/components/Elements'
29
import { SettingsContentLayout } from 'src/components/Layout/SettingsContentLayout/SettingsContentLayout'
10+
import { repository } from 'src/config'
311
import { Tag, useRemoteConfigStore } from 'src/features/remoteConfig'
412
import { trackLanguageAdd, trackLanguageRemove } from 'src/lib/analytics'
513
import { useUserPreferences } from 'src/stores/preferences'
614

15+
const CATEGORY_TO_ICON: Record<string, React.ReactNode> = {
16+
frontend: <FaPaintBrush className="icon" />,
17+
backend: <BsFillGearFill className="icon" />,
18+
fullstack: <RiDeviceFill className="icon" />,
19+
mobile: <AiFillMobile className="icon" />,
20+
devops: <FaServer className="icon" />,
21+
ai: <FaRobot className="icon" />,
22+
data: <FaDatabase className="icon" />,
23+
security: <BsFillShieldLockFill className="icon" />,
24+
blockchain: <SiBnbchain className="icon" />,
25+
}
726
export const TopicSettings = () => {
8-
const { userSelectedTags, setTags } = useUserPreferences()
27+
const { userSelectedTags, occupation, followTag, unfollowTag } = useUserPreferences()
28+
29+
const { tags } = useRemoteConfigStore()
30+
const [searchKeyword, setSearchKeyword] = useState<string>('')
31+
const filteredTags = useMemo(() => {
32+
if (searchKeyword.trim() === '') {
33+
return tags
34+
}
35+
return tags.filter(
36+
(tag) =>
37+
tag.label.toLowerCase().includes(searchKeyword.trim().toLowerCase()) ||
38+
tag.category?.toLowerCase().includes(searchKeyword.trim().toLowerCase())
39+
)
40+
}, [tags, searchKeyword])
41+
42+
const groupedTags = useMemo(() => {
43+
const groups = filteredTags.reduce<Record<string, Tag[]>>((acc, tag) => {
44+
;(acc[tag.category || 'Other'] ??= []).push(tag)
45+
return acc
46+
}, {})
47+
48+
if ('Other' in groups) {
49+
const { Other, ...rest } = groups
50+
return { ...rest, Other }
51+
}
952

10-
const { supportedTags } = useRemoteConfigStore()
53+
return groups
54+
}, [filteredTags])
1155

12-
const tags = supportedTags
13-
.map((tag) => {
14-
return {
15-
label: tag.label,
16-
value: tag.value,
17-
}
18-
})
19-
.sort((a, b) => (a.label > b.label ? 1 : -1))
56+
const [expandedCategories, setExpandedCategories] = useState<string[]>(
57+
occupation ? [occupation] : ['backend', 'frontend']
58+
)
2059

2160
return (
2261
<SettingsContentLayout
23-
title="Topics"
62+
title="Followed Topics"
2463
description={`Your feed will be tailored by following the technologies you are interested in.`}>
25-
<ChipsSet
26-
canSelectMultiple={true}
27-
options={tags}
28-
defaultValues={userSelectedTags.map((tag) => tag.value)}
29-
onChange={(changes, selectedChips) => {
30-
const selectedTags =
31-
(selectedChips
32-
.map((tag) => supportedTags.find((st) => st.value === tag.value))
33-
.filter(Boolean) as Tag[]) || []
34-
setTags(selectedTags)
35-
36-
if (changes.action == 'ADD') {
37-
trackLanguageAdd(changes.option.value)
38-
} else {
39-
trackLanguageRemove(changes.option.value)
40-
}
41-
}}
42-
/>
64+
{userSelectedTags.length > 0 ? (
65+
<ChipsSet
66+
canSelectMultiple={true}
67+
options={userSelectedTags}
68+
defaultValues={userSelectedTags.map((tag) => tag.value)}
69+
onChange={(changes) => {
70+
if (changes.action == 'ADD') {
71+
followTag(changes.option as Tag)
72+
trackLanguageAdd(changes.option.value)
73+
} else {
74+
unfollowTag(changes.option as Tag)
75+
trackLanguageRemove(changes.option.value)
76+
}
77+
}}
78+
/>
79+
) : (
80+
<div>
81+
<p className="errorMsg">You are not following any topics yet. Start exploring below!</p>
82+
</div>
83+
)}
84+
85+
<hr />
86+
87+
<header>
88+
<div className="settingsHeader">
89+
<h3 className="title">Explore new topics</h3>
90+
<p className="description">
91+
Explore and follow new topics to customize your feed further.
92+
</p>
93+
</div>
94+
</header>
95+
<div>
96+
<SearchBar
97+
iconStart={<IoMdSearch className="searchBarIcon" />}
98+
placeholder="Search by programming language, framework, or topic"
99+
onChange={(keyword) => {
100+
setSearchKeyword(keyword)
101+
}}
102+
/>
103+
</div>
104+
<div className="topicsFlex">
105+
{Object.keys(groupedTags).length == 0 ? (
106+
<div>
107+
<p className="errorMsg">
108+
No results found, try adjusting your search keyword.
109+
<br />
110+
If you think this technology is missing, feel free to{' '}
111+
<a
112+
href={`${repository}/issues/new?title=[${searchKeyword}] New technology`}
113+
target="_blank">
114+
open an issue
115+
</a>{' '}
116+
to suggest it.
117+
</p>
118+
</div>
119+
) : (
120+
Object.keys(groupedTags).map((category) => (
121+
<div key={category}>
122+
<button
123+
className="categoryTitle subTitleButton"
124+
onClick={() =>
125+
setExpandedCategories(
126+
expandedCategories.includes(category)
127+
? expandedCategories.filter((c) => c !== category)
128+
: [...expandedCategories, category]
129+
)
130+
}>
131+
{CATEGORY_TO_ICON[category.toLowerCase()]} {category}{' '}
132+
<span className="">
133+
{expandedCategories.includes(category) ? (
134+
<BsChevronUp className="subTitleIcon" />
135+
) : (
136+
<BsChevronDown className="subTitleIcon" />
137+
)}
138+
</span>
139+
</button>
140+
141+
{expandedCategories.includes(category) && (
142+
<ChipsSet
143+
className="categoryContent"
144+
canSelectMultiple={true}
145+
options={groupedTags[category]
146+
.sort((a, b) => (a.label > b.label ? 1 : -1))
147+
.map((tag) => ({
148+
label: tag.label,
149+
value: tag.value,
150+
}))}
151+
defaultValues={userSelectedTags.map((tag) => tag.value)}
152+
onChange={(changes) => {
153+
if (changes.action == 'ADD') {
154+
followTag(changes.option as Tag)
155+
trackLanguageAdd(changes.option.value)
156+
} else {
157+
unfollowTag(changes.option as Tag)
158+
trackLanguageRemove(changes.option.value)
159+
}
160+
}}
161+
/>
162+
)}
163+
</div>
164+
))
165+
)}
166+
</div>
43167
</SettingsContentLayout>
44168
)
45169
}

0 commit comments

Comments
 (0)