Skip to content

Commit 5091a5e

Browse files
Ona banner in Gitpod classic (#20882)
* Add Ona banner component and track waitlist joined event * Add success toast notification for Ona banner waitlist click
1 parent abfbb95 commit 5091a5e

File tree

3 files changed

+103
-78
lines changed

3 files changed

+103
-78
lines changed

components/dashboard/src/Analytics.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ export type Event =
2323
| "ide_configuration_changed"
2424
| "status_rendered"
2525
| "error_rendered"
26-
| "video_clicked";
26+
| "video_clicked"
27+
| "waitlist_joined";
2728
type InternalEvent = Event | "path_changed" | "dashboard_clicked";
2829

2930
export type EventProperties =
@@ -38,7 +39,8 @@ export type EventProperties =
3839
| TrackWorkspaceClassChanged
3940
| TrackStatusRendered
4041
| TrackErrorRendered
41-
| TrackVideoClicked;
42+
| TrackVideoClicked
43+
| TrackWaitlistJoined;
4244
type InternalEventProperties = EventProperties | TrackDashboardClick | TrackPathChanged;
4345

4446
export interface TrackErrorRendered {
@@ -125,6 +127,11 @@ interface TrackPathChanged {
125127
path: string;
126128
}
127129

130+
export interface TrackWaitlistJoined {
131+
email: string;
132+
feature: string;
133+
}
134+
128135
interface Traits {
129136
unsubscribed_onboarding?: boolean;
130137
unsubscribed_changelog?: boolean;
@@ -148,6 +155,7 @@ export function trackEvent(event: "ide_configuration_changed", properties: Track
148155
export function trackEvent(event: "status_rendered", properties: TrackStatusRendered): void;
149156
export function trackEvent(event: "error_rendered", properties: TrackErrorRendered): void;
150157
export function trackEvent(event: "video_clicked", properties: TrackVideoClicked): void;
158+
export function trackEvent(event: "waitlist_joined", properties: TrackWaitlistJoined): void;
151159
export function trackEvent(event: Event, properties: EventProperties): void {
152160
trackEventInternal(event, properties);
153161
}
Lines changed: 5 additions & 0 deletions
Loading

components/dashboard/src/workspaces/BlogBanners.tsx

Lines changed: 88 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -5,96 +5,108 @@
55
*/
66

77
import React, { useEffect, useState } from "react";
8-
import blogBannerBg from "../images/blog-banner-bg.png";
8+
import { trackEvent } from "../Analytics";
9+
import { useCurrentUser } from "../user-context";
10+
import { getPrimaryEmail } from "@gitpod/public-api-common/lib/user-utils";
11+
import { useToast } from "../components/toasts/Toasts";
12+
import onaWordmark from "../images/ona-wordmark.svg";
913

10-
const banners = [
11-
{
12-
type: "Watch recording",
13-
title: "Beyond Kubernetes: A deep-dive into Gitpod Flex with our CTO",
14-
link: "https://www.gitpod.io/events#watch-on-demand",
15-
},
16-
{
17-
type: "Blog Post",
18-
title: "Gitpod Enterprise:<br/> Self-hosted, not self-managed",
19-
link: "https://www.gitpod.io/blog/self-hosted-not-self-managed",
20-
},
21-
{
22-
type: "Customer Story",
23-
title: "Thousands of hours spent on VM-based development environments reduced to zero using Gitpod",
24-
link: "https://www.gitpod.io/customers/kingland",
25-
},
26-
{
27-
type: "Gartner Report",
28-
title: `"By 2026, 60% of cloud workloads will be built and deployed using CDE's"`,
29-
link: "https://www.gitpod.io/blog/gartner-2023-cde-hypecycle",
30-
},
31-
];
32-
33-
const initialBannerIndex = 0; // Index for "Self-hosted, not self-managed"
14+
const onaBanner = {
15+
type: "Introducing",
16+
title: "ONA",
17+
subtitle: "The privacy-first software engineering agent.",
18+
ctaText: "Get early access",
19+
learnMoreText: "Learn more",
20+
link: "https://ona.com/",
21+
};
3422

35-
export const BlogBanners: React.FC = () => {
36-
const [currentBannerIndex, setCurrentBannerIndex] = useState(initialBannerIndex);
23+
export const OnaBanner: React.FC = () => {
24+
const [showOnaBanner, setShowOnaBanner] = useState(true);
25+
const [onaClicked, setOnaClicked] = useState(false);
26+
const user = useCurrentUser();
27+
const { toast } = useToast();
3728

3829
useEffect(() => {
39-
const storedBannerData = localStorage.getItem("blog-banner-data");
40-
const currentTime = new Date().getTime();
30+
const storedOnaData = localStorage.getItem("ona-banner-data");
4131

42-
if (storedBannerData) {
43-
const { lastIndex, lastTime } = JSON.parse(storedBannerData);
32+
// Check Ona banner state
33+
if (storedOnaData) {
34+
const { dismissed, clicked } = JSON.parse(storedOnaData);
35+
setShowOnaBanner(!dismissed);
36+
setOnaClicked(clicked || false);
37+
}
4438

45-
if (currentTime - lastTime >= 2 * 24 * 60 * 60 * 1000) {
46-
// 2 days in milliseconds
47-
const nextIndex = getRandomBannerIndex(lastIndex);
48-
setCurrentBannerIndex(nextIndex);
49-
localStorage.setItem(
50-
"blog-banner-data",
51-
JSON.stringify({ lastIndex: nextIndex, lastTime: currentTime }),
52-
);
53-
} else {
54-
setCurrentBannerIndex(lastIndex);
55-
}
56-
} else {
57-
setCurrentBannerIndex(initialBannerIndex);
58-
localStorage.setItem(
59-
"blog-banner-data",
60-
JSON.stringify({ lastIndex: initialBannerIndex, lastTime: currentTime }),
39+
// Clean up old blog banner data
40+
localStorage.removeItem("blog-banner-data");
41+
}, []);
42+
43+
const handleOnaBannerClick = () => {
44+
if (!onaClicked) {
45+
// Track "Get early access" click
46+
const userEmail = user ? getPrimaryEmail(user) || "" : "";
47+
trackEvent("waitlist_joined", { email: userEmail, feature: "Ona" });
48+
49+
setOnaClicked(true);
50+
localStorage.setItem("ona-banner-data", JSON.stringify({ dismissed: false, clicked: true }));
51+
52+
// Show success toast
53+
toast(
54+
<div>
55+
<div className="font-medium">You're on the waitlist</div>
56+
<div className="text-sm opacity-80">We'll reach out to you soon.</div>
57+
</div>,
6158
);
59+
} else {
60+
// "Learn more" click - open link
61+
window.open(onaBanner.link, "_blank", "noopener,noreferrer");
6262
}
63-
}, []);
63+
};
6464

65-
const getRandomBannerIndex = (excludeIndex: number) => {
66-
let nextIndex;
67-
do {
68-
nextIndex = Math.floor(Math.random() * banners.length);
69-
} while (nextIndex === excludeIndex || nextIndex === initialBannerIndex);
70-
return nextIndex;
65+
const handleOnaBannerDismiss = () => {
66+
setShowOnaBanner(false);
67+
localStorage.setItem("ona-banner-data", JSON.stringify({ dismissed: true, clicked: onaClicked }));
7168
};
7269

7370
return (
74-
<div className="flex flex-col">
75-
<a
76-
href={banners[currentBannerIndex].link}
77-
target="_blank"
78-
rel="noopener noreferrer"
79-
className="bg-pk-surface rounded-lg overflow-hidden flex flex-col gap-2 text-decoration-none text-inherit max-w-[320px] border border-gray-200 dark:border-gray-800 hover:shadow"
80-
aria-label={banners[currentBannerIndex].type + " - " + banners[currentBannerIndex].title}
81-
style={{
82-
backgroundPosition: "top left",
83-
backgroundRepeat: "no-repeat",
84-
backgroundImage: `url(${blogBannerBg})`,
85-
backgroundSize: "contain",
86-
}}
87-
>
88-
<div className="flex flex-col gap-8 mt-6 ml-4 max-w-[320px] overflow-wrap min-h-fit pb-4">
89-
<div className="bg-pk-surface-invert w-fit text-pk-content-invert-primary text-sm leading-[18px] font-bold rounded-2xl py-1 px-4">
90-
{banners[currentBannerIndex].type}
71+
<div className="flex flex-col gap-4">
72+
{showOnaBanner && (
73+
<div
74+
className="relative rounded-lg overflow-hidden flex flex-col gap-4 text-white max-w-[320px] p-6"
75+
style={{
76+
background:
77+
"linear-gradient(340deg, #1F1329 0%, #333A75 20%, #556CA8 40%, #90A898 60%, #E2B15C 80%, #BEA462 100%)",
78+
}}
79+
>
80+
{/* Close button */}
81+
<button
82+
onClick={handleOnaBannerDismiss}
83+
className="absolute top-4 right-4 text-white/70 hover:text-white w-6 h-6 flex items-center justify-center rounded-full hover:bg-white/10 transition-colors"
84+
aria-label="Dismiss banner"
85+
>
86+
87+
</button>
88+
89+
{/* Content */}
90+
<div className="flex flex-col gap-4">
91+
<div className="flex items-center gap-2 text-lg font-normal">
92+
{onaBanner.type}
93+
<img src={onaWordmark} alt="ONA" className="w-16" draggable="false" />
94+
</div>
95+
<div className="text-base font-normal opacity-90">{onaBanner.subtitle}</div>
9196
</div>
92-
<div
93-
className="text-base font-semibold text-pk-content-primary max-w-[285px]"
94-
dangerouslySetInnerHTML={{ __html: banners[currentBannerIndex].title }}
95-
/>
97+
98+
{/* CTA Button */}
99+
<button
100+
onClick={handleOnaBannerClick}
101+
className="bg-white/20 backdrop-blur-sm text-white font-medium py-1 px-6 rounded-full hover:bg-white/30 transition-colors border border-white/20 max-w-[180px]"
102+
>
103+
{onaClicked ? onaBanner.learnMoreText : onaBanner.ctaText}
104+
</button>
96105
</div>
97-
</a>
106+
)}
98107
</div>
99108
);
100109
};
110+
111+
// Export with old name for backward compatibility
112+
export const BlogBanners = OnaBanner;

0 commit comments

Comments
 (0)