From e9ba2cc7ab232902cc335b23c9ca6a2542b13d5f Mon Sep 17 00:00:00 2001 From: John Doe Date: Thu, 12 Mar 2026 22:41:52 +0000 Subject: [PATCH 1/5] feat: remove ADV-related analytics and user preferences for cleaner code --- src/App.tsx | 15 +-------------- src/lib/analytics.ts | 4 ---- src/stores/preferences.ts | 4 ---- 3 files changed, 1 insertion(+), 22 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 698a7cd2..7ccb7274 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,15 +1,9 @@ import clsx from 'clsx' import { useEffect, useLayoutEffect } from 'react' import { DNDLayout } from 'src/components/Layout' -import { - identifyAdvBlocked, - setupAnalytics, - setupIdentification, - trackPageView, -} from 'src/lib/analytics' +import { setupAnalytics, setupIdentification, trackPageView } from 'src/lib/analytics' import { useUserPreferences } from 'src/stores/preferences' import { AppContentLayout } from './components/Layout' -import { verifyAdvStatus } from './features/adv/utils/status' import { lazyImport } from './utils/lazyImport' const { OnboardingModal } = lazyImport(() => import('src/features/onboarding'), 'OnboardingModal') @@ -27,7 +21,6 @@ export const App = () => { const { maxVisibleCards, onboardingCompleted, - setAdvStatus, isDNDModeActive, layout, DNDDuration, @@ -42,12 +35,6 @@ export const App = () => { document.body.classList.remove('preload') setupAnalytics() setupIdentification() - const adVerifier = async () => { - const status = await verifyAdvStatus() - setAdvStatus(status) - identifyAdvBlocked(status) - } - adVerifier() }, []) useEffect(() => { diff --git a/src/lib/analytics.ts b/src/lib/analytics.ts index a0ba3ec8..735d2168 100644 --- a/src/lib/analytics.ts +++ b/src/lib/analytics.ts @@ -76,7 +76,6 @@ export enum Attributes { MAX_VISIBLE_CARDS = 'Max Visible Cards', DURATION = 'Duration', PROVIDER = 'Provider', - ADV = 'ADV', STREAK = 'Streak', DISPLAY_LAYOUT = 'Display Layout', } @@ -467,9 +466,6 @@ export const identifyUserOccupation = (occupation: string) => { export const identifyUserMaxVisibleCards = (maxVisibleCards: number) => { identifyUserProperty(Attributes.MAX_VISIBLE_CARDS, maxVisibleCards) } -export const identifyAdvBlocked = (blocked: boolean) => { - identifyUserProperty(Attributes.ADV, blocked) -} export const identifyUserStreak = (value: number) => { identifyUserProperty(Attributes.STREAK, value) } diff --git a/src/stores/preferences.ts b/src/stores/preferences.ts index 11e7d8b0..54a3fa7d 100644 --- a/src/stores/preferences.ts +++ b/src/stores/preferences.ts @@ -34,7 +34,6 @@ export type UserPreferencesState = { cardsSettings: Record firstSeenDate: number userCustomCards: SupportedCardType[] - advStatus: boolean DNDDuration: DNDDuration showReadPosts: boolean } @@ -61,7 +60,6 @@ type UserPreferencesStoreActions = { isDNDModeActive: () => boolean addSearchEngine: (searchEngine: SearchEngineType) => void removeSearchEngine: (searchEngineUrl: string) => void - setAdvStatus: (status: boolean) => void setShowReadPosts: (value: boolean) => void } @@ -93,7 +91,6 @@ export const useUserPreferences = create( ], userCustomCards: [], DNDDuration: 'never', - advStatus: false, showReadPosts: true, setLayout: (layout) => set({ layout }), setPromptEngine: (promptEngine: string) => set({ promptEngine }), @@ -168,7 +165,6 @@ export const useUserPreferences = create( promptEngines: state.promptEngines.filter((se) => se.url !== engine), } }), - setAdvStatus: (status) => set({ advStatus: status }), setShowReadPosts: (value) => set({ showReadPosts: value }), removeCard: (cardName: string) => set((state) => { From ec17dd9dc1d8ac99f7a0cc7a4a499f14dde381b8 Mon Sep 17 00:00:00 2001 From: John Doe Date: Thu, 12 Mar 2026 23:20:46 +0000 Subject: [PATCH 2/5] feat: remove unused dataSources and report links for cleaner configuration --- src/config/index.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/config/index.tsx b/src/config/index.tsx index 1136a1cd..1670f190 100644 --- a/src/config/index.tsx +++ b/src/config/index.tsx @@ -17,10 +17,8 @@ export const maxCardsPerRow = 4 export const supportLink = 'https://github.com/medyo/hackertab.dev/issues' export const privacyPolicyLink = 'https://www.hackertab.dev/privacy-policy' export const termsAndConditionsLink = 'https://www.hackertab.dev/terms-and-conditions' -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' export const LS_PREFERENCES_KEY = 'hackerTabPrefs' export const MAX_ITEMS_PER_CARD = 50 From 4761815e790d16905e57e41d11262315270d90fd Mon Sep 17 00:00:00 2001 From: John Doe Date: Wed, 18 Mar 2026 21:08:25 +0000 Subject: [PATCH 3/5] feat: implement paywall feature with donation modal and related components --- src/assets/App.css | 9 ++ src/components/Layout/Header.tsx | 18 +++ .../components/MarketingBanner.tsx | 8 +- src/features/adv/api/getAd.ts | 4 +- src/features/adv/components/AdvBanner.css | 18 +++ src/features/adv/components/AdvBanner.tsx | 102 +++------------- src/features/adv/types/index.ts | 14 ++- .../donate/components/DonateModal.tsx | 26 ++++ src/features/donate/components/DonateView.tsx | 68 +++++++++++ src/features/donate/components/donate.css | 112 ++++++++++++++++++ src/features/donate/index.ts | 1 + src/features/feed/components/feed.css | 12 ++ .../feed/components/feedItems/AdvFeedItem.tsx | 84 ++++++++----- .../remoteConfig/api/getRemoteConfig.ts | 2 +- .../remoteConfig/stores/remoteConfig.ts | 27 ++++- src/features/remoteConfig/types/index.ts | 11 ++ src/lib/analytics.ts | 14 ++- 17 files changed, 399 insertions(+), 131 deletions(-) create mode 100644 src/features/donate/components/DonateModal.tsx create mode 100644 src/features/donate/components/DonateView.tsx create mode 100644 src/features/donate/components/donate.css create mode 100644 src/features/donate/index.ts diff --git a/src/assets/App.css b/src/assets/App.css index d5251e0f..19eaf063 100644 --- a/src/assets/App.css +++ b/src/assets/App.css @@ -68,6 +68,15 @@ a { z-index: 1; } +.AppHeader .upgradeButton { + font-weight: bold; + background-color: var(--tab-positive-button-background) !important; + color: white !important; + &:hover { + opacity: 0.8; + } +} + .AppFooter { display: flex; flex-direction: row; diff --git a/src/components/Layout/Header.tsx b/src/components/Layout/Header.tsx index 707527eb..8e798cb4 100644 --- a/src/components/Layout/Header.tsx +++ b/src/components/Layout/Header.tsx @@ -2,6 +2,7 @@ import { clsx } from 'clsx' import { useCallback, useEffect, useState } from 'react' import { BsFillBookmarksFill, BsFillGearFill, BsMoonFill } from 'react-icons/bs' import { CgTab } from 'react-icons/cg' +import { FaCrown } from 'react-icons/fa' import { IoMdSunny } from 'react-icons/io' import { MdDoDisturbOff } from 'react-icons/md' import { RiDashboardHorizontalFill } from 'react-icons/ri' @@ -13,6 +14,7 @@ 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 { useRemoteConfigStore } from 'src/features/remoteConfig' import { identifyUserTheme, trackDNDDisable, @@ -20,12 +22,17 @@ import { trackThemeSelect, } from 'src/lib/analytics' import { useUserPreferences } from 'src/stores/preferences' +import { lazyImport } from 'src/utils/lazyImport' import { Button, CircleButton } from '../Elements' import { SearchEngineBar } from '../Elements/SearchBar/SearchEngineBar' +const { DonateModal } = lazyImport(() => import('src/features/donate'), 'DonateModal') + export const Header = () => { const { openAuthModal, user, isConnected, isConnecting } = useAuth() const [themeIcon, setThemeIcon] = useState() + const [openDonateModal, setOpenDonateModal] = useState(false) + const { paywall } = useRemoteConfigStore() const { theme, setTheme, setDNDDuration, isDNDModeActive, layout, setLayout } = useUserPreferences() const navigate = useNavigate() @@ -68,6 +75,10 @@ export const Header = () => { setDNDDuration('never') }, [setDNDDuration]) + const onUpgradeClicked = useCallback(() => { + setOpenDonateModal(true) + }, []) + return ( <>
@@ -129,7 +140,14 @@ export const Header = () => { )} + {paywall?.enabled && ( + + )} + {openDonateModal && } {location.pathname === '/' && }
diff --git a/src/features/MarketingBanner/components/MarketingBanner.tsx b/src/features/MarketingBanner/components/MarketingBanner.tsx index de7be099..548a3fec 100644 --- a/src/features/MarketingBanner/components/MarketingBanner.tsx +++ b/src/features/MarketingBanner/components/MarketingBanner.tsx @@ -18,8 +18,8 @@ import { Campaign, MarketingConfig } from '../types' export const MarketingBanner = () => { const { setCampaignClosed, closedCampaigns } = useMarketingConfigStore() - const { isConnected } = useAuth() - const { userSelectedTags, cards, firstSeenDate, advStatus } = useUserPreferences() + const { isConnected, user } = useAuth() + const { userSelectedTags, cards, firstSeenDate } = useUserPreferences() const [availableCampaigns, setAvailableCampaigns] = useState([]) const { data: marketingConfig } = useGetMarketingConfig({ config: { @@ -39,11 +39,11 @@ export const MarketingBanner = () => { userTags: userSelectedTags.map((tag) => tag.label), cards: cards.map((card) => card.name), firstSeenDate, - adv: advStatus, isConnected, + isSupported: user?.isSupporter || false, usageInDays: diffBetweenTwoDatesInDays(firstSeenDate, Date.now()), } - }, [userSelectedTags, firstSeenDate, cards, advStatus]) + }, [userSelectedTags, firstSeenDate, cards, user]) useEffect(() => { if (marketingConfig && marketingConfig.version === 1) { diff --git a/src/features/adv/api/getAd.ts b/src/features/adv/api/getAd.ts index f55bbf71..c32dc77b 100644 --- a/src/features/adv/api/getAd.ts +++ b/src/features/adv/api/getAd.ts @@ -5,7 +5,7 @@ import { Ad } from '../types' const getAd = async (keywords: string[], feed: boolean = false): Promise => { let params = { keywords: keywords.join(','), feed: feed ? 'true' : 'false' } - return axios.get('/engine/ads/', { params }) + return axios.get('/engine/ads/adaptive', { params }) } type QueryFnType = typeof getAd @@ -18,7 +18,7 @@ type UseGetAdOptions = { export const useGetAd = ({ keywords, feed, config }: UseGetAdOptions) => { return useQuery>({ ...config, - queryKey: ['ad', keywords.join(',')], + queryKey: ['ad', 'v2', keywords.join(',')], queryFn: () => getAd(keywords, feed), }) } diff --git a/src/features/adv/components/AdvBanner.css b/src/features/adv/components/AdvBanner.css index eae5ae9d..96c0097f 100644 --- a/src/features/adv/components/AdvBanner.css +++ b/src/features/adv/components/AdvBanner.css @@ -285,3 +285,21 @@ .advFeed:has(.banneradv) .rowDetails { margin-left: auto; } + +.houseBanner { + max-width: none; + width: 100%; + img { + border-radius: 8px; + object-fit: contain; + display: block; + margin: 0 auto; + max-width: 100%; + } + a { + width: 100%; + &:hover { + opacity: 0.8; + } + } +} diff --git a/src/features/adv/components/AdvBanner.tsx b/src/features/adv/components/AdvBanner.tsx index 13915e5c..96914359 100644 --- a/src/features/adv/components/AdvBanner.tsx +++ b/src/features/adv/components/AdvBanner.tsx @@ -1,10 +1,8 @@ import { useEffect } from 'react' import { AdPlaceholder } from 'src/components/placeholders' -import { useRemoteConfigStore } from 'src/features/remoteConfig' +import { trackMarketingCampaignOpen } from 'src/lib/analytics' import { useUserPreferences } from 'src/stores/preferences' -import { isWebOrExtensionVersion } from 'src/utils/Environment' import { useGetAd } from '../api/getAd' -import { useDelayedFlag } from '../hooks/useDelayedFlag' import { Ad } from '../types' import './AdvBanner.css' @@ -14,12 +12,8 @@ type AdvBannerProps = { loadingState?: React.ReactNode } -export const AdvBanner = ({ feedDisplay = false, loadingState, onAdLoaded }: AdvBannerProps) => { +export const AdvBanner = ({ loadingState, onAdLoaded }: AdvBannerProps) => { const { userSelectedTags } = useUserPreferences() - const adsFetchDelayMs = useRemoteConfigStore((s) => s.adsFetchDelayMs) - const delay = isWebOrExtensionVersion() === 'extension' ? adsFetchDelayMs : undefined - const isReady = useDelayedFlag(delay) - const { isSuccess, data: ad, @@ -31,7 +25,6 @@ export const AdvBanner = ({ feedDisplay = false, loadingState, onAdLoaded }: Adv config: { cacheTime: 0, staleTime: 0, - enabled: isReady, useErrorBoundary: false, }, }) @@ -50,85 +43,22 @@ export const AdvBanner = ({ feedDisplay = false, loadingState, onAdLoaded }: Adv return null } - if (ad.largeImage) { + const onAdClick = () => { + if (ad?.id) { + trackMarketingCampaignOpen(ad.id, { + source: 'card', + }) + } + } + if (ad.type === 'house-ad-banner') { return ( - <> - - {ad.viewUrl && - ad.viewUrl - .split('||') - .map((viewUrl, i) => ( - - ))} - +
+ + {ad.title} + +
) } - return ( - <> -
- - {ad.title} - - - - {ad.description} - - - {!feedDisplay && ( - - {ad.provider.title} - - )} -
- {ad.viewUrl && - ad.viewUrl - .split('||') - .map((viewUrl, i) => ( - - ))} - - ) + return null } diff --git a/src/features/adv/types/index.ts b/src/features/adv/types/index.ts index f954adbc..7196875b 100644 --- a/src/features/adv/types/index.ts +++ b/src/features/adv/types/index.ts @@ -9,7 +9,11 @@ type NextAdType = { interval: number } -export type Ad = { +export type Ad = DefaultAd | HouseAdBanner + +export type DefaultAd = { + id?: string + type?: string title?: string description: string imageUrl: string @@ -25,3 +29,11 @@ export type Ad = { callToAction?: string company?: string } +export type HouseAdBanner = { + type: 'house-ad-banner' + id: string + title: string + description: string + link: string + imageUrl: string +} diff --git a/src/features/donate/components/DonateModal.tsx b/src/features/donate/components/DonateModal.tsx new file mode 100644 index 00000000..c32cef4c --- /dev/null +++ b/src/features/donate/components/DonateModal.tsx @@ -0,0 +1,26 @@ +import ReactModal from 'react-modal' +import { DonateView } from './DonateView' +import './donate.css' + +type DonateModalProps = { + setModalOpen: (open: boolean) => void +} +export const DonateModal = ({ setModalOpen }: DonateModalProps) => { + return ( + + + + ) +} diff --git a/src/features/donate/components/DonateView.tsx b/src/features/donate/components/DonateView.tsx new file mode 100644 index 00000000..92a6dda7 --- /dev/null +++ b/src/features/donate/components/DonateView.tsx @@ -0,0 +1,68 @@ +import { useEffect } from 'react' +import { FaCrown } from 'react-icons/fa' +import { TbCheck } from 'react-icons/tb' +import { Button } from 'src/components/Elements' +import { useRemoteConfigStore } from 'src/features/remoteConfig' +import { trackMarketingCampaignOpen, trackMarketingCampaignView } from 'src/lib/analytics' + +type DonateViewProps = { + setModalOpen: (open: boolean) => void +} +export const DonateView = ({ setModalOpen }: DonateViewProps) => { + const { paywall } = useRemoteConfigStore() + + useEffect(() => { + if (paywall?.id) { + trackMarketingCampaignView(paywall.id, { + source: 'modal', + }) + } + }, [paywall?.id]) + + if (!paywall) { + return null + } + const { headerImage, ctaUrl, cta, leadDescription, caption, features } = paywall + return ( +
+
+ Header img +
+
+

{leadDescription}

+

+ What you get +

    + {features.map((feature, index) => { + return ( +
  • + {feature} +
  • + ) + }) || []} +
+

+
+ + + +
+

{caption}

+
+
+ ) +} diff --git a/src/features/donate/components/donate.css b/src/features/donate/components/donate.css new file mode 100644 index 00000000..eb86c43e --- /dev/null +++ b/src/features/donate/components/donate.css @@ -0,0 +1,112 @@ +.donateModal { + width: auto; +} +.donateView { + display: flex; + flex-direction: column; + max-width: 400px; + width: auto; + border-radius: 8px; + border: 1px solid var(--button-background-color); + header { + display: flex; + flex-direction: column; + + img { + width: 100%; + border-top-left-radius: 8px; + border-top-right-radius: 8px; + } + + h2 { + margin-bottom: 0; + } + } + .body { + padding: 0 16px 16px 16px; + } + .leadDescription { + font-size: 16px; + color: var(--primary-text-color); + font-weight: bold; + line-height: 24px; + text-align: center; + } + .description { + font-size: 15px; + color: var(--secondary-text-color); + margin-top: 8px; + } + .features { + margin-top: 16px; + display: flex; + flex-direction: column; + gap: 12px; + list-style: none; + padding-left: 0; + + .feature, + li { + display: flex; + align-items: center; + gap: 8px; + color: var(--primary-color); + + * { + margin: 0; + padding: 0; + } + .checkIcon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 18px; + height: 18px; + min-width: 18px; + border-radius: 50%; + background-color: var(--tab-positive-button-background); + color: #e1f5e8; + padding: 6px; + } + + .icon { + font-size: 16px; + color: var(--primary-color); + } + } + } + .buttonsWrapper { + display: flex; + justify-content: center; + margin-top: 32px; + gap: 16px; + + .upgradeButton { + width: auto; + font-size: 16px; + height: 40px; + border: none; + background-color: var(--tab-positive-button-background); + color: white; + font-weight: bold; + text-decoration: none; + border-radius: 20px; + justify-content: center; + align-items: center; + text-align: center; + display: inline-flex; + padding: 0 16px; + cursor: pointer; + &:hover { + opacity: 0.8; + } + } + } + .caption { + font-size: 12px; + color: var(--secondary-text-color); + margin-top: 8px; + text-align: center; + margin-top: 16px; + } +} diff --git a/src/features/donate/index.ts b/src/features/donate/index.ts new file mode 100644 index 00000000..70dffcb6 --- /dev/null +++ b/src/features/donate/index.ts @@ -0,0 +1 @@ +export * from './components/DonateModal' diff --git a/src/features/feed/components/feed.css b/src/features/feed/components/feed.css index a267fab2..351f454e 100644 --- a/src/features/feed/components/feed.css +++ b/src/features/feed/components/feed.css @@ -27,6 +27,18 @@ opacity: 0.9; transform: scale(1.01); } + &.adv { + background-color: rgb(34, 37, 59); + .rowCover { + object-fit: inherit; + border-radius: 20px; + } + .rowLink, + .rowItem { + font-weight: bold; + color: white; + } + } } .blockRow:not(.advFeed):hover { diff --git a/src/features/feed/components/feedItems/AdvFeedItem.tsx b/src/features/feed/components/feedItems/AdvFeedItem.tsx index 96bd7dec..5664fb54 100644 --- a/src/features/feed/components/feedItems/AdvFeedItem.tsx +++ b/src/features/feed/components/feedItems/AdvFeedItem.tsx @@ -1,39 +1,63 @@ +// Removed CardItemWithActions for ad block import clsx from 'clsx' -import { useState } from 'react' -import { RiAdvertisementFill } from 'react-icons/ri' -import { Ad, AdvBanner } from 'src/features/adv' -import { AdFeedItemData, BaseItemPropsType } from 'src/types' +import { useGetAd } from 'src/features/adv' +import { trackMarketingCampaignOpen } from 'src/lib/analytics' +import { useUserPreferences } from 'src/stores/preferences' +import { BaseItemPropsType } from 'src/types' +import { FeedItemHeader } from '../FeedItemHeader' -export const AdvFeedItem = ({ className }: BaseItemPropsType) => { - const [adMeta, setAdMeta] = useState() +export const AdvFeedItem = ({ className }: BaseItemPropsType) => { + const { userSelectedTags } = useUserPreferences() + const { + data: ad, + isLoading, + isError, + } = useGetAd({ + keywords: userSelectedTags.map((tag) => tag.label.toLocaleLowerCase()), + feed: true, + config: { + cacheTime: 0, + staleTime: 0, + useErrorBoundary: false, + }, + }) + + if (isLoading) { + return ( +
+
+
+
+
+ ) + } + + if (isError || !ad) { + return null + } + + const onAdClick = () => { + if (ad?.id) { + trackMarketingCampaignOpen(ad.id, { + source: 'feed', + }) + } + } + + if (ad.type !== 'house-ad-banner') { + return null + } return ( -
- -
-
-
-
- } - /> - {adMeta && ( - <> - {adMeta.company && adMeta.companyTagline && ( - - {[adMeta.company, adMeta.companyTagline].filter(Boolean).join(' - ')} - - )} +
+
+
+
- - {adMeta.provider.title} - + {ad.description}
- - )} +
+
) } diff --git a/src/features/remoteConfig/api/getRemoteConfig.ts b/src/features/remoteConfig/api/getRemoteConfig.ts index 896ea652..2578f0ba 100644 --- a/src/features/remoteConfig/api/getRemoteConfig.ts +++ b/src/features/remoteConfig/api/getRemoteConfig.ts @@ -15,7 +15,7 @@ type UseGetRemoteConfigOptions = { export const useGetRemoteConfig = ({ config }: UseGetRemoteConfigOptions = {}) => { return useQuery>({ ...config, - queryKey: ['remote-config', 'v3'], + queryKey: ['remote-config', 'v4'], queryFn: () => getRemoteConfig(), }) } diff --git a/src/features/remoteConfig/stores/remoteConfig.ts b/src/features/remoteConfig/stores/remoteConfig.ts index b691507d..75286e63 100644 --- a/src/features/remoteConfig/stores/remoteConfig.ts +++ b/src/features/remoteConfig/stores/remoteConfig.ts @@ -7,6 +7,17 @@ const DEFAULT_ADS_FETCH_DELAY_MS = 1750 type RemoteConfigStore = { tags: Tag[] adsFetchDelayMs: number + paywall?: { + id: string + enabled: boolean + headerCta: string + ctaUrl: string + cta: string + leadDescription: string + caption: string + headerImage: string + features: string[] + } setRemoteConfig: (remoteConfig: RemoteConfig) => void } @@ -20,17 +31,27 @@ export const useRemoteConfigStore = create( }, ], adsFetchDelayMs: DEFAULT_ADS_FETCH_DELAY_MS, + paywall: undefined, setRemoteConfig: (remoteConfig: RemoteConfig) => set({ tags: remoteConfig.tags, adsFetchDelayMs: remoteConfig.ads_fetch_delay_ms || DEFAULT_ADS_FETCH_DELAY_MS, + paywall: remoteConfig.paywall + ? { + ...remoteConfig.paywall, + headerCta: remoteConfig.paywall.header_cta, + ctaUrl: remoteConfig.paywall.cta_url, + leadDescription: remoteConfig.paywall.lead_description, + headerImage: remoteConfig.paywall.header_image, + } + : undefined, }), }), { name: 'remote_config_storage', - version: 2, - migrate: (state) => { - return state as RemoteConfigStore + version: 3, + migrate: (state: unknown) => { + return { ...(state as RemoteConfigStore), paywall: undefined } as RemoteConfigStore }, } ) diff --git a/src/features/remoteConfig/types/index.ts b/src/features/remoteConfig/types/index.ts index ab6a7363..4efbd6b1 100644 --- a/src/features/remoteConfig/types/index.ts +++ b/src/features/remoteConfig/types/index.ts @@ -7,4 +7,15 @@ export type Tag = { export type RemoteConfig = { tags: Tag[] ads_fetch_delay_ms?: number + paywall?: { + id: string + enabled: boolean + header_cta: string + cta_url: string + cta: string + lead_description: string + caption: string + header_image: string + features: string[] + } } diff --git a/src/lib/analytics.ts b/src/lib/analytics.ts index 735d2168..2ab7daa5 100644 --- a/src/lib/analytics.ts +++ b/src/lib/analytics.ts @@ -273,11 +273,14 @@ export const trackChangeLogOpen = () => { }) } -export const trackMarketingCampaignOpen = (campaignId: string) => { +export const trackMarketingCampaignOpen = ( + campaignId: string, + additionalParams?: { [key: string]: string } +) => { trackEvent({ object: Objects.MARKETING_CAMPAIGN, verb: Verbs.OPEN, - attributes: { [Attributes.CAMPAIGN_ID]: campaignId }, + attributes: { [Attributes.CAMPAIGN_ID]: campaignId, ...additionalParams }, }) } @@ -289,11 +292,14 @@ export const trackMarketingCampaignClose = (campaignId: string) => { }) } -export const trackMarketingCampaignView = (campaignId: string) => { +export const trackMarketingCampaignView = ( + campaignId: string, + additionalParams?: { [key: string]: string } +) => { trackEvent({ object: Objects.MARKETING_CAMPAIGN, verb: Verbs.VIEW, - attributes: { [Attributes.CAMPAIGN_ID]: campaignId }, + attributes: { [Attributes.CAMPAIGN_ID]: campaignId, ...additionalParams }, }) } From 10ee92410a158ff38b74bc4d17147bbc7b94a112 Mon Sep 17 00:00:00 2001 From: John Doe Date: Wed, 18 Mar 2026 21:54:57 +0000 Subject: [PATCH 4/5] feat: enhance responsive design for settings content layout --- .../Layout/SettingsContentLayout/settingsContentLayout.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/Layout/SettingsContentLayout/settingsContentLayout.css b/src/components/Layout/SettingsContentLayout/settingsContentLayout.css index 22ce4d59..d6030f69 100644 --- a/src/components/Layout/SettingsContentLayout/settingsContentLayout.css +++ b/src/components/Layout/SettingsContentLayout/settingsContentLayout.css @@ -99,9 +99,13 @@ @media only screen and (max-width: 768px) { .settingsContent { padding: 16px; + min-height: 100vh; + height: 100vh; } .settingsBody { max-width: 100%; padding: 0; + flex: 1 1 auto; + min-height: 0; } } From d0e530389f7741ca96baae7c2f2e1d511203eaac Mon Sep 17 00:00:00 2001 From: John Doe Date: Thu, 19 Mar 2026 20:55:38 +0000 Subject: [PATCH 5/5] feat: update web manifest name and description for clarity --- public/web_manifest.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/web_manifest.json b/public/web_manifest.json index d59d6f92..775039cb 100644 --- a/public/web_manifest.json +++ b/public/web_manifest.json @@ -1,7 +1,7 @@ { "short_name": "Hackertab.dev", - "name": "All developer news in one tab", - "description": "Hackertab helps developers stay up-to-date with the latest dev trends and tools", + "name": "Hackertab – Dev News & GitHub in New Tab", + "description": "Developer news from GitHub Trending, Hacker News, and more, all in your new tab.", "icons": [ { "src": "/logos/logoVector.svg",