Add sponsors functionality

feat/sponsors
Kamran Ahmed 4 weeks ago
parent 03a974ad24
commit fd4ef4ea58
  1. 102
      src/components/PageSponsors/BottomRightSponsor.tsx
  2. 187
      src/components/PageSponsors/PageSponsors.tsx
  3. 73
      src/components/PageSponsors/StickyTopSponsor.tsx
  4. 5
      src/layouts/BaseLayout.astro

@ -0,0 +1,102 @@
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();
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>
);
}

@ -0,0 +1,187 @@
import { useEffect, useState } from 'react';
import { httpGet, httpPatch } 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';
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 & {};
export type BottomRightSponsorType = PageSponsorType;
type V1GetSponsorResponse = {
bottomRightAd?: PageSponsorType;
stickyTopAd?: PageSponsorType;
};
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>();
const [bottomRightSponsor, setBottomRightSponsor] =
useState<BottomRightSponsorType>();
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>
);
}

@ -0,0 +1,73 @@
import { cn } from '../../lib/classname.ts';
import { useScrollPosition } from '../../hooks/use-scroll-position.ts';
import { X } from 'lucide-react';
import type { StickyTopSponsorType } from './PageSponsors.tsx';
import { useEffect, useState } from 'react';
type StickyTopSponsorProps = {
sponsor: StickyTopSponsorType;
onSponsorImpression: () => void;
onSponsorClick: () => void;
onSponsorHidden: () => void;
};
const SCROLL_DISTANCE = 100;
export function StickyTopSponsor(props: StickyTopSponsorProps) {
const { sponsor, onSponsorHidden, onSponsorImpression, onSponsorClick } =
props;
const { y: scrollY } = useScrollPosition();
const [isImpressionLogged, setIsImpressionLogged] = useState(false);
const [isHidden, setIsHidden] = useState(false);
useEffect(() => {
if (scrollY < SCROLL_DISTANCE || isImpressionLogged) {
return;
}
setIsImpressionLogged(true);
onSponsorImpression();
}, [scrollY]);
if (scrollY < SCROLL_DISTANCE || isHidden) {
return null;
}
return (
<a
target="_blank"
href="https://www.google.com"
onClick={onSponsorClick}
className={cn(
'fixed left-0 right-0 top-0 z-[91] flex min-h-[45px] w-full flex-row items-center justify-center px-14 pb-2 pt-1.5 text-base font-medium text-yellow-950',
'bg-gradient-to-b from-purple-700 to-purple-800 text-white',
)}
>
<img
className="h-[23px]"
src={'https://tpc.googlesyndication.com/simgad/915843892126714634?'}
alt={'ad'}
/>
<span className="mx-3 truncate">
Register for our free cloud workshop
</span>
<button className="flex-truncate rounded-md bg-yellow-400 px-3 py-1 text-sm text-purple-900 transition-colors hover:bg-yellow-300">
Register now
</button>
<button
type="button"
className="absolute right-5 top-1/2 ml-1 -translate-y-1/2 px-1 py-1 opacity-70 hover:opacity-100"
onClick={(e) => {
e.preventDefault();
setIsHidden(true);
onSponsorHidden();
}}
>
<X className="h-4 w-4" strokeWidth={3} />
</button>
</a>
);
}

@ -8,7 +8,7 @@ import Navigation from '../components/Navigation/Navigation.astro';
import OpenSourceBanner from '../components/OpenSourceBanner.astro'; import OpenSourceBanner from '../components/OpenSourceBanner.astro';
import { PageProgress } from '../components/PageProgress'; import { PageProgress } from '../components/PageProgress';
import { Toaster } from '../components/Toast'; import { Toaster } from '../components/Toast';
import { PageSponsor } from '../components/PageSponsor'; import { PageSponsors } from '../components/PageSponsors/PageSponsors';
import { siteConfig } from '../lib/config'; import { siteConfig } from '../lib/config';
import '../styles/global.css'; import '../styles/global.css';
import { PageVisit } from '../components/PageVisit/PageVisit'; import { PageVisit } from '../components/PageVisit/PageVisit';
@ -184,7 +184,8 @@ const gaPageIdentifier = Astro.url.pathname
<Toaster client:only='react' /> <Toaster client:only='react' />
<CommandMenu client:idle /> <CommandMenu client:idle />
<PageProgress initialMessage={initialLoadingMessage} client:idle /> <PageProgress initialMessage={initialLoadingMessage} client:idle />
<PageSponsor
<PageSponsors
gaPageIdentifier={briefTitle || gaPageIdentifier} gaPageIdentifier={briefTitle || gaPageIdentifier}
client:load client:load
/> />

Loading…
Cancel
Save