diff --git a/public/base.manifest.json b/public/base.manifest.json index c99f0f30..b7bea024 100644 --- a/public/base.manifest.json +++ b/public/base.manifest.json @@ -2,7 +2,7 @@ "manifest_version": 3, "name": "Hackertab.dev - developer news", "description": "All developer news in one tab", - "version": "1.25.3", + "version": "1.26.2", "chrome_url_overrides": { "newtab": "index.html" }, diff --git a/src/assets/App.css b/src/assets/App.css index c904028e..d330c8e0 100644 --- a/src/assets/App.css +++ b/src/assets/App.css @@ -202,6 +202,7 @@ a { padding-bottom: 56px; scroll-snap-align: start; width: 100vw; + position: relative; } .blockContent { @@ -468,7 +469,7 @@ a { padding: 0; pointer-events: all; text-align: center; - transition: opacity 0.3s ease, right 0.3s ease; + transition: opacity 0.3s ease, right 0.3s ease, transform 0.1s ease, background-color 0.15s ease, color 0.15s ease; width: 28px; margin-bottom: 6px; margin-right: 6px; @@ -491,6 +492,59 @@ a { } } +.markAsReadAction { + position: relative; +} + +.markAsReadTooltip { + position: absolute; + bottom: 100%; + right: 5px; + margin-bottom: 6px; + padding: 4px 8px; + background-color: var(--card-action-button-background); + color: var(--card-action-button-color); + font-size: 12px; + white-space: nowrap; + border-radius: 4px; + opacity: 0; + transition: opacity 0.2s ease; +} + +.markAsReadAction:hover .markAsReadTooltip { + opacity: 1; +} + +.centerMessageWrapper.cardLoading { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; +} + +.centerMessageIcon { + display: block; + font-size: 36px; + margin-bottom: 12px; +} + +.centerMessage p { + margin: 0; + color: var(--primary-text-color); +} + +.centerMessage p:first-of-type { + font-size: 16px; + line-height: 24px; + margin-bottom: 6px; +} + +.centerMessageSubtext { + font-size: 14px; + opacity: 0.7; +} + /* Card (element) */ .blockRow { padding: 8px 16px; diff --git a/src/components/Elements/CardWithActions/CardItemWithActions.tsx b/src/components/Elements/CardWithActions/CardItemWithActions.tsx index 3332cc40..3bcd991b 100644 --- a/src/components/Elements/CardWithActions/CardItemWithActions.tsx +++ b/src/components/Elements/CardWithActions/CardItemWithActions.tsx @@ -1,11 +1,12 @@ import React, { useCallback, useEffect, useState } from 'react' -import { BiBookmarkMinus, BiBookmarkPlus, BiShareAlt } from 'react-icons/bi' +import { BiBookmarkMinus, BiBookmarkPlus, BiCheckCircle, BiShareAlt } from 'react-icons/bi' import { MdBugReport } from 'react-icons/md' import { reportLink } from 'src/config' import { ShareModal } from 'src/features/shareModal' import { ShareModalData } from 'src/features/shareModal/types' import { Attributes, trackLinkBookmark, trackLinkUnBookmark } from 'src/lib/analytics' import { useBookmarks } from 'src/stores/bookmarks' +import { useReadPosts } from 'src/stores/readPosts' import { useUserPreferences } from 'src/stores/preferences' type CardItemWithActionsProps = { @@ -35,10 +36,15 @@ export const CardItemWithActions = ({ const [shareModalData, setShareModalData] = useState() const { bookmarkPost, unbookmarkPost, userBookmarks } = useBookmarks() + const { markAsRead } = useReadPosts() const [isBookmarked, setIsBookmarked] = useState( userBookmarks.some((bm) => bm.source === source && bm.url === item.url) ) + const onMarkAsReadClick = useCallback(() => { + markAsRead(source, item.id) + }, [markAsRead, source, item.id]) + const onBookmarkClick = useCallback(() => { const itemToBookmark = { title: item.title, @@ -88,7 +94,7 @@ export const CardItemWithActions = ({ shareData={shareModalData} /> {cardItem} -
+
{source === 'ai' && ( )} + + + Mark as read + +
) diff --git a/src/components/List/ListComponent.tsx b/src/components/List/ListComponent.tsx index 7bab2714..af8c5e37 100644 --- a/src/components/List/ListComponent.tsx +++ b/src/components/List/ListComponent.tsx @@ -1,6 +1,7 @@ import React, { memo, ReactNode, useMemo } from 'react' import { Placeholder } from 'src/components/placeholders' import { MAX_ITEMS_PER_CARD } from 'src/config' +import { useReadPosts } from 'src/stores/readPosts' type PlaceholdersProps = { placeholder: ReactNode @@ -27,6 +28,7 @@ export type ListComponentPropsType = { refresh?: boolean error?: any limit?: number + source?: string } export function ListComponent(props: ListComponentPropsType) { @@ -40,15 +42,28 @@ export function ListComponent(props: ListComponentPropsType) { header, placeholder = , limit = MAX_ITEMS_PER_CARD, + source, } = props + const { readPosts } = useReadPosts() + + const filteredItems = useMemo(() => { + if (!items || items.length === 0) return [] + if (!source) return items + + const readIds = readPosts[source] || [] + const readIdsSet = new Set(readIds) + + return items.filter((item: any) => !readIdsSet.has(item.id)) + }, [items, source, readPosts]) + const sortedData = useMemo(() => { - if (!items || items.length == 0) return [] - if (!sortBy) return items + if (!filteredItems || filteredItems.length == 0) return [] + if (!sortBy) return filteredItems const result = sortFn - ? [...items].sort(sortFn) - : [...items].sort((a, b) => { + ? [...filteredItems].sort(sortFn) + : [...filteredItems].sort((a, b) => { const aVal = a[sortBy] const bVal = b[sortBy] if (typeof aVal === 'number' && typeof bVal === 'number') return bVal - aVal @@ -57,7 +72,7 @@ export function ListComponent(props: ListComponentPropsType) { }) return result - }, [sortBy, sortFn, items]) + }, [sortBy, sortFn, filteredItems]) const enrichedItems = useMemo(() => { if (!sortedData || sortedData.length === 0) { @@ -93,5 +108,17 @@ export function ListComponent(props: ListComponentPropsType) { ) } + if (items && items.length > 0 && filteredItems.length === 0) { + return ( +
+
+ +

You're all caught up!

+

Check back later for fresh content.

+
+
+ ) + } + return <>{enrichedItems} } diff --git a/src/features/cards/components/aiCard/AICard.tsx b/src/features/cards/components/aiCard/AICard.tsx index 6ab131f5..b867a361 100644 --- a/src/features/cards/components/aiCard/AICard.tsx +++ b/src/features/cards/components/aiCard/AICard.tsx @@ -46,6 +46,7 @@ export function AICard(props: CardPropsType) { error={error} isLoading={isLoading} renderItem={renderItem} + source={meta.value} /> ) diff --git a/src/features/cards/components/conferencesCard/ConferencesCard.tsx b/src/features/cards/components/conferencesCard/ConferencesCard.tsx index 66e21121..e3d0db49 100644 --- a/src/features/cards/components/conferencesCard/ConferencesCard.tsx +++ b/src/features/cards/components/conferencesCard/ConferencesCard.tsx @@ -66,6 +66,7 @@ export function ConferencesCard(props: CardPropsType) { error={error} isLoading={isLoading} renderItem={renderItem} + source={meta.value} /> ) diff --git a/src/features/cards/components/devtoCard/DevtoCard.tsx b/src/features/cards/components/devtoCard/DevtoCard.tsx index 2a079794..a83a9318 100644 --- a/src/features/cards/components/devtoCard/DevtoCard.tsx +++ b/src/features/cards/components/devtoCard/DevtoCard.tsx @@ -78,6 +78,7 @@ export function DevtoCard(props: CardPropsType) { error={error} isLoading={isLoading} renderItem={renderItem} + source={meta.value} /> ) diff --git a/src/features/cards/components/freecodecampCard/FreecodecampCard.tsx b/src/features/cards/components/freecodecampCard/FreecodecampCard.tsx index ff11f745..f9db71bb 100644 --- a/src/features/cards/components/freecodecampCard/FreecodecampCard.tsx +++ b/src/features/cards/components/freecodecampCard/FreecodecampCard.tsx @@ -59,6 +59,7 @@ export function FreecodecampCard(props: CardPropsType) { error={error} isLoading={isLoading} renderItem={renderItem} + source={meta.value} /> ) diff --git a/src/features/cards/components/githubCard/GithubCard.tsx b/src/features/cards/components/githubCard/GithubCard.tsx index fb470b98..7330f413 100644 --- a/src/features/cards/components/githubCard/GithubCard.tsx +++ b/src/features/cards/components/githubCard/GithubCard.tsx @@ -114,6 +114,7 @@ export function GithubCard(props: CardPropsType) { error={error} isLoading={isLoading} renderItem={renderItem} + source={meta.value} /> ) diff --git a/src/features/cards/components/hackernewsCard/HackernewsCard.tsx b/src/features/cards/components/hackernewsCard/HackernewsCard.tsx index 03aaeafb..b06d9eea 100644 --- a/src/features/cards/components/hackernewsCard/HackernewsCard.tsx +++ b/src/features/cards/components/hackernewsCard/HackernewsCard.tsx @@ -59,6 +59,7 @@ export function HackernewsCard(props: CardPropsType) { error={error} isLoading={isLoading} renderItem={renderItem} + source={meta.value} /> ) diff --git a/src/features/cards/components/hackernoonCard/HackernoonCard.tsx b/src/features/cards/components/hackernoonCard/HackernoonCard.tsx index 0f5d7cb8..effe3fdc 100644 --- a/src/features/cards/components/hackernoonCard/HackernoonCard.tsx +++ b/src/features/cards/components/hackernoonCard/HackernoonCard.tsx @@ -59,6 +59,7 @@ export function HackernoonCard(props: CardPropsType) { items={data} isLoading={isLoading} renderItem={renderItem} + source={meta.value} /> ) diff --git a/src/features/cards/components/hashnodeCard/HashnodeCard.tsx b/src/features/cards/components/hashnodeCard/HashnodeCard.tsx index 02066181..1cedb0de 100644 --- a/src/features/cards/components/hashnodeCard/HashnodeCard.tsx +++ b/src/features/cards/components/hashnodeCard/HashnodeCard.tsx @@ -72,6 +72,7 @@ export function HashnodeCard(props: CardPropsType) { items={data} isLoading={isLoading} renderItem={renderItem} + source={meta.value} /> ) diff --git a/src/features/cards/components/indiehackersCard/IndiehackersCard.tsx b/src/features/cards/components/indiehackersCard/IndiehackersCard.tsx index 7d9ee416..a8716637 100644 --- a/src/features/cards/components/indiehackersCard/IndiehackersCard.tsx +++ b/src/features/cards/components/indiehackersCard/IndiehackersCard.tsx @@ -55,6 +55,7 @@ export function IndiehackersCard(props: CardPropsType) { error={error} isLoading={isLoading} renderItem={renderItem} + source={meta.value} /> ) diff --git a/src/features/cards/components/lobstersCard/LobstersCard.tsx b/src/features/cards/components/lobstersCard/LobstersCard.tsx index 597aef8a..253e5d5c 100644 --- a/src/features/cards/components/lobstersCard/LobstersCard.tsx +++ b/src/features/cards/components/lobstersCard/LobstersCard.tsx @@ -54,6 +54,7 @@ export function LobstersCard(props: CardPropsType) { error={error} isLoading={isLoading} renderItem={renderItem} + source={meta.value} /> ) diff --git a/src/features/cards/components/mediumCard/MediumCard.tsx b/src/features/cards/components/mediumCard/MediumCard.tsx index 1e342860..88755bc0 100644 --- a/src/features/cards/components/mediumCard/MediumCard.tsx +++ b/src/features/cards/components/mediumCard/MediumCard.tsx @@ -79,6 +79,7 @@ export function MediumCard(props: CardPropsType) { error={error} isLoading={isLoading} renderItem={renderItem} + source={meta.value} /> ) diff --git a/src/features/cards/components/producthuntCard/ProducthuntCard.tsx b/src/features/cards/components/producthuntCard/ProducthuntCard.tsx index f685965a..62fce169 100644 --- a/src/features/cards/components/producthuntCard/ProducthuntCard.tsx +++ b/src/features/cards/components/producthuntCard/ProducthuntCard.tsx @@ -65,6 +65,7 @@ export function ProductHuntCard(props: CardPropsType) { isLoading={isLoading} renderItem={renderItem} placeholder={} + source={meta.value} /> ) diff --git a/src/features/cards/components/redditCard/RedditCard.tsx b/src/features/cards/components/redditCard/RedditCard.tsx index 96d3b31e..26771720 100644 --- a/src/features/cards/components/redditCard/RedditCard.tsx +++ b/src/features/cards/components/redditCard/RedditCard.tsx @@ -73,6 +73,7 @@ export function RedditCard(props: CardPropsType) { items={results} isLoading={isLoading} renderItem={renderItem} + source={meta.value} /> ) diff --git a/src/features/cards/components/rssCard/CustomRssCard.tsx b/src/features/cards/components/rssCard/CustomRssCard.tsx index 388365d2..7014846c 100644 --- a/src/features/cards/components/rssCard/CustomRssCard.tsx +++ b/src/features/cards/components/rssCard/CustomRssCard.tsx @@ -45,7 +45,7 @@ export function CustomRssCard(props: CardPropsType) { showLanguageFilter={false} /> }> - + ) } diff --git a/src/stores/readPosts.ts b/src/stores/readPosts.ts new file mode 100644 index 00000000..4e28e718 --- /dev/null +++ b/src/stores/readPosts.ts @@ -0,0 +1,36 @@ +import { create } from 'zustand' +import { persist } from 'zustand/middleware' + +const MAX_READ_POSTS_PER_SOURCE = 1000 + +type ReadPostsStore = { + readPosts: Record + markAsRead: (source: string, postId: string) => void +} + +export const useReadPosts = create()( + persist( + (set) => ({ + readPosts: {}, + + markAsRead: (source, postId) => + set((state) => { + const ids = state.readPosts[source] ?? [] + if (ids.includes(postId)) return state + + const next = + ids.length >= MAX_READ_POSTS_PER_SOURCE + ? [...ids.slice(1), postId] + : [...ids, postId] + + return { + readPosts: { + ...state.readPosts, + [source]: next, + }, + } + }), + }), + { name: 'read_posts_storage' } + ) +) \ No newline at end of file