mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2025-08-25 18:20:46 +02:00
feat: add support for sticky sponsor banner (#7602)
* Add sponsors functionality * Fix overlapping issue * Add sticky top sponsor
This commit is contained in:
@@ -57,7 +57,7 @@ export class Renderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Clone it so we can use it later
|
// Clone it so we can use it later
|
||||||
this.loaderHTML = this.loaderEl!.innerHTML;
|
this.loaderHTML = this.loaderEl?.innerHTML!;
|
||||||
const dataset = this.containerEl.dataset;
|
const dataset = this.containerEl.dataset;
|
||||||
|
|
||||||
this.resourceType = dataset.resourceType!;
|
this.resourceType = dataset.resourceType!;
|
||||||
@@ -66,11 +66,7 @@ export class Renderer {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
jsonToSvg(jsonUrl: string): Promise<void> | null {
|
||||||
* @param { string } jsonUrl
|
|
||||||
* @returns {Promise<SVGElement>}
|
|
||||||
*/
|
|
||||||
jsonToSvg(jsonUrl: string) {
|
|
||||||
if (!jsonUrl) {
|
if (!jsonUrl) {
|
||||||
console.error('jsonUrl not defined in frontmatter');
|
console.error('jsonUrl not defined in frontmatter');
|
||||||
return null;
|
return null;
|
||||||
|
@@ -124,7 +124,7 @@ export function RoadmapTopicDetail(props: RoadmapTopicDetailProps) {
|
|||||||
const openAIKey = getOpenAIKey();
|
const openAIKey = getOpenAIKey();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'relative z-[90]'}>
|
<div className={'relative z-[92]'}>
|
||||||
<div
|
<div
|
||||||
ref={topicRef}
|
ref={topicRef}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
|
@@ -2,6 +2,8 @@ import { cn } from '../lib/classname.ts';
|
|||||||
import { memo, useEffect, useState } from 'react';
|
import { memo, useEffect, useState } from 'react';
|
||||||
import { useScrollPosition } from '../hooks/use-scroll-position.ts';
|
import { useScrollPosition } from '../hooks/use-scroll-position.ts';
|
||||||
import { X } from 'lucide-react';
|
import { X } from 'lucide-react';
|
||||||
|
import { isOnboardingStripHidden } from '../stores/page.ts';
|
||||||
|
import { useStore } from '@nanostores/react';
|
||||||
|
|
||||||
type OnboardingNudgeProps = {
|
type OnboardingNudgeProps = {
|
||||||
onStartOnboarding: () => void;
|
onStartOnboarding: () => void;
|
||||||
@@ -14,6 +16,7 @@ export function OnboardingNudge(props: OnboardingNudgeProps) {
|
|||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const $isOnboardingStripHidden = useStore(isOnboardingStripHidden);
|
||||||
const { y: scrollY } = useScrollPosition();
|
const { y: scrollY } = useScrollPosition();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -30,10 +33,14 @@ export function OnboardingNudge(props: OnboardingNudgeProps) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($isOnboardingStripHidden) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'fixed left-0 right-0 top-0 z-[91] flex w-full items-center justify-center bg-yellow-300 border-b border-b-yellow-500/30 pt-1.5 pb-2',
|
'fixed left-0 right-0 top-0 z-[91] flex w-full items-center justify-center border-b border-b-yellow-500/30 bg-yellow-300 pb-2 pt-1.5',
|
||||||
{
|
{
|
||||||
'striped-loader': isLoading,
|
'striped-loader': isLoading,
|
||||||
},
|
},
|
||||||
|
104
src/components/PageSponsors/BottomRightSponsor.tsx
Normal file
104
src/components/PageSponsors/BottomRightSponsor.tsx
Normal file
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
195
src/components/PageSponsors/PageSponsors.tsx
Normal file
195
src/components/PageSponsors/PageSponsors.tsx
Normal file
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
87
src/components/PageSponsors/StickyTopSponsor.tsx
Normal file
87
src/components/PageSponsors/StickyTopSponsor.tsx
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
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';
|
||||||
|
import { isOnboardingStripHidden } from '../../stores/page.ts';
|
||||||
|
|
||||||
|
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 (!sponsor) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// hide the onboarding strip when the sponsor is visible
|
||||||
|
isOnboardingStripHidden.set(true);
|
||||||
|
}, [sponsor]);
|
||||||
|
|
||||||
|
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',
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
backgroundImage: `linear-gradient(to bottom, ${sponsor.style?.fromColor}, ${sponsor.style?.toColor})`,
|
||||||
|
color: sponsor.style?.textColor,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img className="h-[23px]" src={sponsor.imageUrl} alt={'ad'} />
|
||||||
|
<span className="mx-3 truncate">{sponsor.description}</span>
|
||||||
|
<button
|
||||||
|
className="flex-truncate rounded-md px-3 py-1 text-sm transition-colors"
|
||||||
|
style={{
|
||||||
|
backgroundColor: sponsor.style?.buttonBackgroundColor,
|
||||||
|
color: sponsor.style?.buttonTextColor,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{sponsor.buttonText}
|
||||||
|
</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();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
setIsHidden(true);
|
||||||
|
onSponsorHidden();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" strokeWidth={3} />
|
||||||
|
</button>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
@@ -340,7 +340,7 @@ export function TopicDetail(props: TopicDetailProps) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'relative z-[90]'}>
|
<div className={'relative z-[92]'}>
|
||||||
<div
|
<div
|
||||||
ref={topicRef}
|
ref={topicRef}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
|
@@ -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
|
||||||
/>
|
/>
|
||||||
|
@@ -4,4 +4,6 @@ export const pageProgressMessage = atom<string | undefined>(undefined);
|
|||||||
export const sponsorHidden = atom(false);
|
export const sponsorHidden = atom(false);
|
||||||
|
|
||||||
export const roadmapsDropdownOpen = atom(false);
|
export const roadmapsDropdownOpen = atom(false);
|
||||||
export const navigationDropdownOpen = atom(false);
|
export const navigationDropdownOpen = atom(false);
|
||||||
|
|
||||||
|
export const isOnboardingStripHidden = atom(false);
|
Reference in New Issue
Block a user