Skip to content

Commit c5c44d8

Browse files
authored
docs: use github gist integration for sharing samples (#12337)
To improve the sharing, specifically the long url for sharing samples, we integrated GitHub gist. We can create GitHub gist from the UI, which stores it on GitHub and get short readable URL.
1 parent e7a538a commit c5c44d8

File tree

6 files changed

+580
-120
lines changed

6 files changed

+580
-120
lines changed
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import React from "react";
2+
import "@ui5/webcomponents/dist/Label.js";
3+
import "@ui5/webcomponents/dist/Input.js";
4+
import "@ui5/webcomponents/dist/Button.js";
5+
import CopyIcon from "@ui5/webcomponents-icons/dist/v5/copy.js";
6+
import GithubIcon from "@ui5/webcomponents-icons-tnt/dist/github.js";
7+
8+
export default function GitHubGist({
9+
githubUser,
10+
isAuthenticating,
11+
isCreatingGist,
12+
gistUrl,
13+
onSignIn,
14+
onSignOut,
15+
onCreateGist,
16+
onCopyGistUrl,
17+
setCopied
18+
}) {
19+
const handleCopyGistUrl = async () => {
20+
try {
21+
await onCopyGistUrl(gistUrl);
22+
setCopied(true);
23+
} catch (error) {
24+
console.error("failed to copy to clipboard:", error);
25+
}
26+
};
27+
28+
return (
29+
<div>
30+
<ui5-label style={{ marginTop: "1rem" }}>GitHub Gist</ui5-label>
31+
32+
{!githubUser ? (
33+
<div style={{ textAlign: "center", padding: "1rem 0" }}>
34+
<ui5-button
35+
onClick={onSignIn}
36+
disabled={isAuthenticating ? true : undefined}
37+
style={{ width: "100%" }}
38+
icon={GithubIcon}
39+
>
40+
{isAuthenticating ? "Signing in..." : "Sign in with GitHub"}
41+
</ui5-button>
42+
</div>
43+
) : (
44+
<div>
45+
{gistUrl ? (
46+
<div>
47+
<div style={{
48+
display: "flex",
49+
gap: "0.5rem",
50+
alignItems: "center",
51+
marginBottom: "0.5rem"
52+
}}>
53+
<ui5-input readonly value={gistUrl}></ui5-input>
54+
<ui5-button
55+
icon={CopyIcon}
56+
design="Transparent"
57+
onClick={handleCopyGistUrl}
58+
></ui5-button>
59+
</div>
60+
</div>
61+
) : (
62+
<div style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}>
63+
<ui5-button
64+
onClick={onCreateGist}
65+
disabled={isCreatingGist ? true : undefined}
66+
icon={GithubIcon}
67+
>
68+
{isCreatingGist ? "Creating Gist..." : "Create GitHub Gist"}
69+
</ui5-button>
70+
71+
<ui5-button
72+
design="Transparent"
73+
onClick={onSignOut}
74+
>
75+
Sign out
76+
</ui5-button>
77+
</div>
78+
)}
79+
80+
<span style={{
81+
fontSize: "0.8rem",
82+
color: "var(--sapSuccessColor)"
83+
}}>
84+
✓ Signed in as {githubUser.login}
85+
{githubUser.avatar_url && (
86+
<img
87+
src={githubUser.avatar_url}
88+
alt="avatar"
89+
style={{
90+
width: "1.5rem",
91+
height: "1.5rem",
92+
borderRadius: "50%",
93+
verticalAlign: "middle",
94+
marginLeft: "0.5rem"
95+
}}
96+
/>
97+
)}
98+
</span>
99+
</div>
100+
)}
101+
</div>
102+
);
103+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/**
2+
* GitHub API utility functions for the playground
3+
*/
4+
5+
const NETLIFY_BASE_URL = "https://ui5-webc-playground-server.netlify.app/.netlify/functions";
6+
7+
export const fetchAuthUrl = async () => {
8+
const response = await fetch(`${NETLIFY_BASE_URL}/github-auth-url`, {
9+
method: "GET",
10+
headers: {
11+
"Content-Type": "application/json"
12+
}
13+
});
14+
15+
if (!response.ok) {
16+
throw new Error(`failed to get oauth url: ${response.status}`);
17+
}
18+
19+
return response.json();
20+
};
21+
22+
export const exchangeCodeForToken = async (code, state) => {
23+
const response = await fetch(`${NETLIFY_BASE_URL}/github-token`, {
24+
method: "POST",
25+
headers: {
26+
"Content-Type": "application/json"
27+
},
28+
body: JSON.stringify({ code, state })
29+
});
30+
31+
if (!response.ok) {
32+
const errorData = await response.json().catch(() => ({}));
33+
throw new Error(errorData.message || `token exchange failed: ${response.status}`);
34+
}
35+
36+
return response.json();
37+
};
38+
39+
export const createGist = async (token, files, description = "ui5 web components playground example") => {
40+
const response = await fetch(`${NETLIFY_BASE_URL}/github-create-gist`, {
41+
method: "POST",
42+
headers: {
43+
"Content-Type": "application/json",
44+
"Authorization": `Bearer ${token}`
45+
},
46+
body: JSON.stringify({ files, description })
47+
});
48+
49+
if (!response.ok) {
50+
const errorData = await response.json().catch(() => ({}));
51+
throw new Error(errorData.message || `server error: ${response.status}`);
52+
}
53+
54+
return response.json();
55+
};
56+
57+
/**
58+
* Load and convert gist files to playground format
59+
*/
60+
export const loadGist = async (gistId) => {
61+
try {
62+
console.log(`loading gist: ${gistId}`);
63+
const response = await fetch(`${NETLIFY_BASE_URL}/github-get-gist?id=${gistId}`, {
64+
method: 'GET',
65+
headers: {
66+
'Content-Type': 'application/json'
67+
}
68+
});
69+
70+
if (!response.ok) {
71+
throw new Error(`failed to load gist: ${response.status}`);
72+
}
73+
74+
const gist = await response.json();
75+
console.log(`gist loaded: ${gist.description}`, Object.keys(gist.files));
76+
77+
// convert gist files to playground format
78+
const playgroundFiles = {};
79+
Object.keys(gist.files).forEach(filename => {
80+
const gistFile = gist.files[filename];
81+
playgroundFiles[filename] = {
82+
name: filename,
83+
content: gistFile.content,
84+
hidden: filename === 'playground-support.js'
85+
};
86+
});
87+
88+
console.log('converted playground files:', Object.keys(playgroundFiles));
89+
return playgroundFiles;
90+
} catch (error) {
91+
console.error('failed to load gist:', error);
92+
throw error;
93+
}
94+
};
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { fetchAuthUrl, exchangeCodeForToken } from "./githubAPI.js";
2+
3+
const TOKEN_EXPIRY_KEY = 'github_token_expiry';
4+
const TOKEN_LIFETIME = 24 * 60 * 60 * 1000; // 24 hours
5+
6+
/**
7+
* Handle OAuth popup window and authentication flow
8+
*/
9+
export const auth = async () => {
10+
const { authUrl, state } = await fetchAuthUrl();
11+
12+
// open github oauth popup
13+
const popup = window.open(authUrl, "github-auth", "width=600,height=700,scrollbars=yes,resizable=yes");
14+
15+
// listen for oauth callback
16+
const authResult = await new Promise((resolve, reject) => {
17+
const messageHandler = (event) => {
18+
// verify origin for security
19+
if (!event.origin.includes("github.com") && !event.origin.includes("ui5-webc-playground-server.netlify.app")) {
20+
return;
21+
}
22+
23+
if (event.data.type === "GITHUB_OAUTH_SUCCESS") {
24+
window.removeEventListener("message", messageHandler);
25+
popup.close();
26+
resolve(event.data);
27+
} else if (event.data.type === "GITHUB_OAUTH_ERROR") {
28+
window.removeEventListener("message", messageHandler);
29+
popup.close();
30+
reject(new Error(event.data.error));
31+
}
32+
};
33+
34+
window.addEventListener("message", messageHandler);
35+
36+
// poll for url changes in popup
37+
const pollTimer = setInterval(() => {
38+
try {
39+
if (popup.closed) {
40+
clearInterval(pollTimer);
41+
window.removeEventListener("message", messageHandler);
42+
reject(new Error("authentication cancelled by user"));
43+
return;
44+
}
45+
46+
// check if we got redirected back with code
47+
const url = popup.location.href;
48+
if (url.includes("code=")) {
49+
clearInterval(pollTimer);
50+
window.removeEventListener("message", messageHandler);
51+
52+
const urlParams = new URLSearchParams(new URL(url).search);
53+
const code = urlParams.get("code");
54+
const returnedState = urlParams.get("state");
55+
56+
popup.close();
57+
resolve({ code, state: returnedState });
58+
}
59+
} catch (error) {
60+
// cross-origin restrictions prevent access to popup url
61+
// continue polling until popup closes or we get a message
62+
}
63+
}, 1000);
64+
});
65+
66+
// exchange code for token
67+
return exchangeCodeForToken(authResult.code, authResult.state);
68+
};
69+
70+
/**
71+
* Manage authentication state in localStorage
72+
*/
73+
export const saveAuthToStorage = (token, user) => {
74+
const expiryTime = Date.now() + TOKEN_LIFETIME;
75+
76+
localStorage.setItem('github_token', token);
77+
localStorage.setItem('github_user', JSON.stringify(user));
78+
localStorage.setItem(TOKEN_EXPIRY_KEY, expiryTime.toString());
79+
};
80+
81+
export const isTokenExpired = () => {
82+
const expiryTime = localStorage.getItem(TOKEN_EXPIRY_KEY);
83+
if (!expiryTime) return true;
84+
85+
return Date.now() > parseInt(expiryTime);
86+
};
87+
88+
export const validateStoredAuth = () => {
89+
const token = localStorage.getItem('github_token');
90+
const userStr = localStorage.getItem('github_user');
91+
92+
if (!token || !userStr || isTokenExpired()) {
93+
clearAuthFromStorage();
94+
return null;
95+
}
96+
97+
try {
98+
const user = JSON.parse(userStr);
99+
return { token, user };
100+
} catch (error) {
101+
clearAuthFromStorage();
102+
return null;
103+
}
104+
};
105+
106+
export const clearAuthFromStorage = () => {
107+
localStorage.removeItem('github_token');
108+
localStorage.removeItem('github_user');
109+
localStorage.removeItem(TOKEN_EXPIRY_KEY);
110+
};
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { fetchGist } from "./githubAPI.js";
2+
3+
/**
4+
* Extract gist ID from URL hash
5+
*/
6+
export const getGistIdFromURL = () => {
7+
if (!location.pathname.includes("/play")) {
8+
return null;
9+
}
10+
11+
// check for gist id in hash, f.e: #gist=e16211951641a1197026a3f9cfb2965d
12+
const hash = location.hash;
13+
if (hash.startsWith("#gist=")) {
14+
return hash.substring(6); // remove "#gist=" prefix
15+
}
16+
17+
return null;
18+
};
19+
20+
/**
21+
* Copy text to clipboard with fallback
22+
*/
23+
export const copyToClipboard = async (text) => {
24+
await navigator.clipboard.writeText(text);
25+
};

0 commit comments

Comments
 (0)