Skip to content

Commit 42fbf4c

Browse files
committed
Merge branch 'develop' into fix/ads
2 parents d4e19dd + 7d3248e commit 42fbf4c

19 files changed

Lines changed: 264 additions & 51 deletions

File tree

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,13 @@
1010
"@testing-library/react": "^11.1.0",
1111
"@testing-library/user-event": "^12.1.10",
1212
"@types/dompurify": "^2.3.4",
13+
"@types/jspath": "^0.4.0",
1314
"axios": "^0.21.2",
1415
"axios-cache-adapter": "^2.7.3",
1516
"country-emoji": "^1.5.4",
1617
"dompurify": "^2.2.7",
1718
"eslint-plugin-react": "^7.28.0",
19+
"jspath": "^0.4.0",
1820
"localforage": "^1.9.0",
1921
"normalize.css": "^8.0.1",
2022
"prop-types": "^15.0.0-0",

src/App.js

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,11 @@ import { ScrollCardsNavigator } from './components/Layout'
77
import { AppContentLayout } from './components/Layout'
88
import 'react-contexify/dist/ReactContexify.css'
99
import { setupAnalytics, trackPageView, setupIdentification } from 'src/lib/analytics'
10-
import { useRemoteConfigStore } from 'src/features/remoteConfig'
1110

1211
function App() {
1312
const [showSideBar, setShowSideBar] = useState(false)
1413
const [showSettings, setShowSettings] = useState(false)
1514

16-
const { marketingBannerConfig } = useRemoteConfigStore()
17-
1815
useEffect(() => {
1916
setupAnalytics()
2017
setupIdentification()
@@ -23,7 +20,7 @@ function App() {
2320

2421
return (
2522
<>
26-
<MarketingBanner {...marketingBannerConfig} />
23+
<MarketingBanner />
2724
<div className="App">
2825
<Header
2926
setShowSideBar={setShowSideBar}

src/assets/App.css

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -921,6 +921,7 @@ Producthunt item
921921
animation-duration: 1.5s;
922922
animation-name: cardPlaceholderPulse;
923923
animation-iteration-count: infinite;
924+
scroll-snap-align: start;
924925
}
925926
.mediaCardPlaceholder {
926927
display: flex;
@@ -936,6 +937,7 @@ Producthunt item
936937
}
937938
.cardPlaceholder .cardContent {
938939
display: flex;
940+
flex: auto;
939941
flex-direction: column;
940942
}
941943
.cardPlaceholder .cardUpvote {
@@ -970,6 +972,17 @@ Producthunt item
970972
margin-right: 16px;
971973
border-radius: 4px;
972974
}
975+
.adCardPlaceholder {
976+
max-width: 300px;
977+
column-gap: 16px;
978+
display: flex;
979+
flex-direction: row;
980+
}
981+
.adCardPlaceholder .image {
982+
background: var(--placeholder-background-color);
983+
flex: 0 0 120px;
984+
height: 90px
985+
}
973986

974987
.floatingFilter {
975988
background: rgb(44, 128, 232);
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export const AdPlaceholder = ({ className = '' }: { className?: string }) => {
2+
return (
3+
<div className={'cardPlaceholder adCardPlaceholder'}>
4+
<span className="image" />
5+
<div className="cardContent">
6+
<span className="line" />
7+
<span className="smallLine" />
8+
<span className="smallLine" />
9+
<span className="smallLine" />
10+
</div>
11+
</div>
12+
)
13+
}
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from "./Placeholder"
2-
export * from "./ProductHuntPlaceholder"
2+
export * from "./ProductHuntPlaceholder"
3+
export * from "./AdPlaceholder"
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { useQuery } from '@tanstack/react-query';
2+
import { ExtractFnReturnType, QueryConfig } from 'src/lib/react-query';
3+
import { MarketingConfig } from "../types";
4+
import { axios } from 'src/lib/axios';
5+
6+
const getMarketingConfig = async (): Promise<MarketingConfig> => {
7+
return axios.get('/data/marketingConfig.json');
8+
}
9+
10+
type QueryFnType = typeof getMarketingConfig;
11+
12+
type UseGetMarketingConfigOptions = {
13+
config?: QueryConfig<QueryFnType>;
14+
};
15+
export const useGetMarketingConfig = ({ config }: UseGetMarketingConfigOptions = {}) => {
16+
return useQuery<ExtractFnReturnType<QueryFnType>>({
17+
...config,
18+
queryKey: ['marketing-config'],
19+
queryFn: () => getMarketingConfig(),
20+
});
21+
}
Lines changed: 98 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,115 @@
11
import DOMPurify from 'dompurify'
2-
import { useMarketingBanner } from '../stores/marketingBanner'
2+
import { useMarketingConfigStore } from '../stores/marketingBanner'
3+
import JSPath from 'jspath'
4+
import { useUserPreferences } from 'src/stores/preferences'
5+
import { getAppVersion } from 'src/utils/Os'
6+
import { isWebOrExtensionVersion, isProduction, getBrowserName } from 'src/utils/Environment'
7+
import { useMemo, useState, useEffect } from 'react'
8+
import { Campaign, MarketingConfig } from '../types'
9+
import { useGetMarketingConfig } from '../api/getMarketingConfig'
10+
import {
11+
trackMarketingCampaignClose,
12+
trackMarketingCampaignView,
13+
trackMarketingCampaignOpen,
14+
} from 'src/lib/analytics'
15+
import { DiffBetweenTwoDatesInDays } from 'src/utils/DateUtils'
316

4-
type MarketingBannerProps = {
5-
show: boolean
6-
campaign_name: string
7-
htmlContent: string
8-
}
9-
export const MarketingBanner = ({ campaign_name, show, htmlContent }: MarketingBannerProps) => {
10-
const { setCampaignClosed, closedCampaigns } = useMarketingBanner()
17+
export const MarketingBanner = () => {
18+
const { setCampaignClosed, closedCampaigns } = useMarketingConfigStore()
19+
const { userSelectedTags, cards, firstSeenDate } = useUserPreferences()
20+
const [availableCampaigns, setAvailableCampaigns] = useState<Campaign[]>([])
21+
const { data: marketingConfig } = useGetMarketingConfig({
22+
config: {
23+
staleTime: 60000,
24+
cacheTime: 3600000,
25+
},
26+
})
27+
28+
const userAtttributes = useMemo(() => {
29+
return {
30+
platform: isWebOrExtensionVersion(),
31+
browser: getBrowserName(),
32+
version: getAppVersion() || '0.0.0',
33+
environment: isProduction() ? 'prod' : 'dev',
34+
userTags: userSelectedTags.map((tag) => tag.label),
35+
cards: cards.map((card) => card.name),
36+
firstSeenDate,
37+
usageInDays: DiffBetweenTwoDatesInDays(firstSeenDate, Date.now()),
38+
}
39+
}, [userSelectedTags, firstSeenDate, cards])
40+
41+
useEffect(() => {
42+
if (marketingConfig) {
43+
const availableCampaigns: Campaign[] = getAvailableCampaigns(marketingConfig)
44+
setAvailableCampaigns(availableCampaigns)
45+
}
46+
47+
// eslint-disable-next-line react-hooks/exhaustive-deps
48+
}, [marketingConfig, closedCampaigns, userSelectedTags, cards])
49+
50+
useEffect(() => {
51+
if (availableCampaigns.length) {
52+
trackMarketingCampaignView(availableCampaigns[0].id)
53+
}
54+
}, [availableCampaigns])
55+
56+
if (!marketingConfig) {
57+
return null
58+
}
59+
60+
const getAvailableCampaigns = (config: MarketingConfig) => {
61+
const campaignsWithUserAttr = config.campaigns.map((camp) => {
62+
return { ...camp, userAtttributes: userAtttributes }
63+
})
64+
65+
const lastVisibleAdDate = Math.max(...closedCampaigns.map((camp) => camp.date))
66+
if (lastVisibleAdDate > Date.now() - config.campaigns_interval) {
67+
return []
68+
}
69+
70+
const closedCampaignsSet = new Set(closedCampaigns.map((closedCamp) => closedCamp.id))
71+
const availableCampaigns = campaignsWithUserAttr
72+
.filter((camp) => camp.enabled && !closedCampaignsSet.has(camp.id))
73+
.flatMap((camp) => JSPath.apply(camp.condition, camp))
74+
.sort((a, b) => (a.priority || 0) - (b.priority || 0))
75+
.reverse()
1176

12-
if (!show || closedCampaigns.includes(campaign_name)) {
77+
return availableCampaigns
78+
}
79+
80+
if (!marketingConfig.enabled) {
1381
return null
1482
}
1583

16-
let cleanHtmlContent = DOMPurify.sanitize(htmlContent)
84+
if (!availableCampaigns.length) {
85+
return null
86+
}
1787

18-
const onBannerClick = (e: React.MouseEvent<HTMLElement>) => {
88+
const onBannerClick = (e: React.MouseEvent<HTMLElement>, campaign: Campaign) => {
1989
if (e.target instanceof Element) {
2090
const closeButton = e.target.closest('.close')
91+
const ctaButton = e.target.closest('.cta')
2192
if (closeButton && e.currentTarget.contains(closeButton)) {
22-
setCampaignClosed(campaign_name)
93+
setCampaignClosed(campaign.id)
94+
trackMarketingCampaignClose(campaign.id)
95+
} else if (ctaButton && e.currentTarget.contains(ctaButton)) {
96+
trackMarketingCampaignOpen(campaign.id)
2397
}
2498
}
2599
}
26100

101+
const currentCampaign = availableCampaigns[0]
102+
27103
return (
28-
<div onClick={(e) => onBannerClick(e)} dangerouslySetInnerHTML={{ __html: cleanHtmlContent }} />
104+
<div
105+
id={currentCampaign.id}
106+
onClick={(e) => onBannerClick(e, currentCampaign)}
107+
dangerouslySetInnerHTML={{
108+
__html: DOMPurify.sanitize(currentCampaign.htmlContent, {
109+
ADD_ATTR: ['target'],
110+
USE_PROFILES: { html: true },
111+
}),
112+
}}
113+
/>
29114
)
30115
}
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
export * from "./components"
1+
export * from "./components"
2+
export * from "./types"

src/features/MarketingBanner/stores/marketingBanner.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,24 @@
11
import create from 'zustand';
22
import { persist } from 'zustand/middleware'
33

4+
type ClosedCampaign = {
5+
id: string;
6+
date: number;
7+
}
8+
49
type MarketingBannerStore = {
5-
closedCampaigns: string[];
6-
setCampaignClosed: (compaignName: string) => void;
10+
closedCampaigns: ClosedCampaign[];
11+
setCampaignClosed: (compaignId: string) => void;
712
};
813

9-
export const useMarketingBanner = create(persist<MarketingBannerStore>((set) => ({
14+
export const useMarketingConfigStore = create(persist<MarketingBannerStore>((set) => ({
1015
closedCampaigns: [],
11-
setCampaignClosed: (compaignName: string) =>
16+
setCampaignClosed: (compaignId: string) =>
1217
set((state) => ({
13-
closedCampaigns: [...state.closedCampaigns, compaignName]
18+
closedCampaigns: [...state.closedCampaigns, {
19+
id: compaignId,
20+
date: Date.now()
21+
}]
1422
})),
1523
}), {
1624
name: 'ht_marketing_storage',
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
export type MarketingConfig = {
2+
enabled: boolean;
3+
campaigns_interval: number;
4+
campaigns: Campaign[]
5+
}
6+
7+
export type Campaign = {
8+
id: string;
9+
name: string;
10+
htmlContent: string;
11+
condition: string;
12+
enabled: boolean;
13+
userAtttributes?: UserAttribute;
14+
priority?: number
15+
}
16+
17+
type UserAttribute = {
18+
[key: string]: string | string[];
19+
}

0 commit comments

Comments
 (0)