Skip to content

Ona banner in Gitpod classic #20882

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions components/dashboard/src/Analytics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ export type Event =
| "ide_configuration_changed"
| "status_rendered"
| "error_rendered"
| "video_clicked";
| "video_clicked"
| "waitlist_joined";
type InternalEvent = Event | "path_changed" | "dashboard_clicked";

export type EventProperties =
Expand All @@ -38,7 +39,8 @@ export type EventProperties =
| TrackWorkspaceClassChanged
| TrackStatusRendered
| TrackErrorRendered
| TrackVideoClicked;
| TrackVideoClicked
| TrackWaitlistJoined;
type InternalEventProperties = EventProperties | TrackDashboardClick | TrackPathChanged;

export interface TrackErrorRendered {
Expand Down Expand Up @@ -125,6 +127,11 @@ interface TrackPathChanged {
path: string;
}

export interface TrackWaitlistJoined {
email: string;
feature: string;
}

interface Traits {
unsubscribed_onboarding?: boolean;
unsubscribed_changelog?: boolean;
Expand All @@ -148,6 +155,7 @@ export function trackEvent(event: "ide_configuration_changed", properties: Track
export function trackEvent(event: "status_rendered", properties: TrackStatusRendered): void;
export function trackEvent(event: "error_rendered", properties: TrackErrorRendered): void;
export function trackEvent(event: "video_clicked", properties: TrackVideoClicked): void;
export function trackEvent(event: "waitlist_joined", properties: TrackWaitlistJoined): void;
export function trackEvent(event: Event, properties: EventProperties): void {
trackEventInternal(event, properties);
}
Expand Down
5 changes: 5 additions & 0 deletions components/dashboard/src/images/ona-wordmark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
164 changes: 88 additions & 76 deletions components/dashboard/src/workspaces/BlogBanners.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,96 +5,108 @@
*/

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

const banners = [
{
type: "Watch recording",
title: "Beyond Kubernetes: A deep-dive into Gitpod Flex with our CTO",
link: "https://www.gitpod.io/events#watch-on-demand",
},
{
type: "Blog Post",
title: "Gitpod Enterprise:<br/> Self-hosted, not self-managed",
link: "https://www.gitpod.io/blog/self-hosted-not-self-managed",
},
{
type: "Customer Story",
title: "Thousands of hours spent on VM-based development environments reduced to zero using Gitpod",
link: "https://www.gitpod.io/customers/kingland",
},
{
type: "Gartner Report",
title: `"By 2026, 60% of cloud workloads will be built and deployed using CDE's"`,
link: "https://www.gitpod.io/blog/gartner-2023-cde-hypecycle",
},
];

const initialBannerIndex = 0; // Index for "Self-hosted, not self-managed"
const onaBanner = {
type: "Introducing",
title: "ONA",
subtitle: "The privacy-first software engineering agent.",
ctaText: "Get early access",
learnMoreText: "Learn more",
link: "https://ona.com/",
};

export const BlogBanners: React.FC = () => {
const [currentBannerIndex, setCurrentBannerIndex] = useState(initialBannerIndex);
export const OnaBanner: React.FC = () => {
const [showOnaBanner, setShowOnaBanner] = useState(true);
const [onaClicked, setOnaClicked] = useState(false);
const user = useCurrentUser();
const { toast } = useToast();

useEffect(() => {
const storedBannerData = localStorage.getItem("blog-banner-data");
const currentTime = new Date().getTime();
const storedOnaData = localStorage.getItem("ona-banner-data");

if (storedBannerData) {
const { lastIndex, lastTime } = JSON.parse(storedBannerData);
// Check Ona banner state
if (storedOnaData) {
const { dismissed, clicked } = JSON.parse(storedOnaData);
setShowOnaBanner(!dismissed);
setOnaClicked(clicked || false);
}

if (currentTime - lastTime >= 2 * 24 * 60 * 60 * 1000) {
// 2 days in milliseconds
const nextIndex = getRandomBannerIndex(lastIndex);
setCurrentBannerIndex(nextIndex);
localStorage.setItem(
"blog-banner-data",
JSON.stringify({ lastIndex: nextIndex, lastTime: currentTime }),
);
} else {
setCurrentBannerIndex(lastIndex);
}
} else {
setCurrentBannerIndex(initialBannerIndex);
localStorage.setItem(
"blog-banner-data",
JSON.stringify({ lastIndex: initialBannerIndex, lastTime: currentTime }),
// Clean up old blog banner data
localStorage.removeItem("blog-banner-data");
}, []);

const handleOnaBannerClick = () => {
if (!onaClicked) {
// Track "Get early access" click
const userEmail = user ? getPrimaryEmail(user) || "" : "";
trackEvent("waitlist_joined", { email: userEmail, feature: "Ona" });

setOnaClicked(true);
localStorage.setItem("ona-banner-data", JSON.stringify({ dismissed: false, clicked: true }));

// Show success toast
toast(
<div>
<div className="font-medium">You're on the waitlist</div>
<div className="text-sm opacity-80">We'll reach out to you soon.</div>
</div>,
);
} else {
// "Learn more" click - open link
window.open(onaBanner.link, "_blank", "noopener,noreferrer");
}
}, []);
};

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

return (
<div className="flex flex-col">
<a
href={banners[currentBannerIndex].link}
target="_blank"
rel="noopener noreferrer"
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"
aria-label={banners[currentBannerIndex].type + " - " + banners[currentBannerIndex].title}
style={{
backgroundPosition: "top left",
backgroundRepeat: "no-repeat",
backgroundImage: `url(${blogBannerBg})`,
backgroundSize: "contain",
}}
>
<div className="flex flex-col gap-8 mt-6 ml-4 max-w-[320px] overflow-wrap min-h-fit pb-4">
<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">
{banners[currentBannerIndex].type}
<div className="flex flex-col gap-4">
{showOnaBanner && (
<div
className="relative rounded-lg overflow-hidden flex flex-col gap-4 text-white max-w-[320px] p-6"
style={{
background:
"linear-gradient(340deg, #1F1329 0%, #333A75 20%, #556CA8 40%, #90A898 60%, #E2B15C 80%, #BEA462 100%)",
}}
>
{/* Close button */}
<button
onClick={handleOnaBannerDismiss}
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"
aria-label="Dismiss banner"
>
</button>

{/* Content */}
<div className="flex flex-col gap-4">
<div className="flex items-center gap-2 text-lg font-normal">
{onaBanner.type}
<img src={onaWordmark} alt="ONA" className="w-16" draggable="false" />
</div>
<div className="text-base font-normal opacity-90">{onaBanner.subtitle}</div>
</div>
<div
className="text-base font-semibold text-pk-content-primary max-w-[285px]"
dangerouslySetInnerHTML={{ __html: banners[currentBannerIndex].title }}
/>

{/* CTA Button */}
<button
onClick={handleOnaBannerClick}
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]"
>
{onaClicked ? onaBanner.learnMoreText : onaBanner.ctaText}
</button>
</div>
</a>
)}
</div>
);
};

// Export with old name for backward compatibility
export const BlogBanners = OnaBanner;
Loading