From 372c0573a5cfeb7e4125b30e90f7bb2b137d3a93 Mon Sep 17 00:00:00 2001 From: John Doe Date: Sat, 4 Oct 2025 19:45:38 +0100 Subject: [PATCH 01/89] refactor: remove unused article fetching APIs for various sources --- src/features/cards/api/getDevtoArticles.ts | 27 ------------------- .../cards/api/getFreeCodeCampArticles.ts | 27 ------------------- .../cards/api/getHackerNewsArticles.ts | 4 +-- src/features/cards/api/getHashnodeArticles.ts | 27 ------------------- src/features/cards/api/getLobstersArticles.ts | 22 --------------- src/features/cards/api/getMediumArticles.ts | 27 ------------------- src/features/cards/api/getRedditArticles.ts | 27 ------------------- 7 files changed, 2 insertions(+), 159 deletions(-) delete mode 100644 src/features/cards/api/getDevtoArticles.ts delete mode 100644 src/features/cards/api/getFreeCodeCampArticles.ts delete mode 100644 src/features/cards/api/getHashnodeArticles.ts delete mode 100644 src/features/cards/api/getLobstersArticles.ts delete mode 100644 src/features/cards/api/getMediumArticles.ts delete mode 100644 src/features/cards/api/getRedditArticles.ts diff --git a/src/features/cards/api/getDevtoArticles.ts b/src/features/cards/api/getDevtoArticles.ts deleted file mode 100644 index 4aeb90d5..00000000 --- a/src/features/cards/api/getDevtoArticles.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { useQueries, UseQueryOptions } from '@tanstack/react-query' -import { QueryConfig } from 'src/lib/react-query' -import { Article } from 'src/types' -import { axios } from 'src/lib/axios' - -const getArticles = async (tag: string): Promise => { - return axios.get(`/data/v2/devto/${tag}.json`) -} - -type QueryFnType = typeof getArticles - -type UseGetArticlesOptions = { - config?: QueryConfig - tags: string[] -} - -export const useGetDevtoArticles = ({ config, tags }: UseGetArticlesOptions) => { - return useQueries({ - queries: tags.map>((tag) => { - return { - ...config, - queryKey: ['devto', tag], - queryFn: () => getArticles(tag), - } - }) - }) -} diff --git a/src/features/cards/api/getFreeCodeCampArticles.ts b/src/features/cards/api/getFreeCodeCampArticles.ts deleted file mode 100644 index dcacd36f..00000000 --- a/src/features/cards/api/getFreeCodeCampArticles.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { useQueries, UseQueryOptions } from '@tanstack/react-query' -import { QueryConfig } from 'src/lib/react-query' -import { Article } from 'src/types' -import { axios } from 'src/lib/axios' - -const getArticles = async (tag: string): Promise => { - return axios.get(`/data/v2/freecodecamp/${tag}.json`) -} - -type QueryFnType = typeof getArticles - -type UseGetArticlesOptions = { - config?: QueryConfig - tags: string[] -} - -export const useGetFreeCodeCampArticles = ({ config, tags }: UseGetArticlesOptions) => { - return useQueries({ - queries: tags.map>((tag) => { - return { - ...config, - queryKey: ['freecodecamp', tag], - queryFn: () => getArticles(tag), - } - }) - }) -} diff --git a/src/features/cards/api/getHackerNewsArticles.ts b/src/features/cards/api/getHackerNewsArticles.ts index f5b5ecd8..4bb2055b 100644 --- a/src/features/cards/api/getHackerNewsArticles.ts +++ b/src/features/cards/api/getHackerNewsArticles.ts @@ -1,10 +1,10 @@ import { useQuery } from '@tanstack/react-query' +import { axios } from 'src/lib/axios' import { ExtractFnReturnType, QueryConfig } from 'src/lib/react-query' import { Article } from 'src/types' -import { axios } from 'src/lib/axios' const getArticles = async (): Promise => { - return axios.get('/data/v2/hackernews.json') + return axios.get(`https://api-dev.hackertab.dev/engine/card?source=hackernews`, {}) } type QueryFnType = typeof getArticles diff --git a/src/features/cards/api/getHashnodeArticles.ts b/src/features/cards/api/getHashnodeArticles.ts deleted file mode 100644 index 69065436..00000000 --- a/src/features/cards/api/getHashnodeArticles.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { useQueries, UseQueryOptions } from '@tanstack/react-query' -import { QueryConfig } from 'src/lib/react-query' -import { Article } from 'src/types' -import { axios } from 'src/lib/axios' - -const getArticles = async (tag: string): Promise => { - return axios.get(`/data/v2/hashnode/${tag}.json`) -} - -type QueryFnType = typeof getArticles - -type UseGetArticlesOptions = { - config?: QueryConfig - tags: string[] -} - -export const useGetHashnodeArticles = ({ config, tags }: UseGetArticlesOptions) => { - return useQueries({ - queries: tags.map>((tag) => { - return { - ...config, - queryKey: ['hashnode', tag], - queryFn: () => getArticles(tag), - } - }) - }) -} diff --git a/src/features/cards/api/getLobstersArticles.ts b/src/features/cards/api/getLobstersArticles.ts deleted file mode 100644 index 560d97ad..00000000 --- a/src/features/cards/api/getLobstersArticles.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { useQuery } from '@tanstack/react-query' -import { ExtractFnReturnType, QueryConfig } from 'src/lib/react-query' -import { Article } from 'src/types' -import { axios } from 'src/lib/axios' - -const getArticles = async (): Promise => { - return axios.get('/data/v2/lobsters.json') -} - -type QueryFnType = typeof getArticles - -type UseGetArticlesOptions = { - config?: QueryConfig -} - -export const useGetLobstersArticles = ({ config }: UseGetArticlesOptions = {}) => { - return useQuery>({ - ...config, - queryKey: ['lobsters'], - queryFn: () => getArticles(), - }) -} diff --git a/src/features/cards/api/getMediumArticles.ts b/src/features/cards/api/getMediumArticles.ts deleted file mode 100644 index ce07b590..00000000 --- a/src/features/cards/api/getMediumArticles.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { useQueries, UseQueryOptions } from '@tanstack/react-query' -import { QueryConfig } from 'src/lib/react-query' -import { Article } from 'src/types' -import { axios } from 'src/lib/axios' - -const getArticles = async (tag: string): Promise => { - return axios.get(`/data/v2/medium/${tag}.json`) -} - -type QueryFnType = typeof getArticles - -type UseGetArticlesOptions = { - config?: QueryConfig - tags: string[] -} - -export const useGetMediumArticles = ({ config, tags }: UseGetArticlesOptions) => { - return useQueries({ - queries: tags.map>((tag) => { - return { - ...config, - queryKey: ['medium', tag], - queryFn: () => getArticles(tag), - } - }) - }) -} diff --git a/src/features/cards/api/getRedditArticles.ts b/src/features/cards/api/getRedditArticles.ts deleted file mode 100644 index 523340c1..00000000 --- a/src/features/cards/api/getRedditArticles.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { useQueries, UseQueryOptions } from '@tanstack/react-query' -import { QueryConfig } from 'src/lib/react-query' -import { Article } from 'src/types' -import { axios } from 'src/lib/axios' - -const getArticles = async (tag: string): Promise => { - return axios.get(`/data/v2/reddit/${tag}.json`) -} - -type QueryFnType = typeof getArticles - -type UseGetArticlesOptions = { - config?: QueryConfig - tags: string[] -} - -export const useGetRedditArticles = ({ config, tags }: UseGetArticlesOptions) => { - return useQueries({ - queries: tags.map>((tag) => { - return { - ...config, - queryKey: ['reddit', tag], - queryFn: () => getArticles(tag), - } - }) - }) -} From ee573712199ff58201e06ef579ba4acf7ca6a87d Mon Sep 17 00:00:00 2001 From: John Doe Date: Sun, 5 Oct 2025 20:17:23 +0100 Subject: [PATCH 02/89] refactor: remove unused API files for Hacker News and Indie Hackers articles --- .../cards/api/getHackerNewsArticles.ts | 22 ------------------- .../cards/api/getIndieHackersArticles.ts | 22 ------------------- 2 files changed, 44 deletions(-) delete mode 100644 src/features/cards/api/getHackerNewsArticles.ts delete mode 100644 src/features/cards/api/getIndieHackersArticles.ts diff --git a/src/features/cards/api/getHackerNewsArticles.ts b/src/features/cards/api/getHackerNewsArticles.ts deleted file mode 100644 index 4bb2055b..00000000 --- a/src/features/cards/api/getHackerNewsArticles.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { useQuery } from '@tanstack/react-query' -import { axios } from 'src/lib/axios' -import { ExtractFnReturnType, QueryConfig } from 'src/lib/react-query' -import { Article } from 'src/types' - -const getArticles = async (): Promise => { - return axios.get(`https://api-dev.hackertab.dev/engine/card?source=hackernews`, {}) -} - -type QueryFnType = typeof getArticles - -type UseGetArticlesOptions = { - config?: QueryConfig -} - -export const useGetHackertNewsArticles = ({ config }: UseGetArticlesOptions = {}) => { - return useQuery>({ - ...config, - queryKey: ['hackernews'], - queryFn: () => getArticles(), - }) -} diff --git a/src/features/cards/api/getIndieHackersArticles.ts b/src/features/cards/api/getIndieHackersArticles.ts deleted file mode 100644 index 938559f3..00000000 --- a/src/features/cards/api/getIndieHackersArticles.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { useQuery } from '@tanstack/react-query' -import { ExtractFnReturnType, QueryConfig } from 'src/lib/react-query' -import { Article } from 'src/types' -import { axios } from 'src/lib/axios' - -const getArticles = async (): Promise => { - return axios.get('/data/v2/indiehackers.json') -} - -type QueryFnType = typeof getArticles - -type UseGetArticlesOptions = { - config?: QueryConfig -} - -export const useGetIndieHackersArticles = ({ config }: UseGetArticlesOptions = {}) => { - return useQuery>({ - ...config, - queryKey: ['indiehackers'], - queryFn: () => getArticles(), - }) -} From 011639e644aae9550718cf6aea6c72719d1312f3 Mon Sep 17 00:00:00 2001 From: John Doe Date: Sun, 12 Oct 2025 19:14:06 +0100 Subject: [PATCH 03/89] refactor: remove unused imports and functions from DataEnhancement utility --- src/utils/DataEnhancement.ts | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/src/utils/DataEnhancement.ts b/src/utils/DataEnhancement.ts index 1de39bad..a61ba5f9 100644 --- a/src/utils/DataEnhancement.ts +++ b/src/utils/DataEnhancement.ts @@ -1,5 +1,4 @@ -import { RemoteConfig, Tag, TagValuesFieldType } from 'src/features/remoteConfig' -import { BaseEntry } from 'src/types' +import { RemoteConfig, Tag } from 'src/features/remoteConfig' export const enhanceTags = (remoteConfigStore: RemoteConfig, tags: string[]): Tag[] => { return tags @@ -8,17 +7,3 @@ export const enhanceTags = (remoteConfigStore: RemoteConfig, tags: string[]): Ta ) .filter(Boolean) as Tag[] } - -export const getCardTagsValue = (tags: Tag[], valuesField: TagValuesFieldType): string[] => { - return tags.reduce((acc: string[], curr) => { - if (!curr[valuesField] || curr[valuesField].length === 0) return acc - acc = [...acc, ...curr[valuesField]] - return acc - }, []) -} - -export const filterUniqueEntries = (entries: BaseEntry[]) => { - const uniqueResults = new Map() - entries.forEach((item) => uniqueResults.set(item.id, item)) - return Array.from(uniqueResults.values()) -} From 9332f2e120a0775a2a022100facf43264ed64e26 Mon Sep 17 00:00:00 2001 From: John Doe Date: Sun, 12 Oct 2025 19:14:51 +0100 Subject: [PATCH 04/89] refactor: clean up Tag type by removing unused values and update Article and Repository types for consistency --- src/features/remoteConfig/stores/remoteConfig.ts | 7 ------- src/features/remoteConfig/types/index.ts | 16 ---------------- src/stores/preferences.ts | 14 +++++++------- src/types/index.ts | 13 ++++++++----- 4 files changed, 15 insertions(+), 35 deletions(-) diff --git a/src/features/remoteConfig/stores/remoteConfig.ts b/src/features/remoteConfig/stores/remoteConfig.ts index d8e8335a..744e4acc 100644 --- a/src/features/remoteConfig/stores/remoteConfig.ts +++ b/src/features/remoteConfig/stores/remoteConfig.ts @@ -27,13 +27,6 @@ export const useRemoteConfigStore = create( { value: 'javascript', label: 'Javascript', - githubValues: ['javascript'], - confsValues: ['javascript'], - devtoValues: ['javascript'], - hashnodeValues: ['javascript'], - mediumValues: ['javascript'], - redditValues: ['javascript'], - freecodecampValues: ['javascript'], }, ], setRemoteConfig: (remoteConfig: RemoteConfig) => diff --git a/src/features/remoteConfig/types/index.ts b/src/features/remoteConfig/types/index.ts index 1357aa78..e75ce7e7 100644 --- a/src/features/remoteConfig/types/index.ts +++ b/src/features/remoteConfig/types/index.ts @@ -1,24 +1,8 @@ export type Tag = { - confsValues: string[] - devtoValues: string[] - hashnodeValues: string[] - redditValues: string[] - githubValues: string[] - freecodecampValues: string[] - mediumValues: string[] label: string value: string } -export type TagValuesFieldType = - | 'confsValues' - | 'devtoValues' - | 'hashnodeValues' - | 'redditValues' - | 'githubValues' - | 'freecodecampValues' - | 'mediumValues' - export type RemoteConfig = { supportedTags: Tag[] marketingBannerConfig?: any diff --git a/src/stores/preferences.ts b/src/stores/preferences.ts index 6ea7f4d7..1c1c19b5 100644 --- a/src/stores/preferences.ts +++ b/src/stores/preferences.ts @@ -40,6 +40,7 @@ type UserPreferencesStoreActions = { setOpenLinksNewTab: (openLinksNewTab: boolean) => void setListingMode: (listingMode: ListingMode) => void setCards: (selectedCards: SelectedCard[]) => void + removeCard: (cardName: string) => void setTags: (selectedTags: Tag[]) => void setMaxVisibleCards: (maxVisibleCards: number) => void setCardSettings: (card: string, settings: CardSettingsType) => void @@ -117,13 +118,6 @@ export const useUserPreferences = create( { value: 'javascript', label: 'Javascript', - githubValues: ['javascript'], - confsValues: ['javascript'], - devtoValues: ['javascript'], - hashnodeValues: ['javascript'], - mediumValues: ['javascript'], - redditValues: ['javascript'], - freecodecampValues: ['javascript'], }, ], layout: 'cards', @@ -212,6 +206,12 @@ export const useUserPreferences = create( } }), setAdvStatus: (status) => set({ advStatus: status }), + removeCard: (cardName: string) => + set((state) => { + return { + cards: state.cards.filter((card) => card.name !== cardName), + } + }), }), { name: 'preferences_storage', diff --git a/src/types/index.ts b/src/types/index.ts index 22fb1201..adb7cd9b 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -37,9 +37,11 @@ export type BaseEntry = { export type Article = BaseEntry & { published_at: number tags: Array - reactions: number - comments: number + points_count: number + comments_count: number + votes_count: number image_url: string + tagline?: string source: string original_url?: string comments_url?: string @@ -91,12 +93,12 @@ export type FeedItemData = | AdFeedItemData export type Repository = BaseEntry & { - programmingLanguage: string - stars: number + technology: string + stars_count: number source: string description: string owner: string - forks: number + forks_count: number starsInDateRange?: number name: string } @@ -143,6 +145,7 @@ export type BaseItemPropsType< export type CardSettingsType = { language: string + sortBy: string dateRange?: string } From dcfb5a1796d69589d0755a280780d7cb9b0d4796 Mon Sep 17 00:00:00 2001 From: John Doe Date: Sun, 12 Oct 2025 19:15:13 +0100 Subject: [PATCH 05/89] fix: update badge to link in SUPPORTED_CARDS for improved navigation --- src/config/supportedCards.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/supportedCards.tsx b/src/config/supportedCards.tsx index cfbd1db8..4ef1fe82 100644 --- a/src/config/supportedCards.tsx +++ b/src/config/supportedCards.tsx @@ -126,6 +126,6 @@ export const SUPPORTED_CARDS: SupportedCardType[] = [ label: 'Powered by AI', component: AICard, type: 'supported', - badge: 'BETA', + link: 'https://hackertab.dev/', }, ] From 21c2aba1ce53cd1652ba8ab878cca50bd1e96999 Mon Sep 17 00:00:00 2001 From: John Doe Date: Sun, 12 Oct 2025 20:31:51 +0100 Subject: [PATCH 06/89] feat: enhance ListComponent with sorting functionality and add ListConference, ListPost, and ListRepo components --- src/components/List/ListComponent.tsx | 65 ++++++++++++++----- .../List/ListConferenceComponent.tsx | 6 ++ src/components/List/ListPostComponent.tsx | 6 ++ src/components/List/ListRepoComponent.tsx | 6 ++ 4 files changed, 67 insertions(+), 16 deletions(-) create mode 100644 src/components/List/ListConferenceComponent.tsx create mode 100644 src/components/List/ListPostComponent.tsx create mode 100644 src/components/List/ListRepoComponent.tsx diff --git a/src/components/List/ListComponent.tsx b/src/components/List/ListComponent.tsx index 49c0feb9..7890a61f 100644 --- a/src/components/List/ListComponent.tsx +++ b/src/components/List/ListComponent.tsx @@ -1,4 +1,4 @@ -import React, { ReactNode } from 'react' +import React, { ReactNode, useMemo } from 'react' import { Placeholder } from 'src/components/placeholders' import { MAX_ITEMS_PER_CARD } from 'src/config' @@ -9,7 +9,7 @@ type PlaceholdersProps = { const Placeholders = React.memo(({ placeholder }) => { return ( <> - {[...Array(7)].map((x, i) => ( + {[...Array(7)].map((_, i) => ( {placeholder} ))} @@ -17,7 +17,9 @@ const Placeholders = React.memo(({ placeholder }) => { }) export type ListComponentPropsType = { - items: T[] + items?: T[] + sortBy?: keyof T + sortFn?: (a: T, b: T) => number isLoading: boolean renderItem: (item: T, index: number) => React.ReactNode placeholder?: React.ReactNode @@ -27,11 +29,13 @@ export type ListComponentPropsType = { limit?: number } -export function ListComponent(props: ListComponentPropsType) { +export function ListComponent(props: ListComponentPropsType) { const { items, + sortBy, isLoading, error, + sortFn, renderItem, header, placeholder = , @@ -42,20 +46,49 @@ export function ListComponent(props: ListComponentPropsType{error?.message || error}

} - const renderItems = () => { - if (!items) { - return + if (items && items.length == 0) { + return ( +

+ No items found, try adjusting your filter or choosing a different tag. +

+ ) + } + + 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 [] } - return items.slice(0, limit).map((item, index) => { - let content: ReactNode[] = [renderItem(item, index)] - if (header && index === 0) { - content.unshift(header) - } + try { + return sortedData.slice(0, limit).map((item, index) => { + let content: ReactNode[] = [renderItem(item, index)] + if (header && index === 0) { + content.unshift(header) + } - return content - }) - } + return content + }) + } catch (e) { + return [] + } + }, [sortedData]) - return <>{isLoading ? : renderItems()} + return <>{isLoading ? : enrichedItems} } diff --git a/src/components/List/ListConferenceComponent.tsx b/src/components/List/ListConferenceComponent.tsx new file mode 100644 index 00000000..1bc60a49 --- /dev/null +++ b/src/components/List/ListConferenceComponent.tsx @@ -0,0 +1,6 @@ +import { Conference } from 'src/types' +import { ListComponent, ListComponentPropsType } from './ListComponent' + +export function ListConferenceComponent(props: ListComponentPropsType) { + return {...props} /> +} diff --git a/src/components/List/ListPostComponent.tsx b/src/components/List/ListPostComponent.tsx new file mode 100644 index 00000000..f9525a96 --- /dev/null +++ b/src/components/List/ListPostComponent.tsx @@ -0,0 +1,6 @@ +import { Article } from 'src/types' +import { ListComponent, ListComponentPropsType } from './ListComponent' + +export function ListPostComponent(props: ListComponentPropsType
) { + return {...props} /> +} diff --git a/src/components/List/ListRepoComponent.tsx b/src/components/List/ListRepoComponent.tsx new file mode 100644 index 00000000..645bc46a --- /dev/null +++ b/src/components/List/ListRepoComponent.tsx @@ -0,0 +1,6 @@ +import { Repository } from 'src/types' +import { ListComponent, ListComponentPropsType } from './ListComponent' + +export function ListRepoComponent(props: ListComponentPropsType) { + return {...props} /> +} From df0e80375c045404835c85c4ed599e38ba5abf78 Mon Sep 17 00:00:00 2001 From: John Doe Date: Sun, 19 Oct 2025 22:47:35 +0100 Subject: [PATCH 07/89] feat: add @szhsin/react-menu dependency and import styles in index.tsx --- package.json | 1 + src/index.tsx | 3 +++ yarn.lock | 12 ++++++++++++ 3 files changed, 16 insertions(+) diff --git a/package.json b/package.json index 59d45ac6..d32fc59a 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", diff --git a/src/index.tsx b/src/index.tsx index fc6fc484..1a0138d3 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -8,8 +8,11 @@ import { persister, queryClient } from 'src/lib/react-query' import { AppErrorBoundary } from 'src/providers/AppErrorBoundary' import { AppRoutes } from './routes/AppRoutes' +import '@szhsin/react-menu/dist/index.css' +import '@szhsin/react-menu/dist/transitions/zoom.css' import { createRoot } from 'react-dom/client' import { initSentry } from './lib/sentry' + const container = document.getElementById('root') if (!container) { throw new Error('Failed to find the root element') diff --git a/yarn.lock b/yarn.lock index 1b50dad1..ff97f18c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2631,6 +2631,13 @@ "@svgr/hast-util-to-babel-ast" "8.0.0" svg-parser "^2.0.4" +"@szhsin/react-menu@^4.5.0": + version "4.5.0" + resolved "https://registry.yarnpkg.com/@szhsin/react-menu/-/react-menu-4.5.0.tgz#c302b652b8de896ec80669b1fcc492210101e290" + integrity sha512-fblZBPxFGjg+QxSbdDsWk3H8brupuQG+ayYXElwg+FdCxwLQLvrHG9K6O9+4pE8qLyDy5REn/2HmffPXcBZviA== + dependencies: + react-transition-state "^2.3.1" + "@tanstack/query-async-storage-persister@^5.8.3": version "5.14.1" resolved "https://registry.yarnpkg.com/@tanstack/query-async-storage-persister/-/query-async-storage-persister-5.14.1.tgz#71666d5be8eba5c09eaebed56918e6df2117a69e" @@ -5978,6 +5985,11 @@ react-transition-group@^4.3.0: loose-envify "^1.4.0" prop-types "^15.6.2" +react-transition-state@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/react-transition-state/-/react-transition-state-2.3.1.tgz#53f5d33a95d1859d1ad2b1673c4466da1518e4ed" + integrity sha512-Z48el73x+7HUEM131dof9YpcQ5IlM4xB+pKWH/lX3FhxGfQaNTZa16zb7pWkC/y5btTZzXfCtglIJEGc57giOw== + react-use-gesture@^8.0.1: version "8.0.1" resolved "https://registry.yarnpkg.com/react-use-gesture/-/react-use-gesture-8.0.1.tgz#4360c0f7c9e26baf9fbe58f63fc9de7ef699c17f" From 47158ef51c69557fda10b0420da39325ad0ce6b6 Mon Sep 17 00:00:00 2001 From: John Doe Date: Sat, 1 Nov 2025 12:22:02 +0100 Subject: [PATCH 08/89] refactor: remove LanguagesTab and SourcesTab components to streamline onboarding process --- .../components/steps/LanguagesTab.tsx | 46 ------------------- .../components/steps/SourcesTab.tsx | 44 ------------------ 2 files changed, 90 deletions(-) delete mode 100644 src/features/onboarding/components/steps/LanguagesTab.tsx delete mode 100644 src/features/onboarding/components/steps/SourcesTab.tsx diff --git a/src/features/onboarding/components/steps/LanguagesTab.tsx b/src/features/onboarding/components/steps/LanguagesTab.tsx deleted file mode 100644 index c9e947c4..00000000 --- a/src/features/onboarding/components/steps/LanguagesTab.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { ChipsSet, StepProps } from 'src/components/Elements' -import { useRemoteConfigStore } from 'src/features/remoteConfig' -import { Occupation } from '../../types' - -export const LanguagesTab = ({ - moveToPrevious, - moveToNext, - setTabsData, - tabsData, -}: StepProps) => { - const { supportedTags } = useRemoteConfigStore() - - const sources = supportedTags - .map((tag) => { - return { - label: tag.label, - value: tag.value, - } - }) - .sort((a, b) => (a.label > b.label ? 1 : -1)) - - return ( -
-
-

🦾 Select your languages & topics

-

Select the languages you're interested in following.

-
-
- { - setTabsData({ ...tabsData, tags: selectedChips.map((chip) => chip.value) }) - }} - /> -
-
- - -
-
- ) -} diff --git a/src/features/onboarding/components/steps/SourcesTab.tsx b/src/features/onboarding/components/steps/SourcesTab.tsx deleted file mode 100644 index ddd860ba..00000000 --- a/src/features/onboarding/components/steps/SourcesTab.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { BsArrowRight } from 'react-icons/bs' -import { ChipsSet, StepProps } from 'src/components/Elements' -import { SUPPORTED_CARDS } from '../../../../config/supportedCards' -import { Occupation } from '../../types' - -export const SourcesTab = ({ - moveToPrevious, - moveToNext, - setTabsData, - tabsData, -}: StepProps) => { - const sources = SUPPORTED_CARDS.map((source) => { - return { - label: source.label, - value: source.value, - icon: source.icon, - } - }).sort((a, b) => (a.label > b.label ? 1 : -1)) - - return ( -
-
-

πŸ“™ Pick your sources

-

Your feed will be tailored by your followed sources

-
-
- { - setTabsData({ ...tabsData, sources: selectedChips.map((chip) => chip.value) }) - }} - /> -
-
- - -
-
- ) -} From 46512afc92dca42143fe42376cfb02feba6b5e21 Mon Sep 17 00:00:00 2001 From: John Doe Date: Sat, 1 Nov 2025 12:22:18 +0100 Subject: [PATCH 09/89] fix: add useEffect to reset selected chips when defaultValues change --- src/components/Elements/ChipsSet/ChipsSet.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/Elements/ChipsSet/ChipsSet.tsx b/src/components/Elements/ChipsSet/ChipsSet.tsx index b215c8e5..8065c220 100644 --- a/src/components/Elements/ChipsSet/ChipsSet.tsx +++ b/src/components/Elements/ChipsSet/ChipsSet.tsx @@ -1,5 +1,5 @@ import clsx from 'clsx' -import { useState } from 'react' +import { useEffect, useState } from 'react' import { IoIosClose } from 'react-icons/io' import { Option } from 'src/types' import './chipset.css' @@ -48,6 +48,10 @@ export const ChipsSet = ({ }: ChipsSetProps) => { const [selectedChips, setSelectedChips] = useState(defaultValues || []) + useEffect(() => { + setSelectedChips(defaultValues || []) + }, [defaultValues]) + const onSelect = (option: Option) => { if (selectedChips?.some((chipValue) => chipValue === option.value)) { if (!canSelectMultiple) { From f7cbe6b187cd0a2321de0bfcc254c710fdfbcd91 Mon Sep 17 00:00:00 2001 From: John Doe Date: Sat, 1 Nov 2025 12:25:00 +0100 Subject: [PATCH 10/89] refactor: simplify onboarding process by removing unused components and props --- src/App.tsx | 20 +---- .../onboarding/components/OnboardingModal.tsx | 73 ++------------- .../onboarding/components/steps/HelloTab.tsx | 89 +++++++++++-------- src/lib/analytics.ts | 6 +- src/stores/preferences.ts | 33 +++++-- 5 files changed, 93 insertions(+), 128 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index e5e3ea0d..7d68c6b5 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,16 +24,8 @@ const intersectionCallback = (entries: IntersectionObserverEntry[]) => { } export const App = () => { - const [showOnboarding, setShowOnboarding] = useState(true) - const { - onboardingCompleted, - maxVisibleCards, - setAdvStatus, - isDNDModeActive, - layout, - DNDDuration, - setDNDDuration, - } = useUserPreferences() + const { maxVisibleCards, setAdvStatus, isDNDModeActive, layout, DNDDuration, setDNDDuration } = + useUserPreferences() useLayoutEffect(() => { document.documentElement.style.setProperty('--max-visible-cards', maxVisibleCards.toString()) @@ -78,10 +69,7 @@ export const App = () => { return ( <> - {!onboardingCompleted && isWebOrExtensionVersion() === 'extension' && ( - - )} - +
void -} - -export const OnboardingModal = ({ showOnboarding, setShowOnboarding }: OnboardingModalProps) => { - const { markOnboardingAsCompleted, setTags, setCards } = useUserPreferences() - const { supportedTags } = useRemoteConfigStore() +export const OnboardingModal = () => { + const { onboardingCompleted } = useUserPreferences() useEffect(() => { trackOnboardingStart() }, []) return ( setShowOnboarding(false)} contentLabel="Onboarding" className="Modal scrollable" style={{ @@ -47,50 +27,7 @@ export const OnboardingModal = ({ showOnboarding, setShowOnboarding }: Onboardin }} overlayClassName="Overlay">
- { - trackOnboardingSkip() - markOnboardingAsCompleted(null) - setShowOnboarding(false) - }} - onFinish={(tabsData) => { - trackOnboardingFinish() - if (tabsData) { - const { icon, ...occupation } = tabsData - markOnboardingAsCompleted(occupation) - identifyUserOccupation(occupation.title) - - const tags = - (occupation.tags - .map((tag) => supportedTags.find((st) => st.value === tag)) - .filter(Boolean) as Tag[]) || [] - - 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 || '', - type: 'supported', - } - }) || []) as SelectedCard[] - - setCards(cards) - identifyUserCards(cards.map((card) => card.name)) - } - - setShowOnboarding(false) - }} - /> +
) diff --git a/src/features/onboarding/components/steps/HelloTab.tsx b/src/features/onboarding/components/steps/HelloTab.tsx index b97b8f9e..40d75cb2 100644 --- a/src/features/onboarding/components/steps/HelloTab.tsx +++ b/src/features/onboarding/components/steps/HelloTab.tsx @@ -1,11 +1,11 @@ import clsx from 'clsx' -import { useState } from 'react' import { AiFillMobile, AiFillSecurityScan } from 'react-icons/ai' import { BsArrowRight, BsFillGearFill } from 'react-icons/bs' import { FaDatabase, FaPaintBrush, FaRobot, FaServer } from 'react-icons/fa' import { RiDeviceFill } from 'react-icons/ri' import { TbDots } from 'react-icons/tb' -import { StepProps } from 'src/components/Elements' +import { Tag, useRemoteConfigStore } from 'src/features/remoteConfig' +import { useUserPreferences } from 'src/stores/preferences' import { Occupation } from '../../types' const OCCUPATIONS: Occupation[] = [ @@ -13,13 +13,13 @@ const OCCUPATIONS: Occupation[] = [ title: 'Front-End Engineer', icon: FaPaintBrush, sources: ['devto', 'github', 'medium', 'hashnode'], - tags: ['javascript', 'typescript'], + tags: ['frontend', 'javascript', 'typescript', 'css', 'react', 'vue', 'angular'], }, { title: 'Back-End Engineer', icon: BsFillGearFill, sources: ['devto', 'github', 'medium', 'hashnode'], - tags: ['go', 'php', 'ruby', 'rust', 'r'], + tags: ['backend', 'go', 'php', 'ruby', 'rust', 'r'], }, { title: 'Full Stack Engineer', @@ -31,74 +31,92 @@ const OCCUPATIONS: Occupation[] = [ title: 'Mobile', icon: AiFillMobile, sources: ['reddit', 'github', 'medium', 'hashnode'], - tags: ['android', 'kotlin', 'java', 'swift', 'objective-c'], + tags: [ + 'android', + 'mobile', + 'kotlin', + 'java', + 'ios', + 'swift', + 'objectivec', + 'react native', + 'flutter', + ], }, { title: 'Devops Engineer', icon: FaServer, sources: ['freecodecamp', 'github', 'reddit', 'devto'], - tags: ['devops', 'bash'], + tags: ['devops', 'kubernetes', 'docker', 'bash'], }, { title: 'Data Engineer', icon: FaDatabase, sources: ['freecodecamp', 'github', 'reddit', 'devto'], - tags: ['data-science', 'python', 'artificial-intelligence', 'machine-learning'], + tags: ['data science', 'python', 'artificial intelligence', 'machine learning'], }, { title: 'Security Engineer', icon: AiFillSecurityScan, sources: ['freecodecamp', 'github', 'reddit', 'devto'], - tags: ['c++', 'bash', 'python'], + tags: ['security', 'cpp', 'bash', 'python'], }, { title: 'ML Engineer', icon: FaRobot, sources: ['github', 'freecodecamp', 'hackernews', 'devto'], - tags: ['machine-learning', 'artificial-intelligence', 'python'], + tags: ['machine learning', 'artificial intelligence', 'python'], }, { title: 'Other', icon: TbDots, sources: ['hackernews', 'github', 'producthunt', 'devto'], - tags: [], + tags: ['webdev', 'mobile'], }, ] -export const HelloTab = ({ - moveToNext, - moveToPrevious, - setTabsData, - tabsData, -}: StepProps) => { - const [selectedOccupation, setSelectedOccupation] = useState( - tabsData || OCCUPATIONS[0] - ) - const onOccupationClicked = (occupation: Occupation) => { - setSelectedOccupation(occupation) - } +export const HelloTab = () => { + const { markOnboardingAsCompleted, setCards, setTags, setOccupation, occupation } = + useUserPreferences() + + const { tags } = useRemoteConfigStore() - const onClickNext = () => { - if (selectedOccupation === undefined) { - return + const onStartClicked = () => { + const selectedOccupation = OCCUPATIONS.find((occ) => occ.title === occupation) + if (selectedOccupation) { + setOccupation(selectedOccupation.title) + setCards( + selectedOccupation.sources.map((source, index) => ({ + id: index, + name: source, + type: 'supported', + })) + ) + const userTags = selectedOccupation.tags + .map((tag) => { + return tags.find((t) => t.value === tag) + }) + .filter(Boolean) as Array + + setTags(userTags) } - setTabsData(selectedOccupation) - moveToNext && moveToNext() + markOnboardingAsCompleted() } + return (
-

Hi, πŸ‘‹ Welcome to Hackertab

-

Let's customize your Hackertab experience!

+

πŸ‘‹ Let’s set up your Hackertab

+

Select your developer role πŸ‘¨πŸ»β€πŸ’» to personalize your Hackertab.

{OCCUPATIONS.map((occ) => { return (
- - + {occupation && ( + + )}
) diff --git a/src/lib/analytics.ts b/src/lib/analytics.ts index f883fa88..6b969f21 100644 --- a/src/lib/analytics.ts +++ b/src/lib/analytics.ts @@ -99,11 +99,11 @@ export const setupAnalytics = () => { export const setupIdentification = () => { const { userSelectedTags, - onboardingResult, theme, cards, listingMode, openLinksNewTab, + occupation, promptEngine, maxVisibleCards, layout, @@ -118,8 +118,8 @@ export const setupIdentification = () => { identifyUserLinksInNewTab(openLinksNewTab) identifyUserMaxVisibleCards(maxVisibleCards) identifyDisplayLayout(layout) - if (onboardingResult?.title) { - identifyUserOccupation(onboardingResult.title) + if (occupation) { + identifyUserOccupation(occupation) } identifySentryUser({ [Attributes.LANGUAGES]: userSelectedTags.map((tag: any) => tag.value), diff --git a/src/stores/preferences.ts b/src/stores/preferences.ts index 1c1c19b5..0c8eac60 100644 --- a/src/stores/preferences.ts +++ b/src/stores/preferences.ts @@ -1,4 +1,3 @@ -import { Occupation } from 'src/features/onboarding/types' import { Tag, useRemoteConfigStore } from 'src/features/remoteConfig' import { enhanceTags } from 'src/utils/DataEnhancement' import { create } from 'zustand' @@ -20,7 +19,7 @@ export type UserPreferencesState = { theme: Theme openLinksNewTab: boolean onboardingCompleted: boolean - onboardingResult: Omit | null + occupation: string | null listingMode: ListingMode promptEngine: string promptEngines: SearchEngineType[] @@ -42,9 +41,12 @@ type UserPreferencesStoreActions = { setCards: (selectedCards: SelectedCard[]) => void removeCard: (cardName: string) => void setTags: (selectedTags: Tag[]) => void + followTag: (tag: Tag) => void + unfollowTag: (tag: Tag) => void setMaxVisibleCards: (maxVisibleCards: number) => void setCardSettings: (card: string, settings: CardSettingsType) => void - markOnboardingAsCompleted: (occupation: Omit | null) => void + setOccupation: (occupation: string | null) => void + markOnboardingAsCompleted: () => void setUserCustomCards: (cards: SupportedCardType[]) => void updateCardOrder: (prevIndex: number, newIndex: number) => void setDNDDuration: (value: DNDDuration) => void @@ -120,12 +122,12 @@ export const useUserPreferences = create( label: 'Javascript', }, ], + occupation: null, layout: 'cards', cardsSettings: {}, maxVisibleCards: 4, theme: 'dark', onboardingCompleted: false, - onboardingResult: null, promptEngine: 'chatgpt', promptEngines: [], listingMode: 'normal', @@ -159,10 +161,13 @@ export const useUserPreferences = create( [card]: { ...state.cardsSettings[card], ...settings }, }, })), - markOnboardingAsCompleted: (occupation: Omit | null) => + markOnboardingAsCompleted: () => set(() => ({ onboardingCompleted: true, - onboardingResult: occupation, + })), + setOccupation: (occupation: string | null) => + set(() => ({ + occupation: occupation, })), setUserCustomCards: (cards: SupportedCardType[]) => set({ userCustomCards: cards }), updateCardOrder: (prevIndex: number, newIndex: number) => @@ -212,6 +217,22 @@ export const useUserPreferences = create( cards: state.cards.filter((card) => card.name !== cardName), } }), + followTag: (tag: Tag) => + set((state) => { + const exists = state.userSelectedTags.find((t) => t.value === tag.value) + if (exists) { + return state // No change if tag already followed + } + return { + userSelectedTags: [...state.userSelectedTags, tag], + } + }), + unfollowTag: (tag: Tag) => + set((state) => { + return { + userSelectedTags: state.userSelectedTags.filter((t) => t.value !== tag.value), + } + }), }), { name: 'preferences_storage', From 461f2c050542fda7e419386e654e546a20d151c5 Mon Sep 17 00:00:00 2001 From: John Doe Date: Sat, 1 Nov 2025 12:26:57 +0100 Subject: [PATCH 11/89] refactor: clean up GLOBAL_TAG and remove unused MY_LANGUAGES_TAG --- src/config/index.tsx | 25 +++++-------------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/src/config/index.tsx b/src/config/index.tsx index 98cc8766..1ccbac55 100644 --- a/src/config/index.tsx +++ b/src/config/index.tsx @@ -24,25 +24,10 @@ export const reportLink = 'https://www.hackertab.dev/report' export const LS_PREFERENCES_KEY = 'hackerTabPrefs' export const GLOBAL_TAG = { - value: 'global', + value: '', label: 'Trending', - githubValues: ['global'], - devtoValues: ['programming'], - hashnodeValues: ['programming'], - mediumValues: ['programming'], - redditValues: ['programming'], - freecodecampValues: ['programming'], -} -export const MY_LANGUAGES_TAG = { - value: 'myLangs', - label: 'My Languages', - githubValues: ['myLangs'], - devtoValues: ['myLangs'], - hashnodeValues: ['myLangs'], - mediumValues: ['myLangs'], - redditValues: ['myLangs'], - freecodecampValues: ['myLangs'], } + export const MAX_ITEMS_PER_CARD = 50 export type DateRangeType = { @@ -50,7 +35,7 @@ export type DateRangeType = { label: string } export const dateRanges: DateRangeType[] = [ - { label: 'the day', value: 'daily' }, - { label: 'the week', value: 'weekly' }, - { label: 'the month', value: 'monthly' }, + { label: 'Today', value: 'daily' }, + { label: 'This week', value: 'weekly' }, + { label: 'This month', value: 'monthly' }, ] From 368f3ac06318e1e7df4d503c4fdef5a88c774276 Mon Sep 17 00:00:00 2001 From: John Doe Date: Sat, 1 Nov 2025 12:28:03 +0100 Subject: [PATCH 12/89] feat: implement SearchEngineBar component and update SearchBarWithLogo to use it --- .../Elements/SearchBar/SearchBar.tsx | 33 ++++++------------ .../Elements/SearchBar/SearchEngineBar.tsx | 34 +++++++++++++++++++ .../SearchBarWithLogo/SearchBarWithLogo.tsx | 4 +-- 3 files changed, 47 insertions(+), 24 deletions(-) create mode 100644 src/components/Elements/SearchBar/SearchEngineBar.tsx diff --git a/src/components/Elements/SearchBar/SearchBar.tsx b/src/components/Elements/SearchBar/SearchBar.tsx index f493dbea..340a927d 100644 --- a/src/components/Elements/SearchBar/SearchBar.tsx +++ b/src/components/Elements/SearchBar/SearchBar.tsx @@ -1,17 +1,13 @@ import React, { useEffect, useRef } from 'react' -import { HiSparkles } from 'react-icons/hi' -import { AI_PROMPT_ENGINES } from 'src/config/SearchEngines' -import { trackSearchEngineUse } from 'src/lib/analytics' -import { useUserPreferences } from 'src/stores/preferences' - -export const SearchBar = () => { - const { promptEngine, promptEngines } = useUserPreferences() - const mergedEngines = [...AI_PROMPT_ENGINES, ...promptEngines] +type SearchBarProps = { + iconStart?: React.ReactNode + placeholder: string + onSubmit?: (keyword: string) => void + onChange?: (keyword: string) => void +} +export const SearchBar = ({ iconStart, placeholder, onChange, onSubmit }: SearchBarProps) => { const keywordsInputRef = useRef(null) - const usedSearchEngine = - mergedEngines.find((engine) => engine.label.toLowerCase() === promptEngine.toLowerCase()) || - promptEngines[0] const handleSubmit = (e: React.FormEvent) => { e.preventDefault() @@ -19,8 +15,7 @@ export const SearchBar = () => { keyword: { value: string } } - trackSearchEngineUse(usedSearchEngine.label) - window.open(`${usedSearchEngine.url}${target.keyword.value}`, '_self') + onSubmit?.(target.keyword.value) } useEffect(() => { @@ -29,20 +24,14 @@ export const SearchBar = () => { return (
- {usedSearchEngine?.default === false ? ( - - ) : ( - - )} + {iconStart} onChange?.(e.target.value)} className="searchBarInput" - placeholder={`Ask ${usedSearchEngine.label}`} + placeholder={placeholder} /> ) diff --git a/src/components/Elements/SearchBar/SearchEngineBar.tsx b/src/components/Elements/SearchBar/SearchEngineBar.tsx new file mode 100644 index 00000000..ed652364 --- /dev/null +++ b/src/components/Elements/SearchBar/SearchEngineBar.tsx @@ -0,0 +1,34 @@ +import { HiSparkles } from 'react-icons/hi' +import { AI_PROMPT_ENGINES } from 'src/config/SearchEngines' +import { trackSearchEngineUse } from 'src/lib/analytics' +import { useUserPreferences } from 'src/stores/preferences' +import { SearchBar } from './SearchBar' + +export const SearchEngineBar = () => { + const { promptEngine, promptEngines } = useUserPreferences() + const mergedEngines = [...AI_PROMPT_ENGINES, ...promptEngines] + + const usedSearchEngine = + mergedEngines.find((engine) => engine.label.toLowerCase() === promptEngine.toLowerCase()) || + promptEngines[0] + + return ( + + ) : ( + + ) + } + onSubmit={(keyword) => { + trackSearchEngineUse(usedSearchEngine.label) + window.open(`${usedSearchEngine.url}${keyword}`, '_self') + }} + placeholder={`Ask ${usedSearchEngine.label}`} + /> + ) +} diff --git a/src/components/Elements/SearchBarWithLogo/SearchBarWithLogo.tsx b/src/components/Elements/SearchBarWithLogo/SearchBarWithLogo.tsx index 5830c483..f228a140 100644 --- a/src/components/Elements/SearchBarWithLogo/SearchBarWithLogo.tsx +++ b/src/components/Elements/SearchBarWithLogo/SearchBarWithLogo.tsx @@ -1,7 +1,7 @@ import { HiSparkles } from 'react-icons/hi' import { AI_PROMPT_ENGINES } from 'src/config/SearchEngines' import { useUserPreferences } from 'src/stores/preferences' -import { SearchBar } from '../SearchBar/SearchBar' +import { SearchEngineBar } from '../SearchBar/SearchEngineBar' import './SearchBarWithLogo.css' export const SearchBarWithLogo = () => { @@ -25,7 +25,7 @@ export const SearchBarWithLogo = () => { /> )} - +
) } From d5d35632475c0659a80522a5547751c15157845e Mon Sep 17 00:00:00 2001 From: John Doe Date: Sat, 1 Nov 2025 12:58:49 +0100 Subject: [PATCH 13/89] feat: replace SearchBar with SearchEngineBar in Header component --- src/components/Layout/Header.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Layout/Header.tsx b/src/components/Layout/Header.tsx index 45225538..d5131be8 100644 --- a/src/components/Layout/Header.tsx +++ b/src/components/Layout/Header.tsx @@ -8,13 +8,13 @@ import { Link, useLocation, useNavigate } from 'react-router-dom' import AvatarPlaceholder from 'src/assets/icons/avatar.svg?react' import StreakIcon from 'src/assets/icons/fire_icon.svg?react' import HackertabLogo from 'src/assets/logo.svg?react' -import { SearchBar } from 'src/components/Elements/SearchBar' import { UserTags } from 'src/components/Elements/UserTags' import { useAuth } from 'src/features/auth' import { Changelog } from 'src/features/changelog' import { identifyUserTheme, trackDNDDisable, trackThemeSelect } from 'src/lib/analytics' import { useUserPreferences } from 'src/stores/preferences' import { Button, CircleButton } from '../Elements' +import { SearchEngineBar } from '../Elements/SearchBar/SearchEngineBar' export const Header = () => { const { openAuthModal, user, isConnected, isConnecting } = useAuth() @@ -66,7 +66,7 @@ export const Header = () => { - +
From ae2d11c0b1ac7908d0e8267c9d8e836f42ceba8e Mon Sep 17 00:00:00 2001 From: John Doe Date: Sat, 1 Nov 2025 12:59:58 +0100 Subject: [PATCH 14/89] refactor: simplify RemoteConfigStore by removing unused properties and configurations --- .../remoteConfig/stores/remoteConfig.ts | 31 +++---------------- 1 file changed, 4 insertions(+), 27 deletions(-) diff --git a/src/features/remoteConfig/stores/remoteConfig.ts b/src/features/remoteConfig/stores/remoteConfig.ts index 744e4acc..4de8ab03 100644 --- a/src/features/remoteConfig/stores/remoteConfig.ts +++ b/src/features/remoteConfig/stores/remoteConfig.ts @@ -4,47 +4,24 @@ import { persist } from 'zustand/middleware' import { RemoteConfig, Tag } from '../types' type RemoteConfigStore = { - supportedTags: Tag[] - marketingBannerConfig?: any - adsConfig: { - rowPosition: number - columnPosition: number - enabled: boolean - } + tags: Tag[] setRemoteConfig: (remoteConfig: RemoteConfig) => void } export const useRemoteConfigStore = create( persist( (set) => ({ - marketingBannerConfig: undefined, - adsConfig: { - rowPosition: 0, - columnPosition: 0, - enabled: false, - }, - supportedTags: [ + tags: [ { value: 'javascript', label: 'Javascript', }, ], - setRemoteConfig: (remoteConfig: RemoteConfig) => - set(() => { - const { marketingBannerConfig, ...otherConfigs } = remoteConfig - return { ...otherConfigs } - }), + setRemoteConfig: (remoteConfig: RemoteConfig) => set({ tags: remoteConfig.tags }), }), { name: 'remote_config_storage', - version: 1, - migrate(persistedState, version) { - const newState = persistedState as RemoteConfigStore - if (version === 0) { - delete newState.marketingBannerConfig - } - return newState - }, + version: 2, } ) ) From cd269f418f485b68564fd21f30d33f5422fd2938 Mon Sep 17 00:00:00 2001 From: John Doe Date: Sat, 1 Nov 2025 20:01:44 +0100 Subject: [PATCH 15/89] refactor the data source logic and add better filters --- src/components/Elements/Card/Card.tsx | 28 +--- src/components/Layout/DesktopCards.tsx | 9 +- src/config/index.tsx | 5 - .../cards/components/CardSettings.tsx | 133 ++++++++++++++++ .../cards/components/aiCard/AICard.tsx | 5 +- .../conferencesCard/ConferenceItem.tsx | 53 +++++-- .../conferencesCard/ConferencesCard.tsx | 54 ++++--- .../components/devtoCard/ArticleItem.tsx | 17 +- .../cards/components/devtoCard/DevtoCard.tsx | 105 ++++++------ .../freecodecampCard/FreecodecampCard.tsx | 85 +++++----- .../components/githubCard/GithubCard.tsx | 149 +++++++++--------- .../cards/components/githubCard/RepoItem.tsx | 25 +-- .../components/hackernewsCard/ArticleItem.tsx | 10 +- .../hackernewsCard/HackernewsCard.tsx | 44 +++++- .../components/hashnodeCard/ArticleItem.tsx | 14 +- .../components/hashnodeCard/HashnodeCard.tsx | 97 ++++++------ .../indiehackersCard/ArticleItem.tsx | 18 +-- .../indiehackersCard/IndiehackersCard.tsx | 42 ++++- .../components/lobstersCard/ArticleItem.tsx | 10 +- .../components/lobstersCard/LobstersCard.tsx | 41 ++++- .../components/mediumCard/ArticleItem.tsx | 18 +-- .../components/mediumCard/MediumCard.tsx | 97 ++++++------ .../producthuntCard/ArticleItem.tsx | 12 +- .../producthuntCard/ProducthuntCard.tsx | 32 +++- .../components/redditCard/ArticleItem.tsx | 27 +++- .../components/redditCard/RedditCard.tsx | 105 ++++++------ .../components/rssCard/CustomRssCard.tsx | 15 +- src/utils/DataEnhancement.ts | 2 +- 28 files changed, 765 insertions(+), 487 deletions(-) create mode 100644 src/features/cards/components/CardSettings.tsx diff --git a/src/components/Elements/Card/Card.tsx b/src/components/Elements/Card/Card.tsx index 120f3342..676a4641 100644 --- a/src/components/Elements/Card/Card.tsx +++ b/src/components/Elements/Card/Card.tsx @@ -1,34 +1,30 @@ import clsx from 'clsx' import React, { useEffect, useState } from 'react' -import { BsBoxArrowInUpRight } from 'react-icons/bs' -import { ref } from 'src/config' import { AdvBanner } from 'src/features/adv' -import { useRemoteConfigStore } from 'src/features/remoteConfig' -import { useUserPreferences } from 'src/stores/preferences' import { CardPropsType } from 'src/types' type RootCardProps = CardPropsType & { children: React.ReactNode titleComponent?: React.ReactNode + settingsComponent?: React.ReactNode fullBlock?: boolean } export const Card = ({ meta, titleComponent, + settingsComponent, className, withAds = false, children, fullBlock = false, knob, }: RootCardProps) => { - const { openLinksNewTab } = useUserPreferences() - const { link, icon, label, badge } = meta + const { icon, label, badge } = meta const [canAdsLoad, setCanAdsLoad] = useState(true) - const { adsConfig } = useRemoteConfigStore() useEffect(() => { - if (!adsConfig.enabled || !withAds) { + if (!withAds) { return } @@ -46,28 +42,20 @@ export const Card = ({ return () => { observer.disconnect() } - }, [withAds, adsConfig.enabled]) - - const handleHeaderLinkClick = (e: React.MouseEvent) => { - e.preventDefault() - let url = `${link}?${ref}` - window.open(url, openLinksNewTab ? '_blank' : '_self') - } + }, [withAds]) return (
{knob} {icon} {titleComponent || label}{' '} - {link && ( - - - + {settingsComponent && ( + {settingsComponent} )} {badge && {badge}}
- {canAdsLoad && adsConfig.enabled && withAds && ( + {canAdsLoad && withAds && (
diff --git a/src/components/Layout/DesktopCards.tsx b/src/components/Layout/DesktopCards.tsx index cc69987e..2f581058 100644 --- a/src/components/Layout/DesktopCards.tsx +++ b/src/components/Layout/DesktopCards.tsx @@ -131,14 +131,7 @@ export const DesktopCards = ({ items={memoCards.map(({ id }) => id)} strategy={horizontalListSortingStrategy}> {memoCards.map(({ id, card }, index) => { - return ( - - ) + return })} diff --git a/src/config/index.tsx b/src/config/index.tsx index 1ccbac55..1136a1cd 100644 --- a/src/config/index.tsx +++ b/src/config/index.tsx @@ -23,11 +23,6 @@ export const twitterHandle = '@hackertabdev' export const reportLink = 'https://www.hackertab.dev/report' export const LS_PREFERENCES_KEY = 'hackerTabPrefs' -export const GLOBAL_TAG = { - value: '', - label: 'Trending', -} - export const MAX_ITEMS_PER_CARD = 50 export type DateRangeType = { diff --git a/src/features/cards/components/CardSettings.tsx b/src/features/cards/components/CardSettings.tsx new file mode 100644 index 00000000..bb7ef69d --- /dev/null +++ b/src/features/cards/components/CardSettings.tsx @@ -0,0 +1,133 @@ +import { Menu, MenuDivider, MenuItem, SubMenu } from '@szhsin/react-menu' +import { useCallback, useMemo } from 'react' +import { AiOutlineCode } from 'react-icons/ai' +import { BsBoxArrowInUpRight } from 'react-icons/bs' +import { GoGear } from 'react-icons/go' +import { IoTrashBinOutline } from 'react-icons/io5' +import { LiaSortSolid } from 'react-icons/lia' +import { MdDateRange } from 'react-icons/md' +import { ref } from 'src/config' +import { useUserPreferences } from 'src/stores/preferences' + +type SortOption = { label: string; value: string; icon?: React.ReactNode } + +type CardSettingsProps = { + url?: string + id: string + sortBy?: string + globalTag?: { label: string; value: string } + language?: string + showLanguageFilter?: boolean + showDateRangeFilter?: boolean + customStartMenuItems?: React.ReactNode + sortOptions?: ((defaults: SortOption[]) => SortOption[]) | SortOption[] +} + +const DEFAULT_SORT_OPTIONS = [{ label: 'Newest', value: 'published_at', icon: }] + +export const CardSettings = ({ + id, + url, + sortBy, + globalTag, + language, + showLanguageFilter = true, + showDateRangeFilter = true, + customStartMenuItems, + sortOptions, +}: CardSettingsProps) => { + const userSelectedTags = useUserPreferences((state) => state.userSelectedTags) + const openLinksNewTab = useUserPreferences((state) => state.openLinksNewTab) + const removeCard = useUserPreferences((state) => state.removeCard) + const setCardSettings = useUserPreferences((state) => state.setCardSettings) + const cardSettings = useUserPreferences((state) => state.cardsSettings[id]) + + const userTagsMemo = useMemo(() => { + const newTags = userSelectedTags.sort((a, b) => a.label.localeCompare(b.label)) + if (globalTag) { + return [globalTag, ...newTags] + } + return newTags + }, [userSelectedTags, globalTag]) + + const resolvedSortOptions = + typeof sortOptions === 'function' + ? sortOptions(DEFAULT_SORT_OPTIONS) + : sortOptions || DEFAULT_SORT_OPTIONS + + const onOpenSourceUrlClicked = useCallback(() => { + let link = `${url}?${ref}` + window.open(link, openLinksNewTab ? '_blank' : '_self') + }, [url, openLinksNewTab]) + + return ( + } + theming="dark" + portal={true} + className={`menuItem`} + transition + direction={'bottom'} + align="start"> + {customStartMenuItems} + {showLanguageFilter && userTagsMemo.length > 0 && ( + + Language + + }> + {userTagsMemo.map((tag) => ( + { + setCardSettings(id, { ...cardSettings, language: tag.value }) + }}> + {tag.label} + + ))} + + )} + + {showDateRangeFilter && ( + + + Sort by + + }> + {resolvedSortOptions.map((option) => ( + { + setCardSettings(id, { ...cardSettings, sortBy: option.value }) + }}> + {option.icon} {option.label} + + ))} + + )} + {(showDateRangeFilter || showLanguageFilter) && } + { + removeCard(id) + }}> + + Remove card + + + + Open in new tab + + + ) +} diff --git a/src/features/cards/components/aiCard/AICard.tsx b/src/features/cards/components/aiCard/AICard.tsx index 0261395a..13b36c12 100644 --- a/src/features/cards/components/aiCard/AICard.tsx +++ b/src/features/cards/components/aiCard/AICard.tsx @@ -3,9 +3,10 @@ import { ListComponent } from 'src/components/List' import { FeedItem, useGetFeed } from 'src/features/feed' import { useUserPreferences } from 'src/stores/preferences' import { CardPropsType, FeedItemData } from 'src/types' +import { CardSettings } from '../CardSettings' export function AICard(props: CardPropsType) { - const { meta, withAds, knob } = props + const { meta } = props const { userSelectedTags } = useUserPreferences() const { data: articles, @@ -25,7 +26,7 @@ export function AICard(props: CardPropsType) { ) return ( - + } {...props}> items={articles?.pages.flatMap((page) => page.data) || []} error={error} diff --git a/src/features/cards/components/conferencesCard/ConferenceItem.tsx b/src/features/cards/components/conferencesCard/ConferenceItem.tsx index 57981d8c..3f0823e9 100644 --- a/src/features/cards/components/conferencesCard/ConferenceItem.tsx +++ b/src/features/cards/components/conferencesCard/ConferenceItem.tsx @@ -1,26 +1,32 @@ -import { CardLink, CardItemWithActions } from 'src/components/Elements' -import { Attributes } from 'src/lib/analytics' -import { BaseItemPropsType, Conference } from 'src/types' -import { MdAccessTime } from 'react-icons/md' -import { ColoredLanguagesBadge } from 'src/components/Elements' import { flag } from 'country-emoji' +import { useMemo } from 'react' import { IoIosPin } from 'react-icons/io' -import { RiCalendarEventFill } from 'react-icons/ri' +import { MdAccessTime } from 'react-icons/md' +import { CardItemWithActions, CardLink, ColoredLanguagesBadge } from 'src/components/Elements' +import { Attributes } from 'src/lib/analytics' import { useUserPreferences } from 'src/stores/preferences' +import { BaseItemPropsType, Conference } from 'src/types' const ConferencesItem = ({ item, index, analyticsTag }: BaseItemPropsType) => { const { listingMode } = useUserPreferences() - const ConferenceLocation = () => { + const conferenceLocation = useMemo(() => { if (item.online) { - return '🌐 Online' + return { + icon: '🌐', + label: 'Online', + } } if (item.country) { - return `${flag(item.country.replace(/[^a-zA-Z ]/g, ''))} ${item.city}` + return { + icon: flag(item.country.replace(/[^a-zA-Z ]/g, '')) || '🏳️', + label: item.city, + } } - } + return null + }, [item.online, item.country, item.city]) - const ConferenceDate = () => { + const conferenceDate = useMemo(() => { if (!item.start_date) { return '' } @@ -50,7 +56,17 @@ const ConferencesItem = ({ item, index, analyticsTag }: BaseItemPropsType { + if (!item.start_date) { + return 0 + } + const startDate = new Date(item.start_date) + const currentDate = new Date() + const diffTime = startDate.getTime() - currentDate.getTime() + return Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + }, [item.start_date]) return ( - + {conferenceLocation?.icon} {item.title} {listingMode === 'normal' ? ( <>
- {ConferenceLocation()} + {conferenceLocation?.label} - {ConferenceDate()} + {` `} + {differenceInDays > 0 + ? `In ${differenceInDays} days, ${conferenceDate}` + : differenceInDays === 0 + ? `Ongoing, ${conferenceDate}` + : `${conferenceDate} (ended)`}
@@ -87,7 +108,7 @@ const ConferencesItem = ({ item, index, analyticsTag }: BaseItemPropsType - {ConferenceDate()} + {conferenceDate}
)} diff --git a/src/features/cards/components/conferencesCard/ConferencesCard.tsx b/src/features/cards/components/conferencesCard/ConferencesCard.tsx index cc07f635..e1aa745f 100644 --- a/src/features/cards/components/conferencesCard/ConferencesCard.tsx +++ b/src/features/cards/components/conferencesCard/ConferencesCard.tsx @@ -1,42 +1,46 @@ import { Card } from 'src/components/Elements' -import { ListComponent } from 'src/components/List' +import { ListConferenceComponent } from 'src/components/List/ListConferenceComponent' import { useUserPreferences } from 'src/stores/preferences' import { CardPropsType, Conference } from 'src/types' -import { filterUniqueEntries, getCardTagsValue } from 'src/utils/DataEnhancement' import { useGetConferences } from '../../api/getConferences' +import { CardSettings } from '../CardSettings' import ConferenceItem from './ConferenceItem' export function ConferencesCard(props: CardPropsType) { const { meta } = props + const cardSettings = useUserPreferences((state) => state.cardsSettings?.[meta.value]) const { userSelectedTags } = useUserPreferences() - - const results = useGetConferences({ tags: getCardTagsValue(userSelectedTags, 'confsValues') }) - - const isLoading = results.some((result) => result.isLoading) - - const getData = () => { - return filterUniqueEntries( - results - .reduce((acc: Conference[], curr) => { - if (!curr.data) return acc - return [...acc, ...curr.data] - }, []) - .sort((a, b) => a.start_date - b.start_date) - ) - } + const { isLoading, data: results } = useGetConferences({ + tags: userSelectedTags.map((tag) => tag.value), + }) const renderItem = (item: Conference, index: number) => ( - + ) return ( - - + + }> + ) } diff --git a/src/features/cards/components/devtoCard/ArticleItem.tsx b/src/features/cards/components/devtoCard/ArticleItem.tsx index 25c642e9..8971bd47 100644 --- a/src/features/cards/components/devtoCard/ArticleItem.tsx +++ b/src/features/cards/components/devtoCard/ArticleItem.tsx @@ -1,12 +1,11 @@ +import { AiOutlineLike } from 'react-icons/ai' import { BiCommentDetail } from 'react-icons/bi' -import { CardLink, CardItemWithActions } from 'src/components/Elements' +import { MdAccessTime } from 'react-icons/md' +import { CardItemWithActions, CardLink, ColoredLanguagesBadge } from 'src/components/Elements' import { Attributes } from 'src/lib/analytics' -import { BaseItemPropsType, Article } from 'src/types' import { useUserPreferences } from 'src/stores/preferences' +import { Article, BaseItemPropsType } from 'src/types' import { format } from 'timeago.js' -import { MdAccessTime } from 'react-icons/md' -import { AiOutlineLike } from 'react-icons/ai' -import { ColoredLanguagesBadge } from 'src/components/Elements' const ArticleItem = (props: BaseItemPropsType
) => { const { item, index, selectedTag, analyticsTag } = props @@ -24,7 +23,7 @@ const ArticleItem = (props: BaseItemPropsType
) => { link={item.url} analyticsAttributes={{ [Attributes.TRIGERED_FROM]: 'card', - [Attributes.POINTS]: item.reactions, + [Attributes.POINTS]: item.points_count, [Attributes.TITLE]: item.title, [Attributes.LINK]: item.url, [Attributes.SOURCE]: analyticsTag, @@ -33,7 +32,7 @@ const ArticleItem = (props: BaseItemPropsType
) => { {listingMode === 'compact' && (
- {item.reactions} + {item.points_count}
)}
{item.title}
@@ -48,11 +47,11 @@ const ArticleItem = (props: BaseItemPropsType
) => { - {item.comments} comments + {item.comments_count} comments - {item.reactions} reactions + {item.points_count} reactions

diff --git a/src/features/cards/components/devtoCard/DevtoCard.tsx b/src/features/cards/components/devtoCard/DevtoCard.tsx index 5cdcd8fe..f33dcaf5 100644 --- a/src/features/cards/components/devtoCard/DevtoCard.tsx +++ b/src/features/cards/components/devtoCard/DevtoCard.tsx @@ -1,45 +1,33 @@ -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 { useMemo } from 'react' +import { AiOutlineLike } from 'react-icons/ai' +import { BiCommentDetail } from 'react-icons/bi' +import { Card, FloatingFilter } from 'src/components/Elements' +import { ListPostComponent } from 'src/components/List/ListPostComponent' import { useUserPreferences } from 'src/stores/preferences' import { Article, CardPropsType } from 'src/types' -import { filterUniqueEntries, getCardTagsValue } from 'src/utils/DataEnhancement' -import { useGetDevtoArticles } from '../../api/getDevtoArticles' +import { useGetSourceArticles } from '../../api/getSourceArticles' +import { CardSettings } 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 { userSelectedTags } = useUserPreferences() + const cardSettings = useUserPreferences((state) => state.cardsSettings?.[meta.value]) - const results = useGetDevtoArticles({ tags: getQueryTags() }) + const selectedTag = useMemo(() => { + return userSelectedTags.find((lang) => lang.value === cardSettings?.language) || GLOBAL_TAG + }, [userSelectedTags, cardSettings]) - const getIsLoading = () => results.some((result) => result.isLoading) - - const getData = () => { - return filterUniqueEntries( - results.reduce((acc: Article[], curr) => { - if (!curr.data) return acc - return [...acc, ...curr.data] - }, []) - ) - } + const { + data: results, + error, + isLoading, + } = useGetSourceArticles({ + source: 'devto', + tags: [selectedTag.value], + }) const renderItem = (item: Article, index: number) => ( { + if (selectedTag.value === GLOBAL_TAG.value) { + return <>{meta.label} + } return ( <> {meta.label} - ({ - label: tag.label, - value: tag.value, - }))} - onChange={(item) => { - setCardSettings(meta.value, { ...cardsSettings[meta.value], language: item.value }) - trackCardLanguageSelect(meta.analyticsTag, item.value) - }} - value={cardsSettings?.[meta.value]?.language} - /> + {selectedTag.label} ) } return ( - } {...props}> + } + settingsComponent={ + [ + ...defaults, + { + label: 'Reactions', + value: 'points_count', + icon: , + }, + { + label: 'Comments', + value: 'comments_count', + icon: , + }, + ]} + /> + } + {...props}> - + ) } diff --git a/src/features/cards/components/freecodecampCard/FreecodecampCard.tsx b/src/features/cards/components/freecodecampCard/FreecodecampCard.tsx index 62a4ff40..2732b32f 100644 --- a/src/features/cards/components/freecodecampCard/FreecodecampCard.tsx +++ b/src/features/cards/components/freecodecampCard/FreecodecampCard.tsx @@ -1,46 +1,28 @@ -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 { useMemo } from 'react' +import { Card, FloatingFilter } from 'src/components/Elements' +import { ListPostComponent } from 'src/components/List/ListPostComponent' import { useUserPreferences } from 'src/stores/preferences' import { Article, CardPropsType } from 'src/types' -import { filterUniqueEntries, getCardTagsValue } from 'src/utils/DataEnhancement' -import { useGetFreeCodeCampArticles } from '../../api/getFreeCodeCampArticles' +import { useGetSourceArticles } from '../../api/getSourceArticles' +import { CardSettings } from '../CardSettings' import ArticleItem from './ArticleItem' +const GLOBAL_TAG = { label: 'Global', value: 'programming' } + export function FreecodecampCard(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.freecodecampValues[0]) { - return getCardTagsValue(userSelectedTags, 'freecodecampValues') - } - return selectedTag.freecodecampValues - } - - const results = useGetFreeCodeCampArticles({ tags: getQueryTags() }) + const { userSelectedTags } = useUserPreferences() + const cardSettings = useUserPreferences((state) => state.cardsSettings?.[meta.value]) - const getIsLoading = () => results.some((result) => result.isLoading) + const selectedTag = useMemo( + () => userSelectedTags.find((lang) => lang.value === cardSettings?.language) || GLOBAL_TAG, + [userSelectedTags, cardSettings] + ) - 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 { data, isLoading } = useGetSourceArticles({ + source: 'freecodecamp', + tags: [selectedTag.value], + }) const renderItem = (item: Article, index: number) => ( {meta.label} - ({ - label: tag.label, - value: tag.value, - }))} - onChange={(item) => { - setCardSettings(meta.value, { ...cardsSettings[meta.value], language: item.value }) - trackCardLanguageSelect(meta.analyticsTag, item.value) - }} - value={cardsSettings?.[meta.value]?.language} - /> + {selectedTag.label} ) } return ( - } {...props}> + } + settingsComponent={ + + } + {...props}> - + ) } diff --git a/src/features/cards/components/githubCard/GithubCard.tsx b/src/features/cards/components/githubCard/GithubCard.tsx index 570fac55..c865172f 100644 --- a/src/features/cards/components/githubCard/GithubCard.tsx +++ b/src/features/cards/components/githubCard/GithubCard.tsx @@ -1,56 +1,41 @@ -import { Card, FloatingFilter, InlineTextFilter } from 'src/components/Elements' -import { ListComponent } from 'src/components/List' -import { GLOBAL_TAG, MY_LANGUAGES_TAG, dateRanges } from 'src/config' -import { trackCardDateRangeSelect, trackCardLanguageSelect } from 'src/lib/analytics' +import { MenuDivider, MenuItem } from '@szhsin/react-menu' +import { useMemo } from 'react' +import { VscRepoForked, VscStarFull } from 'react-icons/vsc' +import { Card, FloatingFilter } from 'src/components/Elements' +import { ListRepoComponent } from 'src/components/List/ListRepoComponent' +import { dateRanges } from 'src/config' import { useUserPreferences } from 'src/stores/preferences' import { CardPropsType, Repository } from 'src/types' -import { filterUniqueEntries, getCardTagsValue } from 'src/utils/DataEnhancement' import { useGetGithubRepos } from '../../api/getGithubRepos' +import { CardSettings } from '../CardSettings' import RepoItem from './RepoItem' -export function GithubCard(props: CardPropsType) { - const { meta, withAds, knob } = 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 GLOBAL_TAG = { label: 'Global', value: 'global' } - const selectedDateRange = - dateRanges.find((date) => date.value === cardsSettings?.[meta.value]?.dateRange) || - dateRanges[0] +export function GithubCard(props: CardPropsType) { + const { meta } = props + const userSelectedTags = useUserPreferences((state) => state.userSelectedTags) + const setCardSettings = useUserPreferences((state) => state.setCardSettings) + const cardSettings = useUserPreferences((state) => state.cardsSettings?.[meta.value]) - const getQueryTags = () => { - if (!selectedTag?.githubValues) { - return [] - } + const selectedTag = useMemo( + () => userSelectedTags.find((lang) => lang.value === cardSettings?.language) || GLOBAL_TAG, + [userSelectedTags, cardSettings] + ) - if (selectedTag.value === MY_LANGUAGES_TAG.githubValues[0]) { - return getCardTagsValue(userSelectedTags, 'githubValues') - } - return selectedTag.githubValues - } + const selectedDateRange = useMemo( + () => dateRanges.find((date) => date.value === cardSettings?.dateRange) || dateRanges[0], + [cardSettings] + ) - const results = useGetGithubRepos({ - tags: getQueryTags(), + const { data, error, isLoading } = useGetGithubRepos({ + tag: selectedTag.value, dateRange: selectedDateRange.value, config: { - enabled: !!selectedTag?.githubValues, + enabled: !!selectedTag?.value, }, }) - const getIsLoading = () => results.some((result) => result.isLoading) - - const getData = () => { - return filterUniqueEntries( - results.reduce((acc: Repository[], curr) => { - if (!curr.data) return acc - return [...acc, ...curr.data] - }, []) - ) - } - const renderItem = (item: Repository, index: number) => ( { return ( - <> - ({ - label: tag.label, - value: tag.value, - }))} - onChange={(item) => { - setCardSettings(meta.value, { ...cardsSettings[meta.value], language: item.value }) - trackCardLanguageSelect(meta.analyticsTag, item.value) - }} - value={cardsSettings?.[meta.value]?.language} - /> - Repos of - { - setCardSettings(meta.value, { ...cardsSettings[meta.value], dateRange: item.value }) - trackCardDateRangeSelect(meta.analyticsTag, item.value) - }} - value={cardsSettings?.[meta.value]?.dateRange} - /> - +

+ Github {selectedTag.label}{' '} + {selectedDateRange.label.toLowerCase()} +
) } - const getError = () => { - if (!selectedTag?.githubValues) { - return `Github Trending does not support ${selectedTag?.label || 'the selected tag'}.` - } else if (results.every((result) => result.isError)) { - return 'Failed to load Github trending repositories' - } else { - return undefined - } - } return ( - } {...props}> + } + settingsComponent={ + + {dateRanges.map((date) => ( + { + setCardSettings(meta.value, { ...cardSettings, dateRange: date.value }) + }}> + {date.label} + + ))} + + + } + sortOptions={[ + { + label: 'Stars', + value: 'stars_count', + icon: , + }, + { + label: 'Forks', + value: 'forks_count', + icon: , + }, + ]} + /> + } + {...props}> - diff --git a/src/features/cards/components/githubCard/RepoItem.tsx b/src/features/cards/components/githubCard/RepoItem.tsx index bec834a9..5a52a6f8 100644 --- a/src/features/cards/components/githubCard/RepoItem.tsx +++ b/src/features/cards/components/githubCard/RepoItem.tsx @@ -1,10 +1,10 @@ -import { CardLink, CardItemWithActions } from 'src/components/Elements' +import { CardItemWithActions, CardLink } from 'src/components/Elements' -import { Attributes } from 'src/lib/analytics' -import { ColoredLanguagesBadge } from 'src/components/Elements' import { VscRepo, VscRepoForked, VscStarFull } from 'react-icons/vsc' -import { BaseItemPropsType, Repository } from 'src/types' +import { ColoredLanguagesBadge } from 'src/components/Elements' +import { Attributes } from 'src/lib/analytics' import { useUserPreferences } from 'src/stores/preferences' +import { BaseItemPropsType, Repository } from 'src/types' function numberWithCommas(x: number | string) { return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') @@ -25,28 +25,29 @@ const RepoItem = ({ item, index, selectedTag, analyticsTag }: BaseItemPropsType< className="githubTitle" link={item.url} analyticsAttributes={{ - [Attributes.POINTS]: item.stars, + [Attributes.POINTS]: item.stars_count, [Attributes.TRIGERED_FROM]: 'card', - [Attributes.TITLE]: item.title, + [Attributes.TITLE]: item.name, [Attributes.LINK]: item.url, [Attributes.SOURCE]: analyticsTag, [Attributes.LANGUAGE]: selectedTag?.value, }}> - {item.title} + {item.owner}/{item.name}

{item.description}

{listingMode === 'normal' && (
- - {numberWithCommas(item.stars) && ( + + {numberWithCommas(item.stars_count) && ( - {numberWithCommas(item.stars)} stars + {numberWithCommas(item.stars_count)} stars )} - {item.forks && ( + {item.forks_count && ( - {numberWithCommas(item.forks)} forks + {numberWithCommas(item.forks_count)}{' '} + forks )}
diff --git a/src/features/cards/components/hackernewsCard/ArticleItem.tsx b/src/features/cards/components/hackernewsCard/ArticleItem.tsx index aefc8dfd..45a07d73 100644 --- a/src/features/cards/components/hackernewsCard/ArticleItem.tsx +++ b/src/features/cards/components/hackernewsCard/ArticleItem.tsx @@ -24,7 +24,7 @@ const ArticleItem = (props: BaseItemPropsType
) => { ) => { {listingMode === 'compact' && ( - {item.reactions} + {item.points_count} )} @@ -43,7 +43,7 @@ const ArticleItem = (props: BaseItemPropsType
) => { {listingMode === 'normal' && (
- {item.reactions} points + {item.points_count} points {format(new Date(item.published_at))} @@ -52,13 +52,13 @@ const ArticleItem = (props: BaseItemPropsType
) => { link={`https://news.ycombinator.com/item?id=${item.id}`} className="rowItem rowItemClickable" analyticsAttributes={{ - [Attributes.POINTS]: item.reactions, + [Attributes.POINTS]: item.comments_count, [Attributes.TRIGERED_FROM]: 'card', [Attributes.TITLE]: `${item.title} comments`, [Attributes.LINK]: `https://news.ycombinator.com/item?id=${item.id}`, [Attributes.SOURCE]: analyticsTag, }}> - {item.comments} comments + {item.comments_count} comments
)} diff --git a/src/features/cards/components/hackernewsCard/HackernewsCard.tsx b/src/features/cards/components/hackernewsCard/HackernewsCard.tsx index d08a2160..85caa9d3 100644 --- a/src/features/cards/components/hackernewsCard/HackernewsCard.tsx +++ b/src/features/cards/components/hackernewsCard/HackernewsCard.tsx @@ -1,20 +1,54 @@ +import { BiCommentDetail, BiSolidCircle } from 'react-icons/bi' import { Card } from 'src/components/Elements' -import { ListComponent } from 'src/components/List' +import { ListPostComponent } from 'src/components/List/ListPostComponent' +import { useUserPreferences } from 'src/stores/preferences' import { Article, CardPropsType } from 'src/types' -import { useGetHackertNewsArticles } from '../../api/getHackerNewsArticles' +import { useGetSourceArticles } from '../../api/getSourceArticles' +import { CardSettings } from '../CardSettings' import ArticleItem from './ArticleItem' export function HackernewsCard(props: CardPropsType) { const { meta } = props - const { data: articles = [], isLoading, error } = useGetHackertNewsArticles() + const cardSettings = useUserPreferences((state) => state.cardsSettings[meta.value]) + const { data, isLoading, error } = useGetSourceArticles({ + source: 'hackernews', + }) const renderItem = (item: Article, index: number) => ( ) return ( - - + [ + ...defaults, + { + label: 'Points', + value: 'points_count', + icon: , + }, + { + label: 'Comments', + value: 'comments_count', + icon: , + }, + ]} + /> + }> + ) } diff --git a/src/features/cards/components/hashnodeCard/ArticleItem.tsx b/src/features/cards/components/hashnodeCard/ArticleItem.tsx index a3bc88b4..2165b620 100644 --- a/src/features/cards/components/hashnodeCard/ArticleItem.tsx +++ b/src/features/cards/components/hashnodeCard/ArticleItem.tsx @@ -1,11 +1,11 @@ import { BiCommentDetail } from 'react-icons/bi' -import { CardLink, CardItemWithActions } from 'src/components/Elements' +import { CardItemWithActions, CardLink } from 'src/components/Elements' -import { BaseItemPropsType, Article } from 'src/types' -import { format } from 'timeago.js' import { MdAccessTime } from 'react-icons/md' import { ColoredLanguagesBadge } from 'src/components/Elements' import { useUserPreferences } from 'src/stores/preferences' +import { Article, BaseItemPropsType } from 'src/types' +import { format } from 'timeago.js' import { AiTwotoneHeart } from 'react-icons/ai' import { Attributes } from 'src/lib/analytics' @@ -25,7 +25,7 @@ const ArticleItem = (props: BaseItemPropsType
) => { ) => { {listingMode === 'compact' && (
- {item.reactions || 0} + {item.points_count || 0}
)}
{item.title}
@@ -50,11 +50,11 @@ const ArticleItem = (props: BaseItemPropsType
) => { - {item.comments || 0} comments + {item.comments_count || 0} comments - {item.reactions || 0} reactions + {item.points_count || 0} reactions

diff --git a/src/features/cards/components/hashnodeCard/HashnodeCard.tsx b/src/features/cards/components/hashnodeCard/HashnodeCard.tsx index 58ca3c03..7a280981 100644 --- a/src/features/cards/components/hashnodeCard/HashnodeCard.tsx +++ b/src/features/cards/components/hashnodeCard/HashnodeCard.tsx @@ -1,46 +1,26 @@ -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 { AiTwotoneHeart } from 'react-icons/ai' +import { BiCommentDetail } from 'react-icons/bi' +import { Card, FloatingFilter } from 'src/components/Elements' +import { ListPostComponent } from 'src/components/List/ListPostComponent' 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 { CardSettings } from '../CardSettings' import ArticleItem from './ArticleItem' +const GLOBAL_TAG = { label: 'Global', value: 'programming' } + export function HashnodeCard(props: CardPropsType) { const { meta } = props - const { userSelectedTags, cardsSettings, setCardSettings } = useUserPreferences() + const cardSettings = useUserPreferences((state) => state.cardsSettings?.[meta.value]) + const { userSelectedTags } = 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() }) + userSelectedTags.find((lang) => lang.value === cardSettings?.language) || GLOBAL_TAG - const getIsLoading = () => results.some((result) => result.isLoading) - - 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 { data, isLoading } = useGetSourceArticles({ + source: 'hashnode', + tags: [selectedTag.value], + }) const renderItem = (item: Article, index: number) => ( {meta.label} - ({ - label: tag.label, - value: tag.value, - }))} - onChange={(item) => { - setCardSettings(meta.value, { ...cardsSettings[meta.value], language: item.value }) - trackCardLanguageSelect(meta.analyticsTag, item.value) - }} - value={cardsSettings?.[meta.value]?.language} - /> + {selectedTag.value != GLOBAL_TAG.value && ( + {selectedTag.label} + )} ) } return ( - } {...props}> + } + settingsComponent={ + [ + ...defaults, + { + label: 'Reactions', + value: 'points_count', + icon: , + }, + { + label: 'Comments', + value: 'comments_count', + icon: , + }, + ]} + /> + } + {...props}> - + ) } diff --git a/src/features/cards/components/indiehackersCard/ArticleItem.tsx b/src/features/cards/components/indiehackersCard/ArticleItem.tsx index fa0248fd..fca31da1 100644 --- a/src/features/cards/components/indiehackersCard/ArticleItem.tsx +++ b/src/features/cards/components/indiehackersCard/ArticleItem.tsx @@ -1,12 +1,12 @@ -import { format } from 'timeago.js' -import { VscTriangleUp } from 'react-icons/vsc' import { BiCommentDetail } from 'react-icons/bi' +import { FaChevronUp } from 'react-icons/fa' import { MdAccessTime } from 'react-icons/md' -import { CardLink, CardItemWithActions } from 'src/components/Elements' +import { VscTriangleUp } from 'react-icons/vsc' +import { CardItemWithActions, CardLink } from 'src/components/Elements' import { Attributes } from 'src/lib/analytics' -import { BaseItemPropsType, Article } from 'src/types' -import { FaChevronUp } from 'react-icons/fa' import { useUserPreferences } from 'src/stores/preferences' +import { Article, BaseItemPropsType } from 'src/types' +import { format } from 'timeago.js' export const ArticleItem = (props: BaseItemPropsType

) => { const { item, index, analyticsTag } = props @@ -24,7 +24,7 @@ export const ArticleItem = (props: BaseItemPropsType
) => { ) => { {listingMode === 'compact' && ( - {item.reactions} + {item.points_count} )} @@ -43,13 +43,13 @@ export const ArticleItem = (props: BaseItemPropsType
) => { {listingMode === 'normal' && (
- {item.reactions} points + {item.points_count} points {format(new Date(item.published_at))} - {item.comments} comments + {item.comments_count} comments
)} diff --git a/src/features/cards/components/indiehackersCard/IndiehackersCard.tsx b/src/features/cards/components/indiehackersCard/IndiehackersCard.tsx index 3309496f..c9161073 100644 --- a/src/features/cards/components/indiehackersCard/IndiehackersCard.tsx +++ b/src/features/cards/components/indiehackersCard/IndiehackersCard.tsx @@ -1,20 +1,50 @@ +import { FaChevronUp } from 'react-icons/fa' import { Card } from 'src/components/Elements' -import { ListComponent } from 'src/components/List' +import { ListPostComponent } from 'src/components/List/ListPostComponent' +import { useUserPreferences } from 'src/stores/preferences' import { Article, CardPropsType } from 'src/types' -import { useGetIndieHackersArticles } from '../../api/getIndieHackersArticles' +import { useGetSourceArticles } from '../../api/getSourceArticles' +import { CardSettings } from '../CardSettings' import { ArticleItem } from './ArticleItem' export function IndiehackersCard(props: CardPropsType) { const { meta } = props - const { data: articles = [], isLoading, error } = useGetIndieHackersArticles() + const cardSettings = useUserPreferences((state) => state.cardsSettings[meta.value]) + + const { data, isLoading, error } = useGetSourceArticles({ + source: 'indiehackers', + }) const renderItem = (item: Article, index: number) => ( - + ) return ( - - + [ + ...defaults, + { + label: 'Points', + value: 'points_count', + icon: , + }, + ]} + /> + } + {...props}> + ) } diff --git a/src/features/cards/components/lobstersCard/ArticleItem.tsx b/src/features/cards/components/lobstersCard/ArticleItem.tsx index 919d4423..13349aec 100644 --- a/src/features/cards/components/lobstersCard/ArticleItem.tsx +++ b/src/features/cards/components/lobstersCard/ArticleItem.tsx @@ -23,7 +23,7 @@ const ArticleItem = ({ item, index, analyticsTag }: BaseItemPropsType
) ) {listingMode === 'compact' && (
- {item.reactions} + {item.points_count}
)} @@ -42,7 +42,7 @@ const ArticleItem = ({ item, index, analyticsTag }: BaseItemPropsType
) {listingMode === 'normal' && (
- {item.reactions} points + {item.points_count} points {format(new Date(item.published_at))} @@ -51,13 +51,13 @@ const ArticleItem = ({ item, index, analyticsTag }: BaseItemPropsType
) link={item.comments_url as string} className="rowItem rowItemClickable" analyticsAttributes={{ - [Attributes.POINTS]: item.reactions, + [Attributes.POINTS]: item.points_count, [Attributes.TRIGERED_FROM]: 'card', [Attributes.TITLE]: `${item.title} comments`, [Attributes.LINK]: item.comments_url, [Attributes.SOURCE]: analyticsTag, }}> - {item.comments} comments + {item.comments_count} comments
)} diff --git a/src/features/cards/components/lobstersCard/LobstersCard.tsx b/src/features/cards/components/lobstersCard/LobstersCard.tsx index b9e9c679..5edab3da 100644 --- a/src/features/cards/components/lobstersCard/LobstersCard.tsx +++ b/src/features/cards/components/lobstersCard/LobstersCard.tsx @@ -1,20 +1,49 @@ +import { BiSolidCircle } from 'react-icons/bi' import { Card } from 'src/components/Elements' -import { ListComponent } from 'src/components/List' +import { ListPostComponent } from 'src/components/List/ListPostComponent' +import { useUserPreferences } from 'src/stores/preferences' import { Article, CardPropsType } from 'src/types' -import { useGetLobstersArticles } from '../../api/getLobstersArticles' +import { useGetSourceArticles } from '../../api/getSourceArticles' +import { CardSettings } from '../CardSettings' import ArticleItem from './ArticleItem' export function LobstersCard(props: CardPropsType) { const { meta } = props - const { data: articles = [], isLoading, error } = useGetLobstersArticles() + const cardSettings = useUserPreferences((state) => state.cardsSettings[meta.value]) + const { data, isLoading, error } = useGetSourceArticles({ + source: 'lobsters', + }) const renderItem = (item: Article, index: number) => ( - + ) return ( - - + [ + ...defaults, + { + label: 'Points', + value: 'points_count', + icon: , + }, + ]} + /> + }> + ) } diff --git a/src/features/cards/components/mediumCard/ArticleItem.tsx b/src/features/cards/components/mediumCard/ArticleItem.tsx index 492ba06c..3330c604 100644 --- a/src/features/cards/components/mediumCard/ArticleItem.tsx +++ b/src/features/cards/components/mediumCard/ArticleItem.tsx @@ -1,11 +1,10 @@ import { BiCommentDetail } from 'react-icons/bi' -import { CardLink, CardItemWithActions } from 'src/components/Elements' +import { MdAccessTime, MdWavingHand } from 'react-icons/md' +import { CardItemWithActions, CardLink } from 'src/components/Elements' import { Attributes } from 'src/lib/analytics' -import { BaseItemPropsType, Article } from 'src/types' import { useUserPreferences } from 'src/stores/preferences' +import { Article, BaseItemPropsType } from 'src/types' import { format } from 'timeago.js' -import { MdAccessTime } from 'react-icons/md' -import { MdWavingHand } from 'react-icons/md' const ArticleItem = ({ item, index, selectedTag, analyticsTag }: BaseItemPropsType
) => { const { listingMode } = useUserPreferences() @@ -21,7 +20,7 @@ const ArticleItem = ({ item, index, selectedTag, analyticsTag }: BaseItemPropsTy - {item.reactions || 0} + {item.points_count || 0}
)}
{item.title}
@@ -40,14 +39,13 @@ const ArticleItem = ({ item, index, selectedTag, analyticsTag }: BaseItemPropsTy {listingMode === 'normal' && (

- {item.reactions || 0} claps + {item.points_count || 0} claps - {item.comments || 0} comments + {item.comments_count || 0} comments - - {format(new Date(item.published_at))} + {format(new Date(item.published_at))}

)} diff --git a/src/features/cards/components/mediumCard/MediumCard.tsx b/src/features/cards/components/mediumCard/MediumCard.tsx index d8eeb9f2..841a9c78 100644 --- a/src/features/cards/components/mediumCard/MediumCard.tsx +++ b/src/features/cards/components/mediumCard/MediumCard.tsx @@ -1,44 +1,26 @@ -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 { BiCommentDetail } from 'react-icons/bi' +import { MdWavingHand } from 'react-icons/md' +import { Card, FloatingFilter } from 'src/components/Elements' +import { ListPostComponent } from 'src/components/List/ListPostComponent' import { useUserPreferences } from 'src/stores/preferences' import { Article, CardPropsType } from 'src/types' -import { filterUniqueEntries, getCardTagsValue } from 'src/utils/DataEnhancement' -import { useGetMediumArticles } from '../../api/getMediumArticles' +import { useGetSourceArticles } from '../../api/getSourceArticles' +import { CardSettings } from '../CardSettings' import ArticleItem from './ArticleItem' +const GLOBAL_TAG = { label: 'Global', value: 'programming' } + export function MediumCard(props: CardPropsType) { - const { meta, withAds } = props - const { userSelectedTags, cardsSettings, setCardSettings } = useUserPreferences() + const { meta } = props + const cardSettings = useUserPreferences((state) => state.cardsSettings?.[meta.value]) + const { userSelectedTags } = 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.mediumValues[0]) { - return getCardTagsValue(userSelectedTags, 'mediumValues') - } - return selectedTag.mediumValues - } - - const results = useGetMediumArticles({ tags: getQueryTags() }) + userSelectedTags.find((lang) => lang.value === cardSettings?.language) || GLOBAL_TAG - const getIsLoading = () => results.some((result) => result.isLoading) - - const getData = () => { - return filterUniqueEntries( - results.reduce((acc: Article[], curr) => { - if (!curr.data) return acc - return [...acc, ...curr.data] - }, []) - ) - } + const { data, isLoading } = useGetSourceArticles({ + source: 'medium', + tags: [selectedTag.value], + }) const renderItem = (item: Article, index: number) => ( {meta.label} - ({ - label: tag.label, - value: tag.value, - }))} - onChange={(item) => { - setCardSettings(meta.value, { ...cardsSettings[meta.value], language: item.value }) - trackCardLanguageSelect(meta.analyticsTag, item.value) - }} - value={cardsSettings?.[meta.value]?.language} - /> + {selectedTag.value != GLOBAL_TAG.value && ( + {selectedTag.label} + )} ) } return ( - } {...props}> + } + settingsComponent={ + [ + ...defaults, + { + label: 'Claps', + value: 'points_count', + icon: , + }, + { + label: 'Comments', + value: 'comments_count', + icon: , + }, + ]} + /> + } + {...props}> - + ) } diff --git a/src/features/cards/components/producthuntCard/ArticleItem.tsx b/src/features/cards/components/producthuntCard/ArticleItem.tsx index e1198730..4dd8a5a1 100644 --- a/src/features/cards/components/producthuntCard/ArticleItem.tsx +++ b/src/features/cards/components/producthuntCard/ArticleItem.tsx @@ -1,9 +1,9 @@ import { BiCommentDetail } from 'react-icons/bi' import { VscTriangleUp } from 'react-icons/vsc' -import { CardLink, CardItemWithActions } from 'src/components/Elements' +import { CardItemWithActions, CardLink } from 'src/components/Elements' import { Attributes } from 'src/lib/analytics' -import { BaseItemPropsType, Article } from 'src/types' import { useUserPreferences } from 'src/stores/preferences' +import { Article, BaseItemPropsType } from 'src/types' const ArticleItem = ({ item, index, analyticsTag }: BaseItemPropsType
) => { const { listingMode } = useUserPreferences() @@ -22,7 +22,7 @@ const ArticleItem = ({ item, index, analyticsTag }: BaseItemPropsType
) link={item.url} appendRef={false} analyticsAttributes={{ - [Attributes.POINTS]: item.reactions, + [Attributes.POINTS]: item.votes_count, [Attributes.TRIGERED_FROM]: 'card', [Attributes.TITLE]: item.title, [Attributes.LINK]: item.url, @@ -30,12 +30,12 @@ const ArticleItem = ({ item, index, analyticsTag }: BaseItemPropsType
) }}> {item.title} -

{item.description}

+

{item.tagline}

{listingMode === 'normal' && (

- {item.comments || 0} comments + {item.comments_count || 0} comments {item.tags && item.tags.length > 0 ? ( {item.tags[0]} @@ -45,7 +45,7 @@ const ArticleItem = ({ item, index, analyticsTag }: BaseItemPropsType

)
- {item.reactions} + {item.votes_count}
} diff --git a/src/features/cards/components/producthuntCard/ProducthuntCard.tsx b/src/features/cards/components/producthuntCard/ProducthuntCard.tsx index 1043006d..0a5574a9 100644 --- a/src/features/cards/components/producthuntCard/ProducthuntCard.tsx +++ b/src/features/cards/components/producthuntCard/ProducthuntCard.tsx @@ -1,17 +1,24 @@ +import { BiCommentDetail } from 'react-icons/bi' +import { VscTriangleUp } from 'react-icons/vsc' import { Card } from 'src/components/Elements' import { ListComponent } from 'src/components/List' import { ProductHuntPlaceholder } from 'src/components/placeholders' +import { useUserPreferences } from 'src/stores/preferences' import { Article, CardPropsType } from 'src/types' import { useGeProductHuntProducts } from '../../api/getProductHuntProducts' +import { CardSettings } from '../CardSettings' import ArticleItem from './ArticleItem' export function ProductHuntCard(props: CardPropsType) { const { meta } = props + const cardSettings = useUserPreferences((state) => state.cardsSettings?.[meta.value]) + const { data: products = [], isLoading, error, } = useGeProductHuntProducts({ + date: new Date().toISOString().split('T')[0], config: { staleTime: 900000, //15 minutes cacheTime: 3600000, // 1 Day @@ -19,11 +26,32 @@ export function ProductHuntCard(props: CardPropsType) { }) const renderItem = (item: Article, index: number) => ( - + ) return ( - + , + }, + { + label: 'Comments', + value: 'comments_count', + icon: , + }, + ]} + /> + }> { const ArticleItem = ({ item, index, analyticsTag }: BaseItemPropsType
) => { const { listingMode } = useUserPreferences() + + const subReddit = useMemo(() => { + const parts = item.url.split('/') + const rIndex = parts.findIndex((part) => part.toLowerCase() === 'r') + if (rIndex !== -1 && parts.length > rIndex + 1) { + return parts[rIndex + 1] + } + return null + }, [item.url]) + return ( ) ) {listingMode === 'compact' && (
- {item.reactions} + {item.points_count}
)} @@ -67,17 +78,19 @@ const ArticleItem = ({ item, index, analyticsTag }: BaseItemPropsType
) {listingMode === 'normal' && ( <> - {item.reactions} points + {item.points_count} upvotes {format(new Date(item.published_at))} - {item.comments} comments - - - {`r/${item.subreddit}`} + {item.comments_count} comments + {subReddit && ( + + {`r/${subReddit}`} + + )} )} diff --git a/src/features/cards/components/redditCard/RedditCard.tsx b/src/features/cards/components/redditCard/RedditCard.tsx index a892d368..a70132ec 100644 --- a/src/features/cards/components/redditCard/RedditCard.tsx +++ b/src/features/cards/components/redditCard/RedditCard.tsx @@ -1,51 +1,34 @@ -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 { useMemo } from 'react' +import { VscTriangleUp } from 'react-icons/vsc' +import { Card, FloatingFilter } from 'src/components/Elements' +import { ListPostComponent } from 'src/components/List/ListPostComponent' + import { useUserPreferences } from 'src/stores/preferences' import { Article, CardPropsType } from 'src/types' -import { filterUniqueEntries, getCardTagsValue } from 'src/utils/DataEnhancement' -import { useGetRedditArticles } from '../../api/getRedditArticles' +import { useGetSourceArticles } from '../../api/getSourceArticles' +import { CardSettings } from '../CardSettings' import ArticleItem from './ArticleItem' +const GLOBAL_TAG = { label: 'Global', value: 'global' } + export function RedditCard(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.redditValues[0]) { - return getCardTagsValue(userSelectedTags, 'redditValues') || [] - } - return selectedTag.redditValues || [] - } - - const results = useGetRedditArticles({ tags: getQueryTags() }) - - const getIsLoading = () => results.some((result) => result.isLoading) + const cardSettings = useUserPreferences((state) => state.cardsSettings[meta.value]) + const { userSelectedTags } = useUserPreferences() + const selectedTag = useMemo( + () => userSelectedTags.find((lang) => lang.value === cardSettings?.language) || GLOBAL_TAG, + [userSelectedTags, cardSettings] + ) - const getData = () => { - return filterUniqueEntries( - results - .reduce((acc: Article[], curr) => { - if (!curr.data) return acc - return [...acc, ...curr.data] - }, []) - .sort((a, b) => b.reactions - a.reactions) - ) - } + const { isLoading, data: results } = useGetSourceArticles({ + source: 'reddit', + tags: [selectedTag.value], + }) const renderItem = (item: Article, index: number) => ( { return ( - <> - {meta.label} - ({ - label: tag.label, - value: tag.value, - }))} - onChange={(item) => { - setCardSettings(meta.value, { ...cardsSettings[meta.value], language: item.value }) - trackCardLanguageSelect(meta.analyticsTag, item.value) - }} - value={cardsSettings?.[meta.value]?.language} - /> - +
+ Reddit{' '} + {selectedTag.value != GLOBAL_TAG.value && ( + {selectedTag.label} + )} +
) } return ( - } {...props}> + } + {...props} + settingsComponent={ + [ + ...defaults, + { + label: 'Upvotes', + value: 'points_count', + icon: , + }, + ]} + /> + }> - + ) } diff --git a/src/features/cards/components/rssCard/CustomRssCard.tsx b/src/features/cards/components/rssCard/CustomRssCard.tsx index 4785ebc5..99a20b9c 100644 --- a/src/features/cards/components/rssCard/CustomRssCard.tsx +++ b/src/features/cards/components/rssCard/CustomRssCard.tsx @@ -1,7 +1,8 @@ import { Card } from 'src/components/Elements' -import { ListComponent } from 'src/components/List' +import { ListPostComponent } from 'src/components/List/ListPostComponent' import { Article, CardPropsType } from 'src/types' import { useRssFeed } from '../../api/getRssFeed' +import { CardSettings } from '../CardSettings' import ArticleItem from './ArticleItem' import CardIcon from './CardIcon' @@ -25,8 +26,16 @@ export function CustomRssCard(props: CardPropsType) { } {...props} - meta={{ ...meta, icon: }}> - + meta={{ ...meta, icon: }} + settingsComponent={ + + }> + ) } diff --git a/src/utils/DataEnhancement.ts b/src/utils/DataEnhancement.ts index a61ba5f9..68fb97c1 100644 --- a/src/utils/DataEnhancement.ts +++ b/src/utils/DataEnhancement.ts @@ -3,7 +3,7 @@ import { RemoteConfig, Tag } from 'src/features/remoteConfig' export const enhanceTags = (remoteConfigStore: RemoteConfig, tags: string[]): Tag[] => { return tags .map((tag) => - remoteConfigStore.supportedTags.find((st) => st.value.toLowerCase() === tag.toLocaleString()) + remoteConfigStore.tags.find((st) => st.value.toLowerCase() === tag.toLocaleString()) ) .filter(Boolean) as Tag[] } From cd2c3907daef6cd7d86dd47ff2cf9d2b7b0e201f Mon Sep 17 00:00:00 2001 From: John Doe Date: Sat, 1 Nov 2025 20:47:40 +0100 Subject: [PATCH 16/89] feat: enhance ConferencesItem and ConferencesCard components with improved date handling and subtitle display --- .../cards/components/conferencesCard/ConferenceItem.tsx | 8 +++++--- .../cards/components/conferencesCard/ConferencesCard.tsx | 8 +++++++- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/features/cards/components/conferencesCard/ConferenceItem.tsx b/src/features/cards/components/conferencesCard/ConferenceItem.tsx index 3f0823e9..07491795 100644 --- a/src/features/cards/components/conferencesCard/ConferenceItem.tsx +++ b/src/features/cards/components/conferencesCard/ConferenceItem.tsx @@ -83,8 +83,10 @@ const ConferencesItem = ({ item, index, analyticsTag }: BaseItemPropsType - {conferenceLocation?.icon} - {item.title} +
+ {differenceInDays < 0 && Ended}{' '} + {conferenceLocation?.icon} {item.title} +
{listingMode === 'normal' ? ( <> @@ -98,7 +100,7 @@ const ConferencesItem = ({ item, index, analyticsTag }: BaseItemPropsType
diff --git a/src/features/cards/components/conferencesCard/ConferencesCard.tsx b/src/features/cards/components/conferencesCard/ConferencesCard.tsx index e1aa745f..31e28392 100644 --- a/src/features/cards/components/conferencesCard/ConferencesCard.tsx +++ b/src/features/cards/components/conferencesCard/ConferencesCard.tsx @@ -1,3 +1,4 @@ +import { useMemo } from 'react' import { Card } from 'src/components/Elements' import { ListConferenceComponent } from 'src/components/List/ListConferenceComponent' import { useUserPreferences } from 'src/stores/preferences' @@ -10,8 +11,13 @@ export function ConferencesCard(props: CardPropsType) { const { meta } = props const cardSettings = useUserPreferences((state) => state.cardsSettings?.[meta.value]) const { userSelectedTags } = useUserPreferences() + + const selectedTag = useMemo(() => { + return userSelectedTags.find((lang) => lang.value === cardSettings?.language) + }, [userSelectedTags, cardSettings]) + const { isLoading, data: results } = useGetConferences({ - tags: userSelectedTags.map((tag) => tag.value), + tags: selectedTag ? [selectedTag.value] : userSelectedTags.map((tag) => tag.value), }) const renderItem = (item: Conference, index: number) => ( From 2309f4a4aa4109f045f953cfed4639f25ea8f992 Mon Sep 17 00:00:00 2001 From: John Doe Date: Sun, 2 Nov 2025 23:08:02 +0100 Subject: [PATCH 17/89] feat: add providerId handling and last connection indicator in AuthModal --- src/features/auth/components/AuthModal.tsx | 6 +++++- src/features/auth/components/authModal.css | 14 +++++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/features/auth/components/AuthModal.tsx b/src/features/auth/components/AuthModal.tsx index be527d4d..e555c850 100644 --- a/src/features/auth/components/AuthModal.tsx +++ b/src/features/auth/components/AuthModal.tsx @@ -20,7 +20,7 @@ const githubAuthProvider = new GithubAuthProvider() type AuthProvider = GoogleAuthProvider | GithubAuthProvider export const AuthModal = ({ showAuth }: AuthModalProps) => { - const { closeAuthModal, authError, setAuthError } = useAuth() + const { closeAuthModal, authError, setAuthError, providerId } = useAuth() const [selectedProvider, setSelectedProvider] = useState(googleAuthProvider) const getOauthLink = useGetOauthLink() @@ -96,14 +96,18 @@ export const AuthModal = ({ showAuth }: AuthModalProps) => { onClick={() => { signIn(githubAuthProvider) }} + className="relative" size="medium"> + {providerId === 'github.com' && Last} Connect with Github
diff --git a/src/features/auth/components/authModal.css b/src/features/auth/components/authModal.css index 9a3ccfb8..df42f150 100644 --- a/src/features/auth/components/authModal.css +++ b/src/features/auth/components/authModal.css @@ -16,7 +16,19 @@ width: 100%; margin: 0 0 20px 0; } - +.authModal .relative { + position: relative; +} +.authModal .lastFlag { + position: absolute; + right: -6px; + top: -6px; + background: var(--tooltip-accent-color); + font-size: 0.7em; + color: white; + border-radius: 12px; + padding: 0 4px; +} .authModal .description { padding: 20px; text-align: center; From 77ec1c1fc7edf091bc69af39689a354857b6406c Mon Sep 17 00:00:00 2001 From: John Doe Date: Mon, 3 Nov 2025 22:02:21 +0100 Subject: [PATCH 18/89] feat: conditionally render OnboardingModal based on onboarding completion status --- src/App.tsx | 13 ++++++++++--- .../onboarding/components/OnboardingModal.tsx | 6 ++---- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 7d68c6b5..a17401b7 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -24,8 +24,15 @@ const intersectionCallback = (entries: IntersectionObserverEntry[]) => { } export const App = () => { - const { maxVisibleCards, setAdvStatus, isDNDModeActive, layout, DNDDuration, setDNDDuration } = - useUserPreferences() + const { + maxVisibleCards, + onboardingCompleted, + setAdvStatus, + isDNDModeActive, + layout, + DNDDuration, + setDNDDuration, + } = useUserPreferences() useLayoutEffect(() => { document.documentElement.style.setProperty('--max-visible-cards', maxVisibleCards.toString()) @@ -69,7 +76,7 @@ export const App = () => { return ( <> - + {!onboardingCompleted && }
{ - const { onboardingCompleted } = useUserPreferences() - useEffect(() => { trackOnboardingStart() }, []) + return ( Date: Mon, 3 Nov 2025 22:02:56 +0100 Subject: [PATCH 19/89] feat: integrate Sentry for session expiration tracking and enhance user data handling --- src/providers/AuthProvider.tsx | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/providers/AuthProvider.tsx b/src/providers/AuthProvider.tsx index 92a4fc92..1ec0b28b 100644 --- a/src/providers/AuthProvider.tsx +++ b/src/providers/AuthProvider.tsx @@ -1,3 +1,4 @@ +import * as Sentry from '@sentry/react' import { GithubAuthProvider, GoogleAuthProvider, @@ -17,6 +18,7 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => { closeAuthModal, initState, setAuthError, + user, openAuthModal, setConnecting, logout, @@ -42,6 +44,7 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => { initState({ user: { id: user.uid, + connectedAt: new Date(), name: user.displayName || 'Anonymous', imageURL: user.photoURL || '', }, @@ -148,10 +151,18 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => { * and logout the user if the session has expired */ useEffect(() => { - const unsubscribe = firebaseAuth.onAuthStateChanged(async (user) => { - if (!user) { + const unsubscribe = firebaseAuth.onAuthStateChanged(async (fbUser) => { + if (!fbUser) { if (isConnected) { toast('Session expired, please reconnect', { theme: 'dangerToast' }) + + Sentry.captureMessage(`Session expired`, { + level: 'info', + extra: { + fbID: user?.id || 'anonymous', + connectedAt: user?.connectedAt?.toISOString(), + }, + }) } await logout() From 73168a03439edecac27aaf72c55471e5bda7102c Mon Sep 17 00:00:00 2001 From: John Doe Date: Wed, 5 Nov 2025 23:42:56 +0100 Subject: [PATCH 20/89] feat: implement CardHeader component and refactor card components to utilize it for improved tag handling --- src/features/cards/components/CardHeader.tsx | 38 ++++++++++++++++++ .../cards/components/CardSettings.tsx | 34 +++++++++------- .../cards/components/devtoCard/DevtoCard.tsx | 39 ++++++------------- .../freecodecampCard/FreecodecampCard.tsx | 38 ++++++------------ .../components/githubCard/GithubCard.tsx | 37 ++++++++---------- .../components/hashnodeCard/HashnodeCard.tsx | 29 +++++--------- .../components/mediumCard/MediumCard.tsx | 29 +++++--------- .../components/redditCard/RedditCard.tsx | 32 +++++---------- .../components/rssCard/CustomRssCard.tsx | 18 ++++----- src/features/cards/hooks/useSelectedTags.tsx | 36 +++++++++++++++++ 10 files changed, 171 insertions(+), 159 deletions(-) create mode 100644 src/features/cards/components/CardHeader.tsx create mode 100644 src/features/cards/hooks/useSelectedTags.tsx diff --git a/src/features/cards/components/CardHeader.tsx b/src/features/cards/components/CardHeader.tsx new file mode 100644 index 00000000..4696573b --- /dev/null +++ b/src/features/cards/components/CardHeader.tsx @@ -0,0 +1,38 @@ +import { MY_LANGUAGES_OPTION } from '../config' + +type HeaderTitleProps = { + label: string + fallbackTag: { + label: string + value: string + } + selectedTag: { + label: string + value: string + } + children?: React.ReactNode +} +export const CardHeader = ({ label, fallbackTag, selectedTag, children }: HeaderTitleProps) => { + if (children) { + return <>{children} + } + + if (!selectedTag || selectedTag.value === fallbackTag.value) { + return <>{label} + } + + if (selectedTag.value === MY_LANGUAGES_OPTION.value) { + return ( + <> + {label} {MY_LANGUAGES_OPTION.label} + + ) + } + + return ( + <> + {label} + {selectedTag.label} + + ) +} diff --git a/src/features/cards/components/CardSettings.tsx b/src/features/cards/components/CardSettings.tsx index bb7ef69d..18501d57 100644 --- a/src/features/cards/components/CardSettings.tsx +++ b/src/features/cards/components/CardSettings.tsx @@ -8,6 +8,7 @@ import { LiaSortSolid } from 'react-icons/lia' import { MdDateRange } from 'react-icons/md' import { ref } from 'src/config' import { useUserPreferences } from 'src/stores/preferences' +import { MY_LANGUAGES_OPTION } from '../config' type SortOption = { label: string; value: string; icon?: React.ReactNode } @@ -24,7 +25,6 @@ type CardSettingsProps = { } const DEFAULT_SORT_OPTIONS = [{ label: 'Newest', value: 'published_at', icon: }] - export const CardSettings = ({ id, url, @@ -44,10 +44,13 @@ export const CardSettings = ({ const userTagsMemo = useMemo(() => { const newTags = userSelectedTags.sort((a, b) => a.label.localeCompare(b.label)) + let tags = [...newTags] if (globalTag) { - return [globalTag, ...newTags] + tags = [globalTag, ...tags] } - return newTags + + tags = [MY_LANGUAGES_OPTION, ...tags] + return tags }, [userSelectedTags, globalTag]) const resolvedSortOptions = @@ -78,17 +81,20 @@ export const CardSettings = ({ }> {userTagsMemo.map((tag) => ( - { - setCardSettings(id, { ...cardSettings, language: tag.value }) - }}> - {tag.label} - + <> + { + setCardSettings(id, { ...cardSettings, language: tag.value }) + }}> + {tag.label} + + {tag.label.toLowerCase() === 'global' && } + ))} )} diff --git a/src/features/cards/components/devtoCard/DevtoCard.tsx b/src/features/cards/components/devtoCard/DevtoCard.tsx index f33dcaf5..a0ca936d 100644 --- a/src/features/cards/components/devtoCard/DevtoCard.tsx +++ b/src/features/cards/components/devtoCard/DevtoCard.tsx @@ -1,11 +1,11 @@ -import { useMemo } from 'react' import { AiOutlineLike } from 'react-icons/ai' import { BiCommentDetail } from 'react-icons/bi' import { Card, FloatingFilter } from 'src/components/Elements' import { ListPostComponent } from 'src/components/List/ListPostComponent' -import { useUserPreferences } from 'src/stores/preferences' import { Article, CardPropsType } from 'src/types' import { useGetSourceArticles } from '../../api/getSourceArticles' +import { useSelectedTags } from '../../hooks/useSelectedTags' +import { CardHeader } from '../CardHeader' import { CardSettings } from '../CardSettings' import ArticleItem from './ArticleItem' @@ -13,12 +13,11 @@ const GLOBAL_TAG = { label: 'Global', value: 'programming' } export function DevtoCard(props: CardPropsType) { const { meta } = props - const { userSelectedTags } = useUserPreferences() - const cardSettings = useUserPreferences((state) => state.cardsSettings?.[meta.value]) - const selectedTag = useMemo(() => { - return userSelectedTags.find((lang) => lang.value === cardSettings?.language) || GLOBAL_TAG - }, [userSelectedTags, cardSettings]) + const { queryTags, selectedTag, cardSettings } = useSelectedTags({ + source: meta.value, + fallbackTag: GLOBAL_TAG, + }) const { data: results, @@ -26,34 +25,18 @@ export function DevtoCard(props: CardPropsType) { isLoading, } = useGetSourceArticles({ source: 'devto', - tags: [selectedTag.value], + tags: queryTags.map((tag) => tag.value), }) const renderItem = (item: Article, index: number) => ( - + ) - const HeaderTitle = () => { - if (selectedTag.value === GLOBAL_TAG.value) { - return <>{meta.label} - } - return ( - <> - {meta.label} - {selectedTag.label} - - ) - } - return ( } + titleComponent={ + + } settingsComponent={ state.cardsSettings?.[meta.value]) - - const selectedTag = useMemo( - () => userSelectedTags.find((lang) => lang.value === cardSettings?.language) || GLOBAL_TAG, - [userSelectedTags, cardSettings] - ) + const { queryTags, selectedTag, cardSettings } = useSelectedTags({ + source: meta.value, + fallbackTag: GLOBAL_TAG, + }) const { data, isLoading } = useGetSourceArticles({ source: 'freecodecamp', - tags: [selectedTag.value], + tags: queryTags.map((tag) => tag.value), }) const renderItem = (item: Article, index: number) => ( - + ) - const HeaderTitle = () => { - return ( - <> - {meta.label} - {selectedTag.label} - - ) - } - return ( } + titleComponent={ + + } settingsComponent={ state.userSelectedTags) - const setCardSettings = useUserPreferences((state) => state.setCardSettings) - const cardSettings = useUserPreferences((state) => state.cardsSettings?.[meta.value]) - const selectedTag = useMemo( - () => userSelectedTags.find((lang) => lang.value === cardSettings?.language) || GLOBAL_TAG, - [userSelectedTags, cardSettings] - ) + const setCardSettings = useUserPreferences((state) => state.setCardSettings) + const { queryTags, selectedTag, cardSettings } = useSelectedTags({ + source: meta.value, + fallbackTag: GLOBAL_TAG, + }) const selectedDateRange = useMemo( () => dateRanges.find((date) => date.value === cardSettings?.dateRange) || dateRanges[0], @@ -29,11 +29,8 @@ export function GithubCard(props: CardPropsType) { ) const { data, error, isLoading } = useGetGithubRepos({ - tag: selectedTag.value, + tags: queryTags.map((tag) => tag.value), dateRange: selectedDateRange.value, - config: { - enabled: !!selectedTag?.value, - }, }) const renderItem = (item: Repository, index: number) => ( @@ -46,19 +43,17 @@ export function GithubCard(props: CardPropsType) { /> ) - const HeaderTitle = () => { - return ( -
- Github {selectedTag.label}{' '} - {selectedDateRange.label.toLowerCase()} -
- ) - } - return ( } + titleComponent={ + +
+ Github {selectedTag.label}{' '} + {selectedDateRange.label.toLowerCase()} +
+
+ } settingsComponent={ state.cardsSettings?.[meta.value]) - const { userSelectedTags } = useUserPreferences() - const selectedTag = - userSelectedTags.find((lang) => lang.value === cardSettings?.language) || GLOBAL_TAG - + const { queryTags, selectedTag, cardSettings } = useSelectedTags({ + source: meta.value, + fallbackTag: GLOBAL_TAG, + }) const { data, isLoading } = useGetSourceArticles({ source: 'hashnode', - tags: [selectedTag.value], + tags: queryTags.map((tag) => tag.value), }) const renderItem = (item: Article, index: number) => ( @@ -32,20 +32,11 @@ export function HashnodeCard(props: CardPropsType) { /> ) - const HeaderTitle = () => { - return ( - <> - {meta.label} - {selectedTag.value != GLOBAL_TAG.value && ( - {selectedTag.label} - )} - - ) - } - return ( } + titleComponent={ + + } settingsComponent={ state.cardsSettings?.[meta.value]) - const { userSelectedTags } = useUserPreferences() - const selectedTag = - userSelectedTags.find((lang) => lang.value === cardSettings?.language) || GLOBAL_TAG - + const { queryTags, selectedTag, cardSettings } = useSelectedTags({ + source: meta.value, + fallbackTag: GLOBAL_TAG, + }) const { data, isLoading } = useGetSourceArticles({ source: 'medium', - tags: [selectedTag.value], + tags: queryTags.map((tag) => tag.value), }) const renderItem = (item: Article, index: number) => ( @@ -32,20 +32,11 @@ export function MediumCard(props: CardPropsType) { /> ) - const HeaderTitle = () => { - return ( - <> - {meta.label} - {selectedTag.value != GLOBAL_TAG.value && ( - {selectedTag.label} - )} - - ) - } - return ( } + titleComponent={ + + } settingsComponent={ state.cardsSettings[meta.value]) - const { userSelectedTags } = useUserPreferences() - const selectedTag = useMemo( - () => userSelectedTags.find((lang) => lang.value === cardSettings?.language) || GLOBAL_TAG, - [userSelectedTags, cardSettings] - ) - + const { queryTags, selectedTag, cardSettings } = useSelectedTags({ + source: meta.value, + fallbackTag: GLOBAL_TAG, + }) const { isLoading, data: results } = useGetSourceArticles({ source: 'reddit', - tags: [selectedTag.value], + tags: queryTags.map((tag) => tag.value), }) const renderItem = (item: Article, index: number) => ( @@ -35,20 +32,11 @@ export function RedditCard(props: CardPropsType) { /> ) - const HeaderTitle = () => { - return ( -
- Reddit{' '} - {selectedTag.value != GLOBAL_TAG.value && ( - {selectedTag.label} - )} -
- ) - } - return ( } + titleComponent={ + + } {...props} settingsComponent={ { + return ( + <> +

{title}

+ + ) +} + export function CustomRssCard(props: CardPropsType) { const { meta } = props const { data = [], isLoading } = useRssFeed({ feedUrl: meta.feedUrl || '' }) @@ -14,17 +22,9 @@ export function CustomRssCard(props: CardPropsType) { ) - const HeaderTitle = () => { - return ( - <> -

{meta.label}

- - ) - } - return ( } + titleComponent={} {...props} meta={{ ...meta, icon: }} settingsComponent={ diff --git a/src/features/cards/hooks/useSelectedTags.tsx b/src/features/cards/hooks/useSelectedTags.tsx new file mode 100644 index 00000000..f4c38c36 --- /dev/null +++ b/src/features/cards/hooks/useSelectedTags.tsx @@ -0,0 +1,36 @@ +import { useMemo } from 'react' +import { useUserPreferences } from 'src/stores/preferences' +import { MY_LANGUAGES_OPTION } from '../config' + +type useSelectedTagsProps = { + source: string + fallbackTag: { + label: string + value: string + } +} +export const useSelectedTags = ({ source, fallbackTag }: useSelectedTagsProps) => { + const cardSettings = useUserPreferences((state) => state.cardsSettings?.[source]) + const { userSelectedTags } = useUserPreferences() + + const selectedTags = useMemo(() => { + if (!cardSettings?.language) { + return [fallbackTag] + } + + if (cardSettings.language === MY_LANGUAGES_OPTION.value) { + return userSelectedTags + } + return [userSelectedTags.find((lang) => lang.value === cardSettings?.language) || fallbackTag] + }, [userSelectedTags, cardSettings]) + + return { + queryTags: selectedTags, + selectedTag: cardSettings?.language + ? [MY_LANGUAGES_OPTION, ...userSelectedTags].find( + (lang) => lang.value === cardSettings.language + ) || fallbackTag + : fallbackTag, + cardSettings, + } +} From ea26e1465c1bed6d630f3674d5730208bfa99706 Mon Sep 17 00:00:00 2001 From: John Doe Date: Thu, 6 Nov 2025 11:21:16 +0100 Subject: [PATCH 21/89] feat: add MY_LANGUAGES_OPTION configuration for language selection --- src/features/cards/config/index.ts | 1 + 1 file changed, 1 insertion(+) create mode 100644 src/features/cards/config/index.ts diff --git a/src/features/cards/config/index.ts b/src/features/cards/config/index.ts new file mode 100644 index 00000000..a088d661 --- /dev/null +++ b/src/features/cards/config/index.ts @@ -0,0 +1 @@ +export const MY_LANGUAGES_OPTION = { label: 'My Stack', value: 'myLangs' } From e0a5b052dd82b579f440ebdc6c99cc2dc89cc757 Mon Sep 17 00:00:00 2001 From: John Doe Date: Thu, 6 Nov 2025 11:21:33 +0100 Subject: [PATCH 22/89] feat: add connectedAt property to User type for tracking connection time --- src/features/auth/types/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/features/auth/types/index.ts b/src/features/auth/types/index.ts index 51b82333..77342c6f 100644 --- a/src/features/auth/types/index.ts +++ b/src/features/auth/types/index.ts @@ -1,6 +1,7 @@ export type User = { id: string name: string + connectedAt?: Date imageURL?: string streak?: number } From 3f0023474ad6569a3eca848f6aaa53b2d69f73f1 Mon Sep 17 00:00:00 2001 From: John Doe Date: Thu, 6 Nov 2025 11:21:47 +0100 Subject: [PATCH 23/89] refactor: remove CardHeader component from GithubCard and simplify title rendering --- .../cards/components/githubCard/GithubCard.tsx | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/features/cards/components/githubCard/GithubCard.tsx b/src/features/cards/components/githubCard/GithubCard.tsx index d81fb862..9211935f 100644 --- a/src/features/cards/components/githubCard/GithubCard.tsx +++ b/src/features/cards/components/githubCard/GithubCard.tsx @@ -8,7 +8,6 @@ import { useUserPreferences } from 'src/stores/preferences' import { CardPropsType, Repository } from 'src/types' import { useGetGithubRepos } from '../../api/getGithubRepos' import { useSelectedTags } from '../../hooks/useSelectedTags' -import { CardHeader } from '../CardHeader' import { CardSettings } from '../CardSettings' import RepoItem from './RepoItem' @@ -47,12 +46,10 @@ export function GithubCard(props: CardPropsType) { -
- Github {selectedTag.label}{' '} - {selectedDateRange.label.toLowerCase()} -
-
+
+ Github {selectedTag.label}{' '} + {selectedDateRange.label.toLowerCase()} +
} settingsComponent={ Date: Thu, 6 Nov 2025 11:33:37 +0100 Subject: [PATCH 24/89] fix: adjust tag order in userTagsMemo and ensure MenuDivider appears correctly for 'global' tag --- src/features/cards/components/CardSettings.tsx | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/features/cards/components/CardSettings.tsx b/src/features/cards/components/CardSettings.tsx index 18501d57..5eaab830 100644 --- a/src/features/cards/components/CardSettings.tsx +++ b/src/features/cards/components/CardSettings.tsx @@ -25,6 +25,8 @@ type CardSettingsProps = { } const DEFAULT_SORT_OPTIONS = [{ label: 'Newest', value: 'published_at', icon: }] +const SPECIAL_LABELS = ['global', MY_LANGUAGES_OPTION.label.toLowerCase()] + export const CardSettings = ({ id, url, @@ -46,10 +48,10 @@ export const CardSettings = ({ const newTags = userSelectedTags.sort((a, b) => a.label.localeCompare(b.label)) let tags = [...newTags] if (globalTag) { - tags = [globalTag, ...tags] + tags = [...tags, globalTag] } - tags = [MY_LANGUAGES_OPTION, ...tags] + tags = [...tags, MY_LANGUAGES_OPTION] return tags }, [userSelectedTags, globalTag]) @@ -63,6 +65,11 @@ export const CardSettings = ({ window.open(link, openLinksNewTab ? '_blank' : '_self') }, [url, openLinksNewTab]) + const firstSpecialIndex = useMemo( + () => userTagsMemo.findIndex((tag) => SPECIAL_LABELS.includes(tag.label.toLowerCase())), + [userTagsMemo] + ) + return ( } @@ -80,8 +87,9 @@ export const CardSettings = ({ Language }> - {userTagsMemo.map((tag) => ( + {userTagsMemo.map((tag, i) => ( <> + {i === firstSpecialIndex && } {tag.label} - {tag.label.toLowerCase() === 'global' && } ))} From a0450a77227425c6f6240139dcb9e1bba5147f32 Mon Sep 17 00:00:00 2001 From: John Doe Date: Thu, 6 Nov 2025 11:38:41 +0100 Subject: [PATCH 25/89] refactor: update ConferencesCard and ConferenceItem components for improved tag handling and rendering --- .../conferencesCard/ConferenceItem.tsx | 6 +++--- .../conferencesCard/ConferencesCard.tsx | 21 ++++++++++--------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/features/cards/components/conferencesCard/ConferenceItem.tsx b/src/features/cards/components/conferencesCard/ConferenceItem.tsx index 07491795..054452d8 100644 --- a/src/features/cards/components/conferencesCard/ConferenceItem.tsx +++ b/src/features/cards/components/conferencesCard/ConferenceItem.tsx @@ -102,9 +102,9 @@ const ConferencesItem = ({ item, index, analyticsTag }: BaseItemPropsType -
-
- + + +
) : ( diff --git a/src/features/cards/components/conferencesCard/ConferencesCard.tsx b/src/features/cards/components/conferencesCard/ConferencesCard.tsx index 31e28392..4972656e 100644 --- a/src/features/cards/components/conferencesCard/ConferencesCard.tsx +++ b/src/features/cards/components/conferencesCard/ConferencesCard.tsx @@ -1,23 +1,21 @@ -import { useMemo } from 'react' import { Card } from 'src/components/Elements' import { ListConferenceComponent } from 'src/components/List/ListConferenceComponent' -import { useUserPreferences } from 'src/stores/preferences' import { CardPropsType, Conference } from 'src/types' import { useGetConferences } from '../../api/getConferences' +import { useSelectedTags } from '../../hooks/useSelectedTags' +import { CardHeader } from '../CardHeader' import { CardSettings } from '../CardSettings' import ConferenceItem from './ConferenceItem' +const GLOBAL_TAG = { label: 'Global', value: 'tech' } export function ConferencesCard(props: CardPropsType) { const { meta } = props - const cardSettings = useUserPreferences((state) => state.cardsSettings?.[meta.value]) - const { userSelectedTags } = useUserPreferences() - - const selectedTag = useMemo(() => { - return userSelectedTags.find((lang) => lang.value === cardSettings?.language) - }, [userSelectedTags, cardSettings]) - + const { queryTags, selectedTag, cardSettings } = useSelectedTags({ + source: meta.value, + fallbackTag: GLOBAL_TAG, + }) const { isLoading, data: results } = useGetConferences({ - tags: selectedTag ? [selectedTag.value] : userSelectedTags.map((tag) => tag.value), + tags: queryTags.map((tag) => tag.value), }) const renderItem = (item: Conference, index: number) => ( @@ -27,6 +25,9 @@ export function ConferencesCard(props: CardPropsType) { return ( + } settingsComponent={ Date: Thu, 6 Nov 2025 19:09:38 +0100 Subject: [PATCH 26/89] refactor: remove FloatingFilter component and its index file --- .../FloatingFilter/FloatingFilter.tsx | 93 ------------------- .../Elements/FloatingFilter/index.ts | 1 - 2 files changed, 94 deletions(-) delete mode 100644 src/components/Elements/FloatingFilter/FloatingFilter.tsx delete mode 100644 src/components/Elements/FloatingFilter/index.ts diff --git a/src/components/Elements/FloatingFilter/FloatingFilter.tsx b/src/components/Elements/FloatingFilter/FloatingFilter.tsx deleted file mode 100644 index c0811e9b..00000000 --- a/src/components/Elements/FloatingFilter/FloatingFilter.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import { useState } from 'react' -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 } from 'src/config' -import { trackCardDateRangeSelect, trackCardLanguageSelect } from 'src/lib/analytics' -import { useUserPreferences } from 'src/stores/preferences' -import { SupportedCardType } from 'src/types' - -type ListingFilterMobileProps = { - card: SupportedCardType - filters?: ('datesRange' | 'language')[] -} - -export const FloatingFilter = ({ card, filters = ['language'] }: ListingFilterMobileProps) => { - const [open, setOpen] = useState(false) - const { userSelectedTags, cardsSettings, setCardSettings } = useUserPreferences() - const [availableTagOptions] = useState( - [GLOBAL_TAG, ...userSelectedTags, MY_LANGUAGES_TAG].map((tag) => ({ - label: tag.label, - value: tag.value, - })) - ) - return ( - <> - - - maxHeight / 2} - open={open} - expandOnContentDrag={true} - onDismiss={() => setOpen(false)}> -
-
-

Customize {card.label}

- - {filters.includes('language') && ( -
-

Language

-
- tag.value === cardsSettings[card.value]?.language) - ?.map((tag) => tag.label) || [GLOBAL_TAG.value] - } - options={availableTagOptions} - onChange={(_, option) => { - setCardSettings(card.value, { - ...cardsSettings[card.value], - language: option[0].value, - }) - trackCardLanguageSelect(card.analyticsTag, option[0].value) - }} - /> -
-
- )} - - {filters.includes('datesRange') && ( -
-

Date Range

-
- date.value === cardsSettings[card.value]?.dateRange) - .map((date) => date.value) || dateRanges[0].value - } - options={dateRanges} - onChange={(_, option) => { - setCardSettings(card.value, { - ...cardsSettings[card.value], - dateRange: option[0].value, - }) - trackCardDateRangeSelect(card.analyticsTag, option[0].value) - }} - /> -
-
- )} -
-
-
- - ) -} diff --git a/src/components/Elements/FloatingFilter/index.ts b/src/components/Elements/FloatingFilter/index.ts deleted file mode 100644 index 9c885e20..00000000 --- a/src/components/Elements/FloatingFilter/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./FloatingFilter" \ No newline at end of file From d3c958807f49f6b0c401f36ebe4107a2175694b0 Mon Sep 17 00:00:00 2001 From: John Doe Date: Thu, 6 Nov 2025 20:50:08 +0100 Subject: [PATCH 27/89] refactor: remove unused useRemoteConfigStore import from DesktopCards component --- src/components/Layout/DesktopCards.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/Layout/DesktopCards.tsx b/src/components/Layout/DesktopCards.tsx index 2f581058..85f8a99c 100644 --- a/src/components/Layout/DesktopCards.tsx +++ b/src/components/Layout/DesktopCards.tsx @@ -19,7 +19,6 @@ import { clsx } from 'clsx' import { MdOutlineDragIndicator } from 'react-icons/md' import { SUPPORTED_CARDS } from 'src/config/supportedCards' import { CustomRssCard } from 'src/features/cards' -import { useRemoteConfigStore } from 'src/features/remoteConfig' import { trackPageDrag } from 'src/lib/analytics' import { DesktopBreakpoint } from 'src/providers/DesktopBreakpoint' import { useUserPreferences } from 'src/stores/preferences' @@ -71,7 +70,6 @@ export const DesktopCards = ({ const AVAILABLE_CARDS = [...SUPPORTED_CARDS, ...userCustomCards] const { updateCardOrder } = useUserPreferences() const cardsWrapperRef = useRef(null) - const { adsConfig } = useRemoteConfigStore() const sensors = useSensors( useSensor(PointerSensor), From 0bc199d6f76e4e216fa5b84f2df48d96512d0eb5 Mon Sep 17 00:00:00 2001 From: John Doe Date: Thu, 6 Nov 2025 20:52:42 +0100 Subject: [PATCH 28/89] refactor: add loading attribute to images in ArticleItem and FeedItemImage components --- .../cards/components/producthuntCard/ArticleItem.tsx | 6 ++---- src/features/feed/components/FeedItemImage.tsx | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/features/cards/components/producthuntCard/ArticleItem.tsx b/src/features/cards/components/producthuntCard/ArticleItem.tsx index 4dd8a5a1..096a9911 100644 --- a/src/features/cards/components/producthuntCard/ArticleItem.tsx +++ b/src/features/cards/components/producthuntCard/ArticleItem.tsx @@ -5,18 +5,16 @@ import { Attributes } from 'src/lib/analytics' import { useUserPreferences } from 'src/stores/preferences' import { Article, BaseItemPropsType } from 'src/types' -const ArticleItem = ({ item, index, analyticsTag }: BaseItemPropsType
) => { +const ArticleItem = ({ item, analyticsTag }: BaseItemPropsType
) => { const { listingMode } = useUserPreferences() return ( - {item.title} + {item.title}
+ return } else { return ( fallbackImage || ( From aa6102122418568e572430949d87aeaebf26931f Mon Sep 17 00:00:00 2001 From: John Doe Date: Thu, 6 Nov 2025 20:54:27 +0100 Subject: [PATCH 29/89] refactor: remove unused index prop from multiple ArticleItem and FeedItem components --- .../components/conferencesCard/ConferenceItem.tsx | 4 +--- .../cards/components/devtoCard/ArticleItem.tsx | 5 +---- .../cards/components/freecodecampCard/ArticleItem.tsx | 11 ++++------- src/features/cards/components/githubCard/RepoItem.tsx | 4 +--- .../cards/components/hackernewsCard/ArticleItem.tsx | 4 +--- .../cards/components/hashnodeCard/ArticleItem.tsx | 4 +--- .../cards/components/indiehackersCard/ArticleItem.tsx | 3 +-- .../cards/components/lobstersCard/ArticleItem.tsx | 4 +--- .../cards/components/mediumCard/ArticleItem.tsx | 3 +-- .../cards/components/redditCard/ArticleItem.tsx | 4 +--- src/features/cards/components/rssCard/ArticleItem.tsx | 4 +--- src/features/feed/components/Feed.tsx | 8 +------- .../feed/components/feedItems/ArticleFeedItem.tsx | 4 +--- .../feed/components/feedItems/ProductFeedItem.tsx | 4 +--- .../feed/components/feedItems/RepoFeedItem.tsx | 4 +--- 15 files changed, 18 insertions(+), 52 deletions(-) diff --git a/src/features/cards/components/conferencesCard/ConferenceItem.tsx b/src/features/cards/components/conferencesCard/ConferenceItem.tsx index 054452d8..050d476d 100644 --- a/src/features/cards/components/conferencesCard/ConferenceItem.tsx +++ b/src/features/cards/components/conferencesCard/ConferenceItem.tsx @@ -7,7 +7,7 @@ import { Attributes } from 'src/lib/analytics' import { useUserPreferences } from 'src/stores/preferences' import { BaseItemPropsType, Conference } from 'src/types' -const ConferencesItem = ({ item, index, analyticsTag }: BaseItemPropsType) => { +const ConferencesItem = ({ item, analyticsTag }: BaseItemPropsType) => { const { listingMode } = useUserPreferences() const conferenceLocation = useMemo(() => { @@ -70,8 +70,6 @@ const ConferencesItem = ({ item, index, analyticsTag }: BaseItemPropsType diff --git a/src/features/cards/components/devtoCard/ArticleItem.tsx b/src/features/cards/components/devtoCard/ArticleItem.tsx index 8971bd47..9a9abed4 100644 --- a/src/features/cards/components/devtoCard/ArticleItem.tsx +++ b/src/features/cards/components/devtoCard/ArticleItem.tsx @@ -8,14 +8,12 @@ import { Article, BaseItemPropsType } from 'src/types' import { format } from 'timeago.js' const ArticleItem = (props: BaseItemPropsType
) => { - const { item, index, selectedTag, analyticsTag } = props + const { item, analyticsTag } = props const { listingMode } = useUserPreferences() return ( @@ -27,7 +25,6 @@ const ArticleItem = (props: BaseItemPropsType
) => { [Attributes.TITLE]: item.title, [Attributes.LINK]: item.url, [Attributes.SOURCE]: analyticsTag, - [Attributes.LANGUAGE]: selectedTag?.value, }}> {listingMode === 'compact' && (
diff --git a/src/features/cards/components/freecodecampCard/ArticleItem.tsx b/src/features/cards/components/freecodecampCard/ArticleItem.tsx index 22cc3638..32babafa 100644 --- a/src/features/cards/components/freecodecampCard/ArticleItem.tsx +++ b/src/features/cards/components/freecodecampCard/ArticleItem.tsx @@ -1,17 +1,14 @@ -import { CardLink, CardItemWithActions } from 'src/components/Elements' +import { MdAccessTime } from 'react-icons/md' +import { CardItemWithActions, CardLink, ColoredLanguagesBadge } from 'src/components/Elements' import { Attributes } from 'src/lib/analytics' -import { BaseItemPropsType, Article } from 'src/types' +import { Article, BaseItemPropsType } from 'src/types' import { format } from 'timeago.js' -import { MdAccessTime } from 'react-icons/md' -import { ColoredLanguagesBadge } from 'src/components/Elements' const ArticleItem = (props: BaseItemPropsType
) => { - const { item, index, selectedTag, analyticsTag } = props + const { item, selectedTag, analyticsTag } = props return ( diff --git a/src/features/cards/components/githubCard/RepoItem.tsx b/src/features/cards/components/githubCard/RepoItem.tsx index 5a52a6f8..d819bbe8 100644 --- a/src/features/cards/components/githubCard/RepoItem.tsx +++ b/src/features/cards/components/githubCard/RepoItem.tsx @@ -10,14 +10,12 @@ function numberWithCommas(x: number | string) { return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') } -const RepoItem = ({ item, index, selectedTag, analyticsTag }: BaseItemPropsType) => { +const RepoItem = ({ item, selectedTag, analyticsTag }: BaseItemPropsType) => { const { listingMode } = useUserPreferences() return ( diff --git a/src/features/cards/components/hackernewsCard/ArticleItem.tsx b/src/features/cards/components/hackernewsCard/ArticleItem.tsx index 45a07d73..aa6de431 100644 --- a/src/features/cards/components/hackernewsCard/ArticleItem.tsx +++ b/src/features/cards/components/hackernewsCard/ArticleItem.tsx @@ -9,15 +9,13 @@ import { Article, BaseItemPropsType } from 'src/types' import { format } from 'timeago.js' const ArticleItem = (props: BaseItemPropsType
) => { - const { item, index, analyticsTag } = props + const { item, analyticsTag } = props const { listingMode } = useUserPreferences() return (

diff --git a/src/features/cards/components/hashnodeCard/ArticleItem.tsx b/src/features/cards/components/hashnodeCard/ArticleItem.tsx index 2165b620..a751fefa 100644 --- a/src/features/cards/components/hashnodeCard/ArticleItem.tsx +++ b/src/features/cards/components/hashnodeCard/ArticleItem.tsx @@ -11,14 +11,12 @@ import { AiTwotoneHeart } from 'react-icons/ai' import { Attributes } from 'src/lib/analytics' const ArticleItem = (props: BaseItemPropsType

) => { - const { item, index, selectedTag, analyticsTag } = props + const { item, selectedTag, analyticsTag } = props const { listingMode } = useUserPreferences() return ( diff --git a/src/features/cards/components/indiehackersCard/ArticleItem.tsx b/src/features/cards/components/indiehackersCard/ArticleItem.tsx index fca31da1..a25b3d7c 100644 --- a/src/features/cards/components/indiehackersCard/ArticleItem.tsx +++ b/src/features/cards/components/indiehackersCard/ArticleItem.tsx @@ -9,13 +9,12 @@ import { Article, BaseItemPropsType } from 'src/types' import { format } from 'timeago.js' export const ArticleItem = (props: BaseItemPropsType
) => { - const { item, index, analyticsTag } = props + const { item, analyticsTag } = props const { listingMode } = useUserPreferences() return ( ) => { +const ArticleItem = ({ item, analyticsTag }: BaseItemPropsType
) => { const { listingMode } = useUserPreferences() return (

diff --git a/src/features/cards/components/mediumCard/ArticleItem.tsx b/src/features/cards/components/mediumCard/ArticleItem.tsx index 3330c604..583cc7d3 100644 --- a/src/features/cards/components/mediumCard/ArticleItem.tsx +++ b/src/features/cards/components/mediumCard/ArticleItem.tsx @@ -6,13 +6,12 @@ import { useUserPreferences } from 'src/stores/preferences' import { Article, BaseItemPropsType } from 'src/types' import { format } from 'timeago.js' -const ArticleItem = ({ item, index, selectedTag, analyticsTag }: BaseItemPropsType

) => { +const ArticleItem = ({ item, selectedTag, analyticsTag }: BaseItemPropsType
) => { const { listingMode } = useUserPreferences() return ( { ) } -const ArticleItem = ({ item, index, analyticsTag }: BaseItemPropsType
) => { +const ArticleItem = ({ item, analyticsTag }: BaseItemPropsType
) => { const { listingMode } = useUserPreferences() const subReddit = useMemo(() => { @@ -41,8 +41,6 @@ const ArticleItem = ({ item, index, analyticsTag }: BaseItemPropsType
) return ( diff --git a/src/features/cards/components/rssCard/ArticleItem.tsx b/src/features/cards/components/rssCard/ArticleItem.tsx index 9eb6361f..195e6a01 100644 --- a/src/features/cards/components/rssCard/ArticleItem.tsx +++ b/src/features/cards/components/rssCard/ArticleItem.tsx @@ -5,7 +5,7 @@ import { Article, BaseItemPropsType } from 'src/types' import { format } from 'timeago.js' const ArticleItem = (props: BaseItemPropsType
) => { - const { item, index, selectedTag, analyticsTag } = props + const { item, selectedTag, analyticsTag } = props if (!item) { return null } @@ -13,8 +13,6 @@ const ArticleItem = (props: BaseItemPropsType
) => { diff --git a/src/features/feed/components/Feed.tsx b/src/features/feed/components/Feed.tsx index 2bdbc531..66589381 100644 --- a/src/features/feed/components/Feed.tsx +++ b/src/features/feed/components/Feed.tsx @@ -75,13 +75,7 @@ export const Feed = () => {
{(feed?.pages.flatMap((page) => page.data) || []).map((article, index) => { return ( - + ) })} {hasNextPage && ( diff --git a/src/features/feed/components/feedItems/ArticleFeedItem.tsx b/src/features/feed/components/feedItems/ArticleFeedItem.tsx index e990dd9a..f284b39a 100644 --- a/src/features/feed/components/feedItems/ArticleFeedItem.tsx +++ b/src/features/feed/components/feedItems/ArticleFeedItem.tsx @@ -7,16 +7,14 @@ import { FeedItemHeader } from '../FeedItemHeader' import { FeedItemSource } from '../FeedItemSource' export const ArticleFeedItem = (props: BaseItemPropsType) => { - const { item, index, analyticsTag, className } = props + const { item, analyticsTag, className } = props const { listingMode } = useUserPreferences() return (
diff --git a/src/features/feed/components/feedItems/ProductFeedItem.tsx b/src/features/feed/components/feedItems/ProductFeedItem.tsx index 91a641d7..cd5db5fa 100644 --- a/src/features/feed/components/feedItems/ProductFeedItem.tsx +++ b/src/features/feed/components/feedItems/ProductFeedItem.tsx @@ -7,16 +7,14 @@ import { FeedItemHeader } from '../FeedItemHeader' import { FeedItemSource } from '../FeedItemSource' export const ProductFeedItem = (props: BaseItemPropsType) => { - const { item, index, analyticsTag, className } = props + const { item, analyticsTag, className } = props const { listingMode } = useUserPreferences() return (
diff --git a/src/features/feed/components/feedItems/RepoFeedItem.tsx b/src/features/feed/components/feedItems/RepoFeedItem.tsx index d2545555..ae7dc72d 100644 --- a/src/features/feed/components/feedItems/RepoFeedItem.tsx +++ b/src/features/feed/components/feedItems/RepoFeedItem.tsx @@ -11,16 +11,14 @@ function numberWithCommas(x: number | string) { } export const RepoFeedItem = (props: BaseItemPropsType) => { - const { item, index, analyticsTag, className } = props + const { item, analyticsTag, className } = props const { listingMode } = useUserPreferences() return (
Date: Sat, 8 Nov 2025 19:34:42 +0100 Subject: [PATCH 30/89] refactor: remove unused initState function from useUserPreferences --- src/stores/preferences.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/stores/preferences.ts b/src/stores/preferences.ts index 0c8eac60..11880f16 100644 --- a/src/stores/preferences.ts +++ b/src/stores/preferences.ts @@ -150,10 +150,7 @@ export const useUserPreferences = create( setCards: (selectedCards: SelectedCard[]) => set({ cards: selectedCards }), setTags: (selectedTags: Tag[]) => set({ userSelectedTags: selectedTags }), setMaxVisibleCards: (maxVisibleCards: number) => set({ maxVisibleCards: maxVisibleCards }), - initState: (newState: UserPreferencesState) => - set(() => { - return { ...newState } - }), + setCardSettings: (card: string, settings: CardSettingsType) => set((state) => ({ cardsSettings: { From 3351dfbb7a77f06456e2aaa8b2082dd2fab2c8d8 Mon Sep 17 00:00:00 2001 From: John Doe Date: Sun, 9 Nov 2025 18:59:17 +0100 Subject: [PATCH 31/89] refactor: change div to span for subTitle in ArticleItem component --- src/features/cards/components/lobstersCard/ArticleItem.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/cards/components/lobstersCard/ArticleItem.tsx b/src/features/cards/components/lobstersCard/ArticleItem.tsx index 5375e24d..c2123c6a 100644 --- a/src/features/cards/components/lobstersCard/ArticleItem.tsx +++ b/src/features/cards/components/lobstersCard/ArticleItem.tsx @@ -34,7 +34,7 @@ const ArticleItem = ({ item, analyticsTag }: BaseItemPropsType
) => {
)} -
{item.title}
+ {item.title}

{listingMode === 'normal' && ( From 51819a0a84859cee6d7d96487e9997f3e86452b2 Mon Sep 17 00:00:00 2001 From: John Doe Date: Sun, 9 Nov 2025 18:59:41 +0100 Subject: [PATCH 32/89] refactor: enhance styling for menu items and block headers in App.css --- src/assets/App.css | 110 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 108 insertions(+), 2 deletions(-) diff --git a/src/assets/App.css b/src/assets/App.css index 27746bed..410663cf 100644 --- a/src/assets/App.css +++ b/src/assets/App.css @@ -35,6 +35,7 @@ a { text-align: center; margin: auto; font-size: 16px; + line-height: 24px; padding: 16px; } @@ -269,16 +270,94 @@ a { max-width: 180px; } -.blockHeader:hover .blockHeaderLink { +.blockHeader:hover .blockHeaderLink, +.blockHeader:hover .blockHeaderSettingsButton { opacity: 1; } +.menuItem { + font-size: 1em; + display: flex; + flex-direction: row; + font-family: 'nunito'; + column-gap: 8px; + svg { + font-size: 1.1em; + } +} + +.szh-menu__header { + margin-bottom: 6px; +} +.light { + .menuItem { + color: var(--card-header-text-color); + } + .szh-menu__item--disabled { + color: var(--secondary-text-color); + opacity: 0.6; + cursor: not-allowed; + } +} + +.dark { + .szh-menu { + background-color: var(--card-background-color); + border-radius: 12px; + border: 1px solid var(--card-border-color); + box-shadow: 0 0 20px var(--card-border-color); + } + .szh-menu__item { + color: var(--primary-text-color); + } + + .szh-menu__divider { + background-color: var(--card-border-color); + } + + .szh-menu__item--hover { + background-color: var(--app-name-text-color); + color: var(--background-color); + } + .szh-menu__item--hover { + background-color: var(--app-name-text-color); + } + .szh-menu__submenu { + background-color: var(--card-background-color); + + .szh-menu__item--hover { + background-color: var(--app-name-text-color); + color: var(--background-color); + } + } + .szh-menu__item--disabled { + color: var(--secondary-text-color); + opacity: 0.6; + cursor: not-allowed; + } +} + +.blockHeaderSettingsButton { + color: #96a2ae; + display: flex; + justify-content: center; + opacity: 0; + font-size: 1.2em; + transition: opacity 0.2s linear; + cursor: pointer; + &:focus { + cursor: pointer; + opacity: 1; + } +} + .blockHeaderLink { align-items: center; color: #96a2ae; display: flex; justify-content: center; opacity: 0; + font-size: 1.2em; transition: opacity 0.2s linear; } @@ -311,7 +390,13 @@ a { transform: rotate(3deg); opacity: 0.5; } - +.blockHeaderHighlight { + border-radius: 15px; + padding: 1px 6px; + border: 1px solid var(--chip-border-color); + background-color: var(--chip-background); + font-size: 0.9em; +} .blockHeaderIcon { display: flex; height: 16px; @@ -330,6 +415,11 @@ a { padding: 0 6px; text-transform: lowercase; color: white; + + &.past { + background-color: #dd5353; + margin-right: 4px; + } } .blockHeaderIcon img { display: block; @@ -567,6 +657,10 @@ a { transition: opacity 0.3s ease-out 0.1s, transform 0.3s ease-out 0.1s, visibility 0.3s ease-out 0.1s; width: 100%; + + button.tag { + cursor: pointer; + } } .tag { @@ -921,7 +1015,14 @@ Producthunt item padding: 0 32px 0 48px; width: 100%; background-color: var(--card-header-background-color); + + &::placeholder { + color: var(--primary-text-color); + font-size: 0.9em; + opacity: 0.5; + } } + .searchBarInput:focus { outline: none; } @@ -1159,6 +1260,11 @@ Producthunt item right: 16px; width: 48px; z-index: 2; + + & svg { + color: white; + font-size: 24px; + } } .floatingFilterBottomSheet .title { From 449b68003bb8f417b128093e17ce4c96655395f2 Mon Sep 17 00:00:00 2001 From: John Doe Date: Sun, 9 Nov 2025 18:59:54 +0100 Subject: [PATCH 33/89] refactor: remove unused index prop from CardItemWithActions component --- .../Elements/CardWithActions/CardItemWithActions.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/Elements/CardWithActions/CardItemWithActions.tsx b/src/components/Elements/CardWithActions/CardItemWithActions.tsx index ca674a2f..2e5591b2 100644 --- a/src/components/Elements/CardWithActions/CardItemWithActions.tsx +++ b/src/components/Elements/CardWithActions/CardItemWithActions.tsx @@ -11,7 +11,6 @@ import { BaseEntry } from 'src/types' type CardItemWithActionsProps = { item: BaseEntry - index: number source: string cardItem: React.ReactNode sourceType?: 'rss' | 'supported' @@ -20,7 +19,6 @@ type CardItemWithActionsProps = { export const CardItemWithActions = ({ cardItem, item, - index, source, sourceType = 'supported', }: CardItemWithActionsProps) => { @@ -73,7 +71,7 @@ export const CardItemWithActions = ({ } return ( -
+
setShareModalData(undefined)} From eafcfef065567365f4b8a27df1eee03d7b4eb1fb Mon Sep 17 00:00:00 2001 From: John Doe Date: Sun, 9 Nov 2025 19:04:12 +0100 Subject: [PATCH 34/89] refactor: update Feed component to use feedLoading class for loading state and enhance placeholder styling --- src/features/feed/components/Feed.tsx | 2 +- src/features/feed/components/feed.css | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/features/feed/components/Feed.tsx b/src/features/feed/components/Feed.tsx index 66589381..c9cd831a 100644 --- a/src/features/feed/components/Feed.tsx +++ b/src/features/feed/components/Feed.tsx @@ -57,7 +57,7 @@ export const Feed = () => { if (isInitialLoading) { return ( -
+
{Array.from({ length: 10, }).map((_, index) => ( diff --git a/src/features/feed/components/feed.css b/src/features/feed/components/feed.css index f791348b..38824082 100644 --- a/src/features/feed/components/feed.css +++ b/src/features/feed/components/feed.css @@ -49,6 +49,14 @@ } } +.feed > * { + box-sizing: border-box; + max-width: 100%; + overflow: hidden; +} +.feedLoading .placeholder { + min-height: 360px; +} .feed .placeholder { animation-duration: 1.5s; animation-name: cardPlaceholderPulse; @@ -56,8 +64,8 @@ padding: 12px; display: flex; flex-direction: column; - min-height: 360px; gap: 16px; + box-sizing: border-box; .image { background-color: var(--placeholder-background-color); border: 1px solid var(--placeholder-border-color); From 34224aa2d0f03658a52de74d9c3bdcc52a1c122a Mon Sep 17 00:00:00 2001 From: John Doe Date: Sun, 9 Nov 2025 19:51:55 +0100 Subject: [PATCH 35/89] refactor: simplify ArticleItem component by removing PostFlair rendering --- .../cards/components/redditCard/ArticleItem.tsx | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/features/cards/components/redditCard/ArticleItem.tsx b/src/features/cards/components/redditCard/ArticleItem.tsx index d78b949f..615ae392 100644 --- a/src/features/cards/components/redditCard/ArticleItem.tsx +++ b/src/features/cards/components/redditCard/ArticleItem.tsx @@ -60,16 +60,7 @@ const ArticleItem = ({ item, analyticsTag }: BaseItemPropsType
) => {
)} -
- {item.flair_text && ( - - )} - {item.title} -
+
{item.title}
From 9a6e2bb8ac44942c52dc2a03a63863041bbc0596 Mon Sep 17 00:00:00 2001 From: John Doe Date: Sun, 9 Nov 2025 19:52:04 +0100 Subject: [PATCH 36/89] refactor: remove FloatingFilter export from index.ts --- src/components/Elements/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/Elements/index.ts b/src/components/Elements/index.ts index 14add55f..7457df73 100644 --- a/src/components/Elements/index.ts +++ b/src/components/Elements/index.ts @@ -6,7 +6,6 @@ export * from './CardWithActions' export * from './ChipsSet' export * from './ClickableItem' export * from './ColoredLanguagesBadges' -export * from './FloatingFilter' export * from './InlineTextFilter' export * from './Modal' export * from './Panel' From d2d3505aba1a67aa6720301c36c428bb4b026024 Mon Sep 17 00:00:00 2001 From: John Doe Date: Sun, 9 Nov 2025 19:52:12 +0100 Subject: [PATCH 37/89] refactor: update Card component to improve settings component rendering with responsive breakpoints --- src/components/Elements/Card/Card.tsx | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/components/Elements/Card/Card.tsx b/src/components/Elements/Card/Card.tsx index 676a4641..f2b1eec8 100644 --- a/src/components/Elements/Card/Card.tsx +++ b/src/components/Elements/Card/Card.tsx @@ -1,6 +1,8 @@ import clsx from 'clsx' import React, { useEffect, useState } from 'react' import { AdvBanner } from 'src/features/adv' +import { DesktopBreakpoint } from 'src/providers/DesktopBreakpoint' +import { MobileBreakpoint } from 'src/providers/MobileBreakpoint' import { CardPropsType } from 'src/types' type RootCardProps = CardPropsType & { @@ -46,12 +48,17 @@ export const Card = ({ return (
+ + {settingsComponent && } +
{knob} {icon} {titleComponent || label}{' '} - {settingsComponent && ( - {settingsComponent} - )} + + {settingsComponent && ( + {settingsComponent} + )} + {badge && {badge}}
From cb597db810904d29e115a82e3f088089d29356a3 Mon Sep 17 00:00:00 2001 From: John Doe Date: Sun, 9 Nov 2025 19:57:47 +0100 Subject: [PATCH 38/89] refactor: update FreecodecampCard component to improve tag handling and optimize rendering --- .../freecodecampCard/FreecodecampCard.tsx | 33 +++++++++++-------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/src/features/cards/components/freecodecampCard/FreecodecampCard.tsx b/src/features/cards/components/freecodecampCard/FreecodecampCard.tsx index f752c06e..7b6c993d 100644 --- a/src/features/cards/components/freecodecampCard/FreecodecampCard.tsx +++ b/src/features/cards/components/freecodecampCard/FreecodecampCard.tsx @@ -1,49 +1,54 @@ -import { Card, FloatingFilter } from 'src/components/Elements' +import { useCallback } from 'react' +import { Card } from 'src/components/Elements' import { ListPostComponent } from 'src/components/List/ListPostComponent' import { Article, CardPropsType } from 'src/types' import { useGetSourceArticles } from '../../api/getSourceArticles' import { useSelectedTags } from '../../hooks/useSelectedTags' -import { CardHeader } from '../CardHeader' -import { CardSettings } from '../CardSettings' +import { MemoizedCardHeader } from '../CardHeader' +import { MemoizedCardSettings } from '../CardSettings' import ArticleItem from './ArticleItem' -const GLOBAL_TAG = { label: 'Global', value: 'programming' } +const GLOBAL_TAG = { label: 'Global', value: 'global' } export function FreecodecampCard(props: CardPropsType) { const { meta } = props - const { queryTags, selectedTag, cardSettings } = useSelectedTags({ + const { + queryTags, + selectedTag, + cardSettings: { sortBy, language } = {}, + } = useSelectedTags({ source: meta.value, fallbackTag: GLOBAL_TAG, }) const { data, isLoading } = useGetSourceArticles({ source: 'freecodecamp', - tags: queryTags.map((tag) => tag.value), + tags: queryTags, }) - const renderItem = (item: Article, index: number) => ( - + const renderItem = useCallback( + (item: Article) => , + [meta.analyticsTag] ) return ( + } settingsComponent={ - } {...props}> - Date: Sun, 9 Nov 2025 19:57:57 +0100 Subject: [PATCH 39/89] refactor: optimize DevtoCard component by removing unused imports and improving tag handling --- .../cards/components/devtoCard/DevtoCard.tsx | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/src/features/cards/components/devtoCard/DevtoCard.tsx b/src/features/cards/components/devtoCard/DevtoCard.tsx index a0ca936d..e0644e31 100644 --- a/src/features/cards/components/devtoCard/DevtoCard.tsx +++ b/src/features/cards/components/devtoCard/DevtoCard.tsx @@ -1,12 +1,13 @@ +import { useCallback } from 'react' import { AiOutlineLike } from 'react-icons/ai' import { BiCommentDetail } from 'react-icons/bi' -import { Card, FloatingFilter } from 'src/components/Elements' +import { Card } from 'src/components/Elements' import { ListPostComponent } from 'src/components/List/ListPostComponent' import { Article, CardPropsType } from 'src/types' import { useGetSourceArticles } from '../../api/getSourceArticles' import { useSelectedTags } from '../../hooks/useSelectedTags' -import { CardHeader } from '../CardHeader' -import { CardSettings } from '../CardSettings' +import { MemoizedCardHeader } from '../CardHeader' +import { MemoizedCardSettings } from '../CardSettings' import ArticleItem from './ArticleItem' const GLOBAL_TAG = { label: 'Global', value: 'programming' } @@ -14,7 +15,11 @@ const GLOBAL_TAG = { label: 'Global', value: 'programming' } export function DevtoCard(props: CardPropsType) { const { meta } = props - const { queryTags, selectedTag, cardSettings } = useSelectedTags({ + const { + queryTags, + selectedTag, + cardSettings: { sortBy, language } = {}, + } = useSelectedTags({ source: meta.value, fallbackTag: GLOBAL_TAG, }) @@ -25,25 +30,26 @@ export function DevtoCard(props: CardPropsType) { isLoading, } = useGetSourceArticles({ source: 'devto', - tags: queryTags.map((tag) => tag.value), + tags: queryTags, }) - const renderItem = (item: Article, index: number) => ( - + const renderItem = useCallback( + (item: Article) => , + [meta.analyticsTag] ) return ( + } settingsComponent={ - [ ...defaults, { @@ -60,9 +66,8 @@ export function DevtoCard(props: CardPropsType) { /> } {...props}> - Date: Sun, 9 Nov 2025 19:58:40 +0100 Subject: [PATCH 40/89] refactor: optimize CardHeader and CardSettings components for improved performance and readability --- src/features/cards/components/CardHeader.tsx | 23 ++++----- .../cards/components/CardSettings.tsx | 48 ++++++++++++------- 2 files changed, 40 insertions(+), 31 deletions(-) diff --git a/src/features/cards/components/CardHeader.tsx b/src/features/cards/components/CardHeader.tsx index 4696573b..a1afc221 100644 --- a/src/features/cards/components/CardHeader.tsx +++ b/src/features/cards/components/CardHeader.tsx @@ -1,3 +1,4 @@ +import { memo, useMemo } from 'react' import { MY_LANGUAGES_OPTION } from '../config' type HeaderTitleProps = { @@ -12,27 +13,23 @@ type HeaderTitleProps = { } children?: React.ReactNode } -export const CardHeader = ({ label, fallbackTag, selectedTag, children }: HeaderTitleProps) => { +const CardHeader = ({ label, fallbackTag, selectedTag, children }: HeaderTitleProps) => { if (children) { return <>{children} } - if (!selectedTag || selectedTag.value === fallbackTag.value) { - return <>{label} - } - - if (selectedTag.value === MY_LANGUAGES_OPTION.value) { - return ( - <> - {label} {MY_LANGUAGES_OPTION.label} - - ) - } + const highlightLabel = useMemo(() => { + if (!selectedTag || selectedTag.value === fallbackTag.value) return null + if (selectedTag.value === MY_LANGUAGES_OPTION.value) return MY_LANGUAGES_OPTION.label + return selectedTag.label + }, [selectedTag, fallbackTag]) return ( <> {label} - {selectedTag.label} + {highlightLabel && {highlightLabel}} ) } + +export const MemoizedCardHeader = memo(CardHeader) diff --git a/src/features/cards/components/CardSettings.tsx b/src/features/cards/components/CardSettings.tsx index 5eaab830..14a19eb6 100644 --- a/src/features/cards/components/CardSettings.tsx +++ b/src/features/cards/components/CardSettings.tsx @@ -1,13 +1,16 @@ import { Menu, MenuDivider, MenuItem, SubMenu } from '@szhsin/react-menu' -import { useCallback, useMemo } from 'react' +import { memo, useCallback, useMemo } from 'react' import { AiOutlineCode } from 'react-icons/ai' import { BsBoxArrowInUpRight } from 'react-icons/bs' +import { FiFilter } from 'react-icons/fi' import { GoGear } from 'react-icons/go' import { IoTrashBinOutline } from 'react-icons/io5' import { LiaSortSolid } from 'react-icons/lia' import { MdDateRange } from 'react-icons/md' +import { useMediaQuery } from 'react-responsive' import { ref } from 'src/config' import { useUserPreferences } from 'src/stores/preferences' +import { useShallow } from 'zustand/shallow' import { MY_LANGUAGES_OPTION } from '../config' type SortOption = { label: string; value: string; icon?: React.ReactNode } @@ -27,7 +30,7 @@ type CardSettingsProps = { const DEFAULT_SORT_OPTIONS = [{ label: 'Newest', value: 'published_at', icon: }] const SPECIAL_LABELS = ['global', MY_LANGUAGES_OPTION.label.toLowerCase()] -export const CardSettings = ({ +const CardSettings = ({ id, url, sortBy, @@ -38,30 +41,34 @@ export const CardSettings = ({ customStartMenuItems, sortOptions, }: CardSettingsProps) => { - const userSelectedTags = useUserPreferences((state) => state.userSelectedTags) - const openLinksNewTab = useUserPreferences((state) => state.openLinksNewTab) - const removeCard = useUserPreferences((state) => state.removeCard) - const setCardSettings = useUserPreferences((state) => state.setCardSettings) - const cardSettings = useUserPreferences((state) => state.cardsSettings[id]) + const { userSelectedTags, openLinksNewTab, removeCard, setCardSettings, cardSettings } = + useUserPreferences( + useShallow((state) => ({ + userSelectedTags: state.userSelectedTags, + openLinksNewTab: state.openLinksNewTab, + removeCard: state.removeCard, + setCardSettings: state.setCardSettings, + cardSettings: state.cardsSettings?.[id], + })) + ) const userTagsMemo = useMemo(() => { - const newTags = userSelectedTags.sort((a, b) => a.label.localeCompare(b.label)) - let tags = [...newTags] - if (globalTag) { - tags = [...tags, globalTag] - } - - tags = [...tags, MY_LANGUAGES_OPTION] + const tags = [...userSelectedTags] + .sort((a, b) => a.label.localeCompare(b.label)) + .concat(globalTag ? [globalTag] : []) + .concat(MY_LANGUAGES_OPTION) return tags }, [userSelectedTags, globalTag]) - const resolvedSortOptions = - typeof sortOptions === 'function' + const resolvedSortOptions = useMemo(() => { + return typeof sortOptions === 'function' ? sortOptions(DEFAULT_SORT_OPTIONS) : sortOptions || DEFAULT_SORT_OPTIONS + }, [sortOptions]) const onOpenSourceUrlClicked = useCallback(() => { - let link = `${url}?${ref}` + if (!url) return + const link = `${url}?${ref}` window.open(link, openLinksNewTab ? '_blank' : '_self') }, [url, openLinksNewTab]) @@ -70,9 +77,12 @@ export const CardSettings = ({ [userTagsMemo] ) + const isMobile = useMediaQuery({ maxWidth: 767 }) + const menuIcon = isMobile ? : + return ( } + menuButton={menuIcon} theming="dark" portal={true} className={`menuItem`} @@ -144,3 +154,5 @@ export const CardSettings = ({ ) } + +export const MemoizedCardSettings = memo(CardSettings) From 4e958539730099b15b114832c7cb1bc665146ecc Mon Sep 17 00:00:00 2001 From: John Doe Date: Sun, 9 Nov 2025 19:58:51 +0100 Subject: [PATCH 41/89] refactor: add value properties to occupations in HelloTab component for improved data handling --- src/features/onboarding/components/steps/HelloTab.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/features/onboarding/components/steps/HelloTab.tsx b/src/features/onboarding/components/steps/HelloTab.tsx index 40d75cb2..ed3529c5 100644 --- a/src/features/onboarding/components/steps/HelloTab.tsx +++ b/src/features/onboarding/components/steps/HelloTab.tsx @@ -11,12 +11,14 @@ import { Occupation } from '../../types' const OCCUPATIONS: Occupation[] = [ { title: 'Front-End Engineer', + value: 'frontend', icon: FaPaintBrush, sources: ['devto', 'github', 'medium', 'hashnode'], tags: ['frontend', 'javascript', 'typescript', 'css', 'react', 'vue', 'angular'], }, { title: 'Back-End Engineer', + value: 'backend', icon: BsFillGearFill, sources: ['devto', 'github', 'medium', 'hashnode'], tags: ['backend', 'go', 'php', 'ruby', 'rust', 'r'], @@ -24,11 +26,13 @@ const OCCUPATIONS: Occupation[] = [ { title: 'Full Stack Engineer', icon: RiDeviceFill, + value: 'fullstack', sources: ['devto', 'github', 'medium', 'hashnode'], tags: ['javascript', 'typescript', 'php', 'ruby', 'rust'], }, { title: 'Mobile', + value: 'mobile', icon: AiFillMobile, sources: ['reddit', 'github', 'medium', 'hashnode'], tags: [ @@ -45,30 +49,35 @@ const OCCUPATIONS: Occupation[] = [ }, { title: 'Devops Engineer', + value: 'devops', icon: FaServer, sources: ['freecodecamp', 'github', 'reddit', 'devto'], tags: ['devops', 'kubernetes', 'docker', 'bash'], }, { title: 'Data Engineer', + value: 'data', icon: FaDatabase, sources: ['freecodecamp', 'github', 'reddit', 'devto'], tags: ['data science', 'python', 'artificial intelligence', 'machine learning'], }, { title: 'Security Engineer', + value: 'security', icon: AiFillSecurityScan, sources: ['freecodecamp', 'github', 'reddit', 'devto'], tags: ['security', 'cpp', 'bash', 'python'], }, { title: 'ML Engineer', + value: 'ai', icon: FaRobot, sources: ['github', 'freecodecamp', 'hackernews', 'devto'], tags: ['machine learning', 'artificial intelligence', 'python'], }, { title: 'Other', + value: 'other', icon: TbDots, sources: ['hackernews', 'github', 'producthunt', 'devto'], tags: ['webdev', 'mobile'], @@ -84,7 +93,7 @@ export const HelloTab = () => { const onStartClicked = () => { const selectedOccupation = OCCUPATIONS.find((occ) => occ.title === occupation) if (selectedOccupation) { - setOccupation(selectedOccupation.title) + setOccupation(selectedOccupation.value) setCards( selectedOccupation.sources.map((source, index) => ({ id: index, From 383dc675a0a1747b91cca4b005d3b8ee57395f72 Mon Sep 17 00:00:00 2001 From: John Doe Date: Sun, 9 Nov 2025 19:59:13 +0100 Subject: [PATCH 42/89] refactor: optimize ListComponent by improving memoization and error handling --- src/components/List/ListComponent.tsx | 30 +++++++++++++-------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/components/List/ListComponent.tsx b/src/components/List/ListComponent.tsx index 7890a61f..c283cf38 100644 --- a/src/components/List/ListComponent.tsx +++ b/src/components/List/ListComponent.tsx @@ -1,4 +1,4 @@ -import React, { ReactNode, useMemo } from 'react' +import React, { memo, ReactNode, useMemo } from 'react' import { Placeholder } from 'src/components/placeholders' import { MAX_ITEMS_PER_CARD } from 'src/config' @@ -6,7 +6,7 @@ type PlaceholdersProps = { placeholder: ReactNode } -const Placeholders = React.memo(({ placeholder }) => { +const Placeholders = memo(({ placeholder }) => { return ( <> {[...Array(7)].map((_, i) => ( @@ -42,18 +42,6 @@ export function ListComponent(props: ListComponentPropsType) { limit = MAX_ITEMS_PER_CARD, } = props - if (error) { - return

{error?.message || error}

- } - - if (items && items.length == 0) { - return ( -

- No items found, try adjusting your filter or choosing a different tag. -

- ) - } - const sortedData = useMemo(() => { if (!items || items.length == 0) return [] if (!sortBy) return items @@ -88,7 +76,19 @@ export function ListComponent(props: ListComponentPropsType) { } catch (e) { return [] } - }, [sortedData]) + }, [sortedData, header, renderItem, limit]) + + if (error) { + return

{error?.message || error}

+ } + + if (items && items.length == 0) { + return ( +

+ No items found, try adjusting your filter or choosing a different tag. +

+ ) + } return <>{isLoading ? : enrichedItems} } From d58d4d17bdb396e00fc573c40c3e44c367c51317 Mon Sep 17 00:00:00 2001 From: John Doe Date: Sun, 9 Nov 2025 19:59:27 +0100 Subject: [PATCH 43/89] refactor: enhance UserTags component by improving tag handling and integrating user preferences --- src/components/Elements/UserTags/UserTags.tsx | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/src/components/Elements/UserTags/UserTags.tsx b/src/components/Elements/UserTags/UserTags.tsx index c07bccb7..68d8d4cb 100644 --- a/src/components/Elements/UserTags/UserTags.tsx +++ b/src/components/Elements/UserTags/UserTags.tsx @@ -1,16 +1,34 @@ +import { useCallback } from 'react' import { TiPlus } from 'react-icons/ti' import { Link } from 'react-router-dom' import { useUserPreferences } from 'src/stores/preferences' +import { useShallow } from 'zustand/shallow' export const UserTags = () => { - const { userSelectedTags } = useUserPreferences() + const { cards, userSelectedTags, cardsSettings, setCardSettings } = useUserPreferences( + useShallow((state) => ({ + cards: state.cards, + userSelectedTags: state.userSelectedTags, + cardsSettings: state.cardsSettings, + setCardSettings: state.setCardSettings, + })) + ) + + const onTagClicked = useCallback((tagValue: string) => { + cards.forEach((card) => { + setCardSettings(card.name, { + ...cardsSettings[card.id], + language: tagValue, + }) + }) + }, []) return (
{userSelectedTags.map((tag, index) => ( - + ))} From b44f7863b8ad8efd89296b2966e381f94c720615 Mon Sep 17 00:00:00 2001 From: John Doe Date: Sun, 9 Nov 2025 20:31:28 +0100 Subject: [PATCH 44/89] feat: implement lazy loading for cards using IntersectionObserver and refactor related components for improved performance --- src/components/Elements/Card/Card.tsx | 110 +++++++++--------- .../cards/components/aiCard/AICard.tsx | 26 ++++- .../conferencesCard/ConferencesCard.tsx | 38 ++++-- .../cards/components/devtoCard/DevtoCard.tsx | 6 + .../freecodecampCard/FreecodecampCard.tsx | 6 + .../components/githubCard/GithubCard.tsx | 69 ++++++----- .../hackernewsCard/HackernewsCard.tsx | 25 ++-- .../components/hashnodeCard/HashnodeCard.tsx | 63 +++++----- .../indiehackersCard/IndiehackersCard.tsx | 25 ++-- .../components/lobstersCard/LobstersCard.tsx | 25 ++-- .../components/mediumCard/MediumCard.tsx | 50 +++++--- .../producthuntCard/ProducthuntCard.tsx | 23 ++-- .../components/redditCard/RedditCard.tsx | 50 +++++--- .../components/rssCard/CustomRssCard.tsx | 20 +++- src/features/cards/hooks/useLazyListLoad.tsx | 37 ++++++ 15 files changed, 367 insertions(+), 206 deletions(-) create mode 100644 src/features/cards/hooks/useLazyListLoad.tsx diff --git a/src/components/Elements/Card/Card.tsx b/src/components/Elements/Card/Card.tsx index f2b1eec8..3396218c 100644 --- a/src/components/Elements/Card/Card.tsx +++ b/src/components/Elements/Card/Card.tsx @@ -11,64 +11,68 @@ type RootCardProps = CardPropsType & { settingsComponent?: React.ReactNode fullBlock?: boolean } +export const Card = React.forwardRef( + ( + { + meta, + titleComponent, + settingsComponent, + className, + withAds = false, + children, + fullBlock = false, + knob, + }, + ref + ) => { + const { icon, label, badge } = meta + const [canAdsLoad, setCanAdsLoad] = useState(true) -export const Card = ({ - meta, - titleComponent, - settingsComponent, - className, - withAds = false, - children, - fullBlock = false, - knob, -}: RootCardProps) => { - const { icon, label, badge } = meta - const [canAdsLoad, setCanAdsLoad] = useState(true) - - useEffect(() => { - if (!withAds) { - return - } - - const handleClassChange = () => { - if (document.documentElement.classList.contains('dndState')) { - setCanAdsLoad(false) - } else { - setCanAdsLoad(true) + useEffect(() => { + if (!withAds) { + return } - } - const observer = new MutationObserver(handleClassChange) - observer.observe(document.documentElement, { attributes: true }) + const handleClassChange = () => { + if (document.documentElement.classList.contains('dndState')) { + setCanAdsLoad(false) + } else { + setCanAdsLoad(true) + } + } - return () => { - observer.disconnect() - } - }, [withAds]) + const observer = new MutationObserver(handleClassChange) + observer.observe(document.documentElement, { attributes: true }) - return ( -
- - {settingsComponent && } - -
- {knob} - {icon} {titleComponent || label}{' '} - - {settingsComponent && ( - {settingsComponent} - )} - - {badge && {badge}} -
+ return () => { + observer.disconnect() + } + }, [withAds]) - {canAdsLoad && withAds && ( -
- + return ( +
+ + {settingsComponent && } + +
+ {knob} + {icon} {titleComponent || label}{' '} + + {settingsComponent && ( + {settingsComponent} + )} + + {badge && {badge}}
- )} -
{children}
-
- ) -} + {canAdsLoad && withAds && ( +
+ +
+ )} + +
{children}
+
+ ) + } +) diff --git a/src/features/cards/components/aiCard/AICard.tsx b/src/features/cards/components/aiCard/AICard.tsx index 13b36c12..6ab131f5 100644 --- a/src/features/cards/components/aiCard/AICard.tsx +++ b/src/features/cards/components/aiCard/AICard.tsx @@ -1,32 +1,46 @@ +import { useCallback, useMemo } from 'react' import { Card } from 'src/components/Elements' import { ListComponent } from 'src/components/List' import { FeedItem, useGetFeed } from 'src/features/feed' import { useUserPreferences } from 'src/stores/preferences' import { CardPropsType, FeedItemData } from 'src/types' -import { CardSettings } from '../CardSettings' +import { useShallow } from 'zustand/shallow' +import { useLazyListLoad } from '../../hooks/useLazyListLoad' +import { MemoizedCardSettings } from '../CardSettings' export function AICard(props: CardPropsType) { const { meta } = props - const { userSelectedTags } = useUserPreferences() + const userSelectedTags = useUserPreferences(useShallow((state) => state.userSelectedTags)) + const { ref, isVisible } = useLazyListLoad() + const queryTags = useMemo( + () => userSelectedTags.map((tag) => tag.label.toLocaleLowerCase()), + [userSelectedTags] + ) + const { data: articles, isLoading, error, } = useGetFeed({ - tags: userSelectedTags.map((tag) => tag.label.toLocaleLowerCase()), + tags: queryTags, config: { cacheTime: 0, staleTime: 0, useErrorBoundary: false, + enabled: isVisible, }, }) - const renderItem = (item: FeedItemData, index: number) => ( - + const renderItem = useCallback( + (item: FeedItemData) => , + [meta.analyticsTag] ) return ( - } {...props}> + } + {...props}> items={articles?.pages.flatMap((page) => page.data) || []} error={error} diff --git a/src/features/cards/components/conferencesCard/ConferencesCard.tsx b/src/features/cards/components/conferencesCard/ConferencesCard.tsx index 4972656e..84173fac 100644 --- a/src/features/cards/components/conferencesCard/ConferencesCard.tsx +++ b/src/features/cards/components/conferencesCard/ConferencesCard.tsx @@ -1,39 +1,53 @@ +import { useCallback } from 'react' import { Card } from 'src/components/Elements' import { ListConferenceComponent } from 'src/components/List/ListConferenceComponent' import { CardPropsType, Conference } from 'src/types' import { useGetConferences } from '../../api/getConferences' +import { useLazyListLoad } from '../../hooks/useLazyListLoad' import { useSelectedTags } from '../../hooks/useSelectedTags' -import { CardHeader } from '../CardHeader' -import { CardSettings } from '../CardSettings' +import { MemoizedCardHeader } from '../CardHeader' +import { MemoizedCardSettings } from '../CardSettings' import ConferenceItem from './ConferenceItem' -const GLOBAL_TAG = { label: 'Global', value: 'tech' } +const GLOBAL_TAG = { label: 'Global', value: 'general' } export function ConferencesCard(props: CardPropsType) { const { meta } = props - const { queryTags, selectedTag, cardSettings } = useSelectedTags({ + const { ref, isVisible } = useLazyListLoad() + const { + queryTags, + selectedTag, + cardSettings: { sortBy, language } = {}, + } = useSelectedTags({ source: meta.value, fallbackTag: GLOBAL_TAG, }) const { isLoading, data: results } = useGetConferences({ - tags: queryTags.map((tag) => tag.value), + tags: queryTags, + config: { + enabled: isVisible, + }, }) - const renderItem = (item: Conference, index: number) => ( - + const renderItem = useCallback( + (item: Conference) => ( + + ), + [meta.analyticsTag] ) return ( + } settingsComponent={ - }> } diff --git a/src/features/cards/components/freecodecampCard/FreecodecampCard.tsx b/src/features/cards/components/freecodecampCard/FreecodecampCard.tsx index 7b6c993d..34bd637c 100644 --- a/src/features/cards/components/freecodecampCard/FreecodecampCard.tsx +++ b/src/features/cards/components/freecodecampCard/FreecodecampCard.tsx @@ -3,6 +3,7 @@ import { Card } from 'src/components/Elements' import { ListPostComponent } from 'src/components/List/ListPostComponent' import { Article, CardPropsType } from 'src/types' import { useGetSourceArticles } from '../../api/getSourceArticles' +import { useLazyListLoad } from '../../hooks/useLazyListLoad' import { useSelectedTags } from '../../hooks/useSelectedTags' import { MemoizedCardHeader } from '../CardHeader' import { MemoizedCardSettings } from '../CardSettings' @@ -12,6 +13,7 @@ const GLOBAL_TAG = { label: 'Global', value: 'global' } export function FreecodecampCard(props: CardPropsType) { const { meta } = props + const { ref, isVisible } = useLazyListLoad() const { queryTags, selectedTag, @@ -24,6 +26,9 @@ export function FreecodecampCard(props: CardPropsType) { const { data, isLoading } = useGetSourceArticles({ source: 'freecodecamp', tags: queryTags, + config: { + enabled: isVisible, + }, }) const renderItem = useCallback( @@ -33,6 +38,7 @@ export function FreecodecampCard(props: CardPropsType) { return ( } diff --git a/src/features/cards/components/githubCard/GithubCard.tsx b/src/features/cards/components/githubCard/GithubCard.tsx index 9211935f..df2d2cf8 100644 --- a/src/features/cards/components/githubCard/GithubCard.tsx +++ b/src/features/cards/components/githubCard/GithubCard.tsx @@ -1,14 +1,15 @@ import { MenuDivider, MenuItem } from '@szhsin/react-menu' -import { useMemo } from 'react' +import { useCallback, useMemo } from 'react' import { VscRepoForked, VscStarFull } from 'react-icons/vsc' -import { Card, FloatingFilter } from 'src/components/Elements' +import { Card } from 'src/components/Elements' import { ListRepoComponent } from 'src/components/List/ListRepoComponent' import { dateRanges } from 'src/config' import { useUserPreferences } from 'src/stores/preferences' import { CardPropsType, Repository } from 'src/types' import { useGetGithubRepos } from '../../api/getGithubRepos' +import { useLazyListLoad } from '../../hooks/useLazyListLoad' import { useSelectedTags } from '../../hooks/useSelectedTags' -import { CardSettings } from '../CardSettings' +import { MemoizedCardSettings } from '../CardSettings' import RepoItem from './RepoItem' const GLOBAL_TAG = { label: 'Global', value: 'global' } @@ -16,48 +17,63 @@ const GLOBAL_TAG = { label: 'Global', value: 'global' } export function GithubCard(props: CardPropsType) { const { meta } = props + const { ref, isVisible } = useLazyListLoad() const setCardSettings = useUserPreferences((state) => state.setCardSettings) - const { queryTags, selectedTag, cardSettings } = useSelectedTags({ + const { + queryTags, + selectedTag, + cardSettings: { dateRange, sortBy, language } = {}, + } = useSelectedTags({ source: meta.value, fallbackTag: GLOBAL_TAG, }) const selectedDateRange = useMemo( - () => dateRanges.find((date) => date.value === cardSettings?.dateRange) || dateRanges[0], - [cardSettings] + () => dateRanges.find((date) => date.value === dateRange) || dateRanges[0], + [dateRange] ) const { data, error, isLoading } = useGetGithubRepos({ - tags: queryTags.map((tag) => tag.value), + tags: queryTags, dateRange: selectedDateRange.value, + config: { + enabled: isVisible, + }, }) - const renderItem = (item: Repository, index: number) => ( - + const renderItem = useCallback( + (item: Repository) => ( + + ), + [meta.analyticsTag, selectedTag] ) + const headerTitle = useMemo(() => { + return ( + <> + Github {selectedTag.label}{' '} + {selectedDateRange.label.toLowerCase()} + + ) + }, [selectedTag, selectedDateRange]) + return ( - Github {selectedTag.label}{' '} - {selectedDateRange.label.toLowerCase()} -
- } + titleComponent={headerTitle} settingsComponent={ - {dateRanges.map((date) => ( @@ -66,7 +82,7 @@ export function GithubCard(props: CardPropsType) { value={date.value} disabled={selectedDateRange.value === date.value} onClick={() => { - setCardSettings(meta.value, { ...cardSettings, dateRange: date.value }) + setCardSettings(meta.value, { dateRange: date.value, language, sortBy }) }}> {date.label} @@ -89,9 +105,8 @@ export function GithubCard(props: CardPropsType) { /> } {...props}> - state.cardsSettings[meta.value]) + const { ref, isVisible } = useLazyListLoad() + const sortBy = useUserPreferences( + useShallow((state) => state.cardsSettings?.[meta.value]?.sortBy) + ) const { data, isLoading, error } = useGetSourceArticles({ source: 'hackernews', + config: { + enabled: isVisible, + }, }) - const renderItem = (item: Article, index: number) => ( - + const renderItem = useCallback( + (item: Article) => , + [meta.analyticsTag] ) return ( [ ...defaults, { @@ -43,7 +54,7 @@ export function HackernewsCard(props: CardPropsType) { /> }> tag.value), + tags: queryTags, + config: { + enabled: isVisible, + }, }) - const renderItem = (item: Article, index: number) => ( - + const renderItem = useCallback( + (item: Article) => , + [meta.analyticsTag] ) return ( + } settingsComponent={ - [ - ...defaults, - { - label: 'Reactions', - value: 'points_count', - icon: , - }, - { - label: 'Comments', - value: 'comments_count', - icon: , - }, - ]} + sortOptions={[]} /> } {...props}> - - + sortBy={sortBy as keyof Article} items={data} isLoading={isLoading} renderItem={renderItem} diff --git a/src/features/cards/components/indiehackersCard/IndiehackersCard.tsx b/src/features/cards/components/indiehackersCard/IndiehackersCard.tsx index c9161073..7d9ee416 100644 --- a/src/features/cards/components/indiehackersCard/IndiehackersCard.tsx +++ b/src/features/cards/components/indiehackersCard/IndiehackersCard.tsx @@ -1,32 +1,43 @@ +import { useCallback } from 'react' import { FaChevronUp } from 'react-icons/fa' import { Card } from 'src/components/Elements' import { ListPostComponent } from 'src/components/List/ListPostComponent' import { useUserPreferences } from 'src/stores/preferences' import { Article, CardPropsType } from 'src/types' +import { useShallow } from 'zustand/shallow' import { useGetSourceArticles } from '../../api/getSourceArticles' -import { CardSettings } from '../CardSettings' +import { useLazyListLoad } from '../../hooks/useLazyListLoad' +import { MemoizedCardSettings } from '../CardSettings' import { ArticleItem } from './ArticleItem' export function IndiehackersCard(props: CardPropsType) { const { meta } = props - const cardSettings = useUserPreferences((state) => state.cardsSettings[meta.value]) + const { ref, isVisible } = useLazyListLoad() + const sortBy = useUserPreferences( + useShallow((state) => state.cardsSettings?.[meta.value]?.sortBy) + ) const { data, isLoading, error } = useGetSourceArticles({ source: 'indiehackers', + config: { + enabled: isVisible, + }, }) - const renderItem = (item: Article, index: number) => ( - + const renderItem = useCallback( + (item: Article) => , + [meta.analyticsTag] ) return ( [ ...defaults, { @@ -39,7 +50,7 @@ export function IndiehackersCard(props: CardPropsType) { } {...props}> state.cardsSettings[meta.value]) + const { ref, isVisible } = useLazyListLoad() + const sortBy = useUserPreferences( + useShallow((state) => state.cardsSettings?.[meta.value]?.sortBy) + ) const { data, isLoading, error } = useGetSourceArticles({ source: 'lobsters', + config: { + enabled: isVisible, + }, }) - const renderItem = (item: Article, index: number) => ( - + const renderItem = useCallback( + (item: Article) => , + [meta.analyticsTag] ) return ( [ ...defaults, { @@ -38,7 +49,7 @@ export function LobstersCard(props: CardPropsType) { /> }> tag.value), + tags: queryTags, + config: { + enabled: isVisible, + }, }) - const renderItem = (item: Article, index: number) => ( - + const renderItem = useCallback( + (item: Article) => ( + + ), + [selectedTag, meta.analyticsTag] ) return ( + } settingsComponent={ - [ ...defaults, { @@ -60,9 +73,8 @@ export function MediumCard(props: CardPropsType) { /> } {...props}> - state.cardsSettings?.[meta.value]) + const { ref, isVisible } = useLazyListLoad() + const sortBy = useUserPreferences( + useShallow((state) => state.cardsSettings?.[meta.value]?.sortBy) + ) const { data: products = [], @@ -20,24 +26,25 @@ export function ProductHuntCard(props: CardPropsType) { } = useGeProductHuntProducts({ date: new Date().toISOString().split('T')[0], config: { - staleTime: 900000, //15 minutes - cacheTime: 3600000, // 1 Day + enabled: isVisible, }, }) - const renderItem = (item: Article, index: number) => ( - + const renderItem = useCallback( + (item: Article) => , + [meta.analyticsTag] ) return ( tag.value), + tags: queryTags, + config: { + enabled: isVisible, + }, }) - const renderItem = (item: Article, index: number) => ( - + const renderItem = useCallback( + (item: Article) => ( + + ), + [selectedTag, meta.analyticsTag] ) return ( + } {...props} settingsComponent={ - [ ...defaults, { @@ -55,9 +68,8 @@ export function RedditCard(props: CardPropsType) { ]} /> }> - { 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) => ( - + const renderItem = useCallback( + (item: Article) => , + [meta.analyticsTag] ) return ( } {...props} meta={{ ...meta, icon: }} settingsComponent={ - { + const ref = useRef(null) + const [isVisible, setIsVisible] = useState(false) + + useEffect(() => { + if (!ref.current) return + + const observer = new IntersectionObserver( + (entries) => { + const entry = entries[0] + if (entry.isIntersecting) { + setIsVisible(true) + observer.unobserve(entry.target) + } + }, + { threshold: 0.1, rootMargin } + ) + + observer.observe(ref.current) + + return () => observer.disconnect() + }, [rootMargin]) + + return { ref, isVisible } +} From a05af28f73f5ad3390a71c2ad2c9e5567a98f7b9 Mon Sep 17 00:00:00 2001 From: John Doe Date: Sun, 16 Nov 2025 19:32:44 +0100 Subject: [PATCH 45/89] refactor: update streak handling in AppLayout and useAuth for improved logic and state management --- src/components/Layout/AppLayout.tsx | 6 +++--- src/features/auth/hooks/useAuth.ts | 18 ++++++++++++++++-- src/features/auth/stores/authStore.ts | 23 ++++++++++++++++++++--- 3 files changed, 39 insertions(+), 8 deletions(-) diff --git a/src/components/Layout/AppLayout.tsx b/src/components/Layout/AppLayout.tsx index bcafb398..b20b5795 100644 --- a/src/components/Layout/AppLayout.tsx +++ b/src/components/Layout/AppLayout.tsx @@ -11,17 +11,17 @@ import { AuthProvider } from 'src/providers/AuthProvider' import { Header } from './Header' export const AppLayout = () => { - const { isAuthModalOpen, setStreak, isConnected } = useAuth() + const { isAuthModalOpen, setStreak, shouldCountStreak } = useAuth() const postStreakMutation = usePostStreak() useEffect(() => { - if (isConnected) { + if (shouldCountStreak()) { postStreakMutation.mutateAsync(undefined).then((data) => { setStreak(data.streak) identifyUserStreak(data.streak) }) } - }, [isConnected]) + }, [shouldCountStreak]) return ( diff --git a/src/features/auth/hooks/useAuth.ts b/src/features/auth/hooks/useAuth.ts index 93fcf6a5..953924d4 100644 --- a/src/features/auth/hooks/useAuth.ts +++ b/src/features/auth/hooks/useAuth.ts @@ -1,4 +1,5 @@ import { signOut } from 'firebase/auth/web-extension' +import { useCallback } from 'react' import { AuthModalStore, AuthStore } from 'src/features/auth' import { trackUserDisconnect } from 'src/lib/analytics' import { firebaseAuth } from 'src/lib/firebase' @@ -9,17 +10,30 @@ export const useAuth = () => { const isConnected = authStore.user != null - const logout = async () => { + const shouldCountStreak = useCallback(() => { + if (!isConnected) return false + + const last = authStore.lastStreakUpdate + if (!last) return true + + const today = new Date().toDateString() + const lastDay = new Date(last).toDateString() + + return today !== lastDay + }, [isConnected, authStore.lastStreakUpdate]) + + const logout = useCallback(async () => { trackUserDisconnect() signOut(firebaseAuth) authStore.clear() return await firebaseAuth.signOut() - } + }, [authStore]) return { ...authModalStore, ...authStore, isConnected, + shouldCountStreak, logout, } } diff --git a/src/features/auth/stores/authStore.ts b/src/features/auth/stores/authStore.ts index 17c18ebc..ab369025 100644 --- a/src/features/auth/stores/authStore.ts +++ b/src/features/auth/stores/authStore.ts @@ -4,20 +4,25 @@ import { persist } from 'zustand/middleware' type AuthState = { user: User | null + lastStreakUpdate?: number providerId: string | null } type AuthActions = { initState: (state: AuthState) => void setStreak: (streak: number) => void + setLastStreakUpdate: (timestamp: number) => void clear: () => void } +type AuthStoreType = AuthState & AuthActions export const AuthStore = create( - persist( + persist( (set) => ({ user: null, providerId: null, + lastStreakUpdate: undefined, + setLastStreakUpdate: (timestamp: number) => set({ lastStreakUpdate: timestamp }), initState: (newState: AuthState) => set({ user: newState.user, @@ -25,15 +30,27 @@ export const AuthStore = create( }), setStreak: (streak: number) => set((state) => ({ + lastStreakUpdate: Date.now(), user: { ...state.user!, streak, }, })), - clear: () => set({ user: null }), + clear: () => set({ user: null, lastStreakUpdate: undefined }), }), { - name: 'auth-storage', // key in localStorage + version: 1, + name: 'auth-storage', + migrate: (persistedState, version) => { + const typedPersistedState = persistedState as unknown as AuthStoreType + if (version === 0) { + return { + ...typedPersistedState, + lastStreakUpdate: undefined, + } + } + return typedPersistedState + }, } ) ) From bb3f5d8e004ffe30c42fcbb476e6473f700e4125 Mon Sep 17 00:00:00 2001 From: John Doe Date: Sun, 16 Nov 2025 19:32:54 +0100 Subject: [PATCH 46/89] refactor: update button labels in AuthModal to clarify last used provider --- src/features/auth/components/AuthModal.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/features/auth/components/AuthModal.tsx b/src/features/auth/components/AuthModal.tsx index e555c850..518e9442 100644 --- a/src/features/auth/components/AuthModal.tsx +++ b/src/features/auth/components/AuthModal.tsx @@ -98,7 +98,7 @@ export const AuthModal = ({ showAuth }: AuthModalProps) => { }} className="relative" size="medium"> - {providerId === 'github.com' && Last} + {providerId === 'github.com' && Last used} Connect with Github
From f82ccc056cbac8c51f87b6d8055698c0c9034ab5 Mon Sep 17 00:00:00 2001 From: John Doe Date: Sun, 16 Nov 2025 19:33:06 +0100 Subject: [PATCH 47/89] refactor: improve ArticleItem component by utilizing user preferences for listing mode --- .../freecodecampCard/ArticleItem.tsx | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/src/features/cards/components/freecodecampCard/ArticleItem.tsx b/src/features/cards/components/freecodecampCard/ArticleItem.tsx index 32babafa..01262328 100644 --- a/src/features/cards/components/freecodecampCard/ArticleItem.tsx +++ b/src/features/cards/components/freecodecampCard/ArticleItem.tsx @@ -1,11 +1,13 @@ import { MdAccessTime } from 'react-icons/md' import { CardItemWithActions, CardLink, ColoredLanguagesBadge } from 'src/components/Elements' import { Attributes } from 'src/lib/analytics' +import { useUserPreferences } from 'src/stores/preferences' import { Article, BaseItemPropsType } from 'src/types' import { format } from 'timeago.js' const ArticleItem = (props: BaseItemPropsType
) => { const { item, selectedTag, analyticsTag } = props + const { listingMode } = useUserPreferences() return ( ) => { }}>
{item.title}
- <> -

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

-

- -

- + {listingMode === 'normal' && ( + <> +

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

+

+ +

+ + )} } /> From f805f0ee18e0fd237bc71ff8b62c436f6de0382b Mon Sep 17 00:00:00 2001 From: John Doe Date: Sun, 16 Nov 2025 19:33:47 +0100 Subject: [PATCH 48/89] refactor: center align tabFooter buttons for improved layout consistency --- src/features/onboarding/components/steps/tabs.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/onboarding/components/steps/tabs.css b/src/features/onboarding/components/steps/tabs.css index 278f16bf..e4426f61 100644 --- a/src/features/onboarding/components/steps/tabs.css +++ b/src/features/onboarding/components/steps/tabs.css @@ -77,7 +77,7 @@ column-gap: 12px; align-items: center; margin-top: 32px; - justify-content: flex-end; + justify-content: center; } .tabFooter button { border: none; From 5971020bd68bcd5ce04931458c534832b62d37da Mon Sep 17 00:00:00 2001 From: John Doe Date: Sun, 16 Nov 2025 19:33:55 +0100 Subject: [PATCH 49/89] refactor: remove duplicate 'value' property from Occupation type definition --- src/features/onboarding/types/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/features/onboarding/types/index.ts b/src/features/onboarding/types/index.ts index 8d3b6bc1..40fdb665 100644 --- a/src/features/onboarding/types/index.ts +++ b/src/features/onboarding/types/index.ts @@ -2,6 +2,7 @@ import { IconType } from 'react-icons/lib' export type Occupation = { title: string + value: string icon: IconType sources: string[] tags: string[] From f6f831f59700eba12965db30ef87efdacbe7bfda Mon Sep 17 00:00:00 2001 From: John Doe Date: Sun, 16 Nov 2025 19:34:03 +0100 Subject: [PATCH 50/89] refactor: remove unused key prop from CardItemWithActions in ArticleItem component --- src/features/cards/components/mediumCard/ArticleItem.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/features/cards/components/mediumCard/ArticleItem.tsx b/src/features/cards/components/mediumCard/ArticleItem.tsx index 583cc7d3..62490ca3 100644 --- a/src/features/cards/components/mediumCard/ArticleItem.tsx +++ b/src/features/cards/components/mediumCard/ArticleItem.tsx @@ -12,7 +12,6 @@ const ArticleItem = ({ item, selectedTag, analyticsTag }: BaseItemPropsType From 166ca1b46970ead7d7d0215b3d02054223f8ee1c Mon Sep 17 00:00:00 2001 From: John Doe Date: Sun, 16 Nov 2025 19:34:28 +0100 Subject: [PATCH 51/89] refactor: simplify loading state handling in ListComponent --- src/components/List/ListComponent.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/List/ListComponent.tsx b/src/components/List/ListComponent.tsx index c283cf38..7bab2714 100644 --- a/src/components/List/ListComponent.tsx +++ b/src/components/List/ListComponent.tsx @@ -78,6 +78,9 @@ export function ListComponent(props: ListComponentPropsType) { } }, [sortedData, header, renderItem, limit]) + if (isLoading) { + return + } if (error) { return

{error?.message || error}

} @@ -90,5 +93,5 @@ export function ListComponent(props: ListComponentPropsType) { ) } - return <>{isLoading ? : enrichedItems} + return <>{enrichedItems} } From 21e11333f7346e995ce268eb467a7de6e057ebcd Mon Sep 17 00:00:00 2001 From: John Doe Date: Sun, 16 Nov 2025 19:34:39 +0100 Subject: [PATCH 52/89] refactor: optimize useSelectedTags hook by consolidating user preferences retrieval --- src/features/cards/hooks/useSelectedTags.tsx | 41 +++++++++++++------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/src/features/cards/hooks/useSelectedTags.tsx b/src/features/cards/hooks/useSelectedTags.tsx index f4c38c36..7b2c7a4c 100644 --- a/src/features/cards/hooks/useSelectedTags.tsx +++ b/src/features/cards/hooks/useSelectedTags.tsx @@ -1,5 +1,6 @@ import { useMemo } from 'react' import { useUserPreferences } from 'src/stores/preferences' +import { useShallow } from 'zustand/shallow' import { MY_LANGUAGES_OPTION } from '../config' type useSelectedTagsProps = { @@ -10,27 +11,41 @@ type useSelectedTagsProps = { } } export const useSelectedTags = ({ source, fallbackTag }: useSelectedTagsProps) => { - const cardSettings = useUserPreferences((state) => state.cardsSettings?.[source]) - const { userSelectedTags } = useUserPreferences() - + const { cardSettings, userSelectedTags } = useUserPreferences( + useShallow((state) => { + return { + cardSettings: state.cardsSettings?.[source], + userSelectedTags: state.userSelectedTags, + } + }) + ) + const { language } = cardSettings || {} const selectedTags = useMemo(() => { - if (!cardSettings?.language) { + if (!language || (language === MY_LANGUAGES_OPTION.value && userSelectedTags.length === 0)) { return [fallbackTag] } - if (cardSettings.language === MY_LANGUAGES_OPTION.value) { + if (language === MY_LANGUAGES_OPTION.value) { return userSelectedTags } - return [userSelectedTags.find((lang) => lang.value === cardSettings?.language) || fallbackTag] - }, [userSelectedTags, cardSettings]) + return [userSelectedTags.find((lang) => lang.value === language) || fallbackTag] + }, [userSelectedTags, language]) + + const selectedTag = useMemo(() => { + return language + ? [MY_LANGUAGES_OPTION, ...userSelectedTags].find((lang) => lang.value === language) || + fallbackTag + : fallbackTag + }, [language, userSelectedTags]) + + const queryTags = useMemo(() => { + return selectedTags.map((tag) => tag.value) + }, [selectedTags]) return { - queryTags: selectedTags, - selectedTag: cardSettings?.language - ? [MY_LANGUAGES_OPTION, ...userSelectedTags].find( - (lang) => lang.value === cardSettings.language - ) || fallbackTag - : fallbackTag, + selectedTags, + queryTags, + selectedTag, cardSettings, } } From a19477e6727e832d0be76bd062d39d16febadf3c Mon Sep 17 00:00:00 2001 From: John Doe Date: Sun, 16 Nov 2025 19:34:48 +0100 Subject: [PATCH 53/89] refactor: update RemoteConfig type by removing unused properties and standardizing tags structure --- src/features/remoteConfig/types/index.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/features/remoteConfig/types/index.ts b/src/features/remoteConfig/types/index.ts index e75ce7e7..ef20d0e5 100644 --- a/src/features/remoteConfig/types/index.ts +++ b/src/features/remoteConfig/types/index.ts @@ -1,14 +1,9 @@ export type Tag = { label: string value: string + category?: string } export type RemoteConfig = { - supportedTags: Tag[] - marketingBannerConfig?: any - adsConfig: { - rowPosition: number - columnPosition: number - enabled: boolean - } + tags: Tag[] } From 868546e311d4758cd35b7ec807dec30ad11a8034 Mon Sep 17 00:00:00 2001 From: John Doe Date: Sun, 16 Nov 2025 19:35:16 +0100 Subject: [PATCH 54/89] refactor: enhance sortOptions in HashnodeCard with reactions and comments --- .../components/hashnodeCard/HashnodeCard.tsx | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/features/cards/components/hashnodeCard/HashnodeCard.tsx b/src/features/cards/components/hashnodeCard/HashnodeCard.tsx index d5e485c1..2082aed7 100644 --- a/src/features/cards/components/hashnodeCard/HashnodeCard.tsx +++ b/src/features/cards/components/hashnodeCard/HashnodeCard.tsx @@ -1,4 +1,6 @@ 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 { Article, CardPropsType } from 'src/types' @@ -48,7 +50,19 @@ export function HashnodeCard(props: CardPropsType) { sortBy={sortBy} language={language || GLOBAL_TAG.value} globalTag={GLOBAL_TAG} - sortOptions={[]} + sortOptions={(defaults) => [ + ...defaults, + { + label: 'Reactions', + value: 'points_count', + icon: , + }, + { + label: 'Comments', + value: 'comments_count', + icon: , + }, + ]} /> } {...props}> From 62def59b2b098b0d0a4f917c647f691827d3ce47 Mon Sep 17 00:00:00 2001 From: John Doe Date: Tue, 18 Nov 2025 21:09:45 +0100 Subject: [PATCH 55/89] refactor: enhance Header component with layout toggle functionality and optimize callbacks --- src/components/Layout/Header.tsx | 35 ++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/src/components/Layout/Header.tsx b/src/components/Layout/Header.tsx index d5131be8..707527eb 100644 --- a/src/components/Layout/Header.tsx +++ b/src/components/Layout/Header.tsx @@ -1,9 +1,11 @@ import { clsx } from 'clsx' -import { useEffect, useState } from 'react' +import { useCallback, useEffect, useState } from 'react' import { BsFillBookmarksFill, BsFillGearFill, BsMoonFill } from 'react-icons/bs' import { CgTab } from 'react-icons/cg' import { IoMdSunny } from 'react-icons/io' import { MdDoDisturbOff } from 'react-icons/md' +import { RiDashboardHorizontalFill } from 'react-icons/ri' +import { TfiLayoutColumn4Alt } from 'react-icons/tfi' import { Link, useLocation, useNavigate } from 'react-router-dom' import AvatarPlaceholder from 'src/assets/icons/avatar.svg?react' import StreakIcon from 'src/assets/icons/fire_icon.svg?react' @@ -11,7 +13,12 @@ import HackertabLogo from 'src/assets/logo.svg?react' import { UserTags } from 'src/components/Elements/UserTags' import { useAuth } from 'src/features/auth' import { Changelog } from 'src/features/changelog' -import { identifyUserTheme, trackDNDDisable, trackThemeSelect } from 'src/lib/analytics' +import { + identifyUserTheme, + trackDNDDisable, + trackDisplayTypeChange, + trackThemeSelect, +} from 'src/lib/analytics' import { useUserPreferences } from 'src/stores/preferences' import { Button, CircleButton } from '../Elements' import { SearchEngineBar } from '../Elements/SearchBar/SearchEngineBar' @@ -19,7 +26,8 @@ export const Header = () => { const { openAuthModal, user, isConnected, isConnecting } = useAuth() const [themeIcon, setThemeIcon] = useState() - const { theme, setTheme, setDNDDuration, isDNDModeActive } = useUserPreferences() + const { theme, setTheme, setDNDDuration, isDNDModeActive, layout, setLayout } = + useUserPreferences() const navigate = useNavigate() const location = useLocation() @@ -38,21 +46,27 @@ export const Header = () => { } }, [theme]) - const onThemeChange = () => { + const onThemeChange = useCallback(() => { const newTheme = theme === 'dark' ? 'light' : 'dark' setTheme(newTheme) trackThemeSelect(newTheme) identifyUserTheme(newTheme) - } + }, [theme, setTheme]) - const onSettingsClick = () => { + const onLayoutChange = useCallback(() => { + const newLayout = layout === 'cards' ? 'grid' : 'cards' + trackDisplayTypeChange(newLayout) + setLayout(newLayout) + }, [layout, setLayout]) + + const onSettingsClick = useCallback(() => { navigate('/settings/general') - } + }, [navigate]) - const onUnpauseClicked = () => { + const onUnpauseClicked = useCallback(() => { trackDNDDisable() setDNDDuration('never') - } + }, [setDNDDuration]) return ( <> @@ -83,6 +97,9 @@ export const Header = () => { + + {layout === 'cards' ? : } + {themeIcon} From 701cb2689e508134bea5bf8cb95489e4a1805e1f Mon Sep 17 00:00:00 2001 From: John Doe Date: Tue, 18 Nov 2025 22:28:57 +0100 Subject: [PATCH 56/89] refactor: streamline Article and Product types by reorganizing properties and enhancing clarity --- src/types/index.ts | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/src/types/index.ts b/src/types/index.ts index adb7cd9b..52efb3ab 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -32,26 +32,25 @@ export type BaseEntry = { id: string url: string title: string -} - -export type Article = BaseEntry & { - published_at: number tags: Array - points_count: number comments_count: number - votes_count: number + points_count: number image_url: string - tagline?: string + published_at: number + description?: string +} + +export type Article = BaseEntry & { source: string original_url?: string comments_url?: string - description?: string - subreddit?: string - flair_text?: string - flair_background_color?: string - flair_text_color?: string } +export type Product = BaseEntry & { + tagline: string + votes_count: number + topics: Array +} export type FeedItem = { title: string id: string @@ -106,7 +105,7 @@ export type Repository = BaseEntry & { export type Conference = BaseEntry & { start_date: number end_date: number - tag: string + tags: string[] online: Boolean city?: string country?: string @@ -136,7 +135,6 @@ export type BaseItemPropsType< id: string } > = { - index: number item: T className?: string analyticsTag: string From 6a23f7ba3608c0e46443bb4dae87929e8d264561 Mon Sep 17 00:00:00 2001 From: John Doe Date: Tue, 18 Nov 2025 22:29:02 +0100 Subject: [PATCH 57/89] refactor: update CardItemWithActionsProps type to define item structure explicitly --- .../Elements/CardWithActions/CardItemWithActions.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/Elements/CardWithActions/CardItemWithActions.tsx b/src/components/Elements/CardWithActions/CardItemWithActions.tsx index 2e5591b2..8c2f8ef3 100644 --- a/src/components/Elements/CardWithActions/CardItemWithActions.tsx +++ b/src/components/Elements/CardWithActions/CardItemWithActions.tsx @@ -7,10 +7,13 @@ import { ShareModalData } from 'src/features/shareModal/types' import { Attributes, trackLinkBookmark, trackLinkUnBookmark } from 'src/lib/analytics' import { useBookmarks } from 'src/stores/bookmarks' import { useUserPreferences } from 'src/stores/preferences' -import { BaseEntry } from 'src/types' type CardItemWithActionsProps = { - item: BaseEntry + item: { + title: string + url: string + id: string + } source: string cardItem: React.ReactNode sourceType?: 'rss' | 'supported' From dfe822bb83075625275b2a34d5630f5df84727ef Mon Sep 17 00:00:00 2001 From: John Doe Date: Tue, 18 Nov 2025 22:57:06 +0100 Subject: [PATCH 58/89] refactor: replace useQueries with useQuery in getConferences and getGithubRepos; update getArticles in getProductHuntProducts and add getSourceArticles --- src/features/cards/api/getConferences.ts | 26 ++++++------- src/features/cards/api/getGithubRepos.ts | 37 +++++++++++-------- .../cards/api/getProductHuntProducts.ts | 19 ++++++---- src/features/cards/api/getSourceArticles.ts | 35 ++++++++++++++++++ 4 files changed, 82 insertions(+), 35 deletions(-) create mode 100644 src/features/cards/api/getSourceArticles.ts diff --git a/src/features/cards/api/getConferences.ts b/src/features/cards/api/getConferences.ts index acbe8a93..c6c0ad33 100644 --- a/src/features/cards/api/getConferences.ts +++ b/src/features/cards/api/getConferences.ts @@ -1,10 +1,14 @@ -import { useQueries, UseQueryOptions } from '@tanstack/react-query' -import { QueryConfig } from 'src/lib/react-query' -import { Conference } from 'src/types' +import { useQuery } from '@tanstack/react-query' import { axios } from 'src/lib/axios' +import { ExtractFnReturnType, QueryConfig } from 'src/lib/react-query' +import { Conference } from 'src/types' -const getConferences = async (tag: string): Promise => { - return axios.get(`/data/v2/conferences/${tag}.json`) +const getConferences = async (tags: string[]): Promise => { + return axios.get(`/engine/conferences`, { + params: { + tags: tags?.join(','), + }, + }) } type QueryFnType = typeof getConferences @@ -15,13 +19,9 @@ type UseGetConferencesOptions = { } export const useGetConferences = ({ config, tags }: UseGetConferencesOptions) => { - return useQueries({ - queries: tags.map>((tag) => { - return { - ...config, - queryKey: ['conferences', tag], - queryFn: () => getConferences(tag), - } - }) + return useQuery>({ + ...config, + queryKey: ['conferences', ...tags], + queryFn: () => getConferences(tags), }) } diff --git a/src/features/cards/api/getGithubRepos.ts b/src/features/cards/api/getGithubRepos.ts index 07b7916a..5618db1e 100644 --- a/src/features/cards/api/getGithubRepos.ts +++ b/src/features/cards/api/getGithubRepos.ts @@ -1,10 +1,21 @@ -import { useQueries, UseQueryOptions } from '@tanstack/react-query' -import { QueryConfig } from 'src/lib/react-query' -import { Repository } from 'src/types' +import { useQuery } from '@tanstack/react-query' import { axios } from 'src/lib/axios' +import { ExtractFnReturnType, QueryConfig } from 'src/lib/react-query' +import { Repository } from 'src/types' -const getRepos = async (tag: string, dateRange: string): Promise => { - return axios.get(`/data/v2/github/${tag}/${dateRange}.json`) +const getRepos = async ({ + tags, + dateRange, +}: { + tags: string[] + dateRange: string +}): Promise => { + return axios.get(`/engine/repos`, { + params: { + range: dateRange, + tags: tags.join(','), + }, + }) } type QueryFnType = typeof getRepos @@ -12,17 +23,13 @@ type QueryFnType = typeof getRepos type UseGetReposOptions = { config?: QueryConfig tags: string[] - dateRange: "daily" | "monthly" | "weekly" + dateRange: 'daily' | 'monthly' | 'weekly' } export const useGetGithubRepos = ({ config, tags, dateRange }: UseGetReposOptions) => { - return useQueries({ - queries: tags.map>((tag) => { - return { - ...config, - queryKey: ['github', tag, dateRange], - queryFn: () => getRepos(tag, dateRange), - } - }) + return useQuery>({ + ...config, + queryKey: ['github', ...tags, dateRange], + queryFn: () => getRepos({ tags, dateRange }), }) -} \ No newline at end of file +} diff --git a/src/features/cards/api/getProductHuntProducts.ts b/src/features/cards/api/getProductHuntProducts.ts index fcff8f87..f37e4809 100644 --- a/src/features/cards/api/getProductHuntProducts.ts +++ b/src/features/cards/api/getProductHuntProducts.ts @@ -1,22 +1,27 @@ import { useQuery } from '@tanstack/react-query' -import { ExtractFnReturnType, QueryConfig } from 'src/lib/react-query' -import { Article } from 'src/types' import { axios } from 'src/lib/axios' +import { ExtractFnReturnType, QueryConfig } from 'src/lib/react-query' +import { Product } from 'src/types' -const getArticles = async (): Promise => { - return axios.get('/data/v2/producthunt.json') +const getArticles = async ({ date }: { date: string }): Promise => { + return axios.get(`/engine/products`, { + params: { + date, + }, + }) } type QueryFnType = typeof getArticles type UseGetArticlesOptions = { config?: QueryConfig + date: string } -export const useGeProductHuntProducts = ({ config }: UseGetArticlesOptions = {}) => { +export const useGeProductHuntProducts = ({ date, config }: UseGetArticlesOptions) => { return useQuery>({ ...config, - queryKey: ['producthunt'], - queryFn: () => getArticles(), + queryKey: ['producthunt', date], + queryFn: () => getArticles({ date }), }) } diff --git a/src/features/cards/api/getSourceArticles.ts b/src/features/cards/api/getSourceArticles.ts new file mode 100644 index 00000000..eef2bdd5 --- /dev/null +++ b/src/features/cards/api/getSourceArticles.ts @@ -0,0 +1,35 @@ +import { useQuery } from '@tanstack/react-query' +import { axios } from 'src/lib/axios' +import { ExtractFnReturnType, QueryConfig } from 'src/lib/react-query' +import { Article } from 'src/types' + +const getArticles = async ({ + source, + tags, +}: { + source: string + tags?: string[] +}): Promise => { + return axios.get(`/engine/feeds`, { + params: { + source, + ...(tags?.length ? { tags: tags.join(',') } : {}), + }, + }) +} + +type QueryFnType = typeof getArticles + +type UseGetArticlesOptions = { + config?: QueryConfig + source: string + tags?: string[] +} + +export const useGetSourceArticles = ({ config, source, tags }: UseGetArticlesOptions) => { + return useQuery>({ + ...config, + queryKey: [source, ...(tags || [])], + queryFn: () => getArticles({ source, tags }), + }) +} From 9f450f1def809164ac548871b5b167d93cd5fb47 Mon Sep 17 00:00:00 2001 From: John Doe Date: Tue, 18 Nov 2025 22:57:13 +0100 Subject: [PATCH 59/89] fix: update getRemoteConfig to fetch from the correct config file path --- src/features/remoteConfig/api/getRemoteConfig.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/remoteConfig/api/getRemoteConfig.ts b/src/features/remoteConfig/api/getRemoteConfig.ts index 166ed770..8d41b728 100644 --- a/src/features/remoteConfig/api/getRemoteConfig.ts +++ b/src/features/remoteConfig/api/getRemoteConfig.ts @@ -5,7 +5,7 @@ import { useRemoteConfigStore } from '../stores/remoteConfig' import { RemoteConfig } from '../types' const getRemoteConfig = async (): Promise => { - return axios.get('/data/remoteConfiguration.json') + return axios.get('/data/config.json') } type QueryFnType = typeof getRemoteConfig From 2b8251feebdfc16a328420381d3cc64f534f11de Mon Sep 17 00:00:00 2001 From: John Doe Date: Tue, 18 Nov 2025 22:57:18 +0100 Subject: [PATCH 60/89] fix: adjust title display in ConferencesItem to improve layout --- .../cards/components/conferencesCard/ConferenceItem.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/features/cards/components/conferencesCard/ConferenceItem.tsx b/src/features/cards/components/conferencesCard/ConferenceItem.tsx index 050d476d..d6811e37 100644 --- a/src/features/cards/components/conferencesCard/ConferenceItem.tsx +++ b/src/features/cards/components/conferencesCard/ConferenceItem.tsx @@ -83,7 +83,8 @@ const ConferencesItem = ({ item, analyticsTag }: BaseItemPropsType) }}>
{differenceInDays < 0 && Ended}{' '} - {conferenceLocation?.icon} {item.title} + {conferenceLocation?.icon} + {item.title}
{listingMode === 'normal' ? ( From 16802976170a089d1d9178bda611e786cba08d4b Mon Sep 17 00:00:00 2001 From: John Doe Date: Tue, 18 Nov 2025 22:57:37 +0100 Subject: [PATCH 61/89] refactor: simplify cardSettings destructuring in GithubCard component --- src/features/cards/components/githubCard/GithubCard.tsx | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/features/cards/components/githubCard/GithubCard.tsx b/src/features/cards/components/githubCard/GithubCard.tsx index df2d2cf8..074634b7 100644 --- a/src/features/cards/components/githubCard/GithubCard.tsx +++ b/src/features/cards/components/githubCard/GithubCard.tsx @@ -19,14 +19,11 @@ export function GithubCard(props: CardPropsType) { const { ref, isVisible } = useLazyListLoad() const setCardSettings = useUserPreferences((state) => state.setCardSettings) - const { - queryTags, - selectedTag, - cardSettings: { dateRange, sortBy, language } = {}, - } = useSelectedTags({ + const { queryTags, selectedTag, cardSettings } = useSelectedTags({ source: meta.value, fallbackTag: GLOBAL_TAG, }) + const { dateRange, language, sortBy } = cardSettings const selectedDateRange = useMemo( () => dateRanges.find((date) => date.value === dateRange) || dateRanges[0], @@ -82,7 +79,7 @@ export function GithubCard(props: CardPropsType) { value={date.value} disabled={selectedDateRange.value === date.value} onClick={() => { - setCardSettings(meta.value, { dateRange: date.value, language, sortBy }) + setCardSettings(meta.value, { ...cardSettings, dateRange: date.value }) }}> {date.label} From 79c268ec9ed4de921652e01105bdae8c8c90aa75 Mon Sep 17 00:00:00 2001 From: John Doe Date: Tue, 18 Nov 2025 22:57:45 +0100 Subject: [PATCH 62/89] fix: update ArticleItem component to use Product type instead of Article type --- src/features/cards/components/producthuntCard/ArticleItem.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/features/cards/components/producthuntCard/ArticleItem.tsx b/src/features/cards/components/producthuntCard/ArticleItem.tsx index 096a9911..c062ba33 100644 --- a/src/features/cards/components/producthuntCard/ArticleItem.tsx +++ b/src/features/cards/components/producthuntCard/ArticleItem.tsx @@ -3,9 +3,9 @@ import { VscTriangleUp } from 'react-icons/vsc' import { CardItemWithActions, CardLink } from 'src/components/Elements' import { Attributes } from 'src/lib/analytics' import { useUserPreferences } from 'src/stores/preferences' -import { Article, BaseItemPropsType } from 'src/types' +import { BaseItemPropsType, Product } from 'src/types' -const ArticleItem = ({ item, analyticsTag }: BaseItemPropsType
) => { +const ArticleItem = ({ item, analyticsTag }: BaseItemPropsType) => { const { listingMode } = useUserPreferences() return ( From 75ddc98eddf4789ba449285f7edbdb1a263778f6 Mon Sep 17 00:00:00 2001 From: John Doe Date: Tue, 18 Nov 2025 22:57:54 +0100 Subject: [PATCH 63/89] fix: update ProductHuntCard to use Product type instead of Article type --- .../cards/components/producthuntCard/ProducthuntCard.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/features/cards/components/producthuntCard/ProducthuntCard.tsx b/src/features/cards/components/producthuntCard/ProducthuntCard.tsx index 75d73b34..f685965a 100644 --- a/src/features/cards/components/producthuntCard/ProducthuntCard.tsx +++ b/src/features/cards/components/producthuntCard/ProducthuntCard.tsx @@ -5,7 +5,7 @@ import { Card } from 'src/components/Elements' import { ListComponent } from 'src/components/List' import { ProductHuntPlaceholder } from 'src/components/placeholders' import { useUserPreferences } from 'src/stores/preferences' -import { Article, CardPropsType } from 'src/types' +import { CardPropsType, Product } from 'src/types' import { useShallow } from 'zustand/shallow' import { useGeProductHuntProducts } from '../../api/getProductHuntProducts' import { useLazyListLoad } from '../../hooks/useLazyListLoad' @@ -31,7 +31,7 @@ export function ProductHuntCard(props: CardPropsType) { }) const renderItem = useCallback( - (item: Article) => , + (item: Product) => , [meta.analyticsTag] ) @@ -59,7 +59,7 @@ export function ProductHuntCard(props: CardPropsType) { ]} /> }> - items={products} error={error} isLoading={isLoading} From 3c336eeeeb57a93650648e720133ec16a9f37604 Mon Sep 17 00:00:00 2001 From: John Doe Date: Tue, 18 Nov 2025 22:58:08 +0100 Subject: [PATCH 64/89] fix: migrate userSelectedTags to enhance tag compatibility and update storage handling --- src/stores/preferences.ts | 64 +++++++++++++++++++++------------------ 1 file changed, 35 insertions(+), 29 deletions(-) diff --git a/src/stores/preferences.ts b/src/stores/preferences.ts index 11880f16..d127fec7 100644 --- a/src/stores/preferences.ts +++ b/src/stores/preferences.ts @@ -72,14 +72,29 @@ const defaultStorage: StateStorage = { state: UserPreferencesState } = JSON.parse(item) - const remoteConfigStore = useRemoteConfigStore.getState() - const newState = { ...state, - userSelectedTags: enhanceTags( - remoteConfigStore, - state.userSelectedTags as unknown as string[] - ), + } + if (version == 0) { + const MAP_OLD_TAGS: Record = { + 'artificial-intelligence': 'artificial intelligence', + 'machine-learning': 'machine learning', + c: 'clang', + cpp: 'c++', + csharp: 'c#', + 'data-science': 'data science', + go: 'golang', + 'objective-c': 'objectivec', + } + + const stateTags = state.userSelectedTags as unknown as string[] + const newTags = stateTags.map((tag) => { + if (MAP_OLD_TAGS[tag]) { + return MAP_OLD_TAGS[tag] + } + return tag + }) + newState.userSelectedTags = enhanceTags(useRemoteConfigStore.getState(), newTags) } return JSON.stringify({ state: newState, version }) @@ -89,21 +104,7 @@ const defaultStorage: StateStorage = { }, setItem: (name: string, value: string) => { try { - let { - state, - version, - }: { - version: number - state: UserPreferencesState - } = JSON.parse(value) - - const newState = { - ...state, - userSelectedTags: state.userSelectedTags.map((tag) => tag.value), - } - - const newValue = JSON.stringify({ state: newState, version }) - window.localStorage.setItem(name, newValue) + window.localStorage.setItem(name, value) } catch (e) { window.localStorage.setItem(name, '') } @@ -142,15 +143,14 @@ export const useUserPreferences = create( userCustomCards: [], DNDDuration: 'never', advStatus: false, - setLayout: (layout) => set({ layout: layout }), - setPromptEngine: (promptEngine: string) => set({ promptEngine: promptEngine }), - setListingMode: (listingMode: ListingMode) => set({ listingMode: listingMode }), - setTheme: (theme: Theme) => set({ theme: theme }), - setOpenLinksNewTab: (openLinksNewTab: boolean) => set({ openLinksNewTab: openLinksNewTab }), + setLayout: (layout) => set({ layout }), + setPromptEngine: (promptEngine: string) => set({ promptEngine }), + setListingMode: (listingMode: ListingMode) => set({ listingMode }), + setTheme: (theme: Theme) => set({ theme }), + setOpenLinksNewTab: (openLinksNewTab: boolean) => set({ openLinksNewTab }), setCards: (selectedCards: SelectedCard[]) => set({ cards: selectedCards }), setTags: (selectedTags: Tag[]) => set({ userSelectedTags: selectedTags }), - setMaxVisibleCards: (maxVisibleCards: number) => set({ maxVisibleCards: maxVisibleCards }), - + setMaxVisibleCards: (maxVisibleCards: number) => set({ maxVisibleCards }), setCardSettings: (card: string, settings: CardSettingsType) => set((state) => ({ cardsSettings: { @@ -218,7 +218,7 @@ export const useUserPreferences = create( set((state) => { const exists = state.userSelectedTags.find((t) => t.value === tag.value) if (exists) { - return state // No change if tag already followed + return state } return { userSelectedTags: [...state.userSelectedTags, tag], @@ -233,7 +233,13 @@ export const useUserPreferences = create( }), { name: 'preferences_storage', + version: 1, storage: createJSONStorage(() => defaultStorage), + migrate: (persistedState) => { + const state = persistedState as unknown as UserPreferencesState & + UserPreferencesStoreActions + return state + }, } ) ) From 91bee9bcda3316ccc9d07f3bb19b413921b56e0e Mon Sep 17 00:00:00 2001 From: John Doe Date: Tue, 18 Nov 2025 22:58:14 +0100 Subject: [PATCH 65/89] fix: enhance TopicSettings component with improved tag handling and search functionality --- .../settings/components/TopicSettings.tsx | 184 +++++++++++++++--- 1 file changed, 154 insertions(+), 30 deletions(-) diff --git a/src/features/settings/components/TopicSettings.tsx b/src/features/settings/components/TopicSettings.tsx index 86d50a7d..0be73258 100644 --- a/src/features/settings/components/TopicSettings.tsx +++ b/src/features/settings/components/TopicSettings.tsx @@ -1,45 +1,169 @@ -import { ChipsSet } from 'src/components/Elements' +import { useMemo, useState } from 'react' +import { AiFillMobile } from 'react-icons/ai' +import { BsChevronDown, BsChevronUp, BsFillGearFill, BsFillShieldLockFill } from 'react-icons/bs' +import { FaDatabase, FaPaintBrush, FaRobot, FaServer } from 'react-icons/fa' +import { IoMdSearch } from 'react-icons/io' +import { RiDeviceFill } from 'react-icons/ri' +import { SiBnbchain } from 'react-icons/si' +import { ChipsSet, SearchBar } from 'src/components/Elements' import { SettingsContentLayout } from 'src/components/Layout/SettingsContentLayout/SettingsContentLayout' +import { repository } from 'src/config' import { Tag, useRemoteConfigStore } from 'src/features/remoteConfig' import { trackLanguageAdd, trackLanguageRemove } from 'src/lib/analytics' import { useUserPreferences } from 'src/stores/preferences' +const CATEGORY_TO_ICON: Record = { + frontend: , + backend: , + fullstack: , + mobile: , + devops: , + ai: , + data: , + security: , + blockchain: , +} export const TopicSettings = () => { - const { userSelectedTags, setTags } = useUserPreferences() + const { userSelectedTags, occupation, followTag, unfollowTag } = useUserPreferences() + + const { tags } = useRemoteConfigStore() + const [searchKeyword, setSearchKeyword] = useState('') + const filteredTags = useMemo(() => { + if (searchKeyword.trim() === '') { + return tags + } + return tags.filter( + (tag) => + tag.label.toLowerCase().includes(searchKeyword.trim().toLowerCase()) || + tag.category?.toLowerCase().includes(searchKeyword.trim().toLowerCase()) + ) + }, [tags, searchKeyword]) + + const groupedTags = useMemo(() => { + const groups = filteredTags.reduce>((acc, tag) => { + ;(acc[tag.category || 'Other'] ??= []).push(tag) + return acc + }, {}) + + if ('Other' in groups) { + const { Other, ...rest } = groups + return { ...rest, Other } + } - const { supportedTags } = useRemoteConfigStore() + return groups + }, [filteredTags]) - const tags = supportedTags - .map((tag) => { - return { - label: tag.label, - value: tag.value, - } - }) - .sort((a, b) => (a.label > b.label ? 1 : -1)) + const [expandedCategories, setExpandedCategories] = useState( + occupation ? [occupation] : ['backend', 'frontend'] + ) return ( - tag.value)} - onChange={(changes, selectedChips) => { - const selectedTags = - (selectedChips - .map((tag) => supportedTags.find((st) => st.value === tag.value)) - .filter(Boolean) as Tag[]) || [] - setTags(selectedTags) - - if (changes.action == 'ADD') { - trackLanguageAdd(changes.option.value) - } else { - trackLanguageRemove(changes.option.value) - } - }} - /> + {userSelectedTags.length > 0 ? ( + tag.value)} + onChange={(changes) => { + if (changes.action == 'ADD') { + followTag(changes.option as Tag) + trackLanguageAdd(changes.option.value) + } else { + unfollowTag(changes.option as Tag) + trackLanguageRemove(changes.option.value) + } + }} + /> + ) : ( +
+

You are not following any topics yet. Start exploring below!

+
+ )} + +
+ +
+
+

Explore new topics

+

+ Explore and follow new topics to customize your feed further. +

+
+
+
+ } + placeholder="Search by programming language, framework, or topic" + onChange={(keyword) => { + setSearchKeyword(keyword) + }} + /> +
+
+ {Object.keys(groupedTags).length == 0 ? ( +
+

+ No results found, try adjusting your search keyword. +
+ If you think this technology is missing, feel free to{' '} + + open an issue + {' '} + to suggest it. +

+
+ ) : ( + Object.keys(groupedTags).map((category) => ( +
+ + + {expandedCategories.includes(category) && ( + (a.label > b.label ? 1 : -1)) + .map((tag) => ({ + label: tag.label, + value: tag.value, + }))} + defaultValues={userSelectedTags.map((tag) => tag.value)} + onChange={(changes) => { + if (changes.action == 'ADD') { + followTag(changes.option as Tag) + trackLanguageAdd(changes.option.value) + } else { + unfollowTag(changes.option as Tag) + trackLanguageRemove(changes.option.value) + } + }} + /> + )} +
+ )) + )} +
) } From 7f6d34aca342a2f200807e34873eabe97de36314 Mon Sep 17 00:00:00 2001 From: John Doe Date: Tue, 18 Nov 2025 22:58:18 +0100 Subject: [PATCH 66/89] fix: add styles for category titles, subtitle buttons, and expand buttons in SettingsContentLayout --- .../settingsContentLayout.css | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/src/components/Layout/SettingsContentLayout/settingsContentLayout.css b/src/components/Layout/SettingsContentLayout/settingsContentLayout.css index a79379a8..6cab9b24 100644 --- a/src/components/Layout/SettingsContentLayout/settingsContentLayout.css +++ b/src/components/Layout/SettingsContentLayout/settingsContentLayout.css @@ -13,6 +13,55 @@ display: flex; flex-direction: column; gap: 16px; + + .categoryTitle { + text-transform: capitalize; + font-size: 16px; + margin-bottom: 16px; + color: var(--primary-text-color); + + & .icon { + vertical-align: top; + } + } + + .subTitleButton { + cursor: pointer; + background: none; + border: none; + margin: 0; + padding: 0; + } + .subTitleIcon { + font-size: 12px; + } + .topicsFlex { + display: flex; + margin-top: 12px; + flex-direction: column; + flex-wrap: wrap; + gap: 24px; + } + .categoryContent { + margin-top: 12px; + } + .expandButton { + width: auto; + font-size: 12px; + height: 20px; + border: none; + background-color: var(--button-background-color); + border-radius: 20px; + justify-content: center; + align-items: center; + text-align: center; + display: inline-flex; + padding: 0 6px; + display: inline-flex; + column-gap: 4px; + text-transform: lowercase; + cursor: pointer; + } } .settingsContent header { display: flex; From cdc2a18a01e1f52938d92e82e2fa4f53843f60b4 Mon Sep 17 00:00:00 2001 From: John Doe Date: Wed, 19 Nov 2025 18:52:22 +0100 Subject: [PATCH 67/89] fix: update Vite configuration to include additional dependencies and disable sourcemaps --- vite.config.mjs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/vite.config.mjs b/vite.config.mjs index cb8180db..e6e6930d 100644 --- a/vite.config.mjs +++ b/vite.config.mjs @@ -75,13 +75,17 @@ export default defineConfig(({ mode }) => { 'react-select', 'react-share', 'react-simple-toasts', + 'react-responsive', 'react-toggle', 'react-tooltip', 'react-icons', 'react-markdown', + 'react-modal', 'react-spring-bottom-sheet', + 'react-infinite-scroll-hook', '@dnd-kit/core', '@dnd-kit/sortable', + '@szhsin/react-menu', ], utils: [ '@amplitude/analytics-browser', @@ -97,6 +101,7 @@ export default defineConfig(({ mode }) => { }, server: { open: true, + sourcemap: false, proxy: { '/api': { target: env.VITE_API_URL, From 8d650bb80c5641c161ac2925241dbc41d05ec67d Mon Sep 17 00:00:00 2001 From: John Doe Date: Wed, 19 Nov 2025 19:04:54 +0100 Subject: [PATCH 68/89] fix: rename shouldCountStreak to shouldIcrementStreak and update date comparison logic --- src/components/Layout/AppLayout.tsx | 6 +++--- src/features/auth/hooks/useAuth.ts | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/Layout/AppLayout.tsx b/src/components/Layout/AppLayout.tsx index b20b5795..30dee94a 100644 --- a/src/components/Layout/AppLayout.tsx +++ b/src/components/Layout/AppLayout.tsx @@ -11,17 +11,17 @@ import { AuthProvider } from 'src/providers/AuthProvider' import { Header } from './Header' export const AppLayout = () => { - const { isAuthModalOpen, setStreak, shouldCountStreak } = useAuth() + const { isAuthModalOpen, setStreak, shouldIcrementStreak } = useAuth() const postStreakMutation = usePostStreak() useEffect(() => { - if (shouldCountStreak()) { + if (shouldIcrementStreak()) { postStreakMutation.mutateAsync(undefined).then((data) => { setStreak(data.streak) identifyUserStreak(data.streak) }) } - }, [shouldCountStreak]) + }, []) return ( diff --git a/src/features/auth/hooks/useAuth.ts b/src/features/auth/hooks/useAuth.ts index 953924d4..c795ddac 100644 --- a/src/features/auth/hooks/useAuth.ts +++ b/src/features/auth/hooks/useAuth.ts @@ -10,14 +10,14 @@ export const useAuth = () => { const isConnected = authStore.user != null - const shouldCountStreak = useCallback(() => { + const shouldIcrementStreak = useCallback(() => { if (!isConnected) return false const last = authStore.lastStreakUpdate if (!last) return true - const today = new Date().toDateString() - const lastDay = new Date(last).toDateString() + const today = new Date().toISOString() + const lastDay = new Date(last).toISOString() return today !== lastDay }, [isConnected, authStore.lastStreakUpdate]) @@ -33,7 +33,7 @@ export const useAuth = () => { ...authModalStore, ...authStore, isConnected, - shouldCountStreak, + shouldIcrementStreak, logout, } } From 0bf827e9bf76ea603733e25651e1c5f1fbaf045f Mon Sep 17 00:00:00 2001 From: John Doe Date: Wed, 19 Nov 2025 19:44:05 +0100 Subject: [PATCH 69/89] fix: handle potential null value for cardSettings in GithubCard component --- src/features/cards/components/githubCard/GithubCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/cards/components/githubCard/GithubCard.tsx b/src/features/cards/components/githubCard/GithubCard.tsx index 074634b7..26f06f63 100644 --- a/src/features/cards/components/githubCard/GithubCard.tsx +++ b/src/features/cards/components/githubCard/GithubCard.tsx @@ -23,7 +23,7 @@ export function GithubCard(props: CardPropsType) { source: meta.value, fallbackTag: GLOBAL_TAG, }) - const { dateRange, language, sortBy } = cardSettings + const { dateRange, language, sortBy } = cardSettings ?? {} const selectedDateRange = useMemo( () => dateRanges.find((date) => date.value === dateRange) || dateRanges[0], From fba2a5149c80957403c9d68c4797968c94dfc60b Mon Sep 17 00:00:00 2001 From: John Doe Date: Wed, 19 Nov 2025 20:45:01 +0100 Subject: [PATCH 70/89] fix: update build command to use development mode and adjust Sentry configuration for development --- .github/workflows/develop.yml | 2 +- vite.config.mjs | 11 +++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) 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/vite.config.mjs b/vite.config.mjs index e6e6930d..45655d7c 100644 --- a/vite.config.mjs +++ b/vite.config.mjs @@ -9,6 +9,7 @@ import viteTsconfigPaths from 'vite-tsconfig-paths' export default defineConfig(({ mode }) => { const env = loadEnv(mode, process.cwd(), '') + const isDev = mode === 'development' const buildTarget = env.VITE_BUILD_TARGET || 'web' const buildPlatform = env.VITE_BUILD_PLATFORM const manifestPath = path.resolve(__dirname, 'public', 'base.manifest.json') @@ -38,13 +39,15 @@ export default defineConfig(({ mode }) => { org: 'hackertabdev', project: 'hackertab', authToken: env.VITE_SENTRY_TOKEN, - disable: mode === 'development', + disable: isDev, release: { name: `${appVersion}-${buildTarget == 'extension' ? buildPlatform : buildTarget}`, }, - sourcemaps: { - filesToDeleteAfterUpload: ['./dist/assets/*.map', './assets/*.map'], - }, + sourcemaps: !isDev + ? { + filesToDeleteAfterUpload: ['./dist/assets/*.map', './assets/*.map'], + } + : false, }), ], define: { From 788498c5188423126ea08903239745cf530dce5c Mon Sep 17 00:00:00 2001 From: John Doe Date: Wed, 19 Nov 2025 20:51:40 +0100 Subject: [PATCH 71/89] fix: set NODE_ENV to development during project build in GitHub Actions workflow --- .github/workflows/develop.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/develop.yml b/.github/workflows/develop.yml index 4a212e3a..765a7eb0 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 --mode development + run: NODE_ENV=development yarn build:web --mode development - name: Copy build to remote host uses: appleboy/scp-action@v0.1.4 From 8a3c49decb73181e8ee29fd8ba683e7317ed9cf6 Mon Sep 17 00:00:00 2001 From: John Doe Date: Wed, 19 Nov 2025 21:21:02 +0100 Subject: [PATCH 72/89] fix: remove NODE_ENV setting from GitHub Actions build step and update build script to pass arguments --- .github/workflows/develop.yml | 2 +- script/build.sh | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/develop.yml b/.github/workflows/develop.yml index 765a7eb0..4a212e3a 100644 --- a/.github/workflows/develop.yml +++ b/.github/workflows/develop.yml @@ -30,7 +30,7 @@ jobs: run: yarn - name: Build project - run: NODE_ENV=development yarn build:web --mode development + run: yarn build:web --mode development - name: Copy build to remote host uses: appleboy/scp-action@v0.1.4 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 From 0309c97419a8405dc4eefe0a97ad9657c6fe4808 Mon Sep 17 00:00:00 2001 From: John Doe Date: Wed, 19 Nov 2025 21:21:07 +0100 Subject: [PATCH 73/89] chore: remove unused dependencies from package.json and yarn.lock - Removed react-spinners and react-spring-bottom-sheet from dependencies in package.json. - Cleaned up related entries in yarn.lock to reflect the removal of these packages. --- package.json | 2 - yarn.lock | 235 ++------------------------------------------------- 2 files changed, 7 insertions(+), 230 deletions(-) diff --git a/package.json b/package.json index d32fc59a..1022c78d 100644 --- a/package.json +++ b/package.json @@ -33,8 +33,6 @@ "react-select": "^5.0.1", "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/yarn.lock b/yarn.lock index ff97f18c..ee059c67 100644 --- a/yarn.lock +++ b/yarn.lock @@ -277,7 +277,7 @@ dependencies: "@babel/types" "^7.23.0" -"@babel/helper-module-imports@^7.0.0", "@babel/helper-module-imports@^7.16.7", "@babel/helper-module-imports@^7.22.15": +"@babel/helper-module-imports@^7.16.7", "@babel/helper-module-imports@^7.22.15": version "7.22.15" resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz#16146307acdc40cc00c3b2c647713076464bdbf0" integrity sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w== @@ -1266,7 +1266,7 @@ resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310" integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== -"@babel/runtime@^7.12.0", "@babel/runtime@^7.12.5", "@babel/runtime@^7.16.3", "@babel/runtime@^7.18.3", "@babel/runtime@^7.23.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7": +"@babel/runtime@^7.12.0", "@babel/runtime@^7.12.5", "@babel/runtime@^7.16.3", "@babel/runtime@^7.18.3", "@babel/runtime@^7.23.2", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7": version "7.23.6" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.6.tgz#c05e610dc228855dc92ef1b53d07389ed8ab521d" integrity sha512-zHd0eUrf5GZoOWVCXp6koAKQTfZV07eit6bGPmJgnZdnSAvvZee6zniW2XMF7Cmc4ISOOnPy3QaSiIJGJkVEDQ== @@ -1385,16 +1385,6 @@ source-map "^0.5.7" stylis "4.2.0" -"@emotion/cache@^10.0.27": - version "10.0.29" - resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-10.0.29.tgz#87e7e64f412c060102d589fe7c6dc042e6f9d1e0" - integrity sha512-fU2VtSVlHiF27empSbxi1O2JFdNWZO+2NFHfwO0pxgTep6Xa3uGb+3pVKfLww2l/IBGLNEZl5Xf/++A4wAYDYQ== - dependencies: - "@emotion/sheet" "0.9.4" - "@emotion/stylis" "0.8.5" - "@emotion/utils" "0.11.3" - "@emotion/weak-memoize" "0.2.5" - "@emotion/cache@^11.11.0", "@emotion/cache@^11.4.0": version "11.11.0" resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.11.0.tgz#809b33ee6b1cb1a625fef7a45bc568ccd9b8f3ff" @@ -1406,42 +1396,11 @@ "@emotion/weak-memoize" "^0.3.1" stylis "4.2.0" -"@emotion/core@^10.0.35": - version "10.3.1" - resolved "https://registry.yarnpkg.com/@emotion/core/-/core-10.3.1.tgz#4021b6d8b33b3304d48b0bb478485e7d7421c69d" - integrity sha512-447aUEjPIm0MnE6QYIaFz9VQOHSXf4Iu6EWOIqq11EAPqinkSZmfymPTmlOE3QjLv846lH4JVZBUOtwGbuQoww== - dependencies: - "@babel/runtime" "^7.5.5" - "@emotion/cache" "^10.0.27" - "@emotion/css" "^10.0.27" - "@emotion/serialize" "^0.11.15" - "@emotion/sheet" "0.9.4" - "@emotion/utils" "0.11.3" - -"@emotion/css@^10.0.27": - version "10.0.27" - resolved "https://registry.yarnpkg.com/@emotion/css/-/css-10.0.27.tgz#3a7458198fbbebb53b01b2b87f64e5e21241e14c" - integrity sha512-6wZjsvYeBhyZQYNrGoR5yPMYbMBNEnanDrqmsqS1mzDm1cOTu12shvl2j4QHNS36UaTE0USIJawCH9C8oW34Zw== - dependencies: - "@emotion/serialize" "^0.11.15" - "@emotion/utils" "0.11.3" - babel-plugin-emotion "^10.0.27" - -"@emotion/hash@0.8.0": - version "0.8.0" - resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.8.0.tgz#bbbff68978fefdbe68ccb533bc8cbe1d1afb5413" - integrity sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow== - "@emotion/hash@^0.9.1": version "0.9.1" resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.9.1.tgz#4ffb0055f7ef676ebc3a5a91fb621393294e2f43" integrity sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ== -"@emotion/memoize@0.7.4": - version "0.7.4" - resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.4.tgz#19bf0f5af19149111c40d98bb0cf82119f5d9eeb" - integrity sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw== - "@emotion/memoize@^0.8.1": version "0.8.1" resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.8.1.tgz#c1ddb040429c6d21d38cc945fe75c818cfb68e17" @@ -1461,17 +1420,6 @@ "@emotion/weak-memoize" "^0.3.1" hoist-non-react-statics "^3.3.1" -"@emotion/serialize@^0.11.15", "@emotion/serialize@^0.11.16": - version "0.11.16" - resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-0.11.16.tgz#dee05f9e96ad2fb25a5206b6d759b2d1ed3379ad" - integrity sha512-G3J4o8by0VRrO+PFeSc3js2myYNOXVJ3Ya+RGVxnshRYgsvErfAOglKAiy1Eo1vhzxqtUvjCyS5gtewzkmvSSg== - dependencies: - "@emotion/hash" "0.8.0" - "@emotion/memoize" "0.7.4" - "@emotion/unitless" "0.7.5" - "@emotion/utils" "0.11.3" - csstype "^2.5.7" - "@emotion/serialize@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-1.1.2.tgz#017a6e4c9b8a803bd576ff3d52a0ea6fa5a62b51" @@ -1483,26 +1431,11 @@ "@emotion/utils" "^1.2.1" csstype "^3.0.2" -"@emotion/sheet@0.9.4": - version "0.9.4" - resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-0.9.4.tgz#894374bea39ec30f489bbfc3438192b9774d32e5" - integrity sha512-zM9PFmgVSqBw4zL101Q0HrBVTGmpAxFZH/pYx/cjJT5advXguvcgjHFTCaIO3enL/xr89vK2bh0Mfyj9aa0ANA== - "@emotion/sheet@^1.2.2": version "1.2.2" resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.2.2.tgz#d58e788ee27267a14342303e1abb3d508b6d0fec" integrity sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA== -"@emotion/stylis@0.8.5": - version "0.8.5" - resolved "https://registry.yarnpkg.com/@emotion/stylis/-/stylis-0.8.5.tgz#deacb389bd6ee77d1e7fcaccce9e16c5c7e78e04" - integrity sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ== - -"@emotion/unitless@0.7.5": - version "0.7.5" - resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.7.5.tgz#77211291c1900a700b8a78cfafda3160d76949ed" - integrity sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg== - "@emotion/unitless@^0.8.1": version "0.8.1" resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.8.1.tgz#182b5a4704ef8ad91bde93f7a860a88fd92c79a3" @@ -1513,21 +1446,11 @@ resolved "https://registry.yarnpkg.com/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz#08de79f54eb3406f9daaf77c76e35313da963963" integrity sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw== -"@emotion/utils@0.11.3": - version "0.11.3" - resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-0.11.3.tgz#a759863867befa7e583400d322652a3f44820924" - integrity sha512-0o4l6pZC+hI88+bzuaX/6BgOvQVhbt2PfmxauVaYOGgbsAw14wdKyvMCZXnsnsHys94iadcF+RG/wZyx6+ZZBw== - "@emotion/utils@^1.2.1": version "1.2.1" resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-1.2.1.tgz#bbab58465738d31ae4cb3dbb6fc00a5991f755e4" integrity sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg== -"@emotion/weak-memoize@0.2.5": - version "0.2.5" - resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz#8eed982e2ee6f7f4e44c253e12962980791efd46" - integrity sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA== - "@emotion/weak-memoize@^0.3.1": version "0.3.1" resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz#d0fce5d07b0620caa282b5131c297bb60f9d87e6" @@ -2185,11 +2108,6 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" -"@juggle/resize-observer@^3.2.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@juggle/resize-observer/-/resize-observer-3.4.0.tgz#08d6c5e20cf7e4cc02fd181c4b0c225cd31dbb60" - integrity sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA== - "@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1": version "5.1.1-v1" resolved "https://registry.yarnpkg.com/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz#dbf733a965ca47b1973177dc0bb6c889edcfb129" @@ -2271,23 +2189,6 @@ resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw== -"@reach/portal@^0.13.0": - version "0.13.2" - resolved "https://registry.yarnpkg.com/@reach/portal/-/portal-0.13.2.tgz#6f2a6f4afc14894bde9c6435667bb9b660887ed9" - integrity sha512-g74BnCdtuTGthzzHn2cWW+bcyIYb0iIE/yRsm89i8oNzNgpopbkh9UY8TPbhNlys52h7U60s4kpRTmcq+JqsTA== - dependencies: - "@reach/utils" "0.13.2" - tslib "^2.1.0" - -"@reach/utils@0.13.2": - version "0.13.2" - resolved "https://registry.yarnpkg.com/@reach/utils/-/utils-0.13.2.tgz#87e8fef8ebfe583fa48250238a1a3ed03189fcc8" - integrity sha512-3ir6cN60zvUrwjOJu7C6jec/samqAeyAB12ZADK+qjnmQPdzSYldrFWwDVV5H0WkhbYXR3uh+eImu13hCetNPQ== - dependencies: - "@types/warning" "^3.0.0" - tslib "^2.1.0" - warning "^4.0.3" - "@remix-run/router@1.14.0": version "1.14.0" resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.14.0.tgz#9bc39a5a3a71b81bdb310eba6def5bc3966695b7" @@ -2921,11 +2822,6 @@ resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.10.tgz#04ffa7f406ab628f7f7e97ca23e290cd8ab15efc" integrity sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA== -"@types/warning@^3.0.0": - version "3.0.3" - resolved "https://registry.yarnpkg.com/@types/warning/-/warning-3.0.3.tgz#d1884c8cc4a426d1ac117ca2611bf333834c6798" - integrity sha512-D1XC7WK8K+zZEveUPY+cf4+kgauk8N4eHr/XIHXGlGYkHLud6hK9lYfZk1ry1TNh798cZUCgb6MqGEG8DkJt6Q== - "@types/yargs-parser@*": version "21.0.3" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.3.tgz#815e30b786d2e8f0dcd85fd5bcf5e1a04d008f15" @@ -3040,14 +2936,6 @@ "@types/babel__core" "^7.20.5" react-refresh "^0.14.0" -"@xstate/react@^1.2.0": - version "1.6.3" - resolved "https://registry.yarnpkg.com/@xstate/react/-/react-1.6.3.tgz#706f3beb7bc5879a78088985c8fd43b9dab7f725" - integrity sha512-NCUReRHPGvvCvj2yLZUTfR0qVp6+apc8G83oXSjN4rl89ZjyujiKrTff55bze/HrsvCsP/sUJASf2n0nzMF1KQ== - dependencies: - use-isomorphic-layout-effect "^1.0.0" - use-subscription "^1.3.0" - acorn@^8.8.1: version "8.15.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.15.0.tgz#a360898bc415edaac46c8241f6383975b930b816" @@ -3244,31 +3132,6 @@ axobject-query@^3.2.1: dependencies: dequal "^2.0.3" -babel-plugin-emotion@^10.0.27: - version "10.2.2" - resolved "https://registry.yarnpkg.com/babel-plugin-emotion/-/babel-plugin-emotion-10.2.2.tgz#a1fe3503cff80abfd0bdda14abd2e8e57a79d17d" - integrity sha512-SMSkGoqTbTyUTDeuVuPIWifPdUGkTk1Kf9BWRiXIOIcuyMfsdp2EjeiiFvOzX8NOBvEh/ypKYvUh2rkgAJMCLA== - dependencies: - "@babel/helper-module-imports" "^7.0.0" - "@emotion/hash" "0.8.0" - "@emotion/memoize" "0.7.4" - "@emotion/serialize" "^0.11.16" - babel-plugin-macros "^2.0.0" - babel-plugin-syntax-jsx "^6.18.0" - convert-source-map "^1.5.0" - escape-string-regexp "^1.0.5" - find-root "^1.1.0" - source-map "^0.5.7" - -babel-plugin-macros@^2.0.0: - version "2.8.0" - resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-2.8.0.tgz#0f958a7cc6556b1e65344465d99111a1e5e10138" - integrity sha512-SEP5kJpfGYqYKpBrj5XU3ahw5p5GOHJ0U5ssOSQ/WBVdwkD2Dzlce95exQTs3jOVWPPKLBN2rlEWkCK7dSmLvg== - dependencies: - "@babel/runtime" "^7.7.2" - cosmiconfig "^6.0.0" - resolve "^1.12.0" - babel-plugin-macros@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz#9ef6dc74deb934b4db344dc973ee851d148c50c1" @@ -3302,11 +3165,6 @@ babel-plugin-polyfill-regenerator@^0.5.3: dependencies: "@babel/helper-define-polyfill-provider" "^0.4.4" -babel-plugin-syntax-jsx@^6.18.0: - version "6.18.0" - resolved "https://registry.yarnpkg.com/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz#0af32a9a6e13ca7a3fd5069e62d7b0f58d0d8946" - integrity sha512-qrPaCSo9c8RHNRHIotaufGbuOBN8rtdC4QrrFFc43vyWCCz7Kl7GL1PGaXtMGQZUXrkCjNEgxDfmAuAabr/rlw== - babel-plugin-transform-react-remove-prop-types@^0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-remove-prop-types/-/babel-plugin-transform-react-remove-prop-types-0.4.24.tgz#f2edaf9b4c6a5fbe5c1d678bfb531078c1555f3a" @@ -3349,11 +3207,6 @@ binary-extensions@^2.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== -body-scroll-lock@^3.1.5: - version "3.1.5" - resolved "https://registry.yarnpkg.com/body-scroll-lock/-/body-scroll-lock-3.1.5.tgz#c1392d9217ed2c3e237fee1e910f6cdd80b7aaec" - integrity sha512-Yi1Xaml0EvNA0OYWxXiYNqY24AfWkbA6w5vxE7GWxtKfzIbZM+Qw+aSmkgsbWzbHiy/RCSkUZBplVxTA+E4jJg== - brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -3584,17 +3437,6 @@ core-js-compat@^3.31.0, core-js-compat@^3.33.1: dependencies: browserslist "^4.22.2" -cosmiconfig@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-6.0.0.tgz#da4fee853c52f6b1e6935f41c1a2fc50bd4a9982" - integrity sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg== - dependencies: - "@types/parse-json" "^4.0.0" - import-fresh "^3.1.0" - parse-json "^5.0.0" - path-type "^4.0.0" - yaml "^1.7.2" - cosmiconfig@^7.0.0: version "7.1.0" resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.1.0.tgz#1443b9afa596b670082ea46cbd8f6a62b84635f6" @@ -3631,11 +3473,6 @@ css-mediaquery@^0.1.2: resolved "https://registry.yarnpkg.com/css-mediaquery/-/css-mediaquery-0.1.2.tgz#6a2c37344928618631c54bd33cedd301da18bea0" integrity sha512-COtn4EROW5dBGlE/4PiKnh6rZpAPxDeFLaEEwt4i10jpDMFt2EhQGS79QmmrO+iKCHv0PU/HrOWEhijFd1x99Q== -csstype@^2.5.7: - version "2.6.21" - resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.21.tgz#2efb85b7cc55c80017c66a5ad7cbd931fda3a90e" - integrity sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w== - csstype@^3.0.2: version "3.1.3" resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" @@ -4384,13 +4221,6 @@ firebase@^11.2.0: "@firebase/util" "1.10.3" "@firebase/vertexai" "1.0.3" -focus-trap@^6.2.2: - version "6.9.4" - resolved "https://registry.yarnpkg.com/focus-trap/-/focus-trap-6.9.4.tgz#436da1a1d935c48b97da63cd8f361c6f3aa16444" - integrity sha512-v2NTsZe2FF59Y+sDykKY+XjqZ0cPfhq/hikWVL88BqLivnNiEffAsac6rP6H45ff9wG9LL5ToiDqrLEP9GX9mw== - dependencies: - tabbable "^5.3.3" - follow-redirects@^1.15.6: version "1.15.9" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1" @@ -4679,7 +4509,7 @@ ignore@^5.2.0: resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.0.tgz#67418ae40d34d6999c95ff56016759c718c82f78" integrity sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg== -import-fresh@^3.1.0, import-fresh@^3.2.1, import-fresh@^3.3.0: +import-fresh@^3.2.1, import-fresh@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== @@ -5738,7 +5568,7 @@ progress@^2.0.3: resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== -prop-types@^15.0.0, prop-types@^15.0.0-0, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: +prop-types@^15.0.0, prop-types@^15.0.0-0, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: version "15.8.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -5931,35 +5761,6 @@ react-simple-toasts@^6.1.0: resolved "https://registry.yarnpkg.com/react-simple-toasts/-/react-simple-toasts-6.1.0.tgz#2bc68c4127b7b41e0a850f806e4c2dfa6b0f5bd7" integrity sha512-6XRkaBNfSFr8m2CqKiY6mwDa3qYqWcClQFc5Qbd+ivl/eLrWqfPTrXQ0t7QQFGqn1p9W72KEV1Xj7AEe/48CIA== -react-spinners@^0.10.4: - version "0.10.6" - resolved "https://registry.yarnpkg.com/react-spinners/-/react-spinners-0.10.6.tgz#7e780144aaf54372231f6168ca075b4cd70e48ef" - integrity sha512-UPLcaMFhFnLWtS1zVDDT14ssW08gnZ44cjPN6GL27dEGboiCvRSthOilZXRDWESp449GB2iI6gjEbxzaMtA+dg== - dependencies: - "@emotion/core" "^10.0.35" - -react-spring-bottom-sheet@^3.4.1: - version "3.4.1" - resolved "https://registry.yarnpkg.com/react-spring-bottom-sheet/-/react-spring-bottom-sheet-3.4.1.tgz#9a4f90b1c0af17eb4a22a606a5efc5d6e62c7b0c" - integrity sha512-yDFqiPMm/fjefjnOe6Q9zxccbCl6HMUKsK5bWgfGHJIj4zmXVKio5d4icQvmOLuwpuCA2pwv4J6nGWS6fUZidQ== - dependencies: - "@juggle/resize-observer" "^3.2.0" - "@reach/portal" "^0.13.0" - "@xstate/react" "^1.2.0" - body-scroll-lock "^3.1.5" - focus-trap "^6.2.2" - react-spring "^8.0.27" - react-use-gesture "^8.0.1" - xstate "^4.15.1" - -react-spring@^8.0.27: - version "8.0.27" - resolved "https://registry.yarnpkg.com/react-spring/-/react-spring-8.0.27.tgz#97d4dee677f41e0b2adcb696f3839680a3aa356a" - integrity sha512-nDpWBe3ZVezukNRandTeLSPcwwTMjNVu1IDq9qA/AMiUqHuRN4BeSWvKr3eIxxg1vtiYiOLy4FqdfCP5IoP77g== - dependencies: - "@babel/runtime" "^7.3.1" - prop-types "^15.5.8" - react-toggle@^4.1.1: version "4.1.3" resolved "https://registry.yarnpkg.com/react-toggle/-/react-toggle-4.1.3.tgz#99193392cca8e495710860c49f55e74c4e6cf452" @@ -5990,11 +5791,6 @@ react-transition-state@^2.3.1: resolved "https://registry.yarnpkg.com/react-transition-state/-/react-transition-state-2.3.1.tgz#53f5d33a95d1859d1ad2b1673c4466da1518e4ed" integrity sha512-Z48el73x+7HUEM131dof9YpcQ5IlM4xB+pKWH/lX3FhxGfQaNTZa16zb7pWkC/y5btTZzXfCtglIJEGc57giOw== -react-use-gesture@^8.0.1: - version "8.0.1" - resolved "https://registry.yarnpkg.com/react-use-gesture/-/react-use-gesture-8.0.1.tgz#4360c0f7c9e26baf9fbe58f63fc9de7ef699c17f" - integrity sha512-CXzUNkulUdgouaAlvAsC5ZVo0fi9KGSBSk81WrE4kOIcJccpANe9zZkAYr5YZZhqpicIFxitsrGVS4wmoMun9A== - react@^19.1.0: version "19.1.0" resolved "https://registry.yarnpkg.com/react/-/react-19.1.0.tgz#926864b6c48da7627f004795d6cce50e90793b75" @@ -6100,7 +5896,7 @@ resolve-from@^4.0.0: resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== -resolve@^1.12.0, resolve@^1.14.2, resolve@^1.19.0, resolve@^1.22.4: +resolve@^1.14.2, resolve@^1.19.0, resolve@^1.22.4: version "1.22.8" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== @@ -6399,11 +6195,6 @@ svg-parser@^2.0.4: resolved "https://registry.yarnpkg.com/svg-parser/-/svg-parser-2.0.4.tgz#fdc2e29e13951736140b76cb122c8ee6630eb6b5" integrity sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ== -tabbable@^5.3.3: - version "5.3.3" - resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-5.3.3.tgz#aac0ff88c73b22d6c3c5a50b1586310006b47fbf" - integrity sha512-QD9qKY3StfbZqWOPLp0++pOrAVb/HbUi5xCc8cUo4XjP19808oaMiDzn0leBY5mCespIBM0CIZePzZjgzR83kA== - terser@^5.19.2: version "5.26.0" resolved "https://registry.yarnpkg.com/terser/-/terser-5.26.0.tgz#ee9f05d929f4189a9c28a0feb889d96d50126fe1" @@ -6679,18 +6470,11 @@ update-browserslist-db@^1.1.3: escalade "^3.2.0" picocolors "^1.1.1" -use-isomorphic-layout-effect@^1.0.0, use-isomorphic-layout-effect@^1.1.2: +use-isomorphic-layout-effect@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz#497cefb13d863d687b08477d9e5a164ad8c1a6fb" integrity sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA== -use-subscription@^1.3.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/use-subscription/-/use-subscription-1.8.0.tgz#f118938c29d263c2bce12fc5585d3fe694d4dbce" - integrity sha512-LISuG0/TmmoDoCRmV5XAqYkd3UCBNM0ML3gGBndze65WITcsExCD3DTvXXTLyNcOC0heFQZzluW88bN/oC1DQQ== - dependencies: - use-sync-external-store "^1.2.0" - use-sync-external-store@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" @@ -6883,11 +6667,6 @@ wrap-ansi@^7.0.0: string-width "^4.1.0" strip-ansi "^6.0.0" -xstate@^4.15.1: - version "4.38.3" - resolved "https://registry.yarnpkg.com/xstate/-/xstate-4.38.3.tgz#4e15e7ad3aa0ca1eea2010548a5379966d8f1075" - integrity sha512-SH7nAaaPQx57dx6qvfcIgqKRXIh4L0A1iYEqim4s1u7c9VoCgzZc+63FY90AKU4ZzOC2cfJzTnpO4zK7fCUzzw== - y18n@^5.0.5: version "5.0.8" resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" @@ -6903,7 +6682,7 @@ yallist@^4.0.0: resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== -yaml@^1.10.0, yaml@^1.7.2: +yaml@^1.10.0: version "1.10.2" resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== From 1898d267307c0f4741d3468b23100f38387ccbda Mon Sep 17 00:00:00 2001 From: John Doe Date: Wed, 19 Nov 2025 21:25:05 +0100 Subject: [PATCH 74/89] redo spinners dependency --- package.json | 1 + yarn.lock | 177 ++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 169 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 1022c78d..c6cc4e3e 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "react-select": "^5.0.1", "react-share": "^4.4.1", "react-simple-toasts": "^6.1.0", + "react-spinners": "^0.10.4", "react-toggle": "^4.1.1", "react-tooltip": "^4.2.21", "timeago.js": "^4.0.2", diff --git a/yarn.lock b/yarn.lock index ee059c67..18b71f91 100644 --- a/yarn.lock +++ b/yarn.lock @@ -277,14 +277,7 @@ dependencies: "@babel/types" "^7.23.0" -"@babel/helper-module-imports@^7.16.7", "@babel/helper-module-imports@^7.22.15": - version "7.22.15" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz#16146307acdc40cc00c3b2c647713076464bdbf0" - integrity sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w== - dependencies: - "@babel/types" "^7.22.15" - -"@babel/helper-module-imports@^7.27.1": +"@babel/helper-module-imports@^7.0.0", "@babel/helper-module-imports@^7.27.1": version "7.27.1" resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz#7ef769a323e2655e126673bb6d2d6913bbead204" integrity sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w== @@ -292,6 +285,13 @@ "@babel/traverse" "^7.27.1" "@babel/types" "^7.27.1" +"@babel/helper-module-imports@^7.16.7", "@babel/helper-module-imports@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz#16146307acdc40cc00c3b2c647713076464bdbf0" + integrity sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w== + dependencies: + "@babel/types" "^7.22.15" + "@babel/helper-module-transforms@^7.23.3": version "7.23.3" resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz#d7d12c3c5d30af5b3c0fcab2a6d5217773e2d0f1" @@ -1273,6 +1273,11 @@ dependencies: regenerator-runtime "^0.14.0" +"@babel/runtime@^7.7.2": + version "7.28.4" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.28.4.tgz#a70226016fabe25c5783b2f22d3e1c9bc5ca3326" + integrity sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ== + "@babel/template@^7.22.15": version "7.22.15" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38" @@ -1385,6 +1390,16 @@ source-map "^0.5.7" stylis "4.2.0" +"@emotion/cache@^10.0.27": + version "10.0.29" + resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-10.0.29.tgz#87e7e64f412c060102d589fe7c6dc042e6f9d1e0" + integrity sha512-fU2VtSVlHiF27empSbxi1O2JFdNWZO+2NFHfwO0pxgTep6Xa3uGb+3pVKfLww2l/IBGLNEZl5Xf/++A4wAYDYQ== + dependencies: + "@emotion/sheet" "0.9.4" + "@emotion/stylis" "0.8.5" + "@emotion/utils" "0.11.3" + "@emotion/weak-memoize" "0.2.5" + "@emotion/cache@^11.11.0", "@emotion/cache@^11.4.0": version "11.11.0" resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.11.0.tgz#809b33ee6b1cb1a625fef7a45bc568ccd9b8f3ff" @@ -1396,11 +1411,42 @@ "@emotion/weak-memoize" "^0.3.1" stylis "4.2.0" +"@emotion/core@^10.0.35": + version "10.3.1" + resolved "https://registry.yarnpkg.com/@emotion/core/-/core-10.3.1.tgz#4021b6d8b33b3304d48b0bb478485e7d7421c69d" + integrity sha512-447aUEjPIm0MnE6QYIaFz9VQOHSXf4Iu6EWOIqq11EAPqinkSZmfymPTmlOE3QjLv846lH4JVZBUOtwGbuQoww== + dependencies: + "@babel/runtime" "^7.5.5" + "@emotion/cache" "^10.0.27" + "@emotion/css" "^10.0.27" + "@emotion/serialize" "^0.11.15" + "@emotion/sheet" "0.9.4" + "@emotion/utils" "0.11.3" + +"@emotion/css@^10.0.27": + version "10.0.27" + resolved "https://registry.yarnpkg.com/@emotion/css/-/css-10.0.27.tgz#3a7458198fbbebb53b01b2b87f64e5e21241e14c" + integrity sha512-6wZjsvYeBhyZQYNrGoR5yPMYbMBNEnanDrqmsqS1mzDm1cOTu12shvl2j4QHNS36UaTE0USIJawCH9C8oW34Zw== + dependencies: + "@emotion/serialize" "^0.11.15" + "@emotion/utils" "0.11.3" + babel-plugin-emotion "^10.0.27" + +"@emotion/hash@0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.8.0.tgz#bbbff68978fefdbe68ccb533bc8cbe1d1afb5413" + integrity sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow== + "@emotion/hash@^0.9.1": version "0.9.1" resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.9.1.tgz#4ffb0055f7ef676ebc3a5a91fb621393294e2f43" integrity sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ== +"@emotion/memoize@0.7.4": + version "0.7.4" + resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.4.tgz#19bf0f5af19149111c40d98bb0cf82119f5d9eeb" + integrity sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw== + "@emotion/memoize@^0.8.1": version "0.8.1" resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.8.1.tgz#c1ddb040429c6d21d38cc945fe75c818cfb68e17" @@ -1420,6 +1466,17 @@ "@emotion/weak-memoize" "^0.3.1" hoist-non-react-statics "^3.3.1" +"@emotion/serialize@^0.11.15", "@emotion/serialize@^0.11.16": + version "0.11.16" + resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-0.11.16.tgz#dee05f9e96ad2fb25a5206b6d759b2d1ed3379ad" + integrity sha512-G3J4o8by0VRrO+PFeSc3js2myYNOXVJ3Ya+RGVxnshRYgsvErfAOglKAiy1Eo1vhzxqtUvjCyS5gtewzkmvSSg== + dependencies: + "@emotion/hash" "0.8.0" + "@emotion/memoize" "0.7.4" + "@emotion/unitless" "0.7.5" + "@emotion/utils" "0.11.3" + csstype "^2.5.7" + "@emotion/serialize@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-1.1.2.tgz#017a6e4c9b8a803bd576ff3d52a0ea6fa5a62b51" @@ -1431,11 +1488,26 @@ "@emotion/utils" "^1.2.1" csstype "^3.0.2" +"@emotion/sheet@0.9.4": + version "0.9.4" + resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-0.9.4.tgz#894374bea39ec30f489bbfc3438192b9774d32e5" + integrity sha512-zM9PFmgVSqBw4zL101Q0HrBVTGmpAxFZH/pYx/cjJT5advXguvcgjHFTCaIO3enL/xr89vK2bh0Mfyj9aa0ANA== + "@emotion/sheet@^1.2.2": version "1.2.2" resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.2.2.tgz#d58e788ee27267a14342303e1abb3d508b6d0fec" integrity sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA== +"@emotion/stylis@0.8.5": + version "0.8.5" + resolved "https://registry.yarnpkg.com/@emotion/stylis/-/stylis-0.8.5.tgz#deacb389bd6ee77d1e7fcaccce9e16c5c7e78e04" + integrity sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ== + +"@emotion/unitless@0.7.5": + version "0.7.5" + resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.7.5.tgz#77211291c1900a700b8a78cfafda3160d76949ed" + integrity sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg== + "@emotion/unitless@^0.8.1": version "0.8.1" resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.8.1.tgz#182b5a4704ef8ad91bde93f7a860a88fd92c79a3" @@ -1446,11 +1518,21 @@ resolved "https://registry.yarnpkg.com/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz#08de79f54eb3406f9daaf77c76e35313da963963" integrity sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw== +"@emotion/utils@0.11.3": + version "0.11.3" + resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-0.11.3.tgz#a759863867befa7e583400d322652a3f44820924" + integrity sha512-0o4l6pZC+hI88+bzuaX/6BgOvQVhbt2PfmxauVaYOGgbsAw14wdKyvMCZXnsnsHys94iadcF+RG/wZyx6+ZZBw== + "@emotion/utils@^1.2.1": version "1.2.1" resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-1.2.1.tgz#bbab58465738d31ae4cb3dbb6fc00a5991f755e4" integrity sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg== +"@emotion/weak-memoize@0.2.5": + version "0.2.5" + resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz#8eed982e2ee6f7f4e44c253e12962980791efd46" + integrity sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA== + "@emotion/weak-memoize@^0.3.1": version "0.3.1" resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz#d0fce5d07b0620caa282b5131c297bb60f9d87e6" @@ -3132,6 +3214,31 @@ axobject-query@^3.2.1: dependencies: dequal "^2.0.3" +babel-plugin-emotion@^10.0.27: + version "10.2.2" + resolved "https://registry.yarnpkg.com/babel-plugin-emotion/-/babel-plugin-emotion-10.2.2.tgz#a1fe3503cff80abfd0bdda14abd2e8e57a79d17d" + integrity sha512-SMSkGoqTbTyUTDeuVuPIWifPdUGkTk1Kf9BWRiXIOIcuyMfsdp2EjeiiFvOzX8NOBvEh/ypKYvUh2rkgAJMCLA== + dependencies: + "@babel/helper-module-imports" "^7.0.0" + "@emotion/hash" "0.8.0" + "@emotion/memoize" "0.7.4" + "@emotion/serialize" "^0.11.16" + babel-plugin-macros "^2.0.0" + babel-plugin-syntax-jsx "^6.18.0" + convert-source-map "^1.5.0" + escape-string-regexp "^1.0.5" + find-root "^1.1.0" + source-map "^0.5.7" + +babel-plugin-macros@^2.0.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-2.8.0.tgz#0f958a7cc6556b1e65344465d99111a1e5e10138" + integrity sha512-SEP5kJpfGYqYKpBrj5XU3ahw5p5GOHJ0U5ssOSQ/WBVdwkD2Dzlce95exQTs3jOVWPPKLBN2rlEWkCK7dSmLvg== + dependencies: + "@babel/runtime" "^7.7.2" + cosmiconfig "^6.0.0" + resolve "^1.12.0" + babel-plugin-macros@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz#9ef6dc74deb934b4db344dc973ee851d148c50c1" @@ -3165,6 +3272,11 @@ babel-plugin-polyfill-regenerator@^0.5.3: dependencies: "@babel/helper-define-polyfill-provider" "^0.4.4" +babel-plugin-syntax-jsx@^6.18.0: + version "6.18.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz#0af32a9a6e13ca7a3fd5069e62d7b0f58d0d8946" + integrity sha512-qrPaCSo9c8RHNRHIotaufGbuOBN8rtdC4QrrFFc43vyWCCz7Kl7GL1PGaXtMGQZUXrkCjNEgxDfmAuAabr/rlw== + babel-plugin-transform-react-remove-prop-types@^0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-remove-prop-types/-/babel-plugin-transform-react-remove-prop-types-0.4.24.tgz#f2edaf9b4c6a5fbe5c1d678bfb531078c1555f3a" @@ -3437,6 +3549,17 @@ core-js-compat@^3.31.0, core-js-compat@^3.33.1: dependencies: browserslist "^4.22.2" +cosmiconfig@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-6.0.0.tgz#da4fee853c52f6b1e6935f41c1a2fc50bd4a9982" + integrity sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg== + dependencies: + "@types/parse-json" "^4.0.0" + import-fresh "^3.1.0" + parse-json "^5.0.0" + path-type "^4.0.0" + yaml "^1.7.2" + cosmiconfig@^7.0.0: version "7.1.0" resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.1.0.tgz#1443b9afa596b670082ea46cbd8f6a62b84635f6" @@ -3473,6 +3596,11 @@ css-mediaquery@^0.1.2: resolved "https://registry.yarnpkg.com/css-mediaquery/-/css-mediaquery-0.1.2.tgz#6a2c37344928618631c54bd33cedd301da18bea0" integrity sha512-COtn4EROW5dBGlE/4PiKnh6rZpAPxDeFLaEEwt4i10jpDMFt2EhQGS79QmmrO+iKCHv0PU/HrOWEhijFd1x99Q== +csstype@^2.5.7: + version "2.6.21" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.21.tgz#2efb85b7cc55c80017c66a5ad7cbd931fda3a90e" + integrity sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w== + csstype@^3.0.2: version "3.1.3" resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" @@ -4509,6 +4637,14 @@ ignore@^5.2.0: resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.0.tgz#67418ae40d34d6999c95ff56016759c718c82f78" integrity sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg== +import-fresh@^3.1.0: + version "3.3.1" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.1.tgz#9cecb56503c0ada1f2741dbbd6546e4b13b57ccf" + integrity sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + import-fresh@^3.2.1, import-fresh@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" @@ -4596,6 +4732,13 @@ is-core-module@^2.13.0, is-core-module@^2.13.1: dependencies: hasown "^2.0.0" +is-core-module@^2.16.1: + version "2.16.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.16.1.tgz#2a98801a849f43e2add644fbb6bc6229b19a4ef4" + integrity sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w== + dependencies: + hasown "^2.0.2" + is-date-object@^1.0.1, is-date-object@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" @@ -5761,6 +5904,13 @@ react-simple-toasts@^6.1.0: resolved "https://registry.yarnpkg.com/react-simple-toasts/-/react-simple-toasts-6.1.0.tgz#2bc68c4127b7b41e0a850f806e4c2dfa6b0f5bd7" integrity sha512-6XRkaBNfSFr8m2CqKiY6mwDa3qYqWcClQFc5Qbd+ivl/eLrWqfPTrXQ0t7QQFGqn1p9W72KEV1Xj7AEe/48CIA== +react-spinners@^0.10.4: + version "0.10.6" + resolved "https://registry.yarnpkg.com/react-spinners/-/react-spinners-0.10.6.tgz#7e780144aaf54372231f6168ca075b4cd70e48ef" + integrity sha512-UPLcaMFhFnLWtS1zVDDT14ssW08gnZ44cjPN6GL27dEGboiCvRSthOilZXRDWESp449GB2iI6gjEbxzaMtA+dg== + dependencies: + "@emotion/core" "^10.0.35" + react-toggle@^4.1.1: version "4.1.3" resolved "https://registry.yarnpkg.com/react-toggle/-/react-toggle-4.1.3.tgz#99193392cca8e495710860c49f55e74c4e6cf452" @@ -5896,6 +6046,15 @@ resolve-from@^4.0.0: resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== +resolve@^1.12.0: + version "1.22.11" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.11.tgz#aad857ce1ffb8bfa9b0b1ac29f1156383f68c262" + integrity sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ== + dependencies: + is-core-module "^2.16.1" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + resolve@^1.14.2, resolve@^1.19.0, resolve@^1.22.4: version "1.22.8" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" @@ -6682,7 +6841,7 @@ yallist@^4.0.0: resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== -yaml@^1.10.0: +yaml@^1.10.0, yaml@^1.7.2: version "1.10.2" resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== From 94a3cb54eb52b6d7901d36126cf5653978e8e7bd Mon Sep 17 00:00:00 2001 From: John Doe Date: Wed, 19 Nov 2025 21:28:13 +0100 Subject: [PATCH 75/89] fix: remove unused 'react-spring-bottom-sheet' dependency from Vite config --- vite.config.mjs | 1 - 1 file changed, 1 deletion(-) diff --git a/vite.config.mjs b/vite.config.mjs index 45655d7c..8202a168 100644 --- a/vite.config.mjs +++ b/vite.config.mjs @@ -84,7 +84,6 @@ export default defineConfig(({ mode }) => { 'react-icons', 'react-markdown', 'react-modal', - 'react-spring-bottom-sheet', 'react-infinite-scroll-hook', '@dnd-kit/core', '@dnd-kit/sortable', From 10498ab02f23a77885b2098a7298bd97c7a588a9 Mon Sep 17 00:00:00 2001 From: John Doe Date: Wed, 19 Nov 2025 22:29:58 +0100 Subject: [PATCH 76/89] fix: update migration function to include version parameter and log migration message --- src/stores/preferences.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/stores/preferences.ts b/src/stores/preferences.ts index d127fec7..13b364ca 100644 --- a/src/stores/preferences.ts +++ b/src/stores/preferences.ts @@ -235,9 +235,12 @@ export const useUserPreferences = create( name: 'preferences_storage', version: 1, storage: createJSONStorage(() => defaultStorage), - migrate: (persistedState) => { + migrate: (persistedState, version) => { const state = persistedState as unknown as UserPreferencesState & UserPreferencesStoreActions + if (version === 0) { + console.log('Migrating preferences_storage to version 1') + } return state }, } From 60afc6f33fad385e8c6b2c944a58fb9317d540cb Mon Sep 17 00:00:00 2001 From: John Doe Date: Thu, 20 Nov 2025 21:25:53 +0100 Subject: [PATCH 77/89] fix: enhance migration logging and update onboardingCompleted state during preferences migration --- src/stores/preferences.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/stores/preferences.ts b/src/stores/preferences.ts index 13b364ca..8bd3dd28 100644 --- a/src/stores/preferences.ts +++ b/src/stores/preferences.ts @@ -239,7 +239,12 @@ export const useUserPreferences = create( const state = persistedState as unknown as UserPreferencesState & UserPreferencesStoreActions if (version === 0) { - console.log('Migrating preferences_storage to version 1') + console.log('Migrating preferences_storage to version 1', state) + + return { + ...state, + onboardingCompleted: true, + } } return state }, From 53f53e0900bee9f2401c1432f0a56be71c000e84 Mon Sep 17 00:00:00 2001 From: John Doe Date: Thu, 20 Nov 2025 22:56:27 +0100 Subject: [PATCH 78/89] fix: update query keys to include version suffix for conferences, GitHub repos, and Product Hunt products --- src/features/cards/api/getConferences.ts | 2 +- src/features/cards/api/getGithubRepos.ts | 2 +- src/features/cards/api/getProductHuntProducts.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/features/cards/api/getConferences.ts b/src/features/cards/api/getConferences.ts index c6c0ad33..e56c2c3b 100644 --- a/src/features/cards/api/getConferences.ts +++ b/src/features/cards/api/getConferences.ts @@ -21,7 +21,7 @@ type UseGetConferencesOptions = { export const useGetConferences = ({ config, tags }: UseGetConferencesOptions) => { return useQuery>({ ...config, - queryKey: ['conferences', ...tags], + queryKey: ['conferences_v2', ...tags], queryFn: () => getConferences(tags), }) } diff --git a/src/features/cards/api/getGithubRepos.ts b/src/features/cards/api/getGithubRepos.ts index 5618db1e..ecbff38b 100644 --- a/src/features/cards/api/getGithubRepos.ts +++ b/src/features/cards/api/getGithubRepos.ts @@ -29,7 +29,7 @@ type UseGetReposOptions = { export const useGetGithubRepos = ({ config, tags, dateRange }: UseGetReposOptions) => { return useQuery>({ ...config, - queryKey: ['github', ...tags, dateRange], + queryKey: ['github_v2', ...tags, dateRange], queryFn: () => getRepos({ tags, dateRange }), }) } diff --git a/src/features/cards/api/getProductHuntProducts.ts b/src/features/cards/api/getProductHuntProducts.ts index f37e4809..cae9ac03 100644 --- a/src/features/cards/api/getProductHuntProducts.ts +++ b/src/features/cards/api/getProductHuntProducts.ts @@ -21,7 +21,7 @@ type UseGetArticlesOptions = { export const useGeProductHuntProducts = ({ date, config }: UseGetArticlesOptions) => { return useQuery>({ ...config, - queryKey: ['producthunt', date], + queryKey: ['producthunt_v2', date], queryFn: () => getArticles({ date }), }) } From adc4681b8e5c7b7c4c67fc08a07f18a5b5cd36a9 Mon Sep 17 00:00:00 2001 From: John Doe Date: Thu, 20 Nov 2025 22:56:41 +0100 Subject: [PATCH 79/89] fix: add occupation field to user preferences state and initialize from onboarding result --- src/stores/preferences.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/stores/preferences.ts b/src/stores/preferences.ts index 8bd3dd28..44b4160b 100644 --- a/src/stores/preferences.ts +++ b/src/stores/preferences.ts @@ -19,6 +19,11 @@ export type UserPreferencesState = { theme: Theme openLinksNewTab: boolean onboardingCompleted: boolean + onboardingResult?: { + title: string + sources: string[] + tags: string[] + } | null occupation: string | null listingMode: ListingMode promptEngine: string @@ -244,6 +249,7 @@ export const useUserPreferences = create( return { ...state, onboardingCompleted: true, + occupation: state.onboardingResult?.title || '', } } return state From a37b1d3d953cb308811e938fdaf0022226be2b7b Mon Sep 17 00:00:00 2001 From: John Doe Date: Sat, 22 Nov 2025 12:27:08 +0100 Subject: [PATCH 80/89] fix: add Hackernews and Lobsters sources to FeedItemSource and update styles for source display --- .../feed/components/FeedItemSource.tsx | 49 +++++++++++++++---- src/features/feed/components/feed.css | 13 +++++ .../components/feedItems/ArticleFeedItem.tsx | 4 +- 3 files changed, 55 insertions(+), 11 deletions(-) diff --git a/src/features/feed/components/FeedItemSource.tsx b/src/features/feed/components/FeedItemSource.tsx index ac477d51..ac7f2e30 100644 --- a/src/features/feed/components/FeedItemSource.tsx +++ b/src/features/feed/components/FeedItemSource.tsx @@ -1,10 +1,11 @@ +import { memo, useState } from 'react' import { FaDev, FaMediumM, FaReddit } from 'react-icons/fa' import { GoDotFill } from 'react-icons/go' -import { SiGithub, SiProducthunt } from 'react-icons/si' +import { SiGithub, SiProducthunt, SiYcombinator } from 'react-icons/si' import HashNodeIcon from 'src/assets/icon_hashnode.png' -const FeedItemKV: { - [key: string]: React.ReactNode -} = { +import LobstersIcon from 'src/assets/icon_lobsters.png' + +const SOURCE_MAP: Record = { producthunt: ( <> Product hunt @@ -35,13 +36,43 @@ const FeedItemKV: { hn Hashnode ), + hackernews: ( + <> + Hackernews + + ), + lobsters: ( + <> + lobsters Lobsters + + ), } -export const FeedItemSource = ({ source }: { source: string }) => { - return ( - FeedItemKV[source] || ( +export const FeedItemSource = memo(({ source }: { source: string }) => { + const [fallback, setFallback] = useState(false) + + if (SOURCE_MAP[source]) { + return SOURCE_MAP[source] + } + + if (!fallback && source.includes('.')) { + return ( <> - {source} + {source} { + setFallback(true) + }} + />{' '} + {source} ) + } + + return ( + <> + {source} + ) -} +}) diff --git a/src/features/feed/components/feed.css b/src/features/feed/components/feed.css index 38824082..1c536659 100644 --- a/src/features/feed/components/feed.css +++ b/src/features/feed/components/feed.css @@ -130,3 +130,16 @@ margin: 0; padding: 0; } + +.sourceName { + text-transform: none; + + &::first-letter { + text-transform: capitalize; + } +} + +.feedItemSourceFallback { + width: 12px; + height: 12px; +} diff --git a/src/features/feed/components/feedItems/ArticleFeedItem.tsx b/src/features/feed/components/feedItems/ArticleFeedItem.tsx index f284b39a..bb51fc47 100644 --- a/src/features/feed/components/feedItems/ArticleFeedItem.tsx +++ b/src/features/feed/components/feedItems/ArticleFeedItem.tsx @@ -20,14 +20,14 @@ export const ArticleFeedItem = (props: BaseItemPropsType) = {listingMode === 'compact' && (
- +
)} {listingMode === 'normal' && (
- + From 13fa8754e764951bf324123b326e611a4dab8a15 Mon Sep 17 00:00:00 2001 From: John Doe Date: Sat, 22 Nov 2025 19:34:53 +0100 Subject: [PATCH 81/89] fix: remove unused PostFlair component and its associated types from ArticleItem --- .../cards/components/redditCard/ArticleItem.tsx | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/features/cards/components/redditCard/ArticleItem.tsx b/src/features/cards/components/redditCard/ArticleItem.tsx index 615ae392..1369f3b4 100644 --- a/src/features/cards/components/redditCard/ArticleItem.tsx +++ b/src/features/cards/components/redditCard/ArticleItem.tsx @@ -10,22 +10,6 @@ import { useUserPreferences } from 'src/stores/preferences' import { Article, BaseItemPropsType } from 'src/types' import { format } from 'timeago.js' -type PostFlairPropsType = { - text: string - textColor?: string - bgColor?: string -} - -const PostFlair = ({ text, bgColor, textColor }: PostFlairPropsType) => { - const color = textColor === 'light' ? '#fff' : '#000' - const backgroundColor = bgColor ? bgColor : '#dadada' - return ( -
- {text} -
- ) -} - const ArticleItem = ({ item, analyticsTag }: BaseItemPropsType
) => { const { listingMode } = useUserPreferences() From d6c16c153ee0041e1e1548a77bbe6026b55b1954 Mon Sep 17 00:00:00 2001 From: John Doe Date: Sat, 22 Nov 2025 19:35:02 +0100 Subject: [PATCH 82/89] fix: update GLOBAL_TAG value to an empty string in HashnodeCard and RedditCard components --- src/features/cards/components/hashnodeCard/HashnodeCard.tsx | 2 +- src/features/cards/components/redditCard/RedditCard.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/features/cards/components/hashnodeCard/HashnodeCard.tsx b/src/features/cards/components/hashnodeCard/HashnodeCard.tsx index 2082aed7..5daddb91 100644 --- a/src/features/cards/components/hashnodeCard/HashnodeCard.tsx +++ b/src/features/cards/components/hashnodeCard/HashnodeCard.tsx @@ -11,7 +11,7 @@ import { MemoizedCardHeader } from '../CardHeader' import { MemoizedCardSettings } from '../CardSettings' import ArticleItem from './ArticleItem' -const GLOBAL_TAG = { label: 'Global', value: 'programming' } +const GLOBAL_TAG = { label: 'Global', value: '' } export function HashnodeCard(props: CardPropsType) { const { meta } = props diff --git a/src/features/cards/components/redditCard/RedditCard.tsx b/src/features/cards/components/redditCard/RedditCard.tsx index 4f03d69d..96d3b31e 100644 --- a/src/features/cards/components/redditCard/RedditCard.tsx +++ b/src/features/cards/components/redditCard/RedditCard.tsx @@ -11,7 +11,7 @@ import { MemoizedCardHeader } from '../CardHeader' import { MemoizedCardSettings } from '../CardSettings' import ArticleItem from './ArticleItem' -const GLOBAL_TAG = { label: 'Global', value: 'global' } +const GLOBAL_TAG = { label: 'Global', value: '' } export function RedditCard(props: CardPropsType) { const { meta } = props From 7e908eb0eac5496e53ebb61d144477359675be04 Mon Sep 17 00:00:00 2001 From: John Doe Date: Sat, 22 Nov 2025 19:44:11 +0100 Subject: [PATCH 83/89] fix: update GLOBAL_TAG value to an empty string in FreecodecampCard component --- .../cards/components/freecodecampCard/FreecodecampCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/cards/components/freecodecampCard/FreecodecampCard.tsx b/src/features/cards/components/freecodecampCard/FreecodecampCard.tsx index 34bd637c..83b93fa7 100644 --- a/src/features/cards/components/freecodecampCard/FreecodecampCard.tsx +++ b/src/features/cards/components/freecodecampCard/FreecodecampCard.tsx @@ -9,7 +9,7 @@ import { MemoizedCardHeader } from '../CardHeader' import { MemoizedCardSettings } from '../CardSettings' import ArticleItem from './ArticleItem' -const GLOBAL_TAG = { label: 'Global', value: 'global' } +const GLOBAL_TAG = { label: 'Global', value: '' } export function FreecodecampCard(props: CardPropsType) { const { meta } = props From 7cb791c4ff625bb700b180410c4d7964e02161cb Mon Sep 17 00:00:00 2001 From: John Doe Date: Sat, 22 Nov 2025 20:30:13 +0100 Subject: [PATCH 84/89] feat: add HackernoonCard component and associated ArticleItem with icon --- src/assets/icon_hackernoon.jpeg | Bin 0 -> 7510 bytes src/config/supportedCards.tsx | 11 +++ .../components/hackernoonCard/ArticleItem.tsx | 47 +++++++++++++ .../hackernoonCard/HackernoonCard.tsx | 64 ++++++++++++++++++ .../cards/components/hackernoonCard/index.ts | 1 + src/features/cards/index.ts | 1 + 6 files changed, 124 insertions(+) create mode 100644 src/assets/icon_hackernoon.jpeg create mode 100644 src/features/cards/components/hackernoonCard/ArticleItem.tsx create mode 100644 src/features/cards/components/hackernoonCard/HackernoonCard.tsx create mode 100644 src/features/cards/components/hackernoonCard/index.ts diff --git a/src/assets/icon_hackernoon.jpeg b/src/assets/icon_hackernoon.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..12b4bd04e1ab7ea2a6dcf41117564aefbe3af737 GIT binary patch literal 7510 zcmcIo2RxNs{6BXg#J$%wvo}dsM!d)-vXxSll|8SOmDMscqo{0>h7}md+Dv-_x-=)^Z)#h>v?|Xdw%=;&Uv2mZ1!#r1GJX9raFMZ0DwV%U~?3nuBoDO zO5adVT~k|)GysovvU2u7;sJ1W@gx{(D03b&InD`wY8F-=Zc6(4+CM>mAeUYfwFA(# zhp6ihga6c}vOeu$1vy9oF-lvx5j-JU5u$Z{Jl%+NEJWk1>@2JyIvb)z2#|vi-9f}H zx9E37+IEXxA<~4yhAIHSsUe!vc8eAv(w18^F&0iMJAyOB;{?&X&Q4yC52Qwng!;70 z5hLi%O8WNzhCl--15RKGynsD$0-j(Gba#Pr_SbUzNOHQs6_T-nu5Q2=5FiO#Uk54TPVD{+u8VsQt+OkDPkgh!gceAyaDsfEwG}To49;JQaYAi<_HknVXv% zSpXn<0jN#V}D@0*8!jn1K>f;56m(LfO06uOoMI~1PhWK1ayVl*Z?q} z2f$7f0O+7xQ=a(0j&J1~(oNJ4vU&hG><@r;D*#uo0l*L0qcz=p15^MCfkYw^DCmMh zp~%RoDafHlOGUMV8bgc4VrVfKItEroIvfi<2E)X~#KOkL!NEbt$j!^m&dbWq!A?{H zgG9;6$Z04jXxMQW9Q%K6oAm%g0gHvjB4FGAj)5UCu+1k>LBIe4Mr_0wNDcCk0*QdZ zp-~pn$j^g-422{|z&HB=H3A0UXapLX#yEE0aZUg*rT&fVx!qFvid%|cl2nq<8r;y| zpt{;>>YXzcn2>CGvzk`R=NcJh^M=zJDu?RbhylJAX zh9K&!qtB^_X>Pn{zXA1dCtl&2YO4)qm`YwM9~XoW*!ztQdBLL^rmxujKi+wG$Sm=Q z`&!ef{J#=4uV=K-tPWCOP1DR4_SUs$6q@@$n_LTka;x%}<*vr&tnw(1Ga?Z12TuXO zn|(&@HjjraV>>M(e*=KaCc|&-$FA`OhgC1;myvEv#i{HvjuV8Z+4%*FQ!3_6@(6wFDeb)G#gWU}>0yTpfTwsA%VcL`~4-x zbE*!lyR1az*`9syjN|Ne=lGq@@qW>&kPVb>x}#Gp@#m?v36ei7WbF~9t}Gj?6Hkqs zU=rW<;`h}F6Jiwf)Q3=U(|d=X*Mh%UVp?i3H$2cEd&Fq2|9LNQ%74+E?PTpWwqP%R zxScob)#Q$hUh<>&{~qR0k}nWC&4UkuK@#M~$qW@d=`;p9eoZ(U>Ewm5vVN zF}N+4_-iRR7_1py?oT}1owbz1QOaz;aOokW2#;Ez8~fv+zE!OYTOPxdbh&+F#^5Hf z{f*l(`)FIgykMo*lNVw%m#%%JUwD04OZ0k?IJHV9f2CT$m#mkOc~065=!x*Vai`?% z7BfG-p~F8R3AP`H1ik+(=*_jvNV6vGJvs8GjAQ>SO+4?@U2=UwdA5p8(UhljNej6y-CvSQ9UgQ?Y?JZqNVsNrvvD_UPjYN5tJhD zQ1e|qh4Ng?Qf*C(wfwBI!{%t6nY>`9xGnF@O@V-K>qdbqxodOUEX%1AK|hVkBeEBN zDDvp|O>b*Z@}jzW%>*hnoxvse_-d+*K!A+ghehtx3AOzl5#~LnZyUZc9oknz74XBH z>(!EYYBM_vk>@LW7Dzs+%8YIdna~Va%X-C5&5ALw%^6XYY4g@*6%~d0lSzN*Kuh8i z4%hE7q9~sqm7nW5c<)B9tfdXpMO7Woh}EgE2mBV7JXQZ4JP>x5$Zg3f zT@(0f*5zK9jMr>1a4BNxIStWb3`z#1RRq0TD8S~D_cTj#8|O8e5Z-zi@;L+? z`A5Jg?6=(c{3OM`s$Dm4%l?)-c`~uVAtLw4LEilx|V%_TPr@h@U=5pl$>=( z#9MK7(+XBM{@rfvS1GJgvmZOZVSj{5*Nx=9AKsGtmSU7rF_U3`SAC*YxC7O#n zB27eybreun2ettZy+0-@X=X7+qd5}#J=aMj9eOM{rvVga;dn5g0+9DHbyj4FgVt8penVeqPQjh{wMdI zwrv+%nF+)0wJCQRB+T3GWrg|myR>(4Bp=ibq}xO0egDHXFX*p|m`0<!tDkZF# z0T*em;H0^VH(~7#ESC~~CKBUt;7uyM^%V_C4(Ha0;zK;wEd&lMY_gsBlG#>ysr%24AN6Wtrh5lZhWlsimFL;{;SIXTKAwVD7~8`{g{W%rCO#11)1b*XXsAr#w+r&@!WNvqAc_Z>WZl$9BPO`PCvDYik}?| zku7G<+U=%_qhpsXakW#bW8C{rD}t3yFl8t`X0oTB|FVG@UWj};Z98&fO=}N>rMJDe zd5dN8*Ib;A{BNn_fM!AldhWnc6ci`~YWrOqh5&G+ViATDFF^N*6V=hl38Ilt`+WZ`8MUPA7njm%ZCgBdV)P7AuHE{> zE#|1#>i&D_YXpeu{=njW>@l^vp639KeI;uL(_va_?M=|J=DW`4RFilo`DIe^SHlOV zw2WImYV`#QYL0fNhuFYhYAz8~`f_FzB#&I0LjI9Y=)~R}SboD138WVNszUS-3UL$)p&C-}`h$)bH_yCu` zmv>;-OB>CP;=J%zsBsPlIV0vGpPk~8aRmEO^iIAZ%Kr**R)%rcrVc%!^Ir*d4MOW! zkr}+c8DS+QI+ANsEUF`?3{$|Z|7>K(O5SKnYh;D}B?iL)#UlpEUjU!cD*d;Yx7H&z zf&JlcYC)CWH@Lt{N_3T1d||#8h+*fraI)2WVJ)}*HzNx+!9z0X z#+mai+%@gOY3g_Hr%--NU&8fjXgTrZNMT;yJa*>tC7S`Wo7Rx5UsNk-RP$9%Ystt- zWwfil^~;3&8l|C`&kr&3h+eqjWR^Mq%e<$)l0Fb0@LRkD~Ac}G6~r4AUAWa?lk zW^;sNb$I2&gLmRxji13Pih7Pcin#nZ{z$|N!$MulBufw6ZMu}4V`+`TPu>e^!rSy= z7laFIf)?$bVOx#_p*6aHWf3wK5YsA>i9iVDof#SDr%=g{tTO8Rl%-&hs6>J4~xY~8V$2d}k_@~Os2 zUVow$&h03BDG96k+6N=d8(uigeOQyu5#EqWv!}8zQLC7vew{p zlIc};b$E4CLW5qn)h>5@1r7K*&~ss?dVkvbzHDM_*G-Ft)Uty3_d>O=PW9(e3zWQE*N-Rs&OENWfB zEJ}7LB-*t?zV^hkFKd>mOah;{{cZi%%fD+-U20oibrtBDif}R?Xqv7r4;-(W5z*#xZJKItULi?|B^3{9$m?B$`IO!Y58j^20VrYWnUYuF$;Rj z7fDU^!p%$_xMJXOiY0u75*KsWiDo4<)>GvaOQ;?BQ$L<(fhn40vXiXX2EQ(7m zzn5(eCpdATrVMpMk^4rvr<6W&dqMkEV(2(mtuy{b6=?Co=MQQi0YG6W~Ne3%!vgRGhlKW%7m(kt6lH!(qU zqb)ziT1%2&)0j{o#W15ie%NWxU}dv<_5^qNCA>bDz&j@zbkgb3-DiRk6m5a~v+Sc! zrp}cjIDzIpudkOsolOtE#k=}=m*a=lm3~+pQlzIcP-9NP4d}sk&5u|r3PrS0 z3qL(?ry75U=)cdtkd}mpi8Q9ye+iP9rrl`h0gFUXKu6i_b1e*DpvtP7<-{xLL*dhCM;C{13Oan!-lq|J3( zv71gIMyO^f8(#P)@wIZ$!;9ii@rAySqK?imZwDrpX^WBMOh zE=qUH)L~ysWHJvVK7bS{xn6PPPd87@$oKe(r{iI5VT!aPzjc9v`?^o}mO?ebTKnvz zp+d&0d0{~@zDKyeIaIlsIupyX8+S@>`=vcknIxx&)x>meqEE!)9S3S7EK?6~mUEi- zRE6M(rd$;3?)kulBAix2dz*IROt9Ei_FG-%=C#2ER})5%^k@1{$$ltQ*1-}mPuzzn zA+|T!BN7Q2`i|m73Vus8m%{F9CLU*JT%{0u*&rs!P;9z$u&RqvotOu6FaP^&uc2Pk zU-qhs?k?6Bku0+v3G25p!ekv1ZP1bAXWhk#izR1!@6kYS zj~~HX{^TUN7X-ZXd(=xak^VI0g83KrDg+r4rTXGA1?+bH%he${OYB5K{9T?4ywW&CiMm%g=MBY#;Y1$jQ*xf?HT*@)49T`@^3TyaJ`b#3tT#3C;voFBCX?7V#26* z3eafk$L?D#u2*&Fxay(R;reN(?Ek04Q z$^EW)w0oLk)ELBNCC&1I@;oyx=jhko;ub;ZYrRcLP}C;4Y-u|;sr^kp{N^&#wAfp# eie~qiF}5bzC?BS2{B7@`#I7IJN-W*YzW)Jsksjv& literal 0 HcmV?d00001 diff --git a/src/config/supportedCards.tsx b/src/config/supportedCards.tsx index 4ef1fe82..cd0c1423 100644 --- a/src/config/supportedCards.tsx +++ b/src/config/supportedCards.tsx @@ -2,6 +2,7 @@ import { CgIndieHackers } from 'react-icons/cg' import { FaDev, FaFreeCodeCamp, FaMediumM, FaReddit } from 'react-icons/fa' import { HiSparkles, HiTicket } from 'react-icons/hi' import { SiGithub, SiProducthunt, SiYcombinator } from 'react-icons/si' +import HackernoonIcon from 'src/assets/icon_hackernoon.jpeg' import HashNodeIcon from 'src/assets/icon_hashnode.png' import LobstersIcon from 'src/assets/icon_lobsters.png' import { AICard } from 'src/features/cards/components/aiCard' @@ -18,6 +19,7 @@ const { IndiehackersCard } = lazyImport(() => import('src/features/cards'), 'Ind const { LobstersCard } = lazyImport(() => import('src/features/cards'), 'LobstersCard') const { ProductHuntCard } = lazyImport(() => import('src/features/cards'), 'ProductHuntCard') const { RedditCard } = lazyImport(() => import('src/features/cards'), 'RedditCard') +const { HackernoonCard } = lazyImport(() => import('src/features/cards'), 'HackernoonCard') export const SUPPORTED_CARDS: SupportedCardType[] = [ { @@ -128,4 +130,13 @@ export const SUPPORTED_CARDS: SupportedCardType[] = [ type: 'supported', link: 'https://hackertab.dev/', }, + { + value: 'hackernoon', + analyticsTag: 'hackernoon', + label: 'Hackernoon', + component: HackernoonCard, + icon: hackernoon, + link: 'https://hackernoon.com/', + type: 'supported', + }, ] diff --git a/src/features/cards/components/hackernoonCard/ArticleItem.tsx b/src/features/cards/components/hackernoonCard/ArticleItem.tsx new file mode 100644 index 00000000..01262328 --- /dev/null +++ b/src/features/cards/components/hackernoonCard/ArticleItem.tsx @@ -0,0 +1,47 @@ +import { MdAccessTime } from 'react-icons/md' +import { CardItemWithActions, CardLink, ColoredLanguagesBadge } from 'src/components/Elements' +import { Attributes } from 'src/lib/analytics' +import { useUserPreferences } from 'src/stores/preferences' +import { Article, BaseItemPropsType } from 'src/types' +import { format } from 'timeago.js' + +const ArticleItem = (props: BaseItemPropsType
) => { + const { item, selectedTag, analyticsTag } = props + const { listingMode } = useUserPreferences() + return ( + + +
{item.title}
+
+ {listingMode === 'normal' && ( + <> +

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

+

+ +

+ + )} + + } + /> + ) +} + +export default ArticleItem diff --git a/src/features/cards/components/hackernoonCard/HackernoonCard.tsx b/src/features/cards/components/hackernoonCard/HackernoonCard.tsx new file mode 100644 index 00000000..060b7c7e --- /dev/null +++ b/src/features/cards/components/hackernoonCard/HackernoonCard.tsx @@ -0,0 +1,64 @@ +import { useCallback } from 'react' +import { Card } from 'src/components/Elements' +import { ListPostComponent } from 'src/components/List/ListPostComponent' +import { Article, CardPropsType } from 'src/types' +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 HackernoonCard(props: CardPropsType) { + const { meta } = props + const { ref, isVisible } = useLazyListLoad() + const { + queryTags, + selectedTag, + cardSettings: { sortBy, language } = {}, + } = useSelectedTags({ + source: meta.value, + fallbackTag: GLOBAL_TAG, + }) + + const { data, isLoading } = useGetSourceArticles({ + source: 'hackernoon', + tags: queryTags, + config: { + enabled: isVisible, + }, + }) + + const renderItem = useCallback( + (item: Article) => , + [meta.analyticsTag] + ) + + return ( + + } + settingsComponent={ + + } + {...props}> + + + ) +} diff --git a/src/features/cards/components/hackernoonCard/index.ts b/src/features/cards/components/hackernoonCard/index.ts new file mode 100644 index 00000000..d54a47dc --- /dev/null +++ b/src/features/cards/components/hackernoonCard/index.ts @@ -0,0 +1 @@ +export * from './HackernoonCard' diff --git a/src/features/cards/index.ts b/src/features/cards/index.ts index 47e18318..6c9e784e 100644 --- a/src/features/cards/index.ts +++ b/src/features/cards/index.ts @@ -6,6 +6,7 @@ export * from './components/devtoCard' export * from './components/freecodecampCard' export * from './components/githubCard' export * from './components/hackernewsCard' +export * from './components/hackernoonCard' export * from './components/hashnodeCard' export * from './components/indiehackersCard' export * from './components/lobstersCard' From 603fb48d571267636777e51da769191823afb50c Mon Sep 17 00:00:00 2001 From: John Doe Date: Sun, 23 Nov 2025 22:24:40 +0100 Subject: [PATCH 85/89] fix: add error handling to source article fetching in multiple card components --- .../cards/components/conferencesCard/ConferencesCard.tsx | 7 ++++++- .../cards/components/freecodecampCard/FreecodecampCard.tsx | 3 ++- .../cards/components/hackernoonCard/HackernoonCard.tsx | 3 ++- .../cards/components/hashnodeCard/HashnodeCard.tsx | 3 ++- src/features/cards/components/mediumCard/MediumCard.tsx | 3 ++- 5 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/features/cards/components/conferencesCard/ConferencesCard.tsx b/src/features/cards/components/conferencesCard/ConferencesCard.tsx index 84173fac..66e21121 100644 --- a/src/features/cards/components/conferencesCard/ConferencesCard.tsx +++ b/src/features/cards/components/conferencesCard/ConferencesCard.tsx @@ -21,7 +21,11 @@ export function ConferencesCard(props: CardPropsType) { source: meta.value, fallbackTag: GLOBAL_TAG, }) - const { isLoading, data: results } = useGetConferences({ + const { + isLoading, + error, + data: results, + } = useGetConferences({ tags: queryTags, config: { enabled: isVisible, @@ -59,6 +63,7 @@ export function ConferencesCard(props: CardPropsType) { diff --git a/src/features/cards/components/freecodecampCard/FreecodecampCard.tsx b/src/features/cards/components/freecodecampCard/FreecodecampCard.tsx index 83b93fa7..ff11f745 100644 --- a/src/features/cards/components/freecodecampCard/FreecodecampCard.tsx +++ b/src/features/cards/components/freecodecampCard/FreecodecampCard.tsx @@ -23,7 +23,7 @@ export function FreecodecampCard(props: CardPropsType) { fallbackTag: GLOBAL_TAG, }) - const { data, isLoading } = useGetSourceArticles({ + const { data, error, isLoading } = useGetSourceArticles({ source: 'freecodecamp', tags: queryTags, config: { @@ -56,6 +56,7 @@ export function FreecodecampCard(props: CardPropsType) { diff --git a/src/features/cards/components/hackernoonCard/HackernoonCard.tsx b/src/features/cards/components/hackernoonCard/HackernoonCard.tsx index 060b7c7e..0f5d7cb8 100644 --- a/src/features/cards/components/hackernoonCard/HackernoonCard.tsx +++ b/src/features/cards/components/hackernoonCard/HackernoonCard.tsx @@ -23,7 +23,7 @@ export function HackernoonCard(props: CardPropsType) { fallbackTag: GLOBAL_TAG, }) - const { data, isLoading } = useGetSourceArticles({ + const { data, error, isLoading } = useGetSourceArticles({ source: 'hackernoon', tags: queryTags, config: { @@ -55,6 +55,7 @@ export function HackernoonCard(props: CardPropsType) { {...props}> sortBy={sortBy as keyof Article} + error={error} items={data} isLoading={isLoading} renderItem={renderItem} diff --git a/src/features/cards/components/mediumCard/MediumCard.tsx b/src/features/cards/components/mediumCard/MediumCard.tsx index 83d64e66..1e342860 100644 --- a/src/features/cards/components/mediumCard/MediumCard.tsx +++ b/src/features/cards/components/mediumCard/MediumCard.tsx @@ -24,7 +24,7 @@ export function MediumCard(props: CardPropsType) { source: meta.value, fallbackTag: GLOBAL_TAG, }) - const { data, isLoading } = useGetSourceArticles({ + const { data, isLoading, error } = useGetSourceArticles({ source: 'medium', tags: queryTags, config: { @@ -76,6 +76,7 @@ export function MediumCard(props: CardPropsType) { From 05944a6a026c19c4a61b240e0afdc721fc775f0f Mon Sep 17 00:00:00 2001 From: John Doe Date: Sun, 23 Nov 2025 22:24:59 +0100 Subject: [PATCH 86/89] feat: add bodyClassName prop to SettingsContentLayout and apply topicsBottomSpacer style in TopicSettings --- .../Layout/SettingsContentLayout/SettingsContentLayout.tsx | 5 ++++- .../Layout/SettingsContentLayout/settingsContentLayout.css | 5 +++++ src/features/settings/components/TopicSettings.tsx | 1 + 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/components/Layout/SettingsContentLayout/SettingsContentLayout.tsx b/src/components/Layout/SettingsContentLayout/SettingsContentLayout.tsx index cd2b596f..c3e15c7d 100644 --- a/src/components/Layout/SettingsContentLayout/SettingsContentLayout.tsx +++ b/src/components/Layout/SettingsContentLayout/SettingsContentLayout.tsx @@ -1,3 +1,4 @@ +import clsx from 'clsx' import './settingsContentLayout.css' type SettingsContentLayoutProps = { @@ -5,6 +6,7 @@ type SettingsContentLayoutProps = { description: string children: React.ReactNode actions?: React.ReactNode + bodyClassName?: string } export const SettingsContentLayout = ({ @@ -12,6 +14,7 @@ export const SettingsContentLayout = ({ description, actions, children, + bodyClassName, }: SettingsContentLayoutProps) => { return (
@@ -23,7 +26,7 @@ export const SettingsContentLayout = ({ {actions &&
{actions}
} -
{children}
+
{children}
) } diff --git a/src/components/Layout/SettingsContentLayout/settingsContentLayout.css b/src/components/Layout/SettingsContentLayout/settingsContentLayout.css index 6cab9b24..22ce4d59 100644 --- a/src/components/Layout/SettingsContentLayout/settingsContentLayout.css +++ b/src/components/Layout/SettingsContentLayout/settingsContentLayout.css @@ -63,6 +63,11 @@ cursor: pointer; } } + +.topicsBottomSpacer { + margin-bottom: 80px; + padding-bottom: 24px; +} .settingsContent header { display: flex; justify-content: space-between; diff --git a/src/features/settings/components/TopicSettings.tsx b/src/features/settings/components/TopicSettings.tsx index 0be73258..c6759997 100644 --- a/src/features/settings/components/TopicSettings.tsx +++ b/src/features/settings/components/TopicSettings.tsx @@ -59,6 +59,7 @@ export const TopicSettings = () => { return ( {userSelectedTags.length > 0 ? ( From 530796dd76a169888266e28eed6a0c06a71be1d2 Mon Sep 17 00:00:00 2001 From: John Doe Date: Sun, 23 Nov 2025 22:25:08 +0100 Subject: [PATCH 87/89] fix: update sources for Devops, Data, Security, and ML Engineer occupations to remove Freecodecamp --- src/features/onboarding/components/steps/HelloTab.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/features/onboarding/components/steps/HelloTab.tsx b/src/features/onboarding/components/steps/HelloTab.tsx index ed3529c5..171f2876 100644 --- a/src/features/onboarding/components/steps/HelloTab.tsx +++ b/src/features/onboarding/components/steps/HelloTab.tsx @@ -51,28 +51,28 @@ const OCCUPATIONS: Occupation[] = [ title: 'Devops Engineer', value: 'devops', icon: FaServer, - sources: ['freecodecamp', 'github', 'reddit', 'devto'], + sources: ['hackernoon', 'github', 'reddit', 'hackernews'], tags: ['devops', 'kubernetes', 'docker', 'bash'], }, { title: 'Data Engineer', value: 'data', icon: FaDatabase, - sources: ['freecodecamp', 'github', 'reddit', 'devto'], + sources: ['hackernoon', 'github', 'reddit', 'devto'], tags: ['data science', 'python', 'artificial intelligence', 'machine learning'], }, { title: 'Security Engineer', value: 'security', icon: AiFillSecurityScan, - sources: ['freecodecamp', 'github', 'reddit', 'devto'], + sources: ['hackernoon', 'github', 'reddit', 'devto'], tags: ['security', 'cpp', 'bash', 'python'], }, { title: 'ML Engineer', value: 'ai', icon: FaRobot, - sources: ['github', 'freecodecamp', 'hackernews', 'devto'], + sources: ['github', 'hackernoon', 'hackernews', 'devto'], tags: ['machine learning', 'artificial intelligence', 'python'], }, { From 6d9188438685e91203a9e6e4059f395426615a05 Mon Sep 17 00:00:00 2001 From: John Doe Date: Sun, 23 Nov 2025 22:33:16 +0100 Subject: [PATCH 88/89] fix: update occupation tags and improve card settings handling in HelloTab component --- .../onboarding/components/steps/HelloTab.tsx | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/features/onboarding/components/steps/HelloTab.tsx b/src/features/onboarding/components/steps/HelloTab.tsx index 171f2876..656e9f74 100644 --- a/src/features/onboarding/components/steps/HelloTab.tsx +++ b/src/features/onboarding/components/steps/HelloTab.tsx @@ -28,7 +28,7 @@ const OCCUPATIONS: Occupation[] = [ icon: RiDeviceFill, value: 'fullstack', sources: ['devto', 'github', 'medium', 'hashnode'], - tags: ['javascript', 'typescript', 'php', 'ruby', 'rust'], + tags: ['webdev', 'javascript', 'typescript', 'php', 'devops'], }, { title: 'Mobile', @@ -36,8 +36,8 @@ const OCCUPATIONS: Occupation[] = [ icon: AiFillMobile, sources: ['reddit', 'github', 'medium', 'hashnode'], tags: [ - 'android', 'mobile', + 'android', 'kotlin', 'java', 'ios', @@ -73,7 +73,7 @@ const OCCUPATIONS: Occupation[] = [ value: 'ai', icon: FaRobot, sources: ['github', 'hackernoon', 'hackernews', 'devto'], - tags: ['machine learning', 'artificial intelligence', 'python'], + tags: ['artificial intelligence', 'machine learning', 'python'], }, { title: 'Other', @@ -85,8 +85,14 @@ const OCCUPATIONS: Occupation[] = [ ] export const HelloTab = () => { - const { markOnboardingAsCompleted, setCards, setTags, setOccupation, occupation } = - useUserPreferences() + const { + markOnboardingAsCompleted, + setCardSettings, + setCards, + setTags, + setOccupation, + occupation, + } = useUserPreferences() const { tags } = useRemoteConfigStore() @@ -108,6 +114,12 @@ export const HelloTab = () => { .filter(Boolean) as Array setTags(userTags) + for (const source of selectedOccupation.sources) { + setCardSettings(source, { + language: selectedOccupation.tags[0], + sortBy: 'published_at', + }) + } } markOnboardingAsCompleted() From 082c6863a31e824aa6ee6dde52eb593c55ab81c1 Mon Sep 17 00:00:00 2001 From: John Doe Date: Tue, 25 Nov 2025 22:26:03 +0100 Subject: [PATCH 89/89] feat: add 'Stars today' metric to GitHub cards and feed items --- src/features/cards/components/githubCard/GithubCard.tsx | 5 +++++ src/features/cards/components/githubCard/RepoItem.tsx | 6 ++++++ src/features/feed/components/feedItems/RepoFeedItem.tsx | 6 ++++++ src/types/index.ts | 3 ++- 4 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/features/cards/components/githubCard/GithubCard.tsx b/src/features/cards/components/githubCard/GithubCard.tsx index 26f06f63..bca8aeab 100644 --- a/src/features/cards/components/githubCard/GithubCard.tsx +++ b/src/features/cards/components/githubCard/GithubCard.tsx @@ -93,6 +93,11 @@ export function GithubCard(props: CardPropsType) { value: 'stars_count', icon: , }, + { + label: 'Stars today', + value: 'stars_in_range', + icon: , + }, { label: 'Forks', value: 'forks_count', diff --git a/src/features/cards/components/githubCard/RepoItem.tsx b/src/features/cards/components/githubCard/RepoItem.tsx index d819bbe8..d915a4e3 100644 --- a/src/features/cards/components/githubCard/RepoItem.tsx +++ b/src/features/cards/components/githubCard/RepoItem.tsx @@ -42,6 +42,12 @@ const RepoItem = ({ item, selectedTag, analyticsTag }: BaseItemPropsType {numberWithCommas(item.stars_count)} stars )} + {item.stars_in_range && ( + + {' '} + {numberWithCommas(item.stars_in_range || 0)} stars today + + )} {item.forks_count && ( {numberWithCommas(item.forks_count)}{' '} diff --git a/src/features/feed/components/feedItems/RepoFeedItem.tsx b/src/features/feed/components/feedItems/RepoFeedItem.tsx index ae7dc72d..14236c18 100644 --- a/src/features/feed/components/feedItems/RepoFeedItem.tsx +++ b/src/features/feed/components/feedItems/RepoFeedItem.tsx @@ -54,6 +54,12 @@ export const RepoFeedItem = (props: BaseItemPropsType) => { stars )} + {item.stars_in_range && ( + + {' '} + {numberWithCommas(item.stars_in_range || 0)} stars today + + )} {numberWithCommas(item?.forks || 0)}{' '} forks diff --git a/src/types/index.ts b/src/types/index.ts index 52efb3ab..51f2e4a8 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -75,6 +75,7 @@ export type ProductHuntFeedItemData = FeedItem & { export type GithubFeedItemData = FeedItem & { type: 'github' stars: number + stars_in_range: number forks: number programmingLanguage: string description?: string @@ -98,7 +99,7 @@ export type Repository = BaseEntry & { description: string owner: string forks_count: number - starsInDateRange?: number + stars_in_range: number name: string }