Skip to content

Commit ccca3a6

Browse files
[PAYG] [dashboard] Onboarding: Product Educational content in / workspaces view (#20021)
* intial commit * Documentation links * Update YouTube Videos * cleanup + performant YT method * Static section of `Personalised for you` 😜 * Add blog banners * Minor fixes * Educational content should only be in Gitpod PAYG & minor fixes * nit fix * nti fix * reorder docs link * nit fix * final nit fix :) * fix the label issue, video bug, line break in banner * use `flex-1` * bring back old description copy for header * improve spacing * Optimize VideoCarousel rendering and key usage - Render only the current video to improve performance - Remove redundant key prop from lite-youtube component - Ensure unique keys for list items in VideoCarousel * Enhance VideoCarousel button accessibility - Add focus rings to video selection buttons - Improve keyboard navigation with visible focus indicators
1 parent 1f5bc61 commit ccca3a6

File tree

10 files changed

+351
-70
lines changed

10 files changed

+351
-70
lines changed

components/dashboard/src/Analytics.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ export const trackButtonOrAnchor = (target: HTMLAnchorElement | HTMLButtonElemen
188188

189189
let trackingMsg: TrackDashboardClick = {
190190
path: window.location.pathname,
191-
label: target.textContent || undefined,
191+
label: target.ariaLabel || target.textContent || undefined,
192192
};
193193

194194
if (target instanceof HTMLButtonElement || target instanceof HTMLDivElement) {

components/dashboard/src/components/podkit/layout/PageHeading.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ type PageHeadingProps = {
1313
};
1414
export const PageHeading: FC<PageHeadingProps> = ({ title, subtitle }) => {
1515
return (
16-
<div className="flex flex-row flex-wrap justify-between py-8 gap-2">
16+
<div className="flex flex-row flex-wrap justify-between py-5 gap-2">
1717
<div>
1818
<Heading1>{title}</Heading1>
1919
{subtitle && <Subheading>{subtitle}</Subheading>}
Lines changed: 14 additions & 0 deletions
Loading
81.7 KB
Loading
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/**
2+
* Copyright (c) 2024 Gitpod GmbH. All rights reserved.
3+
* Licensed under the GNU Affero General Public License (AGPL).
4+
* See License.AGPL.txt in the project root for license information.
5+
*/
6+
7+
import React, { useEffect, useState } from "react";
8+
import blogBannerBg from "../images/blog-banner-bg.png";
9+
10+
const banners = [
11+
{
12+
type: "Blog Post",
13+
title: "Gitpod Enterprise:<br/> Self-hosted, not self-managed",
14+
link: "https://www.gitpod.io/blog/self-hosted-not-self-managed",
15+
},
16+
{
17+
type: "Customer Story",
18+
title: "Thousands of hours spent on VM-based development environments reduced to zero using Gitpod",
19+
link: "https://www.gitpod.io/customers/kingland",
20+
},
21+
{
22+
type: "Gartner Report",
23+
title: `"By 2026, 60% of cloud workloads will be built and deployed using CDE's"`,
24+
link: "https://www.gitpod.io/blog/gartner-2023-cde-hypecycle",
25+
},
26+
{
27+
type: "Webinar Series",
28+
title: "The Platform Engineering maturity model",
29+
link: "https://www.gitpod.io/events#platform-maturity-model-series",
30+
},
31+
];
32+
33+
const initialBannerIndex = 0; // Index for "Self-hosted, not self-managed"
34+
35+
export const BlogBanners: React.FC = () => {
36+
const [currentBannerIndex, setCurrentBannerIndex] = useState(initialBannerIndex);
37+
38+
useEffect(() => {
39+
const storedBannerData = localStorage.getItem("blog-banner-data");
40+
const currentTime = new Date().getTime();
41+
42+
if (storedBannerData) {
43+
const { lastIndex, lastTime } = JSON.parse(storedBannerData);
44+
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 }),
61+
);
62+
}
63+
}, []);
64+
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;
71+
};
72+
73+
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}
91+
</div>
92+
<div
93+
className="text-base font-semibold text-pk-content-primary max-w-[285px]"
94+
dangerouslySetInnerHTML={{ __html: banners[currentBannerIndex].title }}
95+
/>
96+
</div>
97+
</a>
98+
</div>
99+
);
100+
};

components/dashboard/src/workspaces/EmptyWorkspacesContent.tsx

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,6 @@ import "lite-youtube-embed/src/lite-yt-embed.css";
1212
import "lite-youtube-embed/src/lite-yt-embed";
1313

1414
declare global {
15-
interface Window {
16-
onYouTubeIframeAPIReady: () => void;
17-
YT: any;
18-
}
1915
namespace JSX {
2016
interface IntrinsicElements {
2117
"lite-youtube": any;
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/**
2+
* Copyright (c) 2024 Gitpod GmbH. All rights reserved.
3+
* Licensed under the GNU Affero General Public License (AGPL).
4+
* See License.AGPL.txt in the project root for license information.
5+
*/
6+
7+
import React, { useState } from "react";
8+
import { trackVideoClick } from "../Analytics";
9+
10+
import "lite-youtube-embed/src/lite-yt-embed.css";
11+
import "lite-youtube-embed/src/lite-yt-embed";
12+
13+
interface Video {
14+
id: string;
15+
title: string;
16+
analyticsLabel: string;
17+
}
18+
19+
const videos: Video[] = [
20+
{ id: "1ZBN-b2cIB8", title: "Gitpod in 120 seconds", analyticsLabel: "gitpod-demo" },
21+
{ id: "zhZNnzFlZnY", title: "Getting started with Gitpod", analyticsLabel: "getting-started-with-gitpod" },
22+
{ id: "kuoHM2bpBqY", title: "Fully automate your dev setup", analyticsLabel: "automate-gitpod-setup" },
23+
{ id: "_CwFzCbAsoU", title: "Personalise your workspace", analyticsLabel: "personalise-gitpod-workspace" },
24+
];
25+
26+
declare global {
27+
namespace JSX {
28+
interface IntrinsicElements {
29+
"lite-youtube": any;
30+
}
31+
}
32+
}
33+
34+
export const VideoCarousel: React.FC = () => {
35+
const [currentVideo, setCurrentVideo] = useState(0);
36+
37+
const handleDotClick = (index: number) => {
38+
setCurrentVideo(index);
39+
};
40+
41+
const onPlayerStateChange = (index: number) => {
42+
trackVideoClick(videos[index].analyticsLabel);
43+
};
44+
45+
return (
46+
<div className="video-carousel">
47+
<div className="video-container">
48+
{videos.map((video, index) => (
49+
<div key={video.id} style={{ display: index === currentVideo ? "block" : "none" }}>
50+
{index === currentVideo && (
51+
<lite-youtube
52+
videoid={video.id}
53+
style={{
54+
width: "320px",
55+
height: "180px",
56+
}}
57+
class="rounded-lg"
58+
playlabel={video.title}
59+
onClick={() => onPlayerStateChange(index)}
60+
></lite-youtube>
61+
)}
62+
</div>
63+
))}
64+
</div>
65+
<div className="flex justify-center space-x-2 mt-2">
66+
{videos.map((_, index) => (
67+
<button
68+
key={index}
69+
className={`w-3 h-3 rounded-full focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-kumquat-dark transition-colors duration-200 ease-in-out ${
70+
index === currentVideo
71+
? "bg-kumquat-dark"
72+
: "bg-gray-300 dark:bg-gray-600 hover:bg-kumquat-light dark:hover:bg-kumquat-light"
73+
}`}
74+
onClick={() => handleDotClick(index)}
75+
aria-label={`Go to video ${index + 1}`}
76+
></button>
77+
))}
78+
</div>
79+
</div>
80+
);
81+
};

components/dashboard/src/workspaces/WorkspaceEntry.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,10 @@ export const WorkspaceEntry: FunctionComponent<Props> = ({ info, shortVersion })
9393
<div className="min-w-4">
9494
<GitBranchIcon className="h-4 w-4" />
9595
</div>
96-
<Tooltip content={currentBranch} className="truncate overflow-ellipsis">
96+
<Tooltip
97+
content={currentBranch}
98+
className="truncate overflow-ellipsis max-w-[120px] w-auto"
99+
>
97100
{currentBranch}
98101
</Tooltip>
99102
</div>

0 commit comments

Comments
 (0)