Skip to content

Commit aba45df

Browse files
Add Ona banner component and track waitlist joined event
1 parent abfbb95 commit aba45df

File tree

3 files changed

+93
-78
lines changed

3 files changed

+93
-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: 78 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -5,96 +5,98 @@
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 onaWordmark from "../images/ona-wordmark.svg";
912

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"
13+
const onaBanner = {
14+
type: "Introducing",
15+
title: "ONA",
16+
subtitle: "The privacy-first software engineering agent.",
17+
ctaText: "Get early access",
18+
learnMoreText: "Learn more",
19+
link: "https://ona.com/",
20+
};
3421

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

3827
useEffect(() => {
39-
const storedBannerData = localStorage.getItem("blog-banner-data");
40-
const currentTime = new Date().getTime();
28+
const storedOnaData = localStorage.getItem("ona-banner-data");
29+
30+
// Check Ona banner state
31+
if (storedOnaData) {
32+
const { dismissed, clicked } = JSON.parse(storedOnaData);
33+
setShowOnaBanner(!dismissed);
34+
setOnaClicked(clicked || false);
35+
}
36+
37+
// Clean up old blog banner data
38+
localStorage.removeItem("blog-banner-data");
39+
}, []);
4140

42-
if (storedBannerData) {
43-
const { lastIndex, lastTime } = JSON.parse(storedBannerData);
41+
const handleOnaBannerClick = () => {
42+
if (!onaClicked) {
43+
// Track "Get early access" click
44+
const userEmail = user ? getPrimaryEmail(user) || "" : "";
45+
trackEvent("waitlist_joined", { email: userEmail, feature: "Ona" });
4446

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-
}
47+
setOnaClicked(true);
48+
localStorage.setItem("ona-banner-data", JSON.stringify({ dismissed: false, clicked: true }));
5649
} else {
57-
setCurrentBannerIndex(initialBannerIndex);
58-
localStorage.setItem(
59-
"blog-banner-data",
60-
JSON.stringify({ lastIndex: initialBannerIndex, lastTime: currentTime }),
61-
);
50+
// "Learn more" click - open link
51+
window.open(onaBanner.link, "_blank", "noopener,noreferrer");
6252
}
63-
}, []);
53+
};
6454

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;
55+
const handleOnaBannerDismiss = () => {
56+
setShowOnaBanner(false);
57+
localStorage.setItem("ona-banner-data", JSON.stringify({ dismissed: true, clicked: onaClicked }));
7158
};
7259

7360
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}
61+
<div className="flex flex-col gap-4">
62+
{showOnaBanner && (
63+
<div
64+
className="relative rounded-lg overflow-hidden flex flex-col gap-4 text-white max-w-[320px] p-6"
65+
style={{
66+
background:
67+
"linear-gradient(340deg, #1F1329 0%, #333A75 20%, #556CA8 40%, #90A898 60%, #E2B15C 80%, #BEA462 100%)",
68+
}}
69+
>
70+
{/* Close button */}
71+
<button
72+
onClick={handleOnaBannerDismiss}
73+
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"
74+
aria-label="Dismiss banner"
75+
>
76+
77+
</button>
78+
79+
{/* Content */}
80+
<div className="flex flex-col gap-4">
81+
<div className="flex items-center gap-2 text-lg font-normal">
82+
{onaBanner.type}
83+
<img src={onaWordmark} alt="ONA" className="w-16" draggable="false" />
84+
</div>
85+
<div className="text-base font-normal opacity-90">{onaBanner.subtitle}</div>
9186
</div>
92-
<div
93-
className="text-base font-semibold text-pk-content-primary max-w-[285px]"
94-
dangerouslySetInnerHTML={{ __html: banners[currentBannerIndex].title }}
95-
/>
87+
88+
{/* CTA Button */}
89+
<button
90+
onClick={handleOnaBannerClick}
91+
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]"
92+
>
93+
{onaClicked ? onaBanner.learnMoreText : onaBanner.ctaText}
94+
</button>
9695
</div>
97-
</a>
96+
)}
9897
</div>
9998
);
10099
};
100+
101+
// Export with old name for backward compatibility
102+
export const BlogBanners = OnaBanner;

0 commit comments

Comments
 (0)