diff --git a/package.json b/package.json index 5a5bb1c7..968ec7e2 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "country-emoji": "^1.5.4", "dompurify": "^2.2.7", "eslint-plugin-react": "^7.28.0", + "htmlparser2": "^8.0.1", "jsonpath": "^1.1.1", "localforage": "^1.9.0", "normalize.css": "^8.0.1", @@ -35,6 +36,7 @@ "react-spring-bottom-sheet": "^3.4.1", "react-toggle": "^4.1.1", "react-tooltip": "^4.2.21", + "rss-to-json": "^2.1.1", "styled-components": "2", "timeago.js": "^4.0.2", "type-fest": "^1.2.0", diff --git a/src/assets/App.css b/src/assets/App.css index a2758965..7e8d5c7f 100644 --- a/src/assets/App.css +++ b/src/assets/App.css @@ -272,7 +272,9 @@ a { width: 18px; height: 18px; } - +.blockHeaderIcon .rss { + color:#ee802f; +} .blockRow { scroll-snap-align: start; padding: 8px 16px; diff --git a/src/assets/variables.css b/src/assets/variables.css index 54d7c415..eae2e948 100644 --- a/src/assets/variables.css +++ b/src/assets/variables.css @@ -84,6 +84,12 @@ html.dark { --tab-neutral-button-text: #a2a7ad; --tab-positive-button-background: #0366d6; --tab-positive-button-text: white; + + --settings-input-background-color: #0d1116; + --settings-input-border-color: #3a4553; + --settings-input-border-focus-color: #6b7b90; + --settings-input-placeholder-color: #42474e; + --settings-input-text-color: #fff } html.light { @@ -160,4 +166,11 @@ html.light { --tab-neutral-button-text: #3c5065; --tab-positive-button-background: #0366d6; --tab-positive-button-text: white; + + --settings-input-background-color: #ffffff; + --settings-input-border-color: #e9ebec; + --settings-input-border-focus-color: #c4d6df; + --settings-input-placeholder-color: #97a6ad; + --settings-input-text-color: #253b53 + } diff --git a/src/components/Elements/Card/Card.tsx b/src/components/Elements/Card/Card.tsx index ecdee5d7..2c039bce 100644 --- a/src/components/Elements/Card/Card.tsx +++ b/src/components/Elements/Card/Card.tsx @@ -2,11 +2,11 @@ import React from 'react' import { BsBoxArrowInUpRight } from 'react-icons/bs' import { ref } from 'src/config' import { useUserPreferences } from 'src/stores/preferences' -import { SupportedCard } from 'src/config' +import { SupportedCardType } from 'src/types' type CardProps = { children: React.ReactNode - card: SupportedCard + card: SupportedCardType titleComponent?: React.ReactNode fullBlock?: boolean } diff --git a/src/components/Elements/CardWithActions/CardItemWithActions.tsx b/src/components/Elements/CardWithActions/CardItemWithActions.tsx index d6e521cd..23126457 100644 --- a/src/components/Elements/CardWithActions/CardItemWithActions.tsx +++ b/src/components/Elements/CardWithActions/CardItemWithActions.tsx @@ -1,8 +1,7 @@ -import React, { useState, useEffect } from 'react' +import React, { useEffect, useState } from 'react' +import { BiBookmarkMinus, BiBookmarkPlus } from 'react-icons/bi' +import { Attributes, trackLinkBookmark, trackLinkUnBookmark } from 'src/lib/analytics' import { useBookmarks } from 'src/stores/bookmarks' -import { BiBookmarkPlus } from 'react-icons/bi' -import { BiBookmarkMinus } from 'react-icons/bi' -import { trackLinkBookmark, trackLinkUnBookmark, Attributes } from 'src/lib/analytics' import { BaseEntry } from 'src/types' type CardItemWithActionsProps = { @@ -10,6 +9,7 @@ type CardItemWithActionsProps = { index: number source: string cardItem: React.ReactNode + sourceType?: 'rss' | 'supported' } export const CardItemWithActions = ({ @@ -17,6 +17,7 @@ export const CardItemWithActions = ({ item, index, source, + sourceType = 'supported', }: CardItemWithActionsProps) => { const { bookmarkPost, unbookmarkPost, userBookmarks } = useBookmarks() const [isBookmarked, setIsBookmarked] = useState( @@ -27,6 +28,7 @@ export const CardItemWithActions = ({ title: item.title, url: item.url, source, + sourceType: sourceType ?? 'rss', } if (isBookmarked) { unbookmarkPost(itemToBookmark) diff --git a/src/components/Elements/FloatingFilter/FloatingFilter.tsx b/src/components/Elements/FloatingFilter/FloatingFilter.tsx index 07cb3d97..c0811e9b 100644 --- a/src/components/Elements/FloatingFilter/FloatingFilter.tsx +++ b/src/components/Elements/FloatingFilter/FloatingFilter.tsx @@ -3,12 +3,13 @@ import { FiFilter } from 'react-icons/fi' import { BottomSheet } from 'react-spring-bottom-sheet' import 'react-spring-bottom-sheet/dist/style.css' import { ChipsSet } from 'src/components/Elements' -import { dateRanges, GLOBAL_TAG, MY_LANGUAGES_TAG, SupportedCard } from 'src/config' +import { dateRanges, GLOBAL_TAG, MY_LANGUAGES_TAG } from 'src/config' import { trackCardDateRangeSelect, trackCardLanguageSelect } from 'src/lib/analytics' import { useUserPreferences } from 'src/stores/preferences' +import { SupportedCardType } from 'src/types' type ListingFilterMobileProps = { - card: SupportedCard + card: SupportedCardType filters?: ('datesRange' | 'language')[] } diff --git a/src/components/Layout/AppContentLayout.tsx b/src/components/Layout/AppContentLayout.tsx index 6b3e5528..bea63170 100644 --- a/src/components/Layout/AppContentLayout.tsx +++ b/src/components/Layout/AppContentLayout.tsx @@ -1,22 +1,26 @@ import { useState } from 'react' -import { BottomNavigation } from '../Elements' import { isDesktop } from 'react-device-detect' import { useUserPreferences } from 'src/stores/preferences' -import { MobileCards } from './MobileCards' +import { BottomNavigation } from '../Elements' import { DesktopCards } from './DesktopCards' +import { MobileCards } from './MobileCards' export const AppContentLayout = ({ setShowSettings, }: { setShowSettings: (value: boolean | ((prevVar: boolean) => boolean)) => void }) => { - const { cards } = useUserPreferences() + const { cards, userCustomCards } = useUserPreferences() const [selectedCard, setSelectedCard] = useState(cards[0]) return ( <>
- {isDesktop ? : } + {isDesktop ? ( + + ) : ( + + )}
{ +export const DesktopCards = ({ + cards, + userCustomCards, +}: { + cards: SelectedCard[] + userCustomCards: SupportedCardType[] +}) => { + const AVAILABLE_CARDS = [...SUPPORTED_CARDS, ...userCustomCards] return ( <> {cards.map((card, index) => { - const constantCard = SUPPORTED_CARDS.find((c) => c.value === card.name) + const constantCard = AVAILABLE_CARDS.find((c) => c.value === card.name) if (!constantCard) { return null } - return React.createElement(constantCard.component, { + return React.createElement(constantCard?.component || CustomRssCard, { key: card.name, meta: constantCard, withAds: index === 0, diff --git a/src/components/Layout/Header.tsx b/src/components/Layout/Header.tsx index 61c6fec0..d0cbf1a3 100644 --- a/src/components/Layout/Header.tsx +++ b/src/components/Layout/Header.tsx @@ -1,17 +1,15 @@ -import React, { useEffect, useRef, useState } from 'react' -import { BsFillGearFill } from 'react-icons/bs' +import { useEffect, useRef, useState } from 'react' +import { BsFillBookmarksFill, BsFillGearFill, BsMoon } from 'react-icons/bs' import { CgTab } from 'react-icons/cg' -import { BsFillBookmarksFill } from 'react-icons/bs' +import { IoMdSunny } from 'react-icons/io' import { ReactComponent as HackertabLogo } from 'src/assets/logo.svg' +import { SearchBar } from 'src/components/Elements/SearchBar' import { UserTags } from 'src/components/Elements/UserTags' -import { SettingsModal } from 'src/features/settings' -import { BsMoon } from 'react-icons/bs' -import { IoMdSunny } from 'react-icons/io' import { Changelog } from 'src/features/changelog' -import { SearchBar } from 'src/components/Elements/SearchBar' -import { useUserPreferences } from 'src/stores/preferences' +import { SettingsModal } from 'src/features/settings' +import { identifyUserTheme, trackThemeSelect } from 'src/lib/analytics' import { useBookmarks } from 'src/stores/bookmarks' -import { trackThemeSelect, identifyUserTheme } from 'src/lib/analytics' +import { useUserPreferences } from 'src/stores/preferences' type HeaderProps = { showSideBar: boolean diff --git a/src/components/Layout/MobileCards.tsx b/src/components/Layout/MobileCards.tsx index aaf01d16..62cdec3e 100644 --- a/src/components/Layout/MobileCards.tsx +++ b/src/components/Layout/MobileCards.tsx @@ -1,11 +1,12 @@ import React from 'react' import { SUPPORTED_CARDS } from 'src/config' +import { CustomRssCard } from 'src/features/cards' import { SelectedCard } from 'src/types' export const MobileCards = ({ selectedCard }: { selectedCard: SelectedCard }) => { const currentCard = SUPPORTED_CARDS.find((c) => c.value === selectedCard.name) return currentCard - ? React.createElement(currentCard.component, { + ? React.createElement(currentCard?.component || CustomRssCard, { key: currentCard.value, meta: currentCard, withAds: true, diff --git a/src/config/index.tsx b/src/config/index.tsx index 24dfaf5b..9929718b 100644 --- a/src/config/index.tsx +++ b/src/config/index.tsx @@ -1,4 +1,3 @@ -import React from 'react' import { CgIndieHackers } from 'react-icons/cg' import { FaDev, FaFreeCodeCamp, FaMediumM, FaReddit } from 'react-icons/fa' import { HiTicket } from 'react-icons/hi' @@ -18,7 +17,7 @@ import { ProductHuntCard, RedditCard, } from 'src/features/cards' -import { CardPropsType } from 'src/types' +import { SupportedCardType } from 'src/types' // Keys export const ANALYTICS_ENDPOINT = process.env.REACT_APP_AMPLITUDE_URL as string @@ -67,16 +66,7 @@ export const SUPPORTED_SEARCH_ENGINES = [ url: 'https://www.startpage.com/sp/search?query=', }, ] - -export type SupportedCard = { - value: string - icon: React.ReactNode - analyticsTag: string - label: string - link: string - component: React.FunctionComponent -} -export const SUPPORTED_CARDS: SupportedCard[] = [ +export const SUPPORTED_CARDS: SupportedCardType[] = [ { value: 'github', analyticsTag: 'github', @@ -84,6 +74,7 @@ export const SUPPORTED_CARDS: SupportedCard[] = [ component: GithubCard, icon: , link: 'https://github.com/', + type: 'supported', }, { value: 'hackernews', @@ -92,6 +83,7 @@ export const SUPPORTED_CARDS: SupportedCard[] = [ label: 'Hackernews', component: HackernewsCard, link: 'https://news.ycombinator.com/', + type: 'supported', }, { value: 'conferences', @@ -100,6 +92,7 @@ export const SUPPORTED_CARDS: SupportedCard[] = [ label: 'Upcoming events', component: ConferencesCard, link: 'https://confs.tech/', + type: 'supported', }, { value: 'devto', @@ -108,6 +101,7 @@ export const SUPPORTED_CARDS: SupportedCard[] = [ label: 'DevTo', component: DevtoCard, link: 'https://dev.to/', + type: 'supported', }, { value: 'producthunt', @@ -116,6 +110,7 @@ export const SUPPORTED_CARDS: SupportedCard[] = [ label: 'Product Hunt', component: ProductHuntCard, link: 'https://producthunt.com/', + type: 'supported', }, { value: 'reddit', @@ -124,6 +119,7 @@ export const SUPPORTED_CARDS: SupportedCard[] = [ label: 'Reddit', component: RedditCard, link: 'https://reddit.com/', + type: 'supported', }, { value: 'lobsters', @@ -132,6 +128,7 @@ export const SUPPORTED_CARDS: SupportedCard[] = [ label: 'Lobsters', component: LobstersCard, link: 'https://lobste.rs/', + type: 'supported', }, { value: 'hashnode', @@ -140,6 +137,7 @@ export const SUPPORTED_CARDS: SupportedCard[] = [ label: 'Hashnode', component: HashnodeCard, link: 'https://hashnode.com/', + type: 'supported', }, { value: 'freecodecamp', @@ -148,6 +146,7 @@ export const SUPPORTED_CARDS: SupportedCard[] = [ label: 'FreeCodeCamp', component: FreecodecampCard, link: 'https://freecodecamp.com/news', + type: 'supported', }, { value: 'indiehackers', @@ -156,6 +155,7 @@ export const SUPPORTED_CARDS: SupportedCard[] = [ label: 'IndieHackers', component: IndiehackersCard, link: 'https://indiehackers.com/', + type: 'supported', }, { value: 'medium', @@ -164,6 +164,7 @@ export const SUPPORTED_CARDS: SupportedCard[] = [ label: 'Medium', component: MediumCard, link: 'https://medium.com/', + type: 'supported', }, ] diff --git a/src/features/MarketingBanner/components/MarketingBanner.tsx b/src/features/MarketingBanner/components/MarketingBanner.tsx index 7d98bbbb..902ff258 100644 --- a/src/features/MarketingBanner/components/MarketingBanner.tsx +++ b/src/features/MarketingBanner/components/MarketingBanner.tsx @@ -1,19 +1,19 @@ import DOMPurify from 'dompurify' -import { useMarketingConfigStore } from '../stores/marketingBanner' -import { useUserPreferences } from 'src/stores/preferences' -import { getAppVersion } from 'src/utils/Os' -import { isWebOrExtensionVersion, isProduction, getBrowserName } from 'src/utils/Environment' -import { useMemo, useState, useEffect } from 'react' -import { Campaign, MarketingConfig } from '../types' -import { useGetMarketingConfig } from '../api/getMarketingConfig' +import jsonPath from 'jsonpath' +import { useEffect, useMemo, useState } from 'react' +import { isMobile } from 'react-device-detect' import { trackMarketingCampaignClose, - trackMarketingCampaignView, trackMarketingCampaignOpen, + trackMarketingCampaignView, } from 'src/lib/analytics' +import { useUserPreferences } from 'src/stores/preferences' import { diffBetweenTwoDatesInDays } from 'src/utils/DateUtils' -import { isMobile } from 'react-device-detect' -import jsonPath from 'jsonpath' +import { getBrowserName, isProduction, isWebOrExtensionVersion } from 'src/utils/Environment' +import { getAppVersion } from 'src/utils/Os' +import { useGetMarketingConfig } from '../api/getMarketingConfig' +import { useMarketingConfigStore } from '../stores/marketingBanner' +import { Campaign, MarketingConfig } from '../types' export const MarketingBanner = () => { const { setCampaignClosed, closedCampaigns } = useMarketingConfigStore() @@ -79,7 +79,6 @@ export const MarketingBanner = () => { return availableCampaigns } catch (e) { - console.log('getAvailableCampaigns', e) return [] } } diff --git a/src/features/MarketingBanner/stores/marketingBanner.ts b/src/features/MarketingBanner/stores/marketingBanner.ts index 98de65ce..6192f3f2 100644 --- a/src/features/MarketingBanner/stores/marketingBanner.ts +++ b/src/features/MarketingBanner/stores/marketingBanner.ts @@ -1,5 +1,6 @@ -import create from 'zustand'; -import { persist } from 'zustand/middleware' +import { create } from 'zustand'; + +import { persist } from 'zustand/middleware'; type ClosedCampaign = { id: string; diff --git a/src/features/ads/api/getAd.ts b/src/features/ads/api/getAd.ts index 4893858a..78e58009 100644 --- a/src/features/ads/api/getAd.ts +++ b/src/features/ads/api/getAd.ts @@ -1,24 +1,33 @@ import { useQuery } from '@tanstack/react-query' +import { axios } from 'src/lib/axios' import { ExtractFnReturnType, QueryConfig } from 'src/lib/react-query' import { Ad } from '../types' -import { axios } from 'src/lib/axios' -const getAd = async (keywords: string[]): Promise => { - let url = new URL(window.location.href); - let ref = url.searchParams.get("ref"); - return axios.get('/engine/ads/', {params: { ref, keywords: keywords.join(",") }}) + + + +const getAd = async ( + keywords: string[], + aditionalAdQueries: { [key: string]: string } | undefined +): Promise => { + let params = { keywords: keywords.join(',') } + if (aditionalAdQueries) { + params = { ...params, ...aditionalAdQueries } + } + return axios.get('/engine/ads/', { params }) } type QueryFnType = typeof getAd type UseGetAdOptions = { - keywords: string[]; + keywords: string[] config?: QueryConfig + aditionalAdQueries: { [key: string]: string } | undefined } -export const useGetAd = ({ keywords, config }: UseGetAdOptions) => { +export const useGetAd = ({ keywords, config, aditionalAdQueries }: UseGetAdOptions) => { return useQuery>({ ...config, queryKey: ['ad'], - queryFn: () => getAd(keywords), + queryFn: () => getAd(keywords, aditionalAdQueries), }) } diff --git a/src/features/ads/components/BannerAd.tsx b/src/features/ads/components/BannerAd.tsx index 4b3ce8bb..dbdf4765 100644 --- a/src/features/ads/components/BannerAd.tsx +++ b/src/features/ads/components/BannerAd.tsx @@ -1,19 +1,32 @@ -import './BannerAd.css' -import { useGetAd } from '../api/getAd' -import { useUserPreferences } from 'src/stores/preferences' +import { useState } from 'react' import { AdPlaceholder } from 'src/components/placeholders' +import { useUserPreferences } from 'src/stores/preferences' +import { useGetAd } from '../api/getAd' +import './BannerAd.css' export const BannerAd = () => { const { userSelectedTags } = useUserPreferences() + + const [aditionalAdQueries, setAditionalAdQueries] = useState< + { [key: string]: string } | undefined + >() const { data: ad, isLoading, isError, } = useGetAd({ - keywords: userSelectedTags.map(tag => tag.label.toLocaleLowerCase()), + keywords: userSelectedTags.map((tag) => tag.label.toLocaleLowerCase()), + aditionalAdQueries: aditionalAdQueries, config: { cacheTime: 0, staleTime: 0, + refetchInterval(data) { + if (data?.nextAd) { + setAditionalAdQueries(data.nextAd.queries) + return data.nextAd.interval + } + return false + }, }, }) @@ -59,9 +72,7 @@ export const BannerAd = () => { - {ad.viewUrl && ( - - )} + {ad.viewUrl && } ) } diff --git a/src/features/ads/types/index.ts b/src/features/ads/types/index.ts index 0ea6c151..66e3b3e2 100644 --- a/src/features/ads/types/index.ts +++ b/src/features/ads/types/index.ts @@ -4,12 +4,18 @@ type AdProvider = { link?: string, } +type NextAdType = { + queries: { [key: string]: string } + interval: number +} + export type Ad = { - title?: string, - description: string, - imageUrl: string, - viewUrl?: string, - link: string, - backgroundColor?: string, - provider: AdProvider, + title?: string + description: string + imageUrl: string + viewUrl?: string + link: string + backgroundColor?: string + provider: AdProvider + nextAd?: NextAdType } \ No newline at end of file diff --git a/src/features/bookmarks/components/BookmarksSidebar.tsx b/src/features/bookmarks/components/BookmarksSidebar.tsx index 62357269..15139cff 100644 --- a/src/features/bookmarks/components/BookmarksSidebar.tsx +++ b/src/features/bookmarks/components/BookmarksSidebar.tsx @@ -14,14 +14,11 @@ import 'react-pro-sidebar/dist/css/styles.css' import { CardLink } from 'src/components/Elements' import { Attributes, trackLinkUnBookmark } from 'src/lib/analytics' import { useBookmarks } from 'src/stores/bookmarks' +import { BookmarkedPost } from '../types' import './Sidebar.css' type BookmarkItemProps = { - item: { - url: string - title: string - source: string - } + item: BookmarkedPost appendRef?: boolean } const BookmarkItem = ({ item, appendRef = false }: BookmarkItemProps) => { @@ -71,7 +68,7 @@ export const BookmarksSidebar = ({ showSidebar, onClose }: BookmarksSidebarTtype 'freecodecamp', 'medium', 'indiehackers', - ].indexOf(bm.source) !== -1 + ].indexOf(bm.source) !== -1 || bm.sourceType === 'rss' ) const conferencesBookmarks = userBookmarks.filter((bm) => bm.source === 'conferences') const productsBookmarks = userBookmarks.filter((bm) => bm.source === 'producthunt') diff --git a/src/features/bookmarks/types/index.ts b/src/features/bookmarks/types/index.ts index 5b6a0c93..a3a1ef88 100644 --- a/src/features/bookmarks/types/index.ts +++ b/src/features/bookmarks/types/index.ts @@ -1,5 +1,6 @@ export type BookmarkedPost = { - title: string, - source: string, - url: string; + title: string + source: string + url: string + sourceType: 'rss' | 'supported' } \ No newline at end of file diff --git a/src/features/cards/api/getRssFeed.ts b/src/features/cards/api/getRssFeed.ts new file mode 100644 index 00000000..c667ed0a --- /dev/null +++ b/src/features/cards/api/getRssFeed.ts @@ -0,0 +1,47 @@ +import { useQuery } from '@tanstack/react-query' +import * as htmlparser2 from 'htmlparser2' +import { axios } from 'src/lib/axios' +import { ExtractFnReturnType, QueryConfig } from 'src/lib/react-query' +import { Article } from 'src/types' + +type RssInfoType = { + title: string + link: string + icon?: string +} + +export const getRssUrlFeed = async (rssUrl: string): Promise => { + return axios.get('/engine/rss_info/', { params: { url: rssUrl } }) +} + +const getArticles = async (feedUrl: string): Promise => { + const res: string = await axios.get(`/engine/remote_feed?feedUrl=${feedUrl}`) + try { + const feed = htmlparser2.parseFeed(res) + return feed?.items?.map((item) => { + return { + id: item.id, + title: item.title, + url: item.link, + published_at: +(item.pubDate || new Date()), + source: 'customFeed', + } + }) as Article[] + } catch (err) {} + return [] +} + +type QueryFnType = typeof getArticles + +type UseGetArticlesOptions = { + config?: QueryConfig + feedUrl: string +} + +export const useRssFeed = ({ feedUrl, config }: UseGetArticlesOptions) => { + return useQuery>({ + ...config, + queryKey: [feedUrl], + queryFn: () => getArticles(feedUrl), + }) +} diff --git a/src/features/cards/components/freecodecampCard/index.ts b/src/features/cards/components/freecodecampCard/index.ts index 5859d263..6148fd24 100644 --- a/src/features/cards/components/freecodecampCard/index.ts +++ b/src/features/cards/components/freecodecampCard/index.ts @@ -1 +1 @@ -export * from "./FreecodecampCard"; \ No newline at end of file +export * from './FreecodecampCard' diff --git a/src/features/cards/components/rssCard/ArticleItem.tsx b/src/features/cards/components/rssCard/ArticleItem.tsx new file mode 100644 index 00000000..9eb6361f --- /dev/null +++ b/src/features/cards/components/rssCard/ArticleItem.tsx @@ -0,0 +1,49 @@ +import { MdAccessTime } from 'react-icons/md' +import { CardItemWithActions, CardLink } from 'src/components/Elements' +import { Attributes } from 'src/lib/analytics' +import { Article, BaseItemPropsType } from 'src/types' +import { format } from 'timeago.js' + +const ArticleItem = (props: BaseItemPropsType
) => { + const { item, index, selectedTag, analyticsTag } = props + if (!item) { + return null + } + return ( + + +
{item.title}
+
+ <> +

+ + + {format(new Date(item.published_at))} + +

+ {/*

+ +

*/} + + + } + /> + ) +} + +export default ArticleItem diff --git a/src/features/cards/components/rssCard/CardIcon.tsx b/src/features/cards/components/rssCard/CardIcon.tsx new file mode 100644 index 00000000..af3db00b --- /dev/null +++ b/src/features/cards/components/rssCard/CardIcon.tsx @@ -0,0 +1,8 @@ +import { BsRssFill } from 'react-icons/bs' + +const CardIcon = (props: { url?: string }) => { + const { url } = props + return url ? : +} + +export default CardIcon diff --git a/src/features/cards/components/rssCard/CustomRssCard.tsx b/src/features/cards/components/rssCard/CustomRssCard.tsx new file mode 100644 index 00000000..6033b7d7 --- /dev/null +++ b/src/features/cards/components/rssCard/CustomRssCard.tsx @@ -0,0 +1,30 @@ +import { Card } from 'src/components/Elements' +import { ListComponent } from 'src/components/List' +import { Article, CardPropsType } from 'src/types' +import { useRssFeed } from '../../api/getRssFeed' +import ArticleItem from './ArticleItem' +import CardIcon from './CardIcon' + +export function CustomRssCard({ meta, withAds }: CardPropsType) { + const { data = [], isLoading } = useRssFeed({ feedUrl: meta.feedUrl || '' }) + + const renderItem = (item: Article, index: number) => ( + + ) + + const HeaderTitle = () => { + return ( +
+ {meta.label} +
+ ) + } + + return ( + }} + titleComponent={}> + + + ) +} diff --git a/src/features/cards/components/rssCard/index.ts b/src/features/cards/components/rssCard/index.ts new file mode 100644 index 00000000..28cbedbd --- /dev/null +++ b/src/features/cards/components/rssCard/index.ts @@ -0,0 +1 @@ +export * from './CustomRssCard' diff --git a/src/features/cards/index.ts b/src/features/cards/index.ts index a0e9b13d..3cd8dd2e 100644 --- a/src/features/cards/index.ts +++ b/src/features/cards/index.ts @@ -1,11 +1,14 @@ +export * from "./api/getRssFeed" export * from "./components/conferencesCard" -export * from "./components/devtoCard" -export * from "./components/freecodecampCard" -export * from "./components/githubCard" -export * from "./components/hackernewsCard" -export * from "./components/hashnodeCard" -export * from "./components/indiehackersCard" -export * from "./components/lobstersCard" -export * from "./components/mediumCard" -export * from "./components/producthuntCard" -export * from "./components/redditCard" \ No newline at end of file +export * from './components/devtoCard' +export * from './components/freecodecampCard' +export * from './components/githubCard' +export * from './components/hackernewsCard' +export * from './components/hashnodeCard' +export * from './components/indiehackersCard' +export * from './components/lobstersCard' +export * from './components/mediumCard' +export * from './components/producthuntCard' +export * from './components/redditCard' +export * from './components/rssCard' + diff --git a/src/features/changelog/stores/changelog.ts b/src/features/changelog/stores/changelog.ts index b4462a03..73974655 100644 --- a/src/features/changelog/stores/changelog.ts +++ b/src/features/changelog/stores/changelog.ts @@ -1,5 +1,6 @@ -import create from 'zustand'; -import { persist } from 'zustand/middleware' +import { create } from 'zustand'; + +import { persist } from 'zustand/middleware'; type ChangelogVersionStore = { lastReadVersion: string | undefined | null; diff --git a/src/features/onboarding/components/OnboardingModal.tsx b/src/features/onboarding/components/OnboardingModal.tsx index 7e90c749..7c5aabbc 100644 --- a/src/features/onboarding/components/OnboardingModal.tsx +++ b/src/features/onboarding/components/OnboardingModal.tsx @@ -12,6 +12,7 @@ import { trackOnboardingStart, } from 'src/lib/analytics' import { useUserPreferences } from 'src/stores/preferences' +import { SelectedCard } from 'src/types' import { HelloTab } from './steps/HelloTab' import { LanguagesTab } from './steps/LanguagesTab' import { SourcesTab } from './steps/SourcesTab' @@ -72,16 +73,16 @@ export const OnboardingModal = ({ showOnboarding, setShowOnboarding }: Onboardin setTags(tags) identifyUserLanguages(tags.map((tag) => tag.value)) - const cards = - occupation.sources - .map((source) => SUPPORTED_CARDS.find((sc) => sc.value === source)) - .filter(Boolean) - .map((source, index) => { - return { - id: index, - name: source?.value || '', - } - }) || [] + const cards = (occupation.sources + .map((source) => SUPPORTED_CARDS.find((sc) => sc.value === source)) + .filter(Boolean) + .map((source, index) => { + return { + id: index, + name: source?.value || '', + type: 'supported', + } + }) || []) as SelectedCard[] setCards(cards) identifyUserCards(cards.map((card) => card.name)) diff --git a/src/features/remoteConfig/stores/remoteConfig.ts b/src/features/remoteConfig/stores/remoteConfig.ts index 835fad0d..ddf4594f 100644 --- a/src/features/remoteConfig/stores/remoteConfig.ts +++ b/src/features/remoteConfig/stores/remoteConfig.ts @@ -1,5 +1,6 @@ -import create from 'zustand'; -import { persist } from 'zustand/middleware' +import { create } from 'zustand'; + +import { persist } from 'zustand/middleware'; import { RemoteConfig, Tag } from "../types"; type RemoteConfigStore = { diff --git a/src/features/settings/components/RssSetting.tsx b/src/features/settings/components/RssSetting.tsx new file mode 100644 index 00000000..759f600b --- /dev/null +++ b/src/features/settings/components/RssSetting.tsx @@ -0,0 +1,104 @@ +import { useState } from 'react' +import BeatLoader from 'react-spinners/BeatLoader' +import { getRssUrlFeed } from 'src/features/cards' +import { identifyUserCards, trackRssSourceAdd } from 'src/lib/analytics' +import { SelectedCard } from 'src/types' +import { isValidURL } from 'src/utils/UrlUtils' + +import { BsRssFill } from 'react-icons/bs' +import { useUserPreferences } from 'src/stores/preferences' +import { SupportedCardType } from 'src/types' + +type RssSettingProps = { + setSelectedCards: (cards: SelectedCard[]) => void +} + +export const RssSetting = ({ setSelectedCards }: RssSettingProps) => { + const { cards, setCards, userCustomCards, setUserCustomCards } = useUserPreferences() + + const [rssUrl, setRssUrl] = useState() + const [rssInputError, setRssInputError] = useState() + const [isRssInputLoading, setIsRssInputLoading] = useState(false) + + const onRssAddClick = async () => { + if (!rssUrl) { + setRssInputError('Please provide an RSS Feed URL') + return + } + + if (!isValidURL(rssUrl)) { + setRssInputError('Invalid RSS Feed URL. Please check and try again') + return + } + + setIsRssInputLoading(true) + + try { + const info = await getRssUrlFeed(rssUrl) + let value = info.title.toLowerCase() + + if (userCustomCards.some((card) => card.link === info.link)) { + throw Error('RSS Feed already exists', { cause: 'EXISTS' }) + } + + let customCard: SupportedCardType = { + feedUrl: rssUrl.replace('https:', 'http:'), + label: info.title, + value, + analyticsTag: value, + link: info.link, + type: 'rss', + icon: info.icon, + } + setUserCustomCards([...userCustomCards, customCard]) + const newCards = [ + ...cards, + { id: cards.length, name: customCard.value, type: customCard.type }, + ] + setCards(newCards) + setSelectedCards(newCards) + identifyUserCards(newCards.map((card) => card.name)) + trackRssSourceAdd(customCard.value) + setRssUrl('') + } catch (_) { + setRssInputError('Error occured. Please check and try again.') + } finally { + setIsRssInputLoading(false) + } + } + + return ( +
+

+ Add New Source +

+ (RSS Feed) +
+

+
+
+ setRssUrl(e.target.value)} + placeholder="https://url.com/rss/feed" + /> + {isRssInputLoading ? ( + + ) : ( +
+ +
+ )} +
+ {rssInputError && ( +
+

{rssInputError}

+
+ )} +
+
+ ) +} diff --git a/src/features/settings/components/SettingsModal.tsx b/src/features/settings/components/SettingsModal.tsx index d8d476eb..bdc49ad2 100644 --- a/src/features/settings/components/SettingsModal.tsx +++ b/src/features/settings/components/SettingsModal.tsx @@ -1,31 +1,31 @@ import React, { useState } from 'react' -import ReactModal from 'react-modal' -import 'react-toggle/style.css' import { VscClose } from 'react-icons/vsc' +import ReactModal from 'react-modal' import Select, { ActionMeta, MultiValue, SingleValue } from 'react-select' -import { SearchEngineType } from 'src/types' import Toggle from 'react-toggle' -import './settings.css' -import { useUserPreferences } from 'src/stores/preferences' +import 'react-toggle/style.css' import { SUPPORTED_CARDS, SUPPORTED_SEARCH_ENGINES, supportLink } from 'src/config' +import { Tag, useRemoteConfigStore } from 'src/features/remoteConfig' import { + identifyUserCards, + identifyUserLanguages, + identifyUserLinksInNewTab, + identifyUserListingMode, + identifyUserSearchEngine, + identifyUserTheme, trackLanguageAdd, trackLanguageRemove, + trackListingModeSelect, + trackSearchEngineSelect, trackSourceAdd, trackSourceRemove, - trackSearchEngineSelect, - trackListingModeSelect, trackTabTarget, trackThemeSelect, - identifyUserTheme, - identifyUserLinksInNewTab, - identifyUserSearchEngine, - identifyUserCards, - identifyUserListingMode, - identifyUserLanguages, } from 'src/lib/analytics' -import { useRemoteConfigStore } from 'src/features/remoteConfig' -import { Tag } from 'src/features/remoteConfig' +import { useUserPreferences } from 'src/stores/preferences' +import { SearchEngineType, SelectedCard } from 'src/types' +import { RssSetting } from './RssSetting' +import './settings.css' type SettingsModalProps = { showSettings: boolean @@ -53,9 +53,13 @@ export const SettingsModal = ({ showSettings, setShowSettings }: SettingsModalPr setOpenLinksNewTab, setCards, setTags, + userCustomCards, + setUserCustomCards, } = useUserPreferences() const [selectedCards, setSelectedCards] = useState(cards) + const AVAILABLE_CARDS = [...SUPPORTED_CARDS, ...userCustomCards] + const handleCloseModal = () => { setShowSettings(false) } @@ -93,6 +97,11 @@ export const SettingsModal = ({ showSettings, setShowSettings }: SettingsModalPr } break case 'remove-value': + // if removed card is a userCustomCard, remove it + const newUserCustomCards = userCustomCards.filter( + (c) => c.value !== metas.removedValue.value + ) + setUserCustomCards(newUserCustomCards) if (metas.removedValue?.label) { trackSourceRemove(metas.removedValue.label) } @@ -100,8 +109,11 @@ export const SettingsModal = ({ showSettings, setShowSettings }: SettingsModalPr } let newCards = cards.map((c, index) => { - return { id: index, name: c.value } - }) + // Re-Check + let type = AVAILABLE_CARDS.find((ac) => ac.value === c.value)?.type + return { id: index, name: c.value, type } + }) as SelectedCard[] + identifyUserCards(newCards.map((card) => card.name)) setSelectedCards(newCards) setCards(newCards) @@ -180,9 +192,9 @@ export const SettingsModal = ({ showSettings, setShowSettings }: SettingsModalPr

Displayed Cards