-
-
Notifications
You must be signed in to change notification settings - Fork 40.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add support for sticky sponsor banner (#7602)
* Add sponsors functionality * Fix overlapping issue * Add sticky top sponsor
- Loading branch information
1 parent
7f399f5
commit 65f1c9c
Showing
9 changed files
with
404 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
import { useEffect, useState } from 'react'; | ||
import { httpGet, httpPatch, httpPost } from '../../lib/http'; | ||
import { sponsorHidden } from '../../stores/page'; | ||
import { useStore } from '@nanostores/react'; | ||
import { X } from 'lucide-react'; | ||
import { setViewSponsorCookie } from '../../lib/jwt'; | ||
import { isMobile } from '../../lib/is-mobile'; | ||
import Cookies from 'js-cookie'; | ||
import { getUrlUtmParams } from '../../lib/browser.ts'; | ||
|
||
export type BottomRightSponsorType = { | ||
id: string; | ||
company: string; | ||
description: string; | ||
gaLabel: string; | ||
imageUrl: string; | ||
pageUrl: string; | ||
title: string; | ||
url: string; | ||
}; | ||
|
||
type V1GetSponsorResponse = { | ||
id?: string; | ||
href?: string; | ||
sponsor?: BottomRightSponsorType; | ||
}; | ||
|
||
type BottomRightSponsorProps = { | ||
sponsor: BottomRightSponsorType; | ||
|
||
onSponsorClick: () => void; | ||
onSponsorImpression: () => void; | ||
onSponsorHidden: () => void; | ||
}; | ||
|
||
export function BottomRightSponsor(props: BottomRightSponsorProps) { | ||
const { sponsor, onSponsorImpression, onSponsorClick, onSponsorHidden } = | ||
props; | ||
|
||
const [isHidden, setIsHidden] = useState(false); | ||
|
||
useEffect(() => { | ||
if (!sponsor) { | ||
return; | ||
} | ||
|
||
onSponsorImpression(); | ||
}, []); | ||
|
||
const { url, title, imageUrl, description, company, gaLabel } = sponsor; | ||
|
||
const isRoadmapAd = title.toLowerCase() === 'advertise with us!'; | ||
|
||
if (isHidden) { | ||
return null; | ||
} | ||
|
||
return ( | ||
<a | ||
href={url} | ||
target="_blank" | ||
rel="noopener sponsored nofollow" | ||
className="fixed bottom-0 left-0 right-0 z-50 flex bg-white shadow-lg outline-0 outline-transparent sm:bottom-[15px] sm:left-auto sm:right-[15px] sm:max-w-[350px]" | ||
onClick={onSponsorClick} | ||
> | ||
<span | ||
className="absolute right-1 top-1 text-gray-400 hover:text-gray-800 sm:right-1.5 sm:top-1.5 sm:text-gray-300" | ||
aria-label="Close" | ||
onClick={(e) => { | ||
e.preventDefault(); | ||
e.stopPropagation(); | ||
|
||
setIsHidden(true); | ||
onSponsorHidden(); | ||
}} | ||
> | ||
<X className="h-5 w-5 sm:h-4 sm:w-4" /> | ||
</span> | ||
<span> | ||
<img | ||
src={imageUrl} | ||
className="block h-[106px] object-cover sm:h-[153px] sm:w-[118.18px]" | ||
alt="Sponsor Banner" | ||
/> | ||
</span> | ||
<span className="flex flex-1 flex-col justify-between text-xs sm:text-sm"> | ||
<span className="p-[10px]"> | ||
<span className="mb-0.5 block font-semibold">{title}</span> | ||
<span className="block text-gray-500">{description}</span> | ||
</span> | ||
{!isRoadmapAd && ( | ||
<> | ||
<span className="sponsor-footer hidden sm:block"> | ||
Partner Content | ||
</span> | ||
<span className="block pb-1 text-center text-[10px] uppercase text-gray-400 sm:hidden"> | ||
Partner Content | ||
</span> | ||
</> | ||
)} | ||
</span> | ||
</a> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,195 @@ | ||
import { useEffect, useState } from 'react'; | ||
import { httpGet, httpPatch } from '../../lib/http'; | ||
import { sponsorHidden } from '../../stores/page'; | ||
import { useStore } from '@nanostores/react'; | ||
import { setViewSponsorCookie } from '../../lib/jwt'; | ||
import { isMobile } from '../../lib/is-mobile'; | ||
import Cookies from 'js-cookie'; | ||
import { getUrlUtmParams } from '../../lib/browser.ts'; | ||
import { StickyTopSponsor } from './StickyTopSponsor.tsx'; | ||
import { BottomRightSponsor } from './BottomRightSponsor.tsx'; | ||
|
||
type PageSponsorType = { | ||
company: string; | ||
description: string; | ||
gaLabel: string; | ||
imageUrl: string; | ||
pageUrl: string; | ||
title: string; | ||
url: string; | ||
id: string; | ||
}; | ||
|
||
export type StickyTopSponsorType = PageSponsorType & { | ||
buttonText: string; | ||
style?: { | ||
fromColor?: string; | ||
toColor?: string; | ||
textColor?: string; | ||
buttonBackgroundColor?: string; | ||
buttonTextColor?: string; | ||
}; | ||
}; | ||
export type BottomRightSponsorType = PageSponsorType; | ||
|
||
type V1GetSponsorResponse = { | ||
bottomRightAd?: BottomRightSponsorType; | ||
stickyTopAd?: StickyTopSponsorType; | ||
}; | ||
|
||
type PageSponsorsProps = { | ||
gaPageIdentifier?: string; | ||
}; | ||
|
||
const CLOSE_SPONSOR_KEY = 'sponsorClosed'; | ||
|
||
function markSponsorHidden(sponsorId: string) { | ||
Cookies.set(`${CLOSE_SPONSOR_KEY}-${sponsorId}`, '1', { | ||
path: '/', | ||
expires: 1, | ||
sameSite: 'lax', | ||
secure: true, | ||
domain: import.meta.env.DEV ? 'localhost' : '.roadmap.sh', | ||
}); | ||
} | ||
|
||
function isSponsorMarkedHidden(sponsorId: string) { | ||
return Cookies.get(`${CLOSE_SPONSOR_KEY}-${sponsorId}`) === '1'; | ||
} | ||
|
||
export function PageSponsors(props: PageSponsorsProps) { | ||
const { gaPageIdentifier } = props; | ||
|
||
const $isSponsorHidden = useStore(sponsorHidden); | ||
|
||
const [stickyTopSponsor, setStickyTopSponsor] = | ||
useState<StickyTopSponsorType | null>(); | ||
const [bottomRightSponsor, setBottomRightSponsor] = | ||
useState<BottomRightSponsorType | null>(); | ||
|
||
useEffect(() => { | ||
const foundUtmParams = getUrlUtmParams(); | ||
|
||
if (!foundUtmParams.utmSource) { | ||
return; | ||
} | ||
|
||
localStorage.setItem('utm_params', JSON.stringify(foundUtmParams)); | ||
}, []); | ||
|
||
async function loadSponsor() { | ||
const currentPath = window.location.pathname; | ||
if ( | ||
currentPath === '/' || | ||
currentPath === '/best-practices' || | ||
currentPath === '/roadmaps' || | ||
currentPath.startsWith('/guides') || | ||
currentPath.startsWith('/videos') || | ||
currentPath.startsWith('/account') || | ||
currentPath.startsWith('/team/') | ||
) { | ||
return; | ||
} | ||
|
||
const { response, error } = await httpGet<V1GetSponsorResponse>( | ||
`${import.meta.env.PUBLIC_API_URL}/v1-get-sponsor`, | ||
{ | ||
href: window.location.pathname, | ||
mobile: isMobile() ? 'true' : 'false', | ||
}, | ||
); | ||
|
||
if (error) { | ||
console.error(error); | ||
return; | ||
} | ||
|
||
setStickyTopSponsor(response?.stickyTopAd); | ||
setBottomRightSponsor(response?.bottomRightAd); | ||
} | ||
|
||
async function logSponsorImpression( | ||
sponsor: BottomRightSponsorType | StickyTopSponsorType, | ||
) { | ||
window.fireEvent({ | ||
category: 'SponsorImpression', | ||
action: `${sponsor?.company} Impression`, | ||
label: | ||
sponsor?.gaLabel || `${gaPageIdentifier} / ${sponsor?.company} Link`, | ||
}); | ||
} | ||
|
||
async function clickSponsor( | ||
sponsor: BottomRightSponsorType | StickyTopSponsorType, | ||
) { | ||
const { id: sponsorId, company, gaLabel } = sponsor; | ||
|
||
const labelValue = gaLabel || `${gaPageIdentifier} / ${company} Link`; | ||
|
||
window.fireEvent({ | ||
category: 'SponsorClick', | ||
action: `${company} Redirect`, | ||
label: labelValue, | ||
value: labelValue, | ||
}); | ||
|
||
const clickUrl = new URL( | ||
`${import.meta.env.PUBLIC_API_URL}/v1-view-sponsor/${sponsorId}`, | ||
); | ||
|
||
const { response, error } = await httpPatch<{ status: 'ok' }>( | ||
clickUrl.toString(), | ||
{ | ||
mobile: isMobile(), | ||
}, | ||
); | ||
|
||
if (error || !response) { | ||
console.error(error); | ||
return; | ||
} | ||
|
||
setViewSponsorCookie(sponsorId); | ||
} | ||
|
||
useEffect(() => { | ||
window.setTimeout(loadSponsor); | ||
}, []); | ||
|
||
if ($isSponsorHidden) { | ||
return null; | ||
} | ||
|
||
return ( | ||
<div> | ||
{stickyTopSponsor && !isSponsorMarkedHidden(stickyTopSponsor.id) && ( | ||
<StickyTopSponsor | ||
sponsor={stickyTopSponsor} | ||
onSponsorImpression={() => { | ||
logSponsorImpression(stickyTopSponsor).catch(console.error); | ||
}} | ||
onSponsorClick={() => { | ||
clickSponsor(stickyTopSponsor).catch(console.error); | ||
}} | ||
onSponsorHidden={() => { | ||
markSponsorHidden(stickyTopSponsor.id); | ||
}} | ||
/> | ||
)} | ||
{bottomRightSponsor && !isSponsorMarkedHidden(bottomRightSponsor.id) && ( | ||
<BottomRightSponsor | ||
sponsor={bottomRightSponsor} | ||
onSponsorClick={() => { | ||
clickSponsor(bottomRightSponsor).catch(console.error); | ||
}} | ||
onSponsorHidden={() => { | ||
markSponsorHidden(bottomRightSponsor.id); | ||
}} | ||
onSponsorImpression={() => { | ||
logSponsorImpression(bottomRightSponsor).catch(console.error); | ||
}} | ||
/> | ||
)} | ||
</div> | ||
); | ||
} |
Oops, something went wrong.