Skip to content

Commit 6b58e73

Browse files
committed
Add the language selector to the Devto and hashnode cards
1 parent 3f24862 commit 6b58e73

9 files changed

Lines changed: 398 additions & 197 deletions

File tree

src/Constants.js

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,5 +141,19 @@ export const SUPPORTED_SEARCH_ENGINES = [
141141
]
142142
export const LS_PREFERENCES_KEY = 'hackerTabPrefs'
143143
export const LS_ANALYTICS_ID_KEY = 'hackerTabAnalyticsId'
144-
144+
export const GLOBAL_TAG = {
145+
value: 'global',
146+
label: 'Trending',
147+
githubValues: ['global'],
148+
devtoValues: [''],
149+
hashnodeValues: ['programming'],
150+
}
151+
export const MY_LANGUAGES_TAG = {
152+
value: 'myLangs',
153+
label: 'My Languages',
154+
githubValues: ['myLangs'],
155+
devtoValues: ['myLangs'],
156+
hashnodeValues: ['myLangs'],
157+
}
158+
export const MAX_MERGED_ITEMS_PER_LANGUAGE = 10
145159
export { APP }

src/cards/DevToCard.js

Lines changed: 107 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,23 @@
11
import React, { useContext, useState, useEffect } from 'react'
2-
import { FaDev } from 'react-icons/fa';
32
import devtoApi from '../services/devto'
4-
import CardComponent from "../components/CardComponent"
5-
import ListComponent from "../components/ListComponent"
6-
import { format } from 'timeago.js';
3+
import CardComponent from '../components/CardComponent'
4+
import ListComponent from '../components/ListComponent'
5+
import { format } from 'timeago.js'
76
import PreferencesContext from '../preferences/PreferencesContext'
8-
import CardLink from "../components/CardLink"
9-
import { BiCommentDetail } from 'react-icons/bi';
10-
import { MdAccessTime } from "react-icons/md"
11-
import { AiOutlineLike } from "react-icons/ai"
7+
import CardLink from '../components/CardLink'
8+
import { BiCommentDetail } from 'react-icons/bi'
9+
import { MdAccessTime } from 'react-icons/md'
10+
import { AiOutlineLike } from 'react-icons/ai'
1211
import CardItemWithActions from '../components/CardItemWithActions'
13-
import ColoredLanguagesBadge from "../components/ColoredLanguagesBadge"
12+
import ColoredLanguagesBadge from '../components/ColoredLanguagesBadge'
13+
import SelectableCard from '../components/SelectableCard'
14+
import { GLOBAL_TAG, MY_LANGUAGES_TAG, MAX_MERGED_ITEMS_PER_LANGUAGE } from '../Constants'
15+
import { mergeMultipleDataSources } from '../utils/DataUtils'
16+
import { trackCardLanguageChange } from '../utils/Analytics'
1417

18+
const DT_MENU_LANGUAGE_ID = 'DT_MENU_LANGUAGE_ID'
1519

1620
const ArticleItem = ({ item, index, analyticsTag }) => {
17-
1821
const { listingMode } = useContext(PreferencesContext)
1922

2023
return (
@@ -23,81 +26,134 @@ const ArticleItem = ({ item, index, analyticsTag }) => {
2326
index={index}
2427
key={index}
2528
item={item}
26-
cardItem={(
29+
cardItem={
2730
<>
2831
<CardLink link={item.url} analyticsSource={analyticsTag}>
29-
{ listingMode === "compact" &&
30-
<div className="counterWrapper">
31-
<AiOutlineLike/>
32-
<span className="value">{item.public_reactions_count}</span>
33-
</div>
34-
}
35-
<div className="subTitle">
36-
{item.title}
37-
</div>
32+
{listingMode === 'compact' && (
33+
<div className="counterWrapper">
34+
<AiOutlineLike />
35+
<span className="value">{item.public_reactions_count}</span>
36+
</div>
37+
)}
38+
<div className="subTitle">{item.title}</div>
3839
</CardLink>
3940

40-
{
41-
listingMode === "normal" &&
41+
{listingMode === 'normal' && (
4242
<>
4343
<p className="rowDescription">
44-
<span className="rowItem"><MdAccessTime className={"rowTitleIcon"} />{format(new Date(item.published_at))}</span>
45-
<span className="rowItem"><BiCommentDetail className={"rowTitleIcon"} />{item.comments_count} comments</span>
46-
<span className="rowItem"><AiOutlineLike className={"rowTitleIcon"} />{item.public_reactions_count} reactions</span>
44+
<span className="rowItem">
45+
<MdAccessTime className={'rowTitleIcon'} />
46+
{format(new Date(item.published_at))}
47+
</span>
48+
<span className="rowItem">
49+
<BiCommentDetail className={'rowTitleIcon'} />
50+
{item.comments_count} comments
51+
</span>
52+
<span className="rowItem">
53+
<AiOutlineLike className={'rowTitleIcon'} />
54+
{item.public_reactions_count} reactions
55+
</span>
4756
</p>
4857
<p className="rowDetails">
4958
<ColoredLanguagesBadge languages={item.tag_list} />
5059
</p>
5160
</>
52-
}
53-
61+
)}
5462
</>
55-
)}
63+
}
5664
/>
5765
)
5866
}
5967

60-
61-
6268
function DevToCard({ analyticsTag, label, icon, withAds }) {
6369
const preferences = useContext(PreferencesContext)
64-
const { userSelectedTags } = preferences
65-
70+
const { userSelectedTags, cardsSettings, dispatcher } = preferences
6671
const [refresh, setRefresh] = useState(true)
72+
const [selectedLanguage, setSelectedLanguage] = useState()
73+
const [cacheCardData, setCacheCardData] = useState({})
6774

6875
useEffect(() => {
69-
setRefresh(!refresh)
70-
}, [userSelectedTags])
76+
if (selectedLanguage) {
77+
trackCardLanguageChange('Devto', selectedLanguage.value)
78+
dispatcher({
79+
type: 'setCardSettings',
80+
value: { card: label.toLowerCase(), language: selectedLanguage.label },
81+
})
82+
setRefresh(!refresh)
83+
}
84+
}, [selectedLanguage])
7185

7286
const fetchArticles = async () => {
73-
const promises = userSelectedTags.map((tag) => {
74-
if (tag.devtoValues) {
75-
return devtoApi.getArticles(tag.devtoValues[0])
76-
}
87+
if (!selectedLanguage) {
7788
return []
78-
})
79-
80-
const results = await Promise.allSettled(promises)
81-
return results
82-
.map((res) => {
83-
let value = res.value
84-
if (res.status === 'rejected') {
85-
value = []
89+
}
90+
91+
if (!selectedLanguage.devtoValues) {
92+
throw Error(`Devto does not support ${selectedLanguage?.label}.`)
93+
}
94+
95+
let data = []
96+
const cacheKey = `${selectedLanguage.label}`
97+
98+
// Cache found
99+
if (cacheCardData[cacheKey]) {
100+
return cacheCardData[cacheKey]
101+
}
102+
103+
if (selectedLanguage.value == MY_LANGUAGES_TAG.value) {
104+
const selectedTagsArticlesPromises = userSelectedTags.map((tag) => {
105+
if (tag.devtoValues) {
106+
if (cacheCardData[tag.label]) {
107+
return cacheCardData[tag.label]
108+
} else {
109+
return devtoApi.getArticlesByCount(tag.devtoValues[0], MAX_MERGED_ITEMS_PER_LANGUAGE)
110+
}
86111
}
87-
return value
112+
return []
88113
})
89-
.flat()
90-
.sort((a, b) => b.public_reactions_count - a.public_reactions_count)
114+
115+
data = await mergeMultipleDataSources(
116+
selectedTagsArticlesPromises,
117+
MAX_MERGED_ITEMS_PER_LANGUAGE
118+
)
119+
} else {
120+
data = await devtoApi.getArticles(selectedLanguage.devtoValues[0])
121+
}
122+
123+
data = data.sort((a, b) => b.public_reactions_count - a.public_reactions_count)
124+
125+
setCacheCardData({ ...cacheCardData, [cacheKey]: data })
126+
return data
91127
}
92128

93129
const renderItem = (item, index) => (
94130
<ArticleItem item={item} key={`at-${index}`} index={index} analyticsTag={analyticsTag} />
95131
)
96132

133+
function HeaderTitle() {
134+
return (
135+
<div style={{ display: 'inline-block', margin: 0, padding: 0 }}>
136+
<span> DevTo </span>
137+
<SelectableCard
138+
isLanguage={true}
139+
tagId={DT_MENU_LANGUAGE_ID}
140+
selectedTag={selectedLanguage}
141+
setSelectedTag={setSelectedLanguage}
142+
fallbackTag={GLOBAL_TAG}
143+
cardSettings={cardsSettings?.devto?.language}
144+
data={userSelectedTags.map((tag) => ({
145+
label: tag.label,
146+
value: tag.value,
147+
}))}
148+
/>
149+
</div>
150+
)
151+
}
152+
97153
return (
98154
<CardComponent
99155
icon={<span className="blockHeaderIcon">{icon}</span>}
100-
title={label}
156+
title={<HeaderTitle />}
101157
link="https://dev.to/">
102158
<ListComponent
103159
fetchData={fetchArticles}
@@ -109,4 +165,4 @@ function DevToCard({ analyticsTag, label, icon, withAds }) {
109165
)
110166
}
111167

112-
export default DevToCard
168+
export default DevToCard

src/cards/HashNodeCard.js

Lines changed: 74 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,15 @@ import PreferencesContext from '../preferences/PreferencesContext'
77
import CardLink from '../components/CardLink'
88
import { BiCommentDetail } from 'react-icons/bi'
99
import { MdAccessTime } from 'react-icons/md'
10-
import { AiOutlineLike, AiTwotoneHeart } from 'react-icons/ai'
11-
10+
import { AiTwotoneHeart } from 'react-icons/ai'
1211
import CardItemWithActions from '../components/CardItemWithActions'
1312
import ColoredLanguagesBadge from '../components/ColoredLanguagesBadge'
13+
import SelectableCard from '../components/SelectableCard'
14+
import { GLOBAL_TAG, MY_LANGUAGES_TAG, MAX_MERGED_ITEMS_PER_LANGUAGE } from '../Constants'
15+
import { mergeMultipleDataSources } from '../utils/DataUtils'
16+
import { trackCardLanguageChange } from '../utils/Analytics'
17+
18+
const HN_MENU_LANGUAGE_ID = 'HN_MENU_LANGUAGE_ID'
1419

1520
const ArticleItem = ({ item, index, analyticsTag }) => {
1621
const { listingMode } = useContext(PreferencesContext)
@@ -62,33 +67,80 @@ const ArticleItem = ({ item, index, analyticsTag }) => {
6267

6368
function HashNodeCard({ analyticsTag, label, icon, withAds }) {
6469
const preferences = useContext(PreferencesContext)
65-
const { userSelectedTags } = preferences
66-
70+
const { userSelectedTags, cardsSettings, dispatcher } = preferences
71+
const [selectedLanguage, setSelectedLanguage] = useState()
6772
const [refresh, setRefresh] = useState(true)
73+
const [cacheCardData, setCacheCardData] = useState({})
6874

6975
useEffect(() => {
70-
setRefresh(!refresh)
71-
}, [userSelectedTags])
76+
if (selectedLanguage) {
77+
trackCardLanguageChange('Hashnode', selectedLanguage.value)
78+
dispatcher({
79+
type: 'setCardSettings',
80+
value: { card: label, language: selectedLanguage.label.toLowerCase() },
81+
})
82+
setRefresh(!refresh)
83+
}
84+
}, [selectedLanguage])
7285

7386
const fetchArticles = async () => {
74-
const promises = userSelectedTags.map((tag) => {
75-
if (tag.hashnodeValues) {
76-
return hashNodeApi.getArticles(tag.hashnodeValues[0])
77-
}
87+
if (!selectedLanguage) {
7888
return []
79-
})
80-
81-
const results = await Promise.allSettled(promises)
82-
return results
83-
.map((res) => {
84-
let value = res.value
85-
if (res.status === 'rejected') {
86-
value = []
89+
}
90+
if (!selectedLanguage.label) {
91+
throw Error(`Hashnode does not support ${selectedLanguage.label}.`)
92+
}
93+
94+
let data = []
95+
const cacheKey = `${selectedLanguage.label}`
96+
97+
// Cache found
98+
if (cacheCardData[cacheKey]) {
99+
return cacheCardData[cacheKey]
100+
}
101+
102+
if (selectedLanguage.value == MY_LANGUAGES_TAG.value) {
103+
const selectedTagsArticlesPromises = userSelectedTags.map((tag) => {
104+
if (tag.hashnodeValues) {
105+
if (cacheCardData[tag.label]) {
106+
return cacheCardData[tag.label]
107+
} else {
108+
return hashNodeApi.getArticles(tag.hashnodeValues[0])
109+
}
87110
}
88-
return value
111+
return []
89112
})
90-
.flat()
91-
.sort((a, b) => b.public_reactions_count - a.public_reactions_count)
113+
114+
data = await mergeMultipleDataSources(
115+
selectedTagsArticlesPromises,
116+
MAX_MERGED_ITEMS_PER_LANGUAGE
117+
)
118+
} else {
119+
data = await hashNodeApi.getArticles(selectedLanguage.hashnodeValues[0])
120+
}
121+
122+
setCacheCardData({ ...cacheCardData, [cacheKey]: data })
123+
return data
124+
}
125+
126+
function HeaderTitle() {
127+
return (
128+
<div style={{ display: 'inline-block', margin: 0, padding: 0 }}>
129+
<span> Hashnode </span>
130+
<SelectableCard
131+
isLanguage={true}
132+
tagId={HN_MENU_LANGUAGE_ID}
133+
selectedTag={selectedLanguage}
134+
setSelectedTag={setSelectedLanguage}
135+
fallbackTag={GLOBAL_TAG}
136+
cardSettings={cardsSettings?.hashnode?.language}
137+
data={userSelectedTags.map((tag) => ({
138+
label: tag.label,
139+
value: tag.value,
140+
}))}
141+
/>
142+
</div>
143+
)
92144
}
93145

94146
const renderItem = (item, index) => (
@@ -98,7 +150,7 @@ function HashNodeCard({ analyticsTag, label, icon, withAds }) {
98150
return (
99151
<CardComponent
100152
icon={<span className="blockHeaderIcon">{icon}</span>}
101-
title={label}
153+
title={<HeaderTitle />}
102154
link="https://hashnode.com/">
103155
<ListComponent
104156
fetchData={fetchArticles}

0 commit comments

Comments
 (0)