Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion public/base.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
56 changes: 55 additions & 1 deletion src/assets/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ a {
padding-bottom: 56px;
scroll-snap-align: start;
width: 100vw;
position: relative;
}

.blockContent {
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down
20 changes: 18 additions & 2 deletions src/components/Elements/CardWithActions/CardItemWithActions.tsx
Original file line number Diff line number Diff line change
@@ -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 = {
Expand Down Expand Up @@ -35,10 +36,15 @@ export const CardItemWithActions = ({
const [shareModalData, setShareModalData] = useState<ShareModalData>()

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,
Expand Down Expand Up @@ -88,7 +94,7 @@ export const CardItemWithActions = ({
shareData={shareModalData}
/>
{cardItem}
<div className={`blockActions ${isBookmarked ? 'active' : ''} `}>
<div className={`blockActions ${isBookmarked ? 'active' : ''}`}>
{source === 'ai' && (
<button
className={`blockActionButton `}
Expand Down Expand Up @@ -119,6 +125,16 @@ export const CardItemWithActions = ({
{!isBookmarked ? <BiBookmarkPlus /> : <BiBookmarkMinus />}
</button>
)}

<span className="markAsReadAction">
<span className="markAsReadTooltip">Mark as read</span>
<button
className="blockActionButton"
onClick={onMarkAsReadClick}
aria-label="Mark as read">
<BiCheckCircle />
</button>
</span>
</div>
</div>
)
Expand Down
37 changes: 32 additions & 5 deletions src/components/List/ListComponent.tsx
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -27,6 +28,7 @@ export type ListComponentPropsType<T extends unknown> = {
refresh?: boolean
error?: any
limit?: number
source?: string
}

export function ListComponent<T extends any>(props: ListComponentPropsType<T>) {
Expand All @@ -40,15 +42,28 @@ export function ListComponent<T extends any>(props: ListComponentPropsType<T>) {
header,
placeholder = <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
Expand All @@ -57,7 +72,7 @@ export function ListComponent<T extends any>(props: ListComponentPropsType<T>) {
})

return result
}, [sortBy, sortFn, items])
}, [sortBy, sortFn, filteredItems])

const enrichedItems = useMemo(() => {
if (!sortedData || sortedData.length === 0) {
Expand Down Expand Up @@ -93,5 +108,17 @@ export function ListComponent<T extends any>(props: ListComponentPropsType<T>) {
)
}

if (items && items.length > 0 && filteredItems.length === 0) {
return (
<div className="centerMessageWrapper cardLoading">
<div className="centerMessage errorMsg">
<span className="centerMessageIcon">✨</span>
<p><b>You're all caught up!</b></p>
<p className="centerMessageSubtext">Check back later for fresh content.</p>
</div>
</div>
)
}

return <>{enrichedItems}</>
}
1 change: 1 addition & 0 deletions src/features/cards/components/aiCard/AICard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export function AICard(props: CardPropsType) {
error={error}
isLoading={isLoading}
renderItem={renderItem}
source={meta.value}
/>
</Card>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export function ConferencesCard(props: CardPropsType) {
error={error}
isLoading={isLoading}
renderItem={renderItem}
source={meta.value}
/>
</Card>
)
Expand Down
1 change: 1 addition & 0 deletions src/features/cards/components/devtoCard/DevtoCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ export function DevtoCard(props: CardPropsType) {
error={error}
isLoading={isLoading}
renderItem={renderItem}
source={meta.value}
/>
</Card>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export function FreecodecampCard(props: CardPropsType) {
error={error}
isLoading={isLoading}
renderItem={renderItem}
source={meta.value}
/>
</Card>
)
Expand Down
1 change: 1 addition & 0 deletions src/features/cards/components/githubCard/GithubCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ export function GithubCard(props: CardPropsType) {
error={error}
isLoading={isLoading}
renderItem={renderItem}
source={meta.value}
/>
</Card>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export function HackernewsCard(props: CardPropsType) {
error={error}
isLoading={isLoading}
renderItem={renderItem}
source={meta.value}
/>
</Card>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export function HackernoonCard(props: CardPropsType) {
items={data}
isLoading={isLoading}
renderItem={renderItem}
source={meta.value}
/>
</Card>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export function HashnodeCard(props: CardPropsType) {
items={data}
isLoading={isLoading}
renderItem={renderItem}
source={meta.value}
/>
</Card>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export function IndiehackersCard(props: CardPropsType) {
error={error}
isLoading={isLoading}
renderItem={renderItem}
source={meta.value}
/>
</Card>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export function LobstersCard(props: CardPropsType) {
error={error}
isLoading={isLoading}
renderItem={renderItem}
source={meta.value}
/>
</Card>
)
Expand Down
1 change: 1 addition & 0 deletions src/features/cards/components/mediumCard/MediumCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ export function MediumCard(props: CardPropsType) {
error={error}
isLoading={isLoading}
renderItem={renderItem}
source={meta.value}
/>
</Card>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export function ProductHuntCard(props: CardPropsType) {
isLoading={isLoading}
renderItem={renderItem}
placeholder={<ProductHuntPlaceholder />}
source={meta.value}
/>
</Card>
)
Expand Down
1 change: 1 addition & 0 deletions src/features/cards/components/redditCard/RedditCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export function RedditCard(props: CardPropsType) {
items={results}
isLoading={isLoading}
renderItem={renderItem}
source={meta.value}
/>
</Card>
)
Expand Down
2 changes: 1 addition & 1 deletion src/features/cards/components/rssCard/CustomRssCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export function CustomRssCard(props: CardPropsType) {
showLanguageFilter={false}
/>
}>
<ListPostComponent items={data} isLoading={isLoading} renderItem={renderItem} />
<ListPostComponent items={data} isLoading={isLoading} renderItem={renderItem} source={meta.value} />
</Card>
)
}
36 changes: 36 additions & 0 deletions src/stores/readPosts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { create } from 'zustand'
import { persist } from 'zustand/middleware'

const MAX_READ_POSTS_PER_SOURCE = 1000

type ReadPostsStore = {
readPosts: Record<string, string[]>
markAsRead: (source: string, postId: string) => void
}

export const useReadPosts = create<ReadPostsStore>()(
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' }
)
)