Skip to content

Commit 246897e

Browse files
authored
Merge pull request #249 from medyo/develop
New version #minor
2 parents 29cd0e4 + 7aa57f1 commit 246897e

File tree

21 files changed

+234
-114
lines changed

21 files changed

+234
-114
lines changed

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@
44
"private": false,
55
"dependencies": {
66
"@amplitude/analytics-browser": "^1.5.5",
7+
"@dnd-kit/core": "^6.3.1",
8+
"@dnd-kit/sortable": "^10.0.0",
79
"@sentry/react": "^9.38.0",
810
"@tanstack/query-async-storage-persister": "^5.8.3",
911
"@tanstack/react-query": "^4.13.0",
1012
"@tanstack/react-query-persist-client": "^5.8.4",
11-
"axios": "^1.8.4",
13+
"axios": "^1.11.0",
1214
"axios-cache-adapter": "^2.7.3",
1315
"country-emoji": "^1.5.4",
1416
"dompurify": "^3.2.4",
@@ -20,7 +22,6 @@
2022
"react": "^19.1.0",
2123
"react-contexify": "^5.0.0",
2224
"react-dom": "^19.1.0",
23-
"react-easy-sort": "^1.6.0",
2425
"react-error-boundary": "^3.1.4",
2526
"react-icons": "^5.2.1",
2627
"react-infinite-scroll-hook": "^6.0.0",

src/assets/App.css

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,7 @@ a {
307307
display: flex;
308308
}
309309

310-
.draggedBlock .block {
310+
.draggedBlock {
311311
transform: rotate(3deg);
312312
opacity: 0.5;
313313
}
@@ -1405,7 +1405,9 @@ Producthunt item
14051405
.capitalize {
14061406
text-transform: capitalize;
14071407
}
1408-
1408+
.snapDisabled {
1409+
scroll-snap-type: none;
1410+
}
14091411
/**
14101412
Modal
14111413
**/

src/components/Elements/Card/Card.tsx

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,29 @@
1+
import clsx from 'clsx'
12
import React, { useEffect, useState } from 'react'
2-
import { SortableKnob } from 'react-easy-sort'
33
import { BsBoxArrowInUpRight } from 'react-icons/bs'
4-
import { MdOutlineDragIndicator } from 'react-icons/md'
54
import { ref } from 'src/config'
65
import { AdvBanner } from 'src/features/adv'
76
import { useRemoteConfigStore } from 'src/features/remoteConfig'
8-
import { DesktopBreakpoint } from 'src/providers/DesktopBreakpoint'
97
import { useUserPreferences } from 'src/stores/preferences'
10-
import { SupportedCardType } from 'src/types'
8+
import { CardPropsType } from 'src/types'
119

12-
type CardProps = {
10+
type RootCardProps = CardPropsType & {
1311
children: React.ReactNode
14-
card: SupportedCardType
15-
withAds?: boolean
1612
titleComponent?: React.ReactNode
1713
fullBlock?: boolean
1814
}
1915

2016
export const Card = ({
21-
card,
17+
meta,
2218
titleComponent,
19+
className,
2320
withAds = false,
2421
children,
2522
fullBlock = false,
26-
}: CardProps) => {
23+
knob,
24+
}: RootCardProps) => {
2725
const { openLinksNewTab } = useUserPreferences()
28-
const { link, icon, label, badge } = card
26+
const { link, icon, label, badge } = meta
2927
const [canAdsLoad, setCanAdsLoad] = useState(true)
3028
const { adsConfig } = useRemoteConfigStore()
3129

@@ -57,15 +55,9 @@ export const Card = ({
5755
}
5856

5957
return (
60-
<div className={'block' + (fullBlock ? ' fullBlock' : '')}>
58+
<div className={clsx('block', fullBlock && 'fullBlock', className)}>
6159
<div className="blockHeader">
62-
<DesktopBreakpoint>
63-
<SortableKnob>
64-
<button className="blockHeaderDragButton">
65-
<MdOutlineDragIndicator />
66-
</button>
67-
</SortableKnob>
68-
</DesktopBreakpoint>
60+
{knob}
6961
<span className="blockHeaderIcon">{icon}</span> {titleComponent || label}{' '}
7062
{link && (
7163
<a className="blockHeaderLink" href={link} onClick={handleHeaderLinkClick}>
Lines changed: 122 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,66 @@
1-
import { useEffect, useRef } from 'react'
2-
import SortableList, { SortableItem } from 'react-easy-sort'
1+
import { useMemo, useRef } from 'react'
2+
//import SortableList, { SortableItem } from 'react-easy-sort'
3+
import {
4+
DndContext,
5+
DragEndEvent,
6+
KeyboardSensor,
7+
PointerSensor,
8+
useSensor,
9+
useSensors,
10+
} from '@dnd-kit/core'
11+
import {
12+
horizontalListSortingStrategy,
13+
SortableContext,
14+
sortableKeyboardCoordinates,
15+
useSortable,
16+
} from '@dnd-kit/sortable'
17+
import { CSS } from '@dnd-kit/utilities'
18+
import { clsx } from 'clsx'
19+
import { MdOutlineDragIndicator } from 'react-icons/md'
320
import { SUPPORTED_CARDS } from 'src/config/supportedCards'
421
import { CustomRssCard } from 'src/features/cards'
522
import { useRemoteConfigStore } from 'src/features/remoteConfig'
623
import { trackPageDrag } from 'src/lib/analytics'
24+
import { DesktopBreakpoint } from 'src/providers/DesktopBreakpoint'
725
import { useUserPreferences } from 'src/stores/preferences'
826
import { SelectedCard, SupportedCardType } from 'src/types'
927

28+
type SortableItemProps = {
29+
id: string
30+
card: SupportedCardType
31+
withAds: boolean
32+
}
33+
34+
const SortableItem = ({ id, card, withAds }: SortableItemProps) => {
35+
const { attributes, listeners, setNodeRef, isDragging, transform, transition } = useSortable({
36+
id,
37+
})
38+
39+
const style = {
40+
transform: CSS.Transform.toString(transform),
41+
transition,
42+
}
43+
44+
const Component = card.component || CustomRssCard
45+
46+
return (
47+
<div ref={setNodeRef} style={style}>
48+
<Component
49+
meta={card}
50+
className={clsx(isDragging && 'draggedBlock')}
51+
withAds={withAds}
52+
knob={
53+
<DesktopBreakpoint>
54+
<button className="blockHeaderDragButton" {...attributes} {...listeners}>
55+
<MdOutlineDragIndicator />
56+
</button>
57+
</DesktopBreakpoint>
58+
}
59+
/>
60+
</div>
61+
)
62+
}
63+
1064
export const DesktopCards = ({
1165
cards,
1266
userCustomCards,
@@ -16,46 +70,78 @@ export const DesktopCards = ({
1670
}) => {
1771
const AVAILABLE_CARDS = [...SUPPORTED_CARDS, ...userCustomCards]
1872
const { updateCardOrder } = useUserPreferences()
19-
const scrollHolderRef = useRef<HTMLElement | null>(null)
73+
const cardsWrapperRef = useRef<HTMLDivElement>(null)
2074
const { adsConfig } = useRemoteConfigStore()
2175

22-
const onSortEnd = (oldIndex: number, newIndex: number) => {
23-
updateCardOrder(oldIndex, newIndex)
24-
trackPageDrag()
25-
if (newIndex === 0 || (oldIndex > 3 && newIndex < 3)) {
26-
scrollHolderRef.current?.scrollTo(0, 0)
76+
const sensors = useSensors(
77+
useSensor(PointerSensor),
78+
useSensor(KeyboardSensor, {
79+
coordinateGetter: sortableKeyboardCoordinates,
80+
})
81+
)
82+
83+
const handleDragStart = () => {
84+
cardsWrapperRef.current?.classList.add('snapDisabled')
85+
}
86+
87+
const handleDragEnd = (event: DragEndEvent) => {
88+
const { active, over } = event
89+
90+
if (active.id !== over?.id) {
91+
const previousCard = cards.find((card) => card.name === active.id)
92+
const newCard = cards.find((card) => card.name === over?.id)
93+
if (!previousCard || !newCard) {
94+
return
95+
}
96+
97+
const oldIndex = previousCard.id
98+
const newIndex = newCard.id
99+
100+
updateCardOrder(oldIndex, newIndex)
101+
trackPageDrag()
27102
}
103+
104+
cardsWrapperRef.current?.classList.remove('snapDisabled')
28105
}
29106

30-
useEffect(() => {
31-
scrollHolderRef.current = document.querySelector('.Cards')
32-
}, [])
107+
const memoCards = useMemo(() => {
108+
return cards
109+
.map((card) => {
110+
const constantCard = AVAILABLE_CARDS.find((c) => c.value === card.name)
111+
if (!constantCard) {
112+
return null
113+
}
114+
115+
return {
116+
card: constantCard,
117+
id: card.name,
118+
}
119+
})
120+
.filter(Boolean) as { id: string; card: SupportedCardType }[]
121+
}, [cards])
33122

34123
return (
35-
<SortableList
36-
as="div"
37-
onSortEnd={onSortEnd}
38-
lockAxis="x"
39-
className="Cards HorizontalScroll"
40-
draggedItemClassName="draggedBlock">
41-
{[...cards]
42-
.sort((a, b) => a.id - b.id)
43-
.map((card, index) => {
44-
const constantCard = AVAILABLE_CARDS.find((c) => c.value === card.name)
45-
if (!constantCard) {
46-
return null
47-
}
48-
49-
const Component = constantCard?.component || CustomRssCard
50-
51-
return (
52-
<SortableItem key={card.name}>
53-
<div>
54-
<Component meta={constantCard} withAds={index === adsConfig.columnPosition} />
55-
</div>
56-
</SortableItem>
57-
)
58-
})}
59-
</SortableList>
124+
<div ref={cardsWrapperRef} className="Cards HorizontalScroll">
125+
<DndContext
126+
sensors={sensors}
127+
autoScroll={false}
128+
onDragEnd={handleDragEnd}
129+
onDragStart={handleDragStart}>
130+
<SortableContext
131+
items={memoCards.map(({ id }) => id)}
132+
strategy={horizontalListSortingStrategy}>
133+
{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+
)
142+
})}
143+
</SortableContext>
144+
</DndContext>
145+
</div>
60146
)
61147
}

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import { FeedItem, useGetFeed } from 'src/features/feed'
44
import { useUserPreferences } from 'src/stores/preferences'
55
import { CardPropsType, FeedItemData } from 'src/types'
66

7-
export function AICard({ meta, withAds }: CardPropsType) {
7+
export function AICard(props: CardPropsType) {
8+
const { meta, withAds, knob } = props
89
const { userSelectedTags } = useUserPreferences()
910
const {
1011
data: articles,
@@ -24,7 +25,7 @@ export function AICard({ meta, withAds }: CardPropsType) {
2425
)
2526

2627
return (
27-
<Card card={meta} withAds={withAds}>
28+
<Card {...props}>
2829
<ListComponent<FeedItemData>
2930
items={articles?.pages.flatMap((page) => page.data) || []}
3031
error={error}

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ import { filterUniqueEntries, getCardTagsValue } from 'src/utils/DataEnhancement
66
import { useGetConferences } from '../../api/getConferences'
77
import ConferenceItem from './ConferenceItem'
88

9-
export function ConferencesCard({ meta, withAds }: CardPropsType) {
9+
export function ConferencesCard(props: CardPropsType) {
10+
const { meta } = props
1011
const { userSelectedTags } = useUserPreferences()
1112

1213
const results = useGetConferences({ tags: getCardTagsValue(userSelectedTags, 'confsValues') })
@@ -34,7 +35,7 @@ export function ConferencesCard({ meta, withAds }: CardPropsType) {
3435
)
3536

3637
return (
37-
<Card card={meta} withAds={withAds}>
38+
<Card {...props}>
3839
<ListComponent items={getData()} isLoading={isLoading} renderItem={renderItem} />
3940
</Card>
4041
)

src/features/cards/components/devtoCard/DevtoCard.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ import { filterUniqueEntries, getCardTagsValue } from 'src/utils/DataEnhancement
88
import { useGetDevtoArticles } from '../../api/getDevtoArticles'
99
import ArticleItem from './ArticleItem'
1010

11-
export function DevtoCard({ withAds, meta }: CardPropsType) {
11+
export function DevtoCard(props: CardPropsType) {
12+
const { meta } = props
1213
const { userSelectedTags, cardsSettings, setCardSettings } = useUserPreferences()
1314

1415
const selectedTag =
@@ -70,7 +71,7 @@ export function DevtoCard({ withAds, meta }: CardPropsType) {
7071
}
7172

7273
return (
73-
<Card card={meta} titleComponent={<HeaderTitle />} withAds={withAds}>
74+
<Card titleComponent={<HeaderTitle />} {...props}>
7475
<FloatingFilter card={meta} filters={['language']} />
7576
<ListComponent items={getData()} isLoading={getIsLoading()} renderItem={renderItem} />
7677
</Card>

src/features/cards/components/freecodecampCard/FreecodecampCard.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ import { filterUniqueEntries, getCardTagsValue } from 'src/utils/DataEnhancement
88
import { useGetFreeCodeCampArticles } from '../../api/getFreeCodeCampArticles'
99
import ArticleItem from './ArticleItem'
1010

11-
export function FreecodecampCard({ meta, withAds }: CardPropsType) {
11+
export function FreecodecampCard(props: CardPropsType) {
12+
const { meta } = props
1213
const { userSelectedTags, cardsSettings, setCardSettings } = useUserPreferences()
1314
const selectedTag =
1415
[GLOBAL_TAG, MY_LANGUAGES_TAG, ...userSelectedTags].find(
@@ -71,7 +72,7 @@ export function FreecodecampCard({ meta, withAds }: CardPropsType) {
7172
}
7273

7374
return (
74-
<Card card={meta} titleComponent={<HeaderTitle />} withAds={withAds}>
75+
<Card titleComponent={<HeaderTitle />} {...props}>
7576
<FloatingFilter card={meta} filters={['language']} />
7677
<ListComponent items={getData()} isLoading={getIsLoading()} renderItem={renderItem} />
7778
</Card>

src/features/cards/components/githubCard/GithubCard.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ import { filterUniqueEntries, getCardTagsValue } from 'src/utils/DataEnhancement
88
import { useGetGithubRepos } from '../../api/getGithubRepos'
99
import RepoItem from './RepoItem'
1010

11-
export function GithubCard({ meta, withAds }: CardPropsType) {
11+
export function GithubCard(props: CardPropsType) {
12+
const { meta, withAds, knob } = props
1213
const { userSelectedTags, cardsSettings, setCardSettings } = useUserPreferences()
1314

1415
const selectedTag =
@@ -97,7 +98,7 @@ export function GithubCard({ meta, withAds }: CardPropsType) {
9798
}
9899
}
99100
return (
100-
<Card fullBlock={true} card={meta} titleComponent={<HeaderTitle />} withAds={withAds}>
101+
<Card fullBlock={true} titleComponent={<HeaderTitle />} {...props}>
101102
<FloatingFilter card={meta} filters={['datesRange', 'language']} />
102103
<ListComponent
103104
items={getData()}

src/features/cards/components/hackernewsCard/HackernewsCard.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,16 @@ import { Article, CardPropsType } from 'src/types'
44
import { useGetHackertNewsArticles } from '../../api/getHackerNewsArticles'
55
import ArticleItem from './ArticleItem'
66

7-
export function HackernewsCard({ meta, withAds }: CardPropsType) {
7+
export function HackernewsCard(props: CardPropsType) {
8+
const { meta } = props
89
const { data: articles = [], isLoading, error } = useGetHackertNewsArticles()
910

1011
const renderItem = (item: Article, index: number) => (
1112
<ArticleItem item={item} key={`hn-${index}`} index={index} analyticsTag={meta.analyticsTag} />
1213
)
1314

1415
return (
15-
<Card card={meta} withAds={withAds}>
16+
<Card {...props}>
1617
<ListComponent items={articles} error={error} isLoading={isLoading} renderItem={renderItem} />
1718
</Card>
1819
)

0 commit comments

Comments
 (0)