Skip to content

Commit cd269f4

Browse files
committed
refactor the data source logic and add better filters
1 parent ae2d11c commit cd269f4

28 files changed

Lines changed: 765 additions & 487 deletions

File tree

src/components/Elements/Card/Card.tsx

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,30 @@
11
import clsx from 'clsx'
22
import React, { useEffect, useState } from 'react'
3-
import { BsBoxArrowInUpRight } from 'react-icons/bs'
4-
import { ref } from 'src/config'
53
import { AdvBanner } from 'src/features/adv'
6-
import { useRemoteConfigStore } from 'src/features/remoteConfig'
7-
import { useUserPreferences } from 'src/stores/preferences'
84
import { CardPropsType } from 'src/types'
95

106
type RootCardProps = CardPropsType & {
117
children: React.ReactNode
128
titleComponent?: React.ReactNode
9+
settingsComponent?: React.ReactNode
1310
fullBlock?: boolean
1411
}
1512

1613
export const Card = ({
1714
meta,
1815
titleComponent,
16+
settingsComponent,
1917
className,
2018
withAds = false,
2119
children,
2220
fullBlock = false,
2321
knob,
2422
}: RootCardProps) => {
25-
const { openLinksNewTab } = useUserPreferences()
26-
const { link, icon, label, badge } = meta
23+
const { icon, label, badge } = meta
2724
const [canAdsLoad, setCanAdsLoad] = useState(true)
28-
const { adsConfig } = useRemoteConfigStore()
2925

3026
useEffect(() => {
31-
if (!adsConfig.enabled || !withAds) {
27+
if (!withAds) {
3228
return
3329
}
3430

@@ -46,28 +42,20 @@ export const Card = ({
4642
return () => {
4743
observer.disconnect()
4844
}
49-
}, [withAds, adsConfig.enabled])
50-
51-
const handleHeaderLinkClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
52-
e.preventDefault()
53-
let url = `${link}?${ref}`
54-
window.open(url, openLinksNewTab ? '_blank' : '_self')
55-
}
45+
}, [withAds])
5646

5747
return (
5848
<div className={clsx('block', fullBlock && 'fullBlock', className)}>
5949
<div className="blockHeader">
6050
{knob}
6151
<span className="blockHeaderIcon">{icon}</span> {titleComponent || label}{' '}
62-
{link && (
63-
<a className="blockHeaderLink" href={link} onClick={handleHeaderLinkClick}>
64-
<BsBoxArrowInUpRight />
65-
</a>
52+
{settingsComponent && (
53+
<span className="blockHeaderSettingsButton">{settingsComponent}</span>
6654
)}
6755
{badge && <span className="blockHeaderBadge">{badge}</span>}
6856
</div>
6957

70-
{canAdsLoad && adsConfig.enabled && withAds && (
58+
{canAdsLoad && withAds && (
7159
<div className="ad-wrapper blockRow">
7260
<AdvBanner />
7361
</div>

src/components/Layout/DesktopCards.tsx

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -131,14 +131,7 @@ export const DesktopCards = ({
131131
items={memoCards.map(({ id }) => id)}
132132
strategy={horizontalListSortingStrategy}>
133133
{memoCards.map(({ id, card }, index) => {
134-
return (
135-
<SortableItem
136-
key={id}
137-
id={id}
138-
card={card}
139-
withAds={index === adsConfig.columnPosition}
140-
/>
141-
)
134+
return <SortableItem key={id} id={id} card={card} withAds={index === 0} />
142135
})}
143136
</SortableContext>
144137
</DndContext>

src/config/index.tsx

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,6 @@ export const twitterHandle = '@hackertabdev'
2323
export const reportLink = 'https://www.hackertab.dev/report'
2424

2525
export const LS_PREFERENCES_KEY = 'hackerTabPrefs'
26-
export const GLOBAL_TAG = {
27-
value: '',
28-
label: 'Trending',
29-
}
30-
3126
export const MAX_ITEMS_PER_CARD = 50
3227

3328
export type DateRangeType = {
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import { Menu, MenuDivider, MenuItem, SubMenu } from '@szhsin/react-menu'
2+
import { useCallback, useMemo } from 'react'
3+
import { AiOutlineCode } from 'react-icons/ai'
4+
import { BsBoxArrowInUpRight } from 'react-icons/bs'
5+
import { GoGear } from 'react-icons/go'
6+
import { IoTrashBinOutline } from 'react-icons/io5'
7+
import { LiaSortSolid } from 'react-icons/lia'
8+
import { MdDateRange } from 'react-icons/md'
9+
import { ref } from 'src/config'
10+
import { useUserPreferences } from 'src/stores/preferences'
11+
12+
type SortOption = { label: string; value: string; icon?: React.ReactNode }
13+
14+
type CardSettingsProps = {
15+
url?: string
16+
id: string
17+
sortBy?: string
18+
globalTag?: { label: string; value: string }
19+
language?: string
20+
showLanguageFilter?: boolean
21+
showDateRangeFilter?: boolean
22+
customStartMenuItems?: React.ReactNode
23+
sortOptions?: ((defaults: SortOption[]) => SortOption[]) | SortOption[]
24+
}
25+
26+
const DEFAULT_SORT_OPTIONS = [{ label: 'Newest', value: 'published_at', icon: <MdDateRange /> }]
27+
28+
export const CardSettings = ({
29+
id,
30+
url,
31+
sortBy,
32+
globalTag,
33+
language,
34+
showLanguageFilter = true,
35+
showDateRangeFilter = true,
36+
customStartMenuItems,
37+
sortOptions,
38+
}: CardSettingsProps) => {
39+
const userSelectedTags = useUserPreferences((state) => state.userSelectedTags)
40+
const openLinksNewTab = useUserPreferences((state) => state.openLinksNewTab)
41+
const removeCard = useUserPreferences((state) => state.removeCard)
42+
const setCardSettings = useUserPreferences((state) => state.setCardSettings)
43+
const cardSettings = useUserPreferences((state) => state.cardsSettings[id])
44+
45+
const userTagsMemo = useMemo(() => {
46+
const newTags = userSelectedTags.sort((a, b) => a.label.localeCompare(b.label))
47+
if (globalTag) {
48+
return [globalTag, ...newTags]
49+
}
50+
return newTags
51+
}, [userSelectedTags, globalTag])
52+
53+
const resolvedSortOptions =
54+
typeof sortOptions === 'function'
55+
? sortOptions(DEFAULT_SORT_OPTIONS)
56+
: sortOptions || DEFAULT_SORT_OPTIONS
57+
58+
const onOpenSourceUrlClicked = useCallback(() => {
59+
let link = `${url}?${ref}`
60+
window.open(link, openLinksNewTab ? '_blank' : '_self')
61+
}, [url, openLinksNewTab])
62+
63+
return (
64+
<Menu
65+
menuButton={<GoGear />}
66+
theming="dark"
67+
portal={true}
68+
className={`menuItem`}
69+
transition
70+
direction={'bottom'}
71+
align="start">
72+
{customStartMenuItems}
73+
{showLanguageFilter && userTagsMemo.length > 0 && (
74+
<SubMenu
75+
label={
76+
<span className={`menuItem`}>
77+
<AiOutlineCode /> Language
78+
</span>
79+
}>
80+
{userTagsMemo.map((tag) => (
81+
<MenuItem
82+
className={`menuItem`}
83+
type="radio"
84+
value={tag.value}
85+
key={tag.value}
86+
disabled={language === tag.value}
87+
onClick={() => {
88+
setCardSettings(id, { ...cardSettings, language: tag.value })
89+
}}>
90+
{tag.label}
91+
</MenuItem>
92+
))}
93+
</SubMenu>
94+
)}
95+
96+
{showDateRangeFilter && (
97+
<SubMenu
98+
className="subMenuItem"
99+
label={
100+
<span className={`menuItem`}>
101+
<LiaSortSolid />
102+
Sort by
103+
</span>
104+
}>
105+
{resolvedSortOptions.map((option) => (
106+
<MenuItem
107+
className={`menuItem`}
108+
key={option.value}
109+
disabled={sortBy === option.value}
110+
onClick={() => {
111+
setCardSettings(id, { ...cardSettings, sortBy: option.value })
112+
}}>
113+
{option.icon} {option.label}
114+
</MenuItem>
115+
))}
116+
</SubMenu>
117+
)}
118+
{(showDateRangeFilter || showLanguageFilter) && <MenuDivider />}
119+
<MenuItem
120+
className={`menuItem`}
121+
onClick={() => {
122+
removeCard(id)
123+
}}>
124+
<IoTrashBinOutline />
125+
Remove card
126+
</MenuItem>
127+
<MenuItem className={`menuItem`} onClick={onOpenSourceUrlClicked} disabled={!url}>
128+
<BsBoxArrowInUpRight />
129+
Open in new tab
130+
</MenuItem>
131+
</Menu>
132+
)
133+
}

src/features/cards/components/aiCard/AICard.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ import { ListComponent } from 'src/components/List'
33
import { FeedItem, useGetFeed } from 'src/features/feed'
44
import { useUserPreferences } from 'src/stores/preferences'
55
import { CardPropsType, FeedItemData } from 'src/types'
6+
import { CardSettings } from '../CardSettings'
67

78
export function AICard(props: CardPropsType) {
8-
const { meta, withAds, knob } = props
9+
const { meta } = props
910
const { userSelectedTags } = useUserPreferences()
1011
const {
1112
data: articles,
@@ -25,7 +26,7 @@ export function AICard(props: CardPropsType) {
2526
)
2627

2728
return (
28-
<Card {...props}>
29+
<Card settingsComponent={<CardSettings url={meta.link} id={meta.value} />} {...props}>
2930
<ListComponent<FeedItemData>
3031
items={articles?.pages.flatMap((page) => page.data) || []}
3132
error={error}

src/features/cards/components/conferencesCard/ConferenceItem.tsx

Lines changed: 37 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,32 @@
1-
import { CardLink, CardItemWithActions } from 'src/components/Elements'
2-
import { Attributes } from 'src/lib/analytics'
3-
import { BaseItemPropsType, Conference } from 'src/types'
4-
import { MdAccessTime } from 'react-icons/md'
5-
import { ColoredLanguagesBadge } from 'src/components/Elements'
61
import { flag } from 'country-emoji'
2+
import { useMemo } from 'react'
73
import { IoIosPin } from 'react-icons/io'
8-
import { RiCalendarEventFill } from 'react-icons/ri'
4+
import { MdAccessTime } from 'react-icons/md'
5+
import { CardItemWithActions, CardLink, ColoredLanguagesBadge } from 'src/components/Elements'
6+
import { Attributes } from 'src/lib/analytics'
97
import { useUserPreferences } from 'src/stores/preferences'
8+
import { BaseItemPropsType, Conference } from 'src/types'
109

1110
const ConferencesItem = ({ item, index, analyticsTag }: BaseItemPropsType<Conference>) => {
1211
const { listingMode } = useUserPreferences()
1312

14-
const ConferenceLocation = () => {
13+
const conferenceLocation = useMemo(() => {
1514
if (item.online) {
16-
return '🌐 Online'
15+
return {
16+
icon: '🌐',
17+
label: 'Online',
18+
}
1719
}
1820
if (item.country) {
19-
return `${flag(item.country.replace(/[^a-zA-Z ]/g, ''))} ${item.city}`
21+
return {
22+
icon: flag(item.country.replace(/[^a-zA-Z ]/g, '')) || '🏳️',
23+
label: item.city,
24+
}
2025
}
21-
}
26+
return null
27+
}, [item.online, item.country, item.city])
2228

23-
const ConferenceDate = () => {
29+
const conferenceDate = useMemo(() => {
2430
if (!item.start_date) {
2531
return ''
2632
}
@@ -50,7 +56,17 @@ const ConferencesItem = ({ item, index, analyticsTag }: BaseItemPropsType<Confer
5056
endValue = `${monthNames[endDate.getMonth()]} ${endValue}`
5157
}
5258
return `${value} - ${endValue}`
53-
}
59+
}, [item.start_date, item.end_date])
60+
61+
const differenceInDays = useMemo(() => {
62+
if (!item.start_date) {
63+
return 0
64+
}
65+
const startDate = new Date(item.start_date)
66+
const currentDate = new Date()
67+
const diffTime = startDate.getTime() - currentDate.getTime()
68+
return Math.ceil(diffTime / (1000 * 60 * 60 * 24))
69+
}, [item.start_date])
5470
return (
5571
<CardItemWithActions
5672
source={analyticsTag}
@@ -67,17 +83,22 @@ const ConferencesItem = ({ item, index, analyticsTag }: BaseItemPropsType<Confer
6783
[Attributes.LINK]: item.url,
6884
[Attributes.SOURCE]: analyticsTag,
6985
}}>
70-
<RiCalendarEventFill className={'rowTitleIcon'} />
86+
<span className="rowTitleIcon">{conferenceLocation?.icon}</span>
7187
{item.title}
7288
</CardLink>
7389
{listingMode === 'normal' ? (
7490
<>
7591
<div className="rowDescription">
7692
<span className="rowItem">
77-
<IoIosPin className="rowItemIcon" /> {ConferenceLocation()}
93+
<IoIosPin className="rowItemIcon" /> {conferenceLocation?.label}
7894
</span>
7995
<span className="rowItem">
80-
<MdAccessTime className="rowItemIcon" /> {ConferenceDate()}
96+
<MdAccessTime className="rowItemIcon" /> {` `}
97+
{differenceInDays > 0
98+
? `In ${differenceInDays} days, ${conferenceDate}`
99+
: differenceInDays === 0
100+
? `Ongoing, ${conferenceDate}`
101+
: `${conferenceDate} (ended)`}
81102
</span>
82103
</div>
83104
<div className="rowDetails">
@@ -87,7 +108,7 @@ const ConferencesItem = ({ item, index, analyticsTag }: BaseItemPropsType<Confer
87108
) : (
88109
<div className="rowDescription">
89110
<span className="rowItem">
90-
<MdAccessTime className="rowItemIcon" /> {ConferenceDate()}
111+
<MdAccessTime className="rowItemIcon" /> {conferenceDate}
91112
</span>
92113
</div>
93114
)}

0 commit comments

Comments
 (0)