Skip to content

Commit dd265d3

Browse files
committed
add medium source
1 parent b6e35ba commit dd265d3

4 files changed

Lines changed: 187 additions & 1 deletion

File tree

src/Constants.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,12 @@ import LobstersCard from './cards/LobstersCard'
99
import HashNodeCard from './cards/HashNodeCard'
1010
import IndieHackersCard from './cards/IndieHackersCard'
1111
import FreeCodeCampCard from './cards/FreeCodeCampCard'
12+
import MediumCard from './cards/MediumCard'
1213
import { SiGithub } from 'react-icons/si'
1314
import { SiYcombinator } from 'react-icons/si'
1415
import { FaDev } from 'react-icons/fa'
1516
import { SiProducthunt } from 'react-icons/si'
16-
import { FaReddit } from 'react-icons/fa'
17+
import { FaReddit, FaMediumM } from 'react-icons/fa'
1718
import { HiTicket } from 'react-icons/hi'
1819
import HashNodeIcon from './static/icon_hashnode.png'
1920
import LobstersIcon from './static/icon_lobsters.png'
@@ -109,6 +110,13 @@ export const SUPPORTED_CARDS = [
109110
label: 'IndieHackers',
110111
component: IndieHackersCard,
111112
},
113+
{
114+
value: 'medium',
115+
icon: <FaMediumM />,
116+
analyticsTag: 'medium',
117+
label: 'Medium',
118+
component: MediumCard,
119+
}
112120
]
113121

114122
export const SUPPORTED_SEARCH_ENGINES = [
@@ -149,13 +157,15 @@ export const GLOBAL_TAG = {
149157
githubValues: ['global'],
150158
devtoValues: [''],
151159
hashnodeValues: ['programming'],
160+
mediumValues: ['javascript'], //TODO: change javascript
152161
}
153162
export const MY_LANGUAGES_TAG = {
154163
value: 'myLangs',
155164
label: 'My Languages',
156165
githubValues: ['myLangs'],
157166
devtoValues: ['myLangs'],
158167
hashnodeValues: ['myLangs'],
168+
mediumValues: ['myLangs'],
159169
}
160170
export const MAX_MERGED_ITEMS_PER_LANGUAGE = 10
161171
export { APP }

src/cards/MediumCard.js

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import React, { useContext, useState, useEffect } from 'react'
2+
import mediumApi from '../services/medium'
3+
import CardComponent from '../components/CardComponent'
4+
import ListComponent from '../components/ListComponent'
5+
import { format } from 'timeago.js'
6+
import PreferencesContext from '../preferences/PreferencesContext'
7+
import CardLink from '../components/CardLink'
8+
import { BiCommentDetail } from 'react-icons/bi'
9+
import { MdAccessTime } from 'react-icons/md'
10+
import { AiTwotoneHeart } from 'react-icons/ai'
11+
import { IoMdHand } from 'react-icons/io'
12+
import CardItemWithActions from '../components/CardItemWithActions'
13+
import ColoredLanguagesBadge from '../components/ColoredLanguagesBadge'
14+
import SelectableCard from '../components/SelectableCard'
15+
import { GLOBAL_TAG, MY_LANGUAGES_TAG, MAX_MERGED_ITEMS_PER_LANGUAGE } from '../Constants'
16+
import { mergeMultipleDataSources } from '../utils/DataUtils'
17+
import { trackCardLanguageChange } from '../utils/Analytics'
18+
19+
const ArticleItem = ({ item, index, analyticsTag }) => {
20+
const { listingMode } = useContext(PreferencesContext)
21+
22+
return (
23+
<CardItemWithActions
24+
source={'medium'}
25+
index={index}
26+
key={index}
27+
item={{ ...item, url: item.link }}
28+
cardItem={
29+
<>
30+
<CardLink link={item.link} analyticsSource={analyticsTag}>
31+
{listingMode === 'compact' && (
32+
<div className="counterWrapper">
33+
<AiTwotoneHeart />
34+
<span className="value">{item.totalReactions || 0}</span>
35+
</div>
36+
)}
37+
<div className="subTitle">{item.title}</div>
38+
</CardLink>
39+
40+
{listingMode === 'normal' && (
41+
<>
42+
<p className="rowDescription">
43+
<span className="rowItem">
44+
<MdAccessTime className={'rowTitleIcon'} />
45+
{format(new Date(item.latestPublishedAt))}
46+
</span>
47+
<span className="rowItem">
48+
<BiCommentDetail className={'rowTitleIcon'} />
49+
{item.replyCount || 0} comments
50+
</span>
51+
<span className="rowItem">
52+
<IoMdHand className={'rowTitleIcon'} />
53+
{item.totalReactions || 0} claps
54+
</span>
55+
</p>
56+
<p className="rowDetails">
57+
<ColoredLanguagesBadge languages={item.tag_list} />
58+
</p>
59+
</>
60+
)}
61+
</>
62+
}
63+
/>
64+
)
65+
}
66+
67+
function MediumCard({ analyticsTag, label, icon, withAds }) {
68+
const preferences = useContext(PreferencesContext)
69+
const { userSelectedTags, cardsSettings, dispatcher } = preferences
70+
const [selectedLanguage, setSelectedLanguage] = useState()
71+
const [refresh, setRefresh] = useState(true)
72+
const [cacheCardData, setCacheCardData] = useState({})
73+
74+
useEffect(() => {
75+
if (selectedLanguage) {
76+
dispatcher({
77+
type: 'setCardSettings',
78+
value: { card: label, language: selectedLanguage.label.toLowerCase() },
79+
})
80+
setRefresh(!refresh)
81+
}
82+
}, [selectedLanguage])
83+
84+
const fetchArticles = async () => {
85+
if (!selectedLanguage) {
86+
return []
87+
}
88+
if (!selectedLanguage.label) {
89+
throw Error(`Medium does not support ${selectedLanguage.label}.`)
90+
}
91+
92+
let data = []
93+
const cacheKey = `${selectedLanguage.label}`
94+
95+
// Cache found
96+
if (cacheCardData[cacheKey]) {
97+
return cacheCardData[cacheKey]
98+
}
99+
100+
if (selectedLanguage.value == MY_LANGUAGES_TAG.value) {
101+
const selectedTagsArticlesPromises = userSelectedTags.map((tag) => {
102+
if (tag.mediumValues) {
103+
if (cacheCardData[tag.label]) {
104+
return cacheCardData[tag.label]
105+
} else {
106+
return mediumApi.getArticles(tag.mediumValues[0])
107+
}
108+
}
109+
return []
110+
})
111+
112+
data = await mergeMultipleDataSources(
113+
selectedTagsArticlesPromises,
114+
MAX_MERGED_ITEMS_PER_LANGUAGE
115+
)
116+
} else {
117+
data = await mediumApi.getArticles(selectedLanguage.mediumValues[0])
118+
}
119+
120+
setCacheCardData({ ...cacheCardData, [cacheKey]: data })
121+
return data
122+
}
123+
124+
function HeaderTitle() {
125+
return (
126+
<div style={{ display: 'inline-block', margin: 0, padding: 0 }}>
127+
<span> Medium </span>
128+
<SelectableCard
129+
isLanguage={true}
130+
tagId={'MEDIUM_MENU_LANGUAGE_ID'}
131+
selectedTag={selectedLanguage}
132+
setSelectedTag={setSelectedLanguage}
133+
trackEvent={(tag) => trackCardLanguageChange('medium', tag.value)}
134+
fallbackTag={GLOBAL_TAG}
135+
cardSettings={cardsSettings?.medium?.language}
136+
data={userSelectedTags.map((tag) => ({
137+
label: tag.label,
138+
value: tag.value,
139+
}))}
140+
/>
141+
</div>
142+
)
143+
}
144+
145+
const renderItem = (item, index) => (
146+
<ArticleItem item={item} key={`md-${index}`} index={index} analyticsTag={analyticsTag} />
147+
)
148+
149+
return (
150+
<CardComponent
151+
icon={<span className="blockHeaderIcon">{icon}</span>}
152+
title={<HeaderTitle />}
153+
link="https://medium.com/">
154+
<ListComponent
155+
fetchData={fetchArticles}
156+
renderItem={renderItem}
157+
refresh={refresh}
158+
withAds={withAds}
159+
/>
160+
</CardComponent>
161+
)
162+
}
163+
164+
export default MediumCard

src/configuration/ConfigurationContext.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const ConfigurationContext = React.createContext({
99
confsValues: ['javascript'],
1010
devtoValues: ['javascript'],
1111
hashnodeValues: ['javascript'],
12+
mediumValues: ['javascript'],
1213
},
1314
],
1415
})

src/services/medium.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import cachedRequest from './cachedRequest'
2+
3+
const getArticles = async (tag) => {
4+
const url = `/data/medium/${tag}.json`
5+
const data = await cachedRequest(url)
6+
return data
7+
}
8+
9+
export default {
10+
getArticles: getArticles,
11+
}

0 commit comments

Comments
 (0)