diff --git a/.github/workflows/distribute.yml b/.github/workflows/distribute.yml index 3b1e3db0..1effd142 100644 --- a/.github/workflows/distribute.yml +++ b/.github/workflows/distribute.yml @@ -38,16 +38,17 @@ jobs: - name: Bump version and push tag id: tag_version - uses: mathieudutour/github-tag-action@v6.1 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - dry_run: ${{ github.event.inputs.autoTag == 'false' }} - custom_release_rules: | - "fix:patch:Bug Fixes,hotfix:patch:Bug Fixes,minor:minor:Fixes,patch:patch:Quick fixes,refactor:minor:Refactoring,implement:minor:Features,change:minor:Changes,breaking:major:Changes,major:major:Changes" + uses: anothrNick/github-tag-action@1.67.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + WITH_V: true + DRY_RUN: ${{ github.event.inputs.autoTag == 'false' }} - name: Update manifest.json if: github.event.inputs.autoTag == 'true' - run: jq --arg version "${{ steps.tag_version.outputs.new_version }}" '.version=$version' public/base.manifest.json > tmp.json && mv tmp.json public/base.manifest.json + run: | + new_version=$(echo "${{ steps.tag_version.outputs.new_tag }}" | sed 's/^v//') + jq --arg version "$new_version" '.version = $version' public/base.manifest.json > public/base.manifest.json.tmp && mv public/base.manifest.json.tmp public/base.manifest.json - name: Commit changes if: github.event.inputs.autoTag == 'true' diff --git a/README.md b/README.md index 2c543a8a..23041fbd 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ [![Chrome Web Store Rating](https://img.shields.io/chrome-web-store/stars/ocoipcahhaedjhnpoanfflhbdcpmalmp.svg?colorB=%234FC828&label=rating&style=flat)](https://chrome.google.com/webstore/detail/hackertabdev/ocoipcahhaedjhnpoanfflhbdcpmalmp/reviews) # Hackertab.dev — All Developer news in one tab! + **Hackertab makes it easy for you to stay up-to-date with the latest developer news, tools, jobs and events.** Hackertab.dev @@ -12,9 +13,11 @@ As a developer, it can be difficult to stay on top of everything happening in the field. Hackertab makes it easy by allowing you to customize your default tab page to include news, tools and events from top sources such as GitHub Trendings, Hacker News, DevTo, Medium, and Product Hunt. No matter what type of developer you are, you'll find valuable and relevant information with Hackertab. Don't miss out - give it a try today! #### Demo + 👉 [now.hackertab.dev](https://now.hackertab.dev) ## 👩‍💻 How to use it + - Install the extension from the [Chrome store](https://bit.ly/hackertab-ch), or [Mozilla add-ons](https://bit.ly/hackertab-ff) - Open a new tab - The extension should now be running and visible @@ -22,14 +25,18 @@ As a developer, it can be difficult to stay on top of everything happening in th - Enjoy ## 🔥 Features -- 🆕 Hourly updated content -- 💻 Customizable by language or topic -- 👍 Curated content from the best sources -- 🔖 Bookmark and read it later -- 🌙 Dark mode for when it gets late + +- 🆕 Daily updated content +- 💻 Customizable by programming language, framework and topic. +- 👍 Curated content from the best sources. +- 🔖 Bookmark and read it later. +- 🌙 Dark mode for when it gets late. +- ✨ AI-powered recommendations exclusively tailored to your preferences. + Even more features are going to come in the future! ## Data sources + - Github Trendings - Hackernews - DevTo @@ -44,10 +51,13 @@ Even more features are going to come in the future! - **or create an issue to ask for a new data source** ## Support + Please do not hesitate to ask a question, report a bug or add a suggestion. or send an email to hello@hackertab.dev ## Development -Please use the develop branch + +Please use the develop branch. Create an .env file with the necessary + ```bash $ git clone --branch develop git@github.com:medyo/hackertab.dev.git $ cd hackertab.dev @@ -57,9 +67,10 @@ $ # Then visit http://localhost:3000 ``` ## Maintainers + - [medyo](https://github.com/medyo) -- [chouikane](https://github.com/Chouikane) ## Licencing -Hackertab is licensed under the Apache License, Version 2.0. + +Hackertab is licensed under the Apache License, Version 2.0. See [LICENSE](/LICENSE) for the full license text. diff --git a/package.json b/package.json index 6b8c8240..34dcd266 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,6 @@ "dompurify": "^2.2.7", "htmlparser2": "^8.0.1", "jsonpath": "^1.1.1", - "localforage": "^1.9.0", "normalize.css": "^8.0.1", "prop-types": "^15.0.0-0", "react": "^17.0.1", @@ -24,10 +23,10 @@ "react-dom": "^17.0.1", "react-easy-sort": "^1.5.1", "react-error-boundary": "^3.1.4", - "react-icons": "^4.4.0", + "react-icons": "^4.12.0", "react-markdown": "^7.0.1", "react-modal": "^3.12.1", - "react-pro-sidebar": "^0.6.0", + "react-router-dom": "^6.21.0", "react-select": "^5.0.1", "react-share": "^4.4.1", "react-simple-toasts": "^5.10.0", @@ -35,8 +34,6 @@ "react-spring-bottom-sheet": "^3.4.1", "react-toggle": "^4.1.1", "react-tooltip": "^4.2.21", - "rss-to-json": "^2.1.1", - "styled-components": "2", "timeago.js": "^4.0.2", "type-fest": "^1.2.0", "vite-plugin-ejs": "^1.6.4", @@ -47,8 +44,8 @@ "start": "vite", "build:web": "VITE_BUILD_TARGET=web ./script/build.sh", "build:ext": "VITE_BUILD_TARGET=extension ./script/build.sh", - "build:firefox": "./script/build-firefox.sh", - "build:chrome": "./script/build-chrome.sh" + "build:firefox": "VITE_BUILD_PLATFORM=firefox ./script/build-firefox.sh", + "build:chrome": "VITE_BUILD_PLATFORM=chrome ./script/build-chrome.sh" }, "eslintConfig": { "extends": [ diff --git a/src/App.tsx b/src/App.tsx index 2994d374..788dd432 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,9 +1,5 @@ import { useEffect, useLayoutEffect, useState } from 'react' -import 'react-contexify/dist/ReactContexify.css' -import 'src/assets/App.css' -import { DNDLayout, Header } from 'src/components/Layout' -import { MarketingBanner } from 'src/features/MarketingBanner' -import { BookmarksSidebar } from 'src/features/bookmarks' +import { DNDLayout } from 'src/components/Layout' import { setupAnalytics, setupIdentification, trackPageView } from 'src/lib/analytics' import { useUserPreferences } from 'src/stores/preferences' import { AppContentLayout } from './components/Layout' @@ -22,8 +18,6 @@ const intersectionCallback = (entries: IntersectionObserverEntry[]) => { } export const App = () => { - const [showSideBar, setShowSideBar] = useState(false) - const [showSettings, setShowSettings] = useState(false) const [showOnboarding, setShowOnboarding] = useState(true) const { onboardingCompleted, maxVisibleCards, isDNDModeActive, DNDDuration, setDNDDuration } = useUserPreferences() @@ -64,24 +58,13 @@ export const App = () => { return ( <> - + {!onboardingCompleted && isWebOrExtensionVersion() === 'extension' && ( + + )} -
- {!onboardingCompleted && isWebOrExtensionVersion() === 'extension' && ( - - )} -
- -
- {isDNDModeActive() && } - -
- setShowSideBar(false)} /> +
+ {isDNDModeActive() && } +
) diff --git a/src/assets/App.css b/src/assets/App.css index 38485360..0b94107a 100644 --- a/src/assets/App.css +++ b/src/assets/App.css @@ -329,7 +329,20 @@ a { height: 16px; width: 16px; } - +.blockHeaderBadge { + width: auto; + font-size: 12px; + height: 20px; + background-color: var(--tooltip-accent-color); + border-radius: 20px; + justify-content: center; + align-items: center; + text-align: center; + display: inline-flex; + padding: 0 6px; + text-transform: lowercase; + color: white; +} .blockHeaderIcon img { display: block; } @@ -348,7 +361,7 @@ a { position: absolute; right: 0; top: 0; - width: 26%; + width: 30%; } .blockActions.active { @@ -408,6 +421,16 @@ a { .blockRow:not(:last-child) { border-bottom: 1px solid var(--card-content-divider); } +.rowCover { + border-radius: 4px; + display: block; + width: 100%; + min-height: auto; + object-fit: cover; + aspect-ratio: 16/9; + background-color: var(--placeholder-background-color); + margin-bottom: 12px; +} .rowTitle { color: var(--primary-text-color); @@ -418,11 +441,22 @@ a { display: flex; flex-direction: row; } +.rowLink { + color: var(--primary-text-color); + margin: 0; + padding: 0; + font-size: 16px; + text-decoration: none; + display: flex; + flex-direction: row; +} .rowTitle:hover { color: var(--primary-hover-text-color); } - +.titleWithCover { + display: block; +} .dark .blockHeaderWhite { color: white; } @@ -509,6 +543,25 @@ a { color: #99a2ac; } +.backToHome { + display: flex; + order: 4; + width: 100%; +} + +.backToHome a { + display: inline-flex; + align-items: center; + gap: 8px; + font-size: 16px; + vertical-align: middle; + text-decoration: none; +} + +.backToHome a:hover { + opacity: 0.7; +} + .tags { display: flex; flex-wrap: wrap; @@ -834,10 +887,15 @@ Producthunt item width: 100%; } +.dark .themeAdaptiveFillColor { + fill: white; +} + .searchBarIcon { position: absolute; height: 40px; margin: 0 16px; + width: 24px; } .searchBarInput { @@ -976,6 +1034,10 @@ Producthunt item .blockHeader { display: none; } + + .tags { + display: none; + } } @keyframes cardPlaceholderPulse { @@ -1270,3 +1332,6 @@ Producthunt item color: var(--primary-text-color); border-radius: 10px; } +.capitalize { + text-transform: capitalize; +} diff --git a/src/assets/baidu_logo.svg b/src/assets/baidu_logo.svg index 3b6b484b..96f5f072 100644 --- a/src/assets/baidu_logo.svg +++ b/src/assets/baidu_logo.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/assets/bing_logo.svg b/src/assets/bing_logo.svg index 8e0feafa..5334aa7c 100644 --- a/src/assets/bing_logo.svg +++ b/src/assets/bing_logo.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/assets/duckduckgo_logo.svg b/src/assets/duckduckgo_logo.svg index 00d5229c..8215a918 100644 --- a/src/assets/duckduckgo_logo.svg +++ b/src/assets/duckduckgo_logo.svg @@ -1,144 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file +duckduckgo \ No newline at end of file diff --git a/src/assets/google_logo.svg b/src/assets/google_logo.svg index 8330ff3d..18f72bf5 100644 --- a/src/assets/google_logo.svg +++ b/src/assets/google_logo.svg @@ -1,2 +1 @@ - - + \ No newline at end of file diff --git a/src/assets/variables.css b/src/assets/variables.css index f2558e65..a380c418 100644 --- a/src/assets/variables.css +++ b/src/assets/variables.css @@ -94,6 +94,9 @@ html.dark { --settings-input-border-focus-color: #6b7b90; --settings-input-placeholder-color: #42474e; --settings-input-text-color: #fff; + + --horizontal-tabs-layout-active-color: white; + --horizontal-tabs-layout-text-color: #798595; } html.light { @@ -176,4 +179,7 @@ html.light { --settings-input-border-focus-color: #c4d6df; --settings-input-placeholder-color: #97a6ad; --settings-input-text-color: #253b53; + + --horizontal-tabs-layout-active-color: #0366d6; + --horizontal-tabs-layout-text-color: #3c5065; } diff --git a/src/assets/yahoo_logo.svg b/src/assets/yahoo_logo.svg index 6e01c0b8..f6baacd9 100644 --- a/src/assets/yahoo_logo.svg +++ b/src/assets/yahoo_logo.svg @@ -1,9 +1 @@ - - - - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/src/assets/yandex_logo.svg b/src/assets/yandex_logo.svg index fec72fb4..a35d8797 100644 --- a/src/assets/yandex_logo.svg +++ b/src/assets/yandex_logo.svg @@ -1,11 +1 @@ - - - - - - - - - - - + \ No newline at end of file diff --git a/src/components/Elements/BottomNavigation/BottomNavigation.tsx b/src/components/Elements/BottomNavigation/BottomNavigation.tsx index 5245dcd2..49ecef87 100644 --- a/src/components/Elements/BottomNavigation/BottomNavigation.tsx +++ b/src/components/Elements/BottomNavigation/BottomNavigation.tsx @@ -1,5 +1,6 @@ import { AiOutlineMenu } from 'react-icons/ai' import { BsRssFill } from 'react-icons/bs' +import { useNavigate } from 'react-router-dom' import { SUPPORTED_CARDS } from 'src/config/supportedCards' import { useUserPreferences } from 'src/stores/preferences' import { SelectedCard } from 'src/types' @@ -7,16 +8,12 @@ import { SelectedCard } from 'src/types' type BottomNavigationProps = { selectedCard: SelectedCard setSelectedCard: (card: SelectedCard) => void - setShowSettings: (value: boolean | ((prevVar: boolean) => boolean)) => void } -export const BottomNavigation = ({ - selectedCard, - setSelectedCard, - setShowSettings, -}: BottomNavigationProps) => { +export const BottomNavigation = ({ selectedCard, setSelectedCard }: BottomNavigationProps) => { const { cards, userCustomCards } = useUserPreferences() const AVAILABLE_CARDS = [...SUPPORTED_CARDS, ...userCustomCards] + const navigate = useNavigate() return (
@@ -40,7 +37,9 @@ export const BottomNavigation = ({ } diff --git a/src/components/Elements/Card/Card.tsx b/src/components/Elements/Card/Card.tsx index d54cb552..26eafbda 100644 --- a/src/components/Elements/Card/Card.tsx +++ b/src/components/Elements/Card/Card.tsx @@ -16,7 +16,7 @@ type CardProps = { export const Card = ({ card, titleComponent, children, fullBlock = false }: CardProps) => { const { openLinksNewTab } = useUserPreferences() - const { link, icon, label } = card + const { link, icon, label, badge } = card const handleHeaderLinkClick = (e: React.MouseEvent) => { e.preventDefault() let url = `${link}?${ref}` @@ -39,6 +39,7 @@ export const Card = ({ card, titleComponent, children, fullBlock = false }: Card )} + {badge && {badge}}
{children}
diff --git a/src/components/Elements/CardLink/CardLink.tsx b/src/components/Elements/CardLink/CardLink.tsx index 3473d762..c5e3380f 100644 --- a/src/components/Elements/CardLink/CardLink.tsx +++ b/src/components/Elements/CardLink/CardLink.tsx @@ -20,7 +20,7 @@ export const CardLink = ({ return ( {children} diff --git a/src/components/Elements/CardWithActions/CardItemWithActions.tsx b/src/components/Elements/CardWithActions/CardItemWithActions.tsx index 41316b41..ca674a2f 100644 --- a/src/components/Elements/CardWithActions/CardItemWithActions.tsx +++ b/src/components/Elements/CardWithActions/CardItemWithActions.tsx @@ -1,9 +1,12 @@ import React, { useEffect, useState } from 'react' import { BiBookmarkMinus, BiBookmarkPlus, 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 { useUserPreferences } from 'src/stores/preferences' import { BaseEntry } from 'src/types' type CardItemWithActionsProps = { @@ -62,6 +65,13 @@ export const CardItemWithActions = ({ setShareModalData({ title: item.title, link: item.url, source: source }) } + const onReportClicked = () => { + const tags = useUserPreferences + .getState() + .userSelectedTags.map((tag) => tag.label.toLocaleLowerCase()) + window.open(`${reportLink}?tags=${tags.join(',')}&url=${item.url}`, '_blank') + } + return (
setShareModalData(undefined)} shareData={shareModalData} /> - {cardItem}
+ {source === 'ai' && ( + + )} ) } -type ChangeAction = 'ADD' | 'REMOVE' +type ChangeAction = { + option: Option + action: 'ADD' | 'REMOVE' +} type ChipsSetProps = { options: Option[] className?: string defaultValues?: string[] canSelectMultiple?: boolean - onChange?: (action: ChangeAction, options: Option[]) => void + onChange?: (changes: ChangeAction, options: Option[]) => void } export const ChipsSet = ({ @@ -43,7 +46,10 @@ export const ChipsSet = ({ setSelectedChips(newVal) onChange && onChange( - 'REMOVE', + { + option, + action: 'REMOVE', + }, options.filter((opt) => newVal.some((selectedVal) => selectedVal === opt.value)) ) } else { @@ -57,7 +63,10 @@ export const ChipsSet = ({ setSelectedChips(newVal) onChange && onChange( - 'ADD', + { + option, + action: 'ADD', + }, options.filter((opt) => newVal.some((selectedVal) => selectedVal === opt.value)) ) } diff --git a/src/components/Elements/Panel/Panel.tsx b/src/components/Elements/Panel/Panel.tsx new file mode 100644 index 00000000..82f0b123 --- /dev/null +++ b/src/components/Elements/Panel/Panel.tsx @@ -0,0 +1,20 @@ +import './panel.css' + +const variants = { + information: 'information', + warning: 'bg-yellow-100 border-yellow-500 text-yellow-700', +} +type PanelProps = { + title: string | React.ReactNode + body: string | React.ReactNode + variant: keyof typeof variants +} + +export const Panel = ({ title, body, variant = 'information' }: PanelProps) => { + return ( +
+

{title}

+

{body}

+
+ ) +} diff --git a/src/components/Elements/Panel/index.ts b/src/components/Elements/Panel/index.ts new file mode 100644 index 00000000..303f5639 --- /dev/null +++ b/src/components/Elements/Panel/index.ts @@ -0,0 +1 @@ +export * from './Panel' diff --git a/src/components/Elements/Panel/panel.css b/src/components/Elements/Panel/panel.css new file mode 100644 index 00000000..c45d6227 --- /dev/null +++ b/src/components/Elements/Panel/panel.css @@ -0,0 +1,41 @@ +.panel { + height: 200px; + display: flex; + flex-direction: column; + gap: 12px; + scroll-snap-align: start; + padding: 16px 18px; +} +.panel .title { + font-size: 16px; + font-weight: 600; + margin: 0; + padding: 0; + display: inline-flex; + gap: 6px; +} +.panel .body { + font-size: 14px; + line-height: 20px; + margin: 0; + padding: 0; +} + +.dark .panel.information { + background-color: #3b424b; + color: white; +} +.light .panel.information { + background-color: #cce6ff; + color: black; +} +.panel .closeBtn { + background: none; + margin-left: auto; + border: 0; + cursor: pointer; + color: var(--primary-text-color); +} +.panel .closeBtn :hover { + opacity: 0.7; +} diff --git a/src/components/Elements/SearchBar/SearchBar.tsx b/src/components/Elements/SearchBar/SearchBar.tsx index b755a06a..2a1f0bc9 100644 --- a/src/components/Elements/SearchBar/SearchBar.tsx +++ b/src/components/Elements/SearchBar/SearchBar.tsx @@ -1,15 +1,10 @@ import React, { useEffect, useRef } from 'react' -import { GoSearch } from 'react-icons/go' import { SUPPORTED_SEARCH_ENGINES } from 'src/config' import { trackSearchEngineUse } from 'src/lib/analytics' import { useUserPreferences } from 'src/stores/preferences' import { SearchEngine } from 'src/types' -type SearchBarProps = { - withLogo?: boolean -} - -export const SearchBar = ({ withLogo }: SearchBarProps) => { +export const SearchBar = () => { const { searchEngine } = useUserPreferences() const keywordsInputRef = useRef(null) @@ -33,7 +28,7 @@ export const SearchBar = ({ withLogo }: SearchBarProps) => { return (
- + void -} - -export const UserTags = ({ onAddClicked }: UserTagsProps) => { +export const UserTags = () => { const { userSelectedTags } = useUserPreferences() return ( @@ -15,9 +12,9 @@ export const UserTags = ({ onAddClicked }: UserTagsProps) => { {tag.value} ))} - +
) } diff --git a/src/components/Elements/index.ts b/src/components/Elements/index.ts index eab7ae52..850e09f7 100644 --- a/src/components/Elements/index.ts +++ b/src/components/Elements/index.ts @@ -1,14 +1,14 @@ -export * from "./BottomNavigation" -export * from "./Card" -export * from "./CardLink" -export * from "./CardWithActions" -export * from "./ChipsSet" -export * from "./ClickableItem" -export * from "./ColoredLanguagesBadges" -export * from "./FloatingFilter" -export * from "./InlineTextFilter" -export * from "./SearchBar" -export * from "./SearchBarWithLogo" -export * from "./Steps" -export * from "./UserTags" - +export * from './BottomNavigation' +export * from './Card' +export * from './CardLink' +export * from './CardWithActions' +export * from './ChipsSet' +export * from './ClickableItem' +export * from './ColoredLanguagesBadges' +export * from './FloatingFilter' +export * from './InlineTextFilter' +export * from './Panel' +export * from './SearchBar' +export * from './SearchBarWithLogo' +export * from './Steps' +export * from './UserTags' diff --git a/src/components/Layout/AppContentLayout.tsx b/src/components/Layout/AppContentLayout.tsx index fd7ed984..5189bccf 100644 --- a/src/components/Layout/AppContentLayout.tsx +++ b/src/components/Layout/AppContentLayout.tsx @@ -6,11 +6,7 @@ import { ScrollCardsNavigator } from './' import { DesktopCards } from './DesktopCards' import { MobileCards } from './MobileCards' -export const AppContentLayout = ({ - setShowSettings, -}: { - setShowSettings: (value: boolean | ((prevVar: boolean) => boolean)) => void -}) => { +export const AppContentLayout = () => { const { cards, userCustomCards } = useUserPreferences() const [selectedCard, setSelectedCard] = useState(cards[0]) @@ -26,11 +22,7 @@ export const AppContentLayout = ({
)} - + ) } diff --git a/src/components/Layout/AppLayout.tsx b/src/components/Layout/AppLayout.tsx new file mode 100644 index 00000000..5d9fe243 --- /dev/null +++ b/src/components/Layout/AppLayout.tsx @@ -0,0 +1,27 @@ +import React from 'react' +import 'react-contexify/dist/ReactContexify.css' +import { Outlet } from 'react-router-dom' +import { BeatLoader } from 'react-spinners' +import 'src/assets/App.css' +import { MarketingBanner } from 'src/features/MarketingBanner' +import { Header } from './Header' + +export const AppLayout = () => { + return ( + <> + + +
+
+ + +
+ }> + + +
+ + ) +} diff --git a/src/components/Layout/Header.tsx b/src/components/Layout/Header.tsx index 323ab840..386e4a45 100644 --- a/src/components/Layout/Header.tsx +++ b/src/components/Layout/Header.tsx @@ -3,31 +3,22 @@ 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 { RxArrowLeft } from 'react-icons/rx' +import { Link, useLocation, useNavigate } from 'react-router-dom' import { ReactComponent as HackertabLogo } from 'src/assets/logo.svg' import { SearchBar } from 'src/components/Elements/SearchBar' import { UserTags } from 'src/components/Elements/UserTags' import { Changelog } from 'src/features/changelog' -import { SettingsModal } from 'src/features/settings' import { identifyUserTheme, trackDNDDisable, trackThemeSelect } from 'src/lib/analytics' import { useBookmarks } from 'src/stores/bookmarks' import { useUserPreferences } from 'src/stores/preferences' -type HeaderProps = { - showSideBar: boolean - setShowSideBar: (show: boolean) => void - showSettings: boolean - setShowSettings: (show: boolean) => void -} - -export const Header = ({ - showSideBar, - setShowSideBar, - showSettings, - setShowSettings, -}: HeaderProps) => { +export const Header = () => { const [themeIcon, setThemeIcon] = useState() const { theme, setTheme, setDNDDuration, isDNDModeActive } = useUserPreferences() const { userBookmarks } = useBookmarks() + const navigate = useNavigate() + const location = useLocation() useEffect(() => { document.documentElement.classList.add(theme) @@ -52,7 +43,7 @@ export const Header = ({ } const onSettingsClick = () => { - setShowSettings(true) + navigate('/settings/general') } const BookmarksBadgeCount = () => { @@ -70,16 +61,18 @@ export const Header = ({ setDNDDuration('never') } + console.log('location', location) + return ( <> - -
{' '} - + + + @@ -99,15 +92,22 @@ export const Header = ({ onClick={onThemeChange}> {themeIcon} - + + <> + + + + - + {location.pathname === '/' ? ( + + ) : ( +
+ + Back + +
+ )}
) diff --git a/src/components/Layout/SettingsContentLayout/SettingsContentLayout.tsx b/src/components/Layout/SettingsContentLayout/SettingsContentLayout.tsx new file mode 100644 index 00000000..fb6b1c31 --- /dev/null +++ b/src/components/Layout/SettingsContentLayout/SettingsContentLayout.tsx @@ -0,0 +1,23 @@ +import './settingsContentLayout.css' + +type SettingsContentLayoutProps = { + title: string + description: string + children: React.ReactNode +} + +export const SettingsContentLayout = ({ + title, + description, + children, +}: SettingsContentLayoutProps) => { + return ( +
+
+

{title}

+

{description}

+
+
{children}
+
+ ) +} diff --git a/src/components/Layout/SettingsContentLayout/index.ts b/src/components/Layout/SettingsContentLayout/index.ts new file mode 100644 index 00000000..1947c244 --- /dev/null +++ b/src/components/Layout/SettingsContentLayout/index.ts @@ -0,0 +1 @@ +export * from './SettingsContentLayout' diff --git a/src/components/Layout/SettingsContentLayout/settingsContentLayout.css b/src/components/Layout/SettingsContentLayout/settingsContentLayout.css new file mode 100644 index 00000000..07437110 --- /dev/null +++ b/src/components/Layout/SettingsContentLayout/settingsContentLayout.css @@ -0,0 +1,43 @@ +.settingsContent { + padding: 32px; + display: flex; + flex-direction: column; + row-gap: 32px; + height: 100%; +} +.settingsBody { + overflow-y: auto; + height: calc(100% - 100px); + padding-right: 24px; + max-width: 70%; + display: flex; + flex-direction: column; + gap: 16px; +} +.settingsContent header { + display: flex; + flex-direction: column; + row-gap: 8px; +} +.settingsContent header .title { + font-size: 24px; + margin: 0; + padding: 0; + color: var(--primary-text-color); +} +.settingsContent header .description { + font-size: 14px; + margin: 0; + padding: 0; + color: var(--secondary-text-color); +} + +@media only screen and (max-width: 768px) { + .settingsContent { + padding: 16px; + } + .settingsBody { + max-width: 100%; + padding: 0; + } +} diff --git a/src/components/Layout/SettingsLayout/SettingsLayout.tsx b/src/components/Layout/SettingsLayout/SettingsLayout.tsx new file mode 100644 index 00000000..42229748 --- /dev/null +++ b/src/components/Layout/SettingsLayout/SettingsLayout.tsx @@ -0,0 +1,50 @@ +import React from 'react' +import 'react-contexify/dist/ReactContexify.css' +import { NavLink, Outlet } from 'react-router-dom' +import './settings.css' + +export const SettingsLayout = () => { + const navigation = [ + { + name: 'Topics', + path: '/settings/topics', + }, + { + name: 'Sources', + path: '/settings/sources', + }, + { + name: 'Bookmarks', + path: '/settings/bookmarks', + }, + + { + name: 'Settings', + path: '/settings/general', + }, + ] + return ( +
+
+ + +
+ }> + + +
+
+
+ ) +} diff --git a/src/components/Layout/SettingsLayout/index.ts b/src/components/Layout/SettingsLayout/index.ts new file mode 100644 index 00000000..0d56872c --- /dev/null +++ b/src/components/Layout/SettingsLayout/index.ts @@ -0,0 +1 @@ +export * from './SettingsLayout' diff --git a/src/components/Layout/SettingsLayout/settings.css b/src/components/Layout/SettingsLayout/settings.css new file mode 100644 index 00000000..63f79bc3 --- /dev/null +++ b/src/components/Layout/SettingsLayout/settings.css @@ -0,0 +1,58 @@ +.settings { + display: flex; + flex-direction: column; + height: 100vh; +} +.horizontalTabsLayout { + display: flex; + flex-direction: row; + min-height: 100%; + padding: 20px; + box-sizing: border-box; +} + +.horizontalTabsLayout .content { + background-color: var(--card-background-color); + border-radius: 10px; + box-shadow: 0 0 12px var(--card-border-color); + width: 100%; + height: auto; +} + +.horizontalTabsLayout .navigation { + list-style: none; + padding: 24px 0 40px 24px; + display: flex; + width: 200px; + flex-direction: column; + gap: 10px; +} +.horizontalTabsLayout .navigation .link { + padding: 10px; + height: 24px; + font-size: 18px; + cursor: pointer; + text-decoration: none; + color: var(--horizontal-tabs-layout-text-color); +} +.horizontalTabsLayout .navigation .link.active { + color: var(--horizontal-tabs-layout-active-color); + border-right: 4px solid var(--horizontal-tabs-layout-active-color); + font-weight: bold; +} + +@media only screen and (max-width: 768px) { + .horizontalTabsLayout { + flex-direction: column; + padding: 0; + } + .horizontalTabsLayout .navigation { + flex-direction: row; + height: 48px; + padding: 0; + } + .horizontalTabsLayout .navigation .link.active { + border-right: none; + border-bottom: 4px solid var(--horizontal-tabs-layout-active-color); + } +} diff --git a/src/components/Layout/index.ts b/src/components/Layout/index.ts index 89466be4..d9684115 100644 --- a/src/components/Layout/index.ts +++ b/src/components/Layout/index.ts @@ -3,4 +3,4 @@ export * from './DNDLayout/' export * from './Footer' export * from './Header' export * from './ScrollCardsNavigator' - +export * from './SettingsLayout' diff --git a/src/components/List/ListComponent.tsx b/src/components/List/ListComponent.tsx index 40f4f9b6..7489fe84 100644 --- a/src/components/List/ListComponent.tsx +++ b/src/components/List/ListComponent.tsx @@ -1,7 +1,7 @@ import React, { ReactNode, useEffect } from 'react' import { Placeholder } from 'src/components/placeholders' import { MAX_ITEMS_PER_CARD } from 'src/config' -import { BannerAd } from 'src/features/ads' +import { AdvBanner } from 'src/features/adv' import { useRemoteConfigStore } from 'src/features/remoteConfig' import { BaseEntry } from 'src/types' @@ -25,6 +25,7 @@ export type ListComponentPropsType = { renderItem: (item: T, index: number) => React.ReactNode withAds: boolean placeholder?: React.ReactNode + header?: React.ReactNode refresh?: boolean error?: any limit?: number @@ -37,6 +38,7 @@ export function ListComponent(props: ListComponentPropsType error, renderItem, withAds, + header, placeholder = , limit = MAX_ITEMS_PER_CARD, } = props @@ -75,9 +77,14 @@ export function ListComponent(props: ListComponentPropsType return items.slice(0, limit).map((item, index) => { let content: ReactNode[] = [renderItem(item, index)] + if (header && index === 0) { + content.unshift(header) + } + if (canAdsLoad && adsConfig.enabled && withAds && index === adsConfig.rowPosition) { - content.unshift() + content.unshift() } + return content }) } diff --git a/src/config/index.tsx b/src/config/index.tsx index abc9d5ed..85be9f68 100644 --- a/src/config/index.tsx +++ b/src/config/index.tsx @@ -25,6 +25,7 @@ export const termsAndConditionsLink = 'https://www.hackertab.dev/terms-and-condi export const dataSourcesLink = 'https://www.hackertab.dev/data-sources' export const changeLogLink = 'https://api.github.com/repos/medyo/hackertab.dev/releases' export const twitterHandle = '@hackertabdev' +export const reportLink = 'https://www.hackertab.dev/report' // Cfgs export const SUPPORTED_SEARCH_ENGINES = [ { diff --git a/src/config/supportedCards.tsx b/src/config/supportedCards.tsx index fdb455d8..21418ecc 100644 --- a/src/config/supportedCards.tsx +++ b/src/config/supportedCards.tsx @@ -1,9 +1,10 @@ import { CgIndieHackers } from 'react-icons/cg' import { FaDev, FaFreeCodeCamp, FaMediumM, FaReddit } from 'react-icons/fa' -import { HiTicket } from 'react-icons/hi' +import { HiSparkles, HiTicket } from 'react-icons/hi' import { SiGithub, SiProducthunt, SiYcombinator } from 'react-icons/si' import HashNodeIcon from 'src/assets/icon_hashnode.png' import LobstersIcon from 'src/assets/icon_lobsters.png' +import { AICard } from 'src/features/cards/components/aiCard' import { SupportedCardType } from 'src/types' import { lazyImport } from 'src/utils/lazyImport' const { MediumCard } = lazyImport(() => import('src/features/cards'), 'MediumCard') @@ -118,4 +119,13 @@ export const SUPPORTED_CARDS: SupportedCardType[] = [ link: 'https://medium.com/', type: 'supported', }, + { + value: 'ai', + icon: , + analyticsTag: 'ai', + label: 'Powered by AI', + component: AICard, + type: 'supported', + badge: 'ALPHA', + }, ] diff --git a/src/features/ads/index.ts b/src/features/ads/index.ts deleted file mode 100644 index 95b08d86..00000000 --- a/src/features/ads/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./components/BannerAd"; \ No newline at end of file diff --git a/src/features/ads/api/getAd.ts b/src/features/adv/api/getAd.ts similarity index 95% rename from src/features/ads/api/getAd.ts rename to src/features/adv/api/getAd.ts index 78e58009..71a08330 100644 --- a/src/features/ads/api/getAd.ts +++ b/src/features/adv/api/getAd.ts @@ -3,9 +3,6 @@ import { axios } from 'src/lib/axios' import { ExtractFnReturnType, QueryConfig } from 'src/lib/react-query' import { Ad } from '../types' - - - const getAd = async ( keywords: string[], aditionalAdQueries: { [key: string]: string } | undefined @@ -27,7 +24,7 @@ type UseGetAdOptions = { export const useGetAd = ({ keywords, config, aditionalAdQueries }: UseGetAdOptions) => { return useQuery>({ ...config, - queryKey: ['ad'], + queryKey: ['ad', keywords.join(',')], queryFn: () => getAd(keywords, aditionalAdQueries), }) } diff --git a/src/features/ads/components/BannerAd.css b/src/features/adv/components/AdvBanner.css similarity index 100% rename from src/features/ads/components/BannerAd.css rename to src/features/adv/components/AdvBanner.css diff --git a/src/features/ads/components/BannerAd.tsx b/src/features/adv/components/AdvBanner.tsx similarity index 95% rename from src/features/ads/components/BannerAd.tsx rename to src/features/adv/components/AdvBanner.tsx index dbdf4765..41ec23d1 100644 --- a/src/features/ads/components/BannerAd.tsx +++ b/src/features/adv/components/AdvBanner.tsx @@ -2,9 +2,9 @@ import { useState } from 'react' import { AdPlaceholder } from 'src/components/placeholders' import { useUserPreferences } from 'src/stores/preferences' import { useGetAd } from '../api/getAd' -import './BannerAd.css' +import './AdvBanner.css' -export const BannerAd = () => { +export const AdvBanner = () => { const { userSelectedTags } = useUserPreferences() const [aditionalAdQueries, setAditionalAdQueries] = useState< @@ -20,6 +20,7 @@ export const BannerAd = () => { config: { cacheTime: 0, staleTime: 0, + useErrorBoundary: false, refetchInterval(data) { if (data?.nextAd) { setAditionalAdQueries(data.nextAd.queries) diff --git a/src/features/adv/index.ts b/src/features/adv/index.ts new file mode 100644 index 00000000..aace063a --- /dev/null +++ b/src/features/adv/index.ts @@ -0,0 +1 @@ +export * from './components/AdvBanner' diff --git a/src/features/ads/types/index.ts b/src/features/adv/types/index.ts similarity index 100% rename from src/features/ads/types/index.ts rename to src/features/adv/types/index.ts diff --git a/src/features/bookmarks/components/BookmarksSidebar.tsx b/src/features/bookmarks/components/BookmarksSidebar.tsx deleted file mode 100644 index 8a6a22fa..00000000 --- a/src/features/bookmarks/components/BookmarksSidebar.tsx +++ /dev/null @@ -1,138 +0,0 @@ -import { HiTicket } from 'react-icons/hi' -import { SiGithub, SiProducthunt, SiReddit, SiYcombinator } from 'react-icons/si' -import { TiDelete } from 'react-icons/ti' -import { VscChromeClose } from 'react-icons/vsc' -import { - Menu, - MenuItem, - ProSidebar, - SidebarContent, - SidebarHeader, - SubMenu, -} from 'react-pro-sidebar' -import 'react-pro-sidebar/dist/css/styles.css' -import { CardLink } from 'src/components/Elements' -import { Attributes, trackLinkUnBookmark } from 'src/lib/analytics' -import { useBookmarks } from 'src/stores/bookmarks' -import { BookmarkedPost } from '../types' -import './Sidebar.css' - -type BookmarkItemProps = { - item: BookmarkedPost - appendRef?: boolean -} -const BookmarkItem = ({ item, appendRef = false }: BookmarkItemProps) => { - const { unbookmarkPost } = useBookmarks() - const analyticsAttrs = { - [Attributes.TRIGERED_FROM]: 'bookmarks', - [Attributes.TITLE]: item.title, - [Attributes.LINK]: item.url, - [Attributes.SOURCE]: item.source, - } - const unBookmark = () => { - unbookmarkPost(item) - trackLinkUnBookmark(analyticsAttrs) - } - return ( - - - - }> - - {`${item.title}`} - - - ) -} - -type BookmarksSidebarTtypes = { - showSidebar: boolean - onClose: () => void -} -export const BookmarksSidebar = ({ showSidebar, onClose }: BookmarksSidebarTtypes) => { - const { userBookmarks } = useBookmarks() - const githubBookmarks = userBookmarks.filter((bm) => bm.source === 'github') - const newsBookmarks = userBookmarks.filter( - (bm) => - [ - 'hackernews', - 'devto', - 'hashnode', - 'lobsters', - 'freecodecamp', - 'medium', - 'indiehackers', - ].indexOf(bm.source) !== -1 || bm.sourceType === 'rss' - ) - const conferencesBookmarks = userBookmarks.filter((bm) => bm.source === 'conferences') - const productsBookmarks = userBookmarks.filter((bm) => bm.source === 'producthunt') - const redditBookmarks = userBookmarks.filter((bm) => bm.source === 'reddit') - - return ( - - -
- Bookmarks - -
-
- - - } - suffix={{githubBookmarks.length}}> - {githubBookmarks.map((bm, index) => ( - - ))} - - - } - suffix={{newsBookmarks.length}}> - {newsBookmarks.map((bm, index) => ( - - ))} - - - } - suffix={{productsBookmarks.length}}> - {productsBookmarks.map((bm, index) => ( - - ))} - - - } - suffix={{conferencesBookmarks.length}}> - {conferencesBookmarks.map((bm, index) => ( - - ))} - - } - suffix={{redditBookmarks.length}}> - {redditBookmarks.map((bm, index) => ( - - ))} - - - -
- ) -} - -export default BookmarksSidebar diff --git a/src/features/bookmarks/components/Sidebar.css b/src/features/bookmarks/components/Sidebar.css deleted file mode 100644 index d08a2a6d..00000000 --- a/src/features/bookmarks/components/Sidebar.css +++ /dev/null @@ -1,114 +0,0 @@ -.sidebar { - top: 0; - right: 0; - position: absolute !important; - color: var(--primary-text-color) !important; - background-color: var(--card-background-color); - overflow: hidden; - flex-direction: column; - - /* Fix flickering issue on Chrome (Mac)*/ - -webkit-backface-visibility: hidden; - -webkit-transform: translate3d(0, 0, 0); -} - -.dark .sidebar { - border-left: 1px solid var(--card-border-color); -} -.light .sidebar { - box-shadow: -4px 0 20px var(--card-border-color); -} - -.sidebarHeader { - align-items: center; - display: flex; - justify-content: space-between; - padding: 12px 24px; -} - -.sidebarHeader .title { - text-transform: uppercase; - font-weight: bold; - font-size: 14px; - letter-spacing: 1px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} -.sidebarHeader .closeBtn { - align-items: center; - background-color: transparent; - border-radius: 50%; - border: 0; - color: inherit; - cursor: pointer; - display: flex; - height: 40px; - justify-content: center; - width: 40px; -} - -.pro-sidebar-inner { - background: var(--card-background-color) !important; -} - -.pro-sidebar .pro-menu > ul > .pro-sub-menu > .pro-inner-list-item { - background: var(--card-background-color) !important; -} - -.pro-sidebar .pro-menu .pro-menu-item > .pro-inner-item:hover { - color: var(--primary-text-color) !important; -} - -.pro-sidebar .pro-menu .pro-menu-item > .pro-inner-item:focus { - color: var(--primary-text-color) !important; - outline: inherit; -} - -.pro-sidebar .pro-menu a { - color: var(--primary-text-color) !important; -} - -.pro-sidebar .pro-menu.shaped .pro-menu-item > .pro-inner-item > .pro-icon-wrapper { - background: var(--button-background-color) !important; -} - -.pro-sidebar .pro-menu a:before { - position: static !important; -} - -.unbookmarkBtn { - font-size: 20px; -} -@media (max-width: 2000px) { - .sidebar:not(.collapsed) { - width: 16% !important; - } -} -@media (max-width: 1800px) { - .sidebar:not(.collapsed) { - width: 22% !important; - } -} - -@media (max-width: 1200px) { - .sidebar:not(.collapsed) { - width: 30% !important; - } -} -@media (max-width: 991.98px) { - .sidebar:not(.collapsed) { - width: 40% !important; - } -} -@media (max-width: 768px) { - .sidebar:not(.collapsed) { - width: 50% !important; - } -} - -@media (max-width: 575.98px) { - .sidebar:not(.collapsed) { - width: 100% !important; - } -} diff --git a/src/features/bookmarks/index.ts b/src/features/bookmarks/index.ts index 9d030294..b2274e03 100644 --- a/src/features/bookmarks/index.ts +++ b/src/features/bookmarks/index.ts @@ -1,2 +1,2 @@ -export * from "./components/BookmarksSidebar" -export * from "./types" \ No newline at end of file +export * from './routes' +export * from './types' diff --git a/src/features/bookmarks/routes/index.tsx b/src/features/bookmarks/routes/index.tsx new file mode 100644 index 00000000..639f2238 --- /dev/null +++ b/src/features/bookmarks/routes/index.tsx @@ -0,0 +1,29 @@ +import { Route, Routes } from 'react-router-dom' +import { SettingsContentLayout } from 'src/components/Layout/SettingsContentLayout/SettingsContentLayout' +import { useBookmarks } from 'src/stores/bookmarks' + +const Bookmarks = () => { + const { userBookmarks } = useBookmarks() + return ( + +
+ {userBookmarks.map((bm) => ( +
+

{bm.title}

+

{bm.url}

+
+ ))} +
+
+ ) +} + +export const BookmarksRoutes = () => { + return ( + + } /> + + ) +} diff --git a/src/features/bookmarks/types/index.ts b/src/features/bookmarks/types/index.ts index a3a1ef88..f18fc5a8 100644 --- a/src/features/bookmarks/types/index.ts +++ b/src/features/bookmarks/types/index.ts @@ -3,4 +3,4 @@ export type BookmarkedPost = { source: string url: string sourceType: 'rss' | 'supported' -} \ No newline at end of file +} diff --git a/src/features/cards/api/getAIArticles.ts b/src/features/cards/api/getAIArticles.ts new file mode 100644 index 00000000..3cab0f0a --- /dev/null +++ b/src/features/cards/api/getAIArticles.ts @@ -0,0 +1,28 @@ +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 getAIArticles = async (userTopics: string[]): Promise => { + return axios.get('/engine/feed/get', { + params: { + tags: userTopics.join(','), + limit: 50, + }, + }) +} + +type QueryFnType = typeof getAIArticles + +type UseGetArticlesOptions = { + userTopics: string[] + config?: QueryConfig +} + +export const useGetAIArticles = ({ userTopics, config }: UseGetArticlesOptions) => { + return useQuery>({ + ...config, + queryKey: ['ai', userTopics.join(',')], + queryFn: () => getAIArticles(userTopics), + }) +} diff --git a/src/features/cards/components/aiCard/AICard.tsx b/src/features/cards/components/aiCard/AICard.tsx new file mode 100644 index 00000000..875f1b2d --- /dev/null +++ b/src/features/cards/components/aiCard/AICard.tsx @@ -0,0 +1,72 @@ +import { MdBugReport } from 'react-icons/md' +import { TbTestPipe } from 'react-icons/tb' +import { VscClose } from 'react-icons/vsc' +import { Card, Panel } from 'src/components/Elements' +import { ListComponent } from 'src/components/List' +import { useFeatureFlags } from 'src/stores/featureFlags' +import { useUserPreferences } from 'src/stores/preferences' +import { Article, CardPropsType } from 'src/types' +import { useGetAIArticles } from '../../api/getAIArticles' +import ArticleItem from './ArticleItem' + +const SHOW_PANEL_FEATURE_FLAG = 'ai_panel_shown' + +export function AICard({ meta, withAds }: CardPropsType) { + const { userSelectedTags } = useUserPreferences() + const { isDone, markDone } = useFeatureFlags() + const { + data: articles = [], + isLoading, + error, + } = useGetAIArticles({ + userTopics: userSelectedTags.map((tag) => tag.label.toLocaleLowerCase()), + config: { + cacheTime: 0, + staleTime: 0, + useErrorBoundary: false, + }, + }) + + const renderItem = (item: Article, index: number) => ( + + ) + + return ( + + + + Alpha feature + + + + } + body={ + <> + This AI-powered card compiles articles from various sources. If you spot + irrelevant content, hover an item then use{' '} + to report it. + + } + /> + ) + } + isLoading={isLoading} + renderItem={renderItem} + withAds={withAds} + /> + + ) +} diff --git a/src/features/cards/components/aiCard/ArticleItem.tsx b/src/features/cards/components/aiCard/ArticleItem.tsx new file mode 100644 index 00000000..8fee50ca --- /dev/null +++ b/src/features/cards/components/aiCard/ArticleItem.tsx @@ -0,0 +1,67 @@ +import { BiCommentDetail } from 'react-icons/bi' +import { GoDotFill } from 'react-icons/go' +import { MdAccessTime } from 'react-icons/md' +import { CardItemWithActions, CardLink, ClickableItem } 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, index, analyticsTag } = props + const { listingMode } = useUserPreferences() + + return ( + +

+ + {item.image_url && listingMode === 'normal' && ( + + )} + {item.title} + +

+ {listingMode === 'normal' && ( +
+ + {item.source} + + + {format(new Date(item.published_at))} + + + {item.comments} comments + +
+ )} + + } + /> + ) +} + +export default ArticleItem diff --git a/src/features/cards/components/aiCard/index.ts b/src/features/cards/components/aiCard/index.ts new file mode 100644 index 00000000..27f867f9 --- /dev/null +++ b/src/features/cards/components/aiCard/index.ts @@ -0,0 +1 @@ +export * from './AICard' diff --git a/src/features/cards/components/hackernewsCard/ArticleItem.tsx b/src/features/cards/components/hackernewsCard/ArticleItem.tsx index 5dbbb717..aefc8dfd 100644 --- a/src/features/cards/components/hackernewsCard/ArticleItem.tsx +++ b/src/features/cards/components/hackernewsCard/ArticleItem.tsx @@ -1,12 +1,12 @@ -import { format } from 'timeago.js' -import { VscTriangleUp } from 'react-icons/vsc' import { BiCommentDetail } from 'react-icons/bi' +import { GoDotFill } from 'react-icons/go' import { MdAccessTime } from 'react-icons/md' -import { GoPrimitiveDot } from 'react-icons/go' -import { CardLink, CardItemWithActions, ClickableItem } from 'src/components/Elements' +import { VscTriangleUp } from 'react-icons/vsc' +import { CardItemWithActions, CardLink, ClickableItem } 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' const ArticleItem = (props: BaseItemPropsType
) => { const { item, index, analyticsTag } = props @@ -43,7 +43,7 @@ const ArticleItem = (props: BaseItemPropsType
) => { {listingMode === 'normal' && (
- {item.reactions} points + {item.reactions} points {format(new Date(item.published_at))} diff --git a/src/features/cards/components/lobstersCard/ArticleItem.tsx b/src/features/cards/components/lobstersCard/ArticleItem.tsx index 1ce325dd..919d4423 100644 --- a/src/features/cards/components/lobstersCard/ArticleItem.tsx +++ b/src/features/cards/components/lobstersCard/ArticleItem.tsx @@ -1,12 +1,12 @@ -import { format } from 'timeago.js' -import { VscTriangleUp } from 'react-icons/vsc' import { BiCommentDetail } from 'react-icons/bi' +import { GoDotFill } from 'react-icons/go' import { MdAccessTime } from 'react-icons/md' -import { GoPrimitiveDot } from 'react-icons/go' -import { CardLink, CardItemWithActions, ClickableItem } from 'src/components/Elements' +import { VscTriangleUp } from 'react-icons/vsc' +import { CardItemWithActions, CardLink, ClickableItem } 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' const ArticleItem = ({ item, index, analyticsTag }: BaseItemPropsType
) => { const { listingMode } = useUserPreferences() @@ -42,7 +42,7 @@ const ArticleItem = ({ item, index, analyticsTag }: BaseItemPropsType
) {listingMode === 'normal' && (
- {item.reactions} points + {item.reactions} points {format(new Date(item.published_at))} diff --git a/src/features/cards/components/redditCard/ArticleItem.tsx b/src/features/cards/components/redditCard/ArticleItem.tsx index c354d62e..3a3f0984 100644 --- a/src/features/cards/components/redditCard/ArticleItem.tsx +++ b/src/features/cards/components/redditCard/ArticleItem.tsx @@ -1,13 +1,13 @@ import { BiCommentDetail } from 'react-icons/bi' +import { BsArrowReturnRight } from 'react-icons/bs' +import { GoDotFill } from 'react-icons/go' +import { MdAccessTime } from 'react-icons/md' 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' import { format } from 'timeago.js' -import { MdAccessTime } from 'react-icons/md' -import { GoPrimitiveDot } from 'react-icons/go' -import { BsArrowReturnRight } from 'react-icons/bs' type PostFlairPropsType = { text: string @@ -67,7 +67,7 @@ const ArticleItem = ({ item, index, analyticsTag }: BaseItemPropsType
) {listingMode === 'normal' && ( <> - {item.reactions} points + {item.reactions} points {format(new Date(item.published_at))} diff --git a/src/features/settings/components/BookmarkSettings/BookmarkSettings.tsx b/src/features/settings/components/BookmarkSettings/BookmarkSettings.tsx new file mode 100644 index 00000000..86ca31d3 --- /dev/null +++ b/src/features/settings/components/BookmarkSettings/BookmarkSettings.tsx @@ -0,0 +1,76 @@ +import { BiBookmarkMinus } from 'react-icons/bi' +import toast from 'react-simple-toasts' +import { CardLink } from 'src/components/Elements' +import { SettingsContentLayout } from 'src/components/Layout/SettingsContentLayout' +import { SUPPORTED_CARDS } from 'src/config/supportedCards' +import { BookmarkedPost } from 'src/features/bookmarks' +import { Attributes, trackLinkUnBookmark } from 'src/lib/analytics' +import { useBookmarks } from 'src/stores/bookmarks' +import { useUserPreferences } from 'src/stores/preferences' +import './bookmarkSettings.css' + +type BookmarkItemProps = { + item: BookmarkedPost + appendRef?: boolean +} +const BookmarkItem = ({ item, appendRef = false }: BookmarkItemProps) => { + const { unbookmarkPost } = useBookmarks() + const { userCustomCards } = useUserPreferences() + + const AVAILABLE_CARDS = [...SUPPORTED_CARDS, ...userCustomCards] + const source = AVAILABLE_CARDS.find((card) => card.value === item.source) + + const analyticsAttrs = { + [Attributes.TRIGERED_FROM]: 'bookmarks', + [Attributes.TITLE]: item.title, + [Attributes.LINK]: item.url, + [Attributes.SOURCE]: item.source, + } + const unBookmark = () => { + unbookmarkPost(item) + trackLinkUnBookmark(analyticsAttrs) + toast('Link removed from the bookmarks', { theme: 'defaultToast' }) + } + return ( +
+ +
{item.title}
+ {source && ( +
+ {source.type === 'supported' ? ( + {source.icon} + ) : ( + + )} + {source.label} +
+ )} +
+
+ +
+
+ ) +} + +export const BookmarkSettings = () => { + const { userBookmarks } = useBookmarks() + + return ( + +
+ {userBookmarks.map((bm) => ( + + ))} +
+
+ ) +} diff --git a/src/features/settings/components/BookmarkSettings/bookmarkSettings.css b/src/features/settings/components/BookmarkSettings/bookmarkSettings.css new file mode 100644 index 00000000..6775a57c --- /dev/null +++ b/src/features/settings/components/BookmarkSettings/bookmarkSettings.css @@ -0,0 +1,55 @@ +.bookmarks { + display: flex; + flex-direction: column; + row-gap: 12px; +} + +.bookmarks .bookmarkItem { + display: flex; + flex-direction: row; + border-bottom: 1px solid var(--card-content-divider); + padding-bottom: 12px; + padding-right: 12px; +} +.bookmarks .body { + display: flex; + flex-direction: column; + row-gap: 8px; +} +.bookmarks .actions { + margin-left: auto; +} +.bookmarks .title { + font-weight: bold; + font-size: 16px; + color: var(--primary-text-color); +} + +.bookmarks .source { + display: flex; + flex-direction: row; + align-items: center; + column-gap: 4px; + color: var(--secondary-text-color); + font-size: 14px; +} +.bookmarks .source svg, +.bookmarks .source img.icon { + width: 16px; + height: 16px; +} +.bookmarks .btn { + width: 40px; + height: 40px; + background-color: var(--button-background-color); + color: var(--button-text-color); + font-size: 20px; + text-align: center; + border: 0; + border-radius: 20px; + cursor: pointer; + + display: inline-flex; + align-items: center; + justify-content: center; +} diff --git a/src/features/settings/components/BookmarkSettings/index.ts b/src/features/settings/components/BookmarkSettings/index.ts new file mode 100644 index 00000000..0b7df07c --- /dev/null +++ b/src/features/settings/components/BookmarkSettings/index.ts @@ -0,0 +1 @@ +export * from './BookmarkSettings' diff --git a/src/features/settings/components/DNDSettings.tsx b/src/features/settings/components/GeneralSettings/DNDSettings.tsx similarity index 94% rename from src/features/settings/components/DNDSettings.tsx rename to src/features/settings/components/GeneralSettings/DNDSettings.tsx index 1b9802f0..2385ec01 100644 --- a/src/features/settings/components/DNDSettings.tsx +++ b/src/features/settings/components/GeneralSettings/DNDSettings.tsx @@ -1,4 +1,5 @@ import { useState } from 'react' +import { useNavigate } from 'react-router-dom' import Select, { SingleValue } from 'react-select' import { trackDNDEnable } from 'src/lib/analytics' import { useUserPreferences } from 'src/stores/preferences' @@ -16,13 +17,9 @@ const DNDDurations: DndOption[] = [ { value: 'always', label: 'Until you turn it off' }, ] -type DNDSettingsProps = { - setShowSettings: (show: boolean) => void -} - -export const DNDSettings = ({ setShowSettings }: DNDSettingsProps) => { +export const DNDSettings = () => { const [selectedDNDDuration, setSelectedDNDDuration] = useState() - + const navigate = useNavigate() const { DNDDuration, setDNDDuration } = useUserPreferences() const onApplyClicked = () => { @@ -43,7 +40,7 @@ export const DNDSettings = ({ setShowSettings }: DNDSettingsProps) => { } trackDNDEnable(selectedDNDDuration) - setShowSettings(false) + navigate('/') } const onPeriodSelect = (selectedOption: SingleValue) => { diff --git a/src/features/settings/components/GeneralSettings/GeneralSettings.tsx b/src/features/settings/components/GeneralSettings/GeneralSettings.tsx new file mode 100644 index 00000000..f7c6eff1 --- /dev/null +++ b/src/features/settings/components/GeneralSettings/GeneralSettings.tsx @@ -0,0 +1,173 @@ +import React from 'react' +import Select, { SingleValue } from 'react-select' +import Toggle from 'react-toggle' +import 'react-toggle/style.css' +import { ChipsSet } from 'src/components/Elements' +import { Footer } from 'src/components/Layout' +import { SettingsContentLayout } from 'src/components/Layout/SettingsContentLayout' +import { SUPPORTED_SEARCH_ENGINES, supportLink } from 'src/config' +import { + identifyUserLinksInNewTab, + identifyUserListingMode, + identifyUserMaxVisibleCards, + identifyUserSearchEngine, + identifyUserTheme, + trackListingModeSelect, + trackMaxVisibleCardsChange, + trackSearchEngineSelect, + trackTabTarget, + trackThemeSelect, +} from 'src/lib/analytics' +import { useUserPreferences } from 'src/stores/preferences' +import { Option, SearchEngineType } from 'src/types' +import { DNDSettings } from './DNDSettings' +import './generalSettings.css' + +export const GeneralSettings = () => { + const { + openLinksNewTab, + listingMode, + theme, + searchEngine, + maxVisibleCards, + setTheme, + setListingMode, + setMaxVisibleCards, + setSearchEngine, + setOpenLinksNewTab, + } = useUserPreferences() + const onSearchEngineSelectChange = (value: SingleValue) => { + if (!value) { + return + } + + identifyUserSearchEngine(value.label) + trackSearchEngineSelect(value.label) + setSearchEngine(value.label) + } + + const onOpenLinksNewTabChange = (e: React.ChangeEvent) => { + const checked = e.target.checked + trackTabTarget(checked) + identifyUserLinksInNewTab(checked) + setOpenLinksNewTab(checked) + } + + const onlistingModeChange = (e: React.ChangeEvent) => { + const value = e.target.checked ? 'compact' : 'normal' + trackListingModeSelect(value) + identifyUserListingMode(value) + setListingMode(value) + } + + const onDarkModeChange = () => { + const newTheme = theme === 'dark' ? 'light' : 'dark' + setTheme(newTheme) + trackThemeSelect(newTheme) + identifyUserTheme(newTheme) + } + + const onMaxVisibleCardsChange = (selectedChips: Option[]) => { + if (selectedChips.length) { + const maxVisibleCards = parseInt(selectedChips[0].value) + setMaxVisibleCards(maxVisibleCards) + identifyUserMaxVisibleCards(maxVisibleCards) + trackMaxVisibleCardsChange(maxVisibleCards) + } + } + + return ( + +
+
+

Max number of cards to display

+
+ { + onMaxVisibleCardsChange(selectedChips) + }} + /> + +

+ To ensure a seamless experience, we may adjust the selected number to align with the + resolution of your screen. +

+
+
+ +
+

Dark Mode

+
+ +
+
+ +
+

Open links in a new tab

+
+ +
+
+ +
+

Compact mode

+
+ +
+
+ +
+

Favorite search engine

+
+ void -} - -type OptionType = { - value: string - label: string -} - -export const SettingsModal = ({ showSettings, setShowSettings }: SettingsModalProps) => { - const { supportedTags } = useRemoteConfigStore() - - const { - cards, - userSelectedTags, - openLinksNewTab, - listingMode, - theme, - searchEngine, - maxVisibleCards, - setTheme, - setListingMode, - setMaxVisibleCards, - setSearchEngine, - setOpenLinksNewTab, - setCards, - setTags, - userCustomCards, - setUserCustomCards, - } = useUserPreferences() - const [selectedCards, setSelectedCards] = useState(cards) - - const AVAILABLE_CARDS = [...SUPPORTED_CARDS, ...userCustomCards] - - const handleCloseModal = () => { - setShowSettings(false) - } - - const onTagsSelectChange = (tags: MultiValue, metas: ActionMeta) => { - switch (metas.action) { - case 'select-option': - if (metas.option?.label) { - trackLanguageAdd(metas.option.label) - } - break - case 'remove-value': - if (metas.removedValue?.label) { - trackLanguageRemove(metas.removedValue.label) - } - - break - } - setTags(tags as Tag[]) - identifyUserLanguages(tags.map((tag) => tag.value)) - } - - const onlistingModeChange = (e: React.ChangeEvent) => { - const value = e.target.checked ? 'compact' : 'normal' - trackListingModeSelect(value) - identifyUserListingMode(value) - setListingMode(value) - } - - const onCardSelectChange = (cards: MultiValue, metas: ActionMeta) => { - switch (metas.action) { - case 'select-option': - if (metas.option?.label) { - trackSourceAdd(metas.option.label) - } - break - case 'remove-value': - // if removed card is a userCustomCard, remove it - const newUserCustomCards = userCustomCards.filter( - (c) => c.value !== metas.removedValue.value - ) - setUserCustomCards(newUserCustomCards) - if (metas.removedValue?.label) { - trackSourceRemove(metas.removedValue.label) - } - break - } - - let newCards = cards.map((c, index) => { - // Re-Check - let type = AVAILABLE_CARDS.find((ac) => ac.value === c.value)?.type - return { id: index, name: c.value, type } - }) as SelectedCard[] - - identifyUserCards(newCards.map((card) => card.name)) - setSelectedCards(newCards) - setCards(newCards) - } - - const onSearchEngineSelectChange = (value: SingleValue) => { - if (!value) { - return - } - - identifyUserSearchEngine(value.label) - trackSearchEngineSelect(value.label) - setSearchEngine(value.label) - } - - const onOpenLinksNewTabChange = (e: React.ChangeEvent) => { - const checked = e.target.checked - trackTabTarget(checked) - identifyUserLinksInNewTab(checked) - setOpenLinksNewTab(checked) - } - - const onDarkModeChange = () => { - const newTheme = theme === 'dark' ? 'light' : 'dark' - setTheme(newTheme) - trackThemeSelect(newTheme) - identifyUserTheme(newTheme) - } - - const onMaxVisibleCardsChange = (selectedChips: Option[]) => { - if (selectedChips.length) { - const maxVisibleCards = parseInt(selectedChips[0].value) - setMaxVisibleCards(maxVisibleCards) - identifyUserMaxVisibleCards(maxVisibleCards) - trackMaxVisibleCardsChange(maxVisibleCards) - } - } - - return ( - handleCloseModal()} - className="Modal scrollable" - style={{ - overlay: { - zIndex: 3, - }, - }} - overlayClassName="Overlay"> -
-

Settings

- -
- -
-
-

Programming languages you're interested in

-
- ({ - label: AVAILABLE_CARDS.find((c2) => c.name === c2.value)?.label || '', - value: c.name, - }))} - onChange={onCardSelectChange} - isMulti={true} - isClearable={false} - isSearchable={false} - classNamePrefix={'hackertab'} - /> -

- Missing a cool data source? Add it below as an RSS or create an issue{' '} - - here! - -

-
-
- - - -
-

Max number of cards to display

-
- { - onMaxVisibleCardsChange(selectedChips) - }} - /> - -

- To ensure a seamless experience, we may adjust the selected number to align with the - resolution of your screen. -

-
-
- -
-

Dark Mode

-
- -
-
- -
-

Open links in a new tab

-
- -
-
- -
-

Compact mode

-
- -
-
- -
-

Favorite search engine

-
-