Skip to content

Commit 98db547

Browse files
committed
add rss from settings modal
1 parent 54e39c6 commit 98db547

5 files changed

Lines changed: 145 additions & 3 deletions

File tree

src/components/Layout/MobileCards.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import React from 'react'
22
import { SUPPORTED_CARDS } from 'src/config'
3+
import { CustomRssCard } from 'src/features/cards'
34
import { SelectedCard } from 'src/types'
45

56
export const MobileCards = ({ selectedCard }: { selectedCard: SelectedCard }) => {
67
const currentCard = SUPPORTED_CARDS.find((c) => c.value === selectedCard.name)
78
return currentCard
8-
? React.createElement(currentCard.component, {
9+
? React.createElement(currentCard?.component || CustomRssCard, {
910
key: currentCard.value,
1011
meta: currentCard,
1112
withAds: true,

src/features/cards/components/customRssCard/CustomRssCard.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { BsRssFill } from 'react-icons/bs'
12
import { Card, FloatingFilter } from 'src/components/Elements'
23
import { ListComponent } from 'src/components/List'
34
import { Article, CardPropsType } from 'src/types'
@@ -20,7 +21,7 @@ export function CustomRssCard({ meta, withAds }: CardPropsType) {
2021
}
2122

2223
return (
23-
<Card card={meta} titleComponent={<HeaderTitle />}>
24+
<Card card={{ ...meta, icon: <BsRssFill /> }} titleComponent={<HeaderTitle />}>
2425
<FloatingFilter card={meta} filters={['language']} />
2526
<ListComponent items={data} isLoading={isLoading} renderItem={renderItem} withAds={withAds} />
2627
</Card>

src/features/settings/components/SettingsModal.tsx

Lines changed: 103 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ import React, { useState } from 'react'
22
import { VscClose } from 'react-icons/vsc'
33
import ReactModal from 'react-modal'
44
import Select, { ActionMeta, MultiValue, SingleValue } from 'react-select'
5+
import BeatLoader from 'react-spinners/BeatLoader'
56
import Toggle from 'react-toggle'
67
import 'react-toggle/style.css'
78
import { SUPPORTED_CARDS, SUPPORTED_SEARCH_ENGINES, supportLink } from 'src/config'
89
import { Tag, useRemoteConfigStore } from 'src/features/remoteConfig'
10+
import { getRssUrlFeed } from 'src/features/rssFeed/api/getRssFeed'
911
import {
1012
identifyUserCards,
1113
identifyUserLanguages,
@@ -16,14 +18,17 @@ import {
1618
trackLanguageAdd,
1719
trackLanguageRemove,
1820
trackListingModeSelect,
21+
trackRssSourceAdd,
22+
trackRssSourceRemove,
1923
trackSearchEngineSelect,
2024
trackSourceAdd,
2125
trackSourceRemove,
2226
trackTabTarget,
2327
trackThemeSelect,
2428
} from 'src/lib/analytics'
2529
import { useUserPreferences } from 'src/stores/preferences'
26-
import { SearchEngineType, SelectedCard } from 'src/types'
30+
import { SearchEngineType, SelectedCard, SupportedCardType } from 'src/types'
31+
import { isValidURL } from 'src/utils/UrlUtils'
2732
import './settings.css'
2833

2934
type SettingsModalProps = {
@@ -53,9 +58,14 @@ export const SettingsModal = ({ showSettings, setShowSettings }: SettingsModalPr
5358
setCards,
5459
setTags,
5560
userCustomCards,
61+
setUserCustomCards,
5662
} = useUserPreferences()
5763
const [selectedCards, setSelectedCards] = useState(cards)
5864

65+
const [rssUrl, setRssUrl] = useState('')
66+
const [rssInputError, setRssInputError] = useState('')
67+
const [isRssInputLoading, setIsRssInputLoading] = useState(false)
68+
5969
const AVAILABLE_CARDS = [...SUPPORTED_CARDS, ...userCustomCards]
6070

6171
const handleCloseModal = () => {
@@ -135,6 +145,64 @@ export const SettingsModal = ({ showSettings, setShowSettings }: SettingsModalPr
135145
identifyUserTheme(newTheme)
136146
}
137147

148+
const onRssAddClick = async () => {
149+
if (!isValidURL(rssUrl)) {
150+
setRssInputError('Invalid RSS Feed URL. Please check and try again.')
151+
return
152+
}
153+
154+
// check if card exists
155+
const exists = userCustomCards.find((card) => card.feedUrl === rssUrl)
156+
if (exists) {
157+
setRssInputError('RSS Feed already exists')
158+
return
159+
}
160+
161+
setIsRssInputLoading(true)
162+
163+
// get rssUrl Info
164+
try {
165+
const info = await getRssUrlFeed(rssUrl)
166+
let customCard: SupportedCardType = {
167+
feedUrl: rssUrl.replace('https:', 'http:'),
168+
label: info.title,
169+
value: info.title.toLowerCase(),
170+
analyticsTag: info.title.toLowerCase(),
171+
link: info.link,
172+
type: 'rss',
173+
// icon: <BsFillRssFill className="blockHeaderWhite" />,
174+
}
175+
// add card to userCustomCards and selected cards
176+
setUserCustomCards([...userCustomCards, customCard])
177+
const newCards = [
178+
...cards,
179+
{ id: cards.length, name: customCard.value, type: customCard.type },
180+
]
181+
setCards(newCards)
182+
setSelectedCards(newCards)
183+
identifyUserCards(newCards.map((card) => card.name))
184+
trackRssSourceAdd(customCard.value)
185+
setRssUrl('')
186+
} catch (err) {
187+
setRssInputError('rssInputError occured. Please check and try again.')
188+
} finally {
189+
setIsRssInputLoading(false)
190+
}
191+
}
192+
193+
const onRssSelectChange = (newCards: MultiValue<OptionType>, metas: ActionMeta<OptionType>) => {
194+
if (metas.action === 'remove-value') {
195+
setUserCustomCards(newCards as SupportedCardType[])
196+
let newSelectedCards = cards.filter(
197+
(c) => c.type !== 'rss' || c.name !== metas.removedValue.value
198+
)
199+
setSelectedCards(newSelectedCards)
200+
setCards(newSelectedCards)
201+
identifyUserCards(newSelectedCards.map((card) => card.name))
202+
trackRssSourceRemove(metas.removedValue.value)
203+
}
204+
}
205+
138206
return (
139207
<ReactModal
140208
isOpen={showSettings}
@@ -204,6 +272,40 @@ export const SettingsModal = ({ showSettings, setShowSettings }: SettingsModalPr
204272
</div>
205273
</div>
206274

275+
<div className="settingRow">
276+
<p className="settingTitle">Add Custom Source</p>
277+
<div className="settingContent">
278+
<Select
279+
menuIsOpen={false}
280+
options={[]}
281+
value={userCustomCards}
282+
onChange={onRssSelectChange}
283+
isMulti={true}
284+
isClearable={false}
285+
isSearchable={false}
286+
classNamePrefix={'hackertab'}
287+
className={'rss-sources'}
288+
/>
289+
<div className="rssUrlControl">
290+
<input
291+
className="rssUrlInput"
292+
value={rssUrl}
293+
onChange={(e) => setRssUrl(e.target.value)}
294+
/>
295+
{isRssInputLoading ? (
296+
<BeatLoader color={'#A9B2BD'} loading={isRssInputLoading} size={6} />
297+
) : (
298+
<button onClick={onRssAddClick}>ADD</button>
299+
)}
300+
</div>
301+
{rssInputError && (
302+
<div className="settingHint">
303+
<p>{rssInputError}</p>
304+
</div>
305+
)}
306+
</div>
307+
</div>
308+
207309
<div className="settingRow">
208310
<p className="settingTitle">Dark Mode</p>
209311
<div className="settingContent">

src/features/settings/components/settings.css

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,23 @@ Select styles
129129
border-radius: 20px !important;
130130
color: var(--tag-secondary-color) !important;
131131
}
132+
.rss-sources .hackertab__indicators {
133+
display: none;
134+
}
135+
136+
.rssUrlControl {
137+
margin: 6px 0;
138+
color: var(--primary-text-color);
139+
}
140+
141+
.rssUrlInput {
142+
width: 66%;
143+
margin-right: 4px;
144+
padding: 4px;
145+
background-color: var(--card-background-color) !important;
146+
border-color: var(--tag-border-color) !important;
147+
color: var(--primary-text-color)
148+
}
132149

133150
@media (max-width: 768px) {
134151
.Modal {

src/lib/analytics.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ enum Objects {
1919
CHANGE_LOG = 'Change Log',
2020
MARKETING_CAMPAIGN = 'Marketing Campaign',
2121
ONBOARDING = 'Onboarding',
22+
CUSTOM_SOURCE = 'Custom Source',
2223
}
2324

2425
enum Verbs {
@@ -301,6 +302,26 @@ export const identifyUserOccupation = (occupation: string) => {
301302
identifyUserProperty(Attributes.OCCUPATION, occupation)
302303
}
303304

305+
// Custom RSS Sources
306+
307+
export const trackRssSourceRemove = (source: string) => {
308+
trackEvent({
309+
object: Objects.CUSTOM_SOURCE,
310+
verb: Verbs.REMOVE,
311+
attributes: { [Attributes.SOURCE]: source },
312+
})
313+
}
314+
315+
export const trackRssSourceAdd = (source: string) => {
316+
trackEvent({
317+
object: Objects.CUSTOM_SOURCE,
318+
verb: Verbs.ADD,
319+
attributes: { [Attributes.SOURCE]: source },
320+
})
321+
}
322+
323+
324+
304325
// Private functions
305326
type trackEventProps = {
306327
object: Exclude<Objects, null | undefined>

0 commit comments

Comments
 (0)