diff --git a/.github/workflows/develop.yml b/.github/workflows/develop.yml
index de6f1dd6..4a212e3a 100644
--- a/.github/workflows/develop.yml
+++ b/.github/workflows/develop.yml
@@ -30,7 +30,7 @@ jobs:
run: yarn
- name: Build project
- run: yarn build:web
+ run: yarn build:web --mode development
- name: Copy build to remote host
uses: appleboy/scp-action@v0.1.4
diff --git a/package.json b/package.json
index 59d45ac6..c6cc4e3e 100644
--- a/package.json
+++ b/package.json
@@ -7,6 +7,7 @@
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@sentry/react": "^9.38.0",
+ "@szhsin/react-menu": "^4.5.0",
"@tanstack/query-async-storage-persister": "^5.8.3",
"@tanstack/react-query": "^4.13.0",
"@tanstack/react-query-persist-client": "^5.8.4",
@@ -33,7 +34,6 @@
"react-share": "^4.4.1",
"react-simple-toasts": "^6.1.0",
"react-spinners": "^0.10.4",
- "react-spring-bottom-sheet": "^3.4.1",
"react-toggle": "^4.1.1",
"react-tooltip": "^4.2.21",
"timeago.js": "^4.0.2",
diff --git a/script/build.sh b/script/build.sh
index 8cc7aceb..806d0383 100755
--- a/script/build.sh
+++ b/script/build.sh
@@ -4,7 +4,7 @@ build() {
echo 'Building Hackertab...'
rm -rf dist
tsc
- vite build
+ vite build "$@"
}
-build
\ No newline at end of file
+build "$@"
\ No newline at end of file
diff --git a/src/App.tsx b/src/App.tsx
index e5e3ea0d..a17401b7 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,5 +1,5 @@
import clsx from 'clsx'
-import { useEffect, useLayoutEffect, useState } from 'react'
+import { useEffect, useLayoutEffect } from 'react'
import { DNDLayout } from 'src/components/Layout'
import {
identifyAdvBlocked,
@@ -10,7 +10,6 @@ import {
import { useUserPreferences } from 'src/stores/preferences'
import { AppContentLayout } from './components/Layout'
import { verifyAdvStatus } from './features/adv/utils/status'
-import { isWebOrExtensionVersion } from './utils/Environment'
import { lazyImport } from './utils/lazyImport'
const { OnboardingModal } = lazyImport(() => import('src/features/onboarding'), 'OnboardingModal')
@@ -25,10 +24,9 @@ const intersectionCallback = (entries: IntersectionObserverEntry[]) => {
}
export const App = () => {
- const [showOnboarding, setShowOnboarding] = useState(true)
const {
- onboardingCompleted,
maxVisibleCards,
+ onboardingCompleted,
setAdvStatus,
isDNDModeActive,
layout,
@@ -78,10 +76,7 @@ export const App = () => {
return (
<>
- {!onboardingCompleted && isWebOrExtensionVersion() === 'extension' && (
-
Language
-Date Range
-{error?.message || error}
- } + const sortedData = useMemo(() => { + if (!items || items.length == 0) return [] + if (!sortBy) return items + + const result = sortFn + ? [...items].sort(sortFn) + : [...items].sort((a, b) => { + const aVal = a[sortBy] + const bVal = b[sortBy] + if (typeof aVal === 'number' && typeof bVal === 'number') return bVal - aVal + if (typeof aVal === 'string' && typeof bVal === 'string') return bVal.localeCompare(aVal) + return 0 + }) + + return result + }, [sortBy, sortFn, items]) + + const enrichedItems = useMemo(() => { + if (!sortedData || sortedData.length === 0) { + return [] + } + + try { + return sortedData.slice(0, limit).map((item, index) => { + let content: ReactNode[] = [renderItem(item, index)] + if (header && index === 0) { + content.unshift(header) + } - const renderItems = () => { - if (!items) { - return + return content + }) + } catch (e) { + return [] } + }, [sortedData, header, renderItem, limit]) - return items.slice(0, limit).map((item, index) => { - let content: ReactNode[] = [renderItem(item, index)] - if (header && index === 0) { - content.unshift(header) - } + if (isLoading) { + return{error?.message || error}
+ } - return content - }) + if (items && items.length == 0) { + return ( ++ No items found, try adjusting your filter or choosing a different tag. +
+ ) } - return <>{isLoading ?
diff --git a/src/features/cards/components/devtoCard/DevtoCard.tsx b/src/features/cards/components/devtoCard/DevtoCard.tsx
index 5cdcd8fe..2a079794 100644
--- a/src/features/cards/components/devtoCard/DevtoCard.tsx
+++ b/src/features/cards/components/devtoCard/DevtoCard.tsx
@@ -1,79 +1,84 @@
-import { Card, FloatingFilter, InlineTextFilter } from 'src/components/Elements'
-import { ListComponent } from 'src/components/List'
-import { GLOBAL_TAG, MY_LANGUAGES_TAG } from 'src/config'
-import { trackCardLanguageSelect } from 'src/lib/analytics'
-import { useUserPreferences } from 'src/stores/preferences'
+import { useCallback } from 'react'
+import { AiOutlineLike } from 'react-icons/ai'
+import { BiCommentDetail } from 'react-icons/bi'
+import { Card } from 'src/components/Elements'
+import { ListPostComponent } from 'src/components/List/ListPostComponent'
import { Article, CardPropsType } from 'src/types'
-import { filterUniqueEntries, getCardTagsValue } from 'src/utils/DataEnhancement'
-import { useGetDevtoArticles } from '../../api/getDevtoArticles'
+import { useGetSourceArticles } from '../../api/getSourceArticles'
+import { useLazyListLoad } from '../../hooks/useLazyListLoad'
+import { useSelectedTags } from '../../hooks/useSelectedTags'
+import { MemoizedCardHeader } from '../CardHeader'
+import { MemoizedCardSettings } from '../CardSettings'
import ArticleItem from './ArticleItem'
+const GLOBAL_TAG = { label: 'Global', value: 'programming' }
+
export function DevtoCard(props: CardPropsType) {
const { meta } = props
- const { userSelectedTags, cardsSettings, setCardSettings } = useUserPreferences()
-
- const selectedTag =
- [GLOBAL_TAG, MY_LANGUAGES_TAG, ...userSelectedTags].find(
- (lang) => lang.value === cardsSettings?.[meta.value]?.language
- ) || GLOBAL_TAG
-
- const getQueryTags = () => {
- if (!selectedTag) {
- return []
- }
-
- if (selectedTag.value === MY_LANGUAGES_TAG.devtoValues[0]) {
- return getCardTagsValue(userSelectedTags, 'devtoValues')
- }
- return selectedTag.devtoValues
- }
+ const { ref, isVisible } = useLazyListLoad()
- const results = useGetDevtoArticles({ tags: getQueryTags() })
+ const {
+ queryTags,
+ selectedTag,
+ cardSettings: { sortBy, language } = {},
+ } = useSelectedTags({
+ source: meta.value,
+ fallbackTag: GLOBAL_TAG,
+ })
- const getIsLoading = () => results.some((result) => result.isLoading)
+ const {
+ data: results,
+ error,
+ isLoading,
+ } = useGetSourceArticles({
+ source: 'devto',
+ tags: queryTags,
+ config: {
+ enabled: isVisible,
+ },
+ })
- const getData = () => {
- return filterUniqueEntries(
- results.reduce((acc: Article[], curr) => {
- if (!curr.data) return acc
- return [...acc, ...curr.data]
- }, [])
- )
- }
-
- const renderItem = (item: Article, index: number) => (
-
-
-
-
+
+
+
{item.description}
{listingMode === 'normal' && (
+
+
+
diff --git a/src/features/cards/components/hashnodeCard/HashnodeCard.tsx b/src/features/cards/components/hashnodeCard/HashnodeCard.tsx
index 58ca3c03..02066181 100644
--- a/src/features/cards/components/hashnodeCard/HashnodeCard.tsx
+++ b/src/features/cards/components/hashnodeCard/HashnodeCard.tsx
@@ -1,80 +1,78 @@
-import { Card, FloatingFilter, InlineTextFilter } from 'src/components/Elements'
+import { useCallback } from 'react'
+import { AiTwotoneHeart } from 'react-icons/ai'
+import { BiCommentDetail } from 'react-icons/bi'
+import { Card } from 'src/components/Elements'
import { ListComponent } from 'src/components/List'
-import { GLOBAL_TAG, MY_LANGUAGES_TAG } from 'src/config'
-import { trackCardLanguageSelect } from 'src/lib/analytics'
-import { useUserPreferences } from 'src/stores/preferences'
import { Article, CardPropsType } from 'src/types'
-import { filterUniqueEntries, getCardTagsValue } from 'src/utils/DataEnhancement'
-import { useGetHashnodeArticles } from '../../api/getHashnodeArticles'
+import { useGetSourceArticles } from '../../api/getSourceArticles'
+import { useLazyListLoad } from '../../hooks/useLazyListLoad'
+import { useSelectedTags } from '../../hooks/useSelectedTags'
+import { MemoizedCardHeader } from '../CardHeader'
+import { MemoizedCardSettings } from '../CardSettings'
import ArticleItem from './ArticleItem'
+const GLOBAL_TAG = { label: 'Global', value: '' }
+
export function HashnodeCard(props: CardPropsType) {
const { meta } = props
- const { userSelectedTags, cardsSettings, setCardSettings } = useUserPreferences()
- const selectedTag =
- [GLOBAL_TAG, MY_LANGUAGES_TAG, ...userSelectedTags].find(
- (lang) => lang.value === cardsSettings?.[meta.value]?.language
- ) || GLOBAL_TAG
-
- const getQueryTags = () => {
- if (!selectedTag) {
- return []
- }
-
- if (selectedTag.value === MY_LANGUAGES_TAG.hashnodeValues[0]) {
- return getCardTagsValue(userSelectedTags, 'hashnodeValues')
- }
- return selectedTag.hashnodeValues
- }
-
- const results = useGetHashnodeArticles({ tags: getQueryTags() })
-
- const getIsLoading = () => results.some((result) => result.isLoading)
+ const { ref, isVisible } = useLazyListLoad()
+ const {
+ queryTags,
+ selectedTag,
+ cardSettings: { sortBy, language } = {},
+ } = useSelectedTags({
+ source: meta.value,
+ fallbackTag: GLOBAL_TAG,
+ })
+ const { data, error, isLoading } = useGetSourceArticles({
+ source: 'hashnode',
+ tags: queryTags,
+ config: {
+ enabled: isVisible,
+ },
+ })
- const getData = () => {
- return filterUniqueEntries(
- results
- .reduce((acc: Article[], curr) => {
- if (!curr.data) return acc
- return [...acc, ...curr.data]
- }, [])
- .sort((a, b) => b.published_at - a.published_at)
- )
- }
-
- const renderItem = (item: Article, index: number) => (
-
-
{item.description}
+{item.tagline}
{listingMode === 'normal' && (
-
{title}
+ > + ) +} + export function CustomRssCard(props: CardPropsType) { const { meta } = props - const { data = [], isLoading } = useRssFeed({ feedUrl: meta.feedUrl || '' }) + const { ref, isVisible } = useLazyListLoad() + const { data = [], isLoading } = useRssFeed({ + feedUrl: meta.feedUrl || '', + config: { + enabled: isVisible, + }, + }) - const renderItem = (item: Article, index: number) => ( -{meta.label}
- > - ) - } - return (Let's customize your Hackertab experience!
+Select your developer role π¨π»βπ» to personalize your Hackertab.
Select the languages you're interested in following.
-Your feed will be tailored by your followed sources
-You are not following any topics yet. Start exploring below!
++ Explore and follow new topics to customize your feed further. +
+
+ No results found, try adjusting your search keyword.
+
+ If you think this technology is missing, feel free to{' '}
+
+ open an issue
+ {' '}
+ to suggest it.
+