Skip to content

Commit 50a9e25

Browse files
authored
First version of new webcomponent banner that can be reused across sites. (#58)
* added new logo * uploaded svg * initial banner
1 parent fa7f33b commit 50a9e25

File tree

3 files changed

+352
-0
lines changed

3 files changed

+352
-0
lines changed

www/global.d.ts

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
declare namespace JSX {
2+
interface IntrinsicElements {
3+
"zuplo-banner": React.DetailedHTMLProps<
4+
React.HTMLAttributes<HTMLElement>,
5+
HTMLElement
6+
> & {
7+
mode?: "dark" | "light";
8+
};
9+
}
10+
}

www/pages/_document.tsx

+5
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ export default function Document() {
3333
property="og:image"
3434
content="https://cdn.zuplo.com/assets/8e93df64-1a75-4cfe-afb7-10a99def9e0c.png"
3535
/>
36+
{/* eslint-disable-next-line @next/next/no-sync-scripts */}
37+
<script src="zuplo-banner.js"></script>
3638
<link
3739
href="https://fonts.googleapis.com/css?family=Fira+Code"
3840
rel="stylesheet"
@@ -65,6 +67,9 @@ export default function Document() {
6567
) : null}
6668
</Head>
6769
<body>
70+
<div className="w-full px-14 bg-white mb-5">
71+
<zuplo-banner mode="light"></zuplo-banner>
72+
</div>
6873
<Main />
6974
<NextScript />
7075
</body>

www/public/zuplo-banner.js

+337
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,337 @@
1+
class ZuploBanner extends HTMLElement {
2+
constructor() {
3+
super();
4+
5+
// Attach a shadow root
6+
const shadow = this.attachShadow({ mode: "open" });
7+
8+
// JSON data for the tools
9+
const toolsData = {
10+
zudoku: {
11+
name: "Zudoku",
12+
logo: "https://cdn.zuplo.com/uploads/zudoku-logo-only.svg",
13+
description: "API documentation should be free.",
14+
url: "https://zudoku.dev",
15+
},
16+
ratemyopenapi: {
17+
name: "Rate My OpenAPI",
18+
logo: "https://cdn.zuplo.com/uploads/rmoa-logo-only.svg",
19+
description: "Get feedback and a rating on your OpenAPI spec",
20+
url: "https://ratemyopenapi.com",
21+
},
22+
mockbin: {
23+
name: "Mockbin",
24+
logo: "https://cdn.zuplo.com/uploads/mockbin-logo-only.svg",
25+
description: "Mock an API from OpenAPI in seconds",
26+
url: "https://mockbin.io",
27+
},
28+
};
29+
30+
// Create wrapper
31+
const wrapper = document.createElement("div");
32+
wrapper.setAttribute("class", "zuplo-banner");
33+
34+
// Left side: Text and logo
35+
const leftDiv = document.createElement("div");
36+
leftDiv.setAttribute("class", "left");
37+
38+
const openSourceText = document.createElement("span");
39+
openSourceText.textContent = "Open source by ";
40+
41+
// Zuplo Logo using the provided SVG
42+
const zuploLogoContainer = document.createElement("div");
43+
zuploLogoContainer.setAttribute("class", "zuplo-logo");
44+
zuploLogoContainer.innerHTML = `
45+
<!-- Zuplo SVG Logo -->
46+
${this.getZuploLogoSVG()}
47+
`;
48+
49+
leftDiv.appendChild(openSourceText);
50+
leftDiv.appendChild(zuploLogoContainer);
51+
52+
// Right side: Button with grip icon and "View Tools" text
53+
const rightDiv = document.createElement("div");
54+
rightDiv.setAttribute("class", "right");
55+
56+
const menuButton = document.createElement("button");
57+
menuButton.setAttribute("class", "menu-button");
58+
59+
// Grip icon SVG
60+
const gripIconSVG = `
61+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
62+
stroke="currentColor" fill="none" stroke-width="2"
63+
stroke-linecap="round" stroke-linejoin="round">
64+
<circle cx="2" cy="2" r="1"/>
65+
<circle cx="8" cy="2" r="1"/>
66+
<circle cx="14" cy="2" r="1"/>
67+
<circle cx="2" cy="8" r="1"/>
68+
<circle cx="8" cy="8" r="1"/>
69+
<circle cx="14" cy="8" r="1"/>
70+
<circle cx="2" cy="14" r="1"/>
71+
<circle cx="8" cy="14" r="1"/>
72+
<circle cx="14" cy="14" r="1"/>
73+
</svg>
74+
`;
75+
76+
// Add "View Tools" text and icon to the button
77+
const buttonContent = document.createElement("span");
78+
buttonContent.setAttribute("class", "button-content");
79+
buttonContent.innerHTML = `${gripIconSVG}<span class="button-text">View Tools</span>`;
80+
81+
menuButton.appendChild(buttonContent);
82+
83+
rightDiv.appendChild(menuButton);
84+
85+
// Append left and right divs to wrapper
86+
wrapper.appendChild(leftDiv);
87+
wrapper.appendChild(rightDiv);
88+
89+
// Append wrapper to shadow root
90+
shadow.appendChild(wrapper);
91+
92+
// Create the menu (initially hidden)
93+
const menu = document.createElement("div");
94+
menu.setAttribute("class", "menu");
95+
96+
// Create menu items based on toolsData
97+
for (const key in toolsData) {
98+
const tool = toolsData[key];
99+
100+
const menuItem = document.createElement("a");
101+
menuItem.setAttribute("href", tool.url);
102+
menuItem.setAttribute("class", "menu-item");
103+
menuItem.setAttribute("target", "_blank");
104+
105+
const logo = document.createElement("img");
106+
logo.setAttribute("src", tool.logo);
107+
logo.setAttribute("alt", tool.name);
108+
109+
const textContainer = document.createElement("div");
110+
textContainer.setAttribute("class", "text-container");
111+
112+
const name = document.createElement("div");
113+
name.setAttribute("class", "tool-name");
114+
name.textContent = tool.name;
115+
116+
const description = document.createElement("div");
117+
description.setAttribute("class", "tool-description");
118+
description.textContent = tool.description;
119+
120+
textContainer.appendChild(name);
121+
textContainer.appendChild(description);
122+
menuItem.appendChild(logo);
123+
menuItem.appendChild(textContainer);
124+
125+
menu.appendChild(menuItem);
126+
}
127+
128+
// Add the footer to the menu
129+
const menuFooter = document.createElement("div");
130+
menuFooter.setAttribute("class", "menu-footer");
131+
menuFooter.textContent = "Created with ❤️ by Zuplo";
132+
133+
menu.appendChild(menuFooter);
134+
135+
// Append menu to the rightDiv instead of shadow root
136+
rightDiv.appendChild(menu);
137+
138+
// Handle button click to toggle menu visibility
139+
menuButton.addEventListener("click", (event) => {
140+
event.stopPropagation();
141+
menu.classList.toggle("visible");
142+
});
143+
144+
// Close menu when clicking outside
145+
document.addEventListener("click", (event) => {
146+
if (!this.contains(event.target)) {
147+
menu.classList.remove("visible");
148+
}
149+
});
150+
151+
// Styles
152+
const style = document.createElement("style");
153+
style.textContent = `
154+
/* Set font family to Helvetica throughout */
155+
* {
156+
font-family: 'Helvetica', sans-serif;
157+
box-sizing: border-box;
158+
}
159+
.zuplo-banner {
160+
display: flex;
161+
justify-content: space-between;
162+
align-items: center;
163+
background-color: white;
164+
color: black;
165+
padding: 10px 20px;
166+
width: 100%;
167+
flex-wrap: nowrap; /* Prevent wrapping */
168+
}
169+
.left {
170+
display: flex;
171+
align-items: center;
172+
}
173+
.left .zuplo-logo {
174+
height: 20px;
175+
margin-left: 10px;
176+
display: flex;
177+
align-items: center; /* Center vertically */
178+
position: relative;
179+
top: 1px; /* Move down slightly */
180+
}
181+
.left .zuplo-logo svg {
182+
height: 100%;
183+
width: auto;
184+
}
185+
.right {
186+
position: relative;
187+
display: flex;
188+
align-items: center;
189+
}
190+
.menu-button {
191+
display: flex;
192+
align-items: center;
193+
background-color: #ff00bd;
194+
color: white;
195+
border: none;
196+
padding: 10px 16px;
197+
border-radius: 5px;
198+
cursor: pointer;
199+
font-size: 16px;
200+
line-height: 1;
201+
white-space: nowrap; /* Prevent text wrapping */
202+
}
203+
.menu-button .button-content {
204+
display: flex;
205+
align-items: center;
206+
justify-content: center;
207+
}
208+
.menu-button svg {
209+
width: 20px;
210+
height: 20px;
211+
margin-right: 8px;
212+
}
213+
.menu-button .button-text {
214+
display: inline-block;
215+
}
216+
.menu {
217+
display: none;
218+
position: absolute;
219+
right: 0;
220+
top: calc(100% + 5px); /* Place below the button with 5px margin */
221+
background-color: white;
222+
color: black;
223+
box-shadow: 0 4px 10px rgba(0,0,0,0.1); /* Add shadow */
224+
z-index: 9999; /* High z-index */
225+
width: auto; /* Adjust width */
226+
min-width: 200px; /* Optional: set a minimum width */
227+
padding: 10px;
228+
}
229+
.menu.visible {
230+
display: block;
231+
}
232+
.menu-item {
233+
display: flex;
234+
align-items: center;
235+
text-decoration: none;
236+
color: black;
237+
padding: 10px 0;
238+
border-bottom: 1px solid #eee;
239+
}
240+
.menu-item:last-child {
241+
border-bottom: none;
242+
}
243+
.menu-item img {
244+
width: 40px;
245+
height: 40px;
246+
margin-right: 10px;
247+
}
248+
.text-container {
249+
display: flex;
250+
flex-direction: column;
251+
}
252+
.tool-name {
253+
font-weight: bold;
254+
white-space: nowrap; /* Prevent wrapping */
255+
}
256+
.tool-description {
257+
font-size: 12px;
258+
color: #666;
259+
}
260+
.menu-footer {
261+
text-align: center;
262+
margin-top: 10px;
263+
font-size: 12px;
264+
color: #666;
265+
}
266+
/* Responsive */
267+
@media (max-width: 600px) {
268+
.zuplo-banner {
269+
flex-direction: column-reverse; /* Stack left content below right */
270+
align-items: center; /* Center horizontally */
271+
}
272+
.left {
273+
margin-top: 10px;
274+
}
275+
.right {
276+
margin-top: 0;
277+
width: auto;
278+
justify-content: center;
279+
}
280+
.menu {
281+
right: 0;
282+
width: auto; /* Ensure menu is as wide as needed */
283+
}
284+
}
285+
`;
286+
287+
// Handle mode attribute
288+
const mode = this.getAttribute("mode") || "light";
289+
290+
if (mode === "dark") {
291+
style.textContent += `
292+
.zuplo-banner {
293+
background-color: black;
294+
color: white;
295+
}
296+
.menu {
297+
background-color: black;
298+
color: white;
299+
}
300+
.menu-item {
301+
color: white;
302+
}
303+
.menu-footer {
304+
color: #ccc;
305+
}
306+
.menu-button {
307+
background-color: #ff00bd;
308+
color: white;
309+
}
310+
/* Invert Zuplo logo for dark mode */
311+
.left .zuplo-logo svg path {
312+
fill: white;
313+
}
314+
`;
315+
} else {
316+
/* Set Zuplo logo color for light mode */
317+
style.textContent += `
318+
.left .zuplo-logo svg path {
319+
fill: black;
320+
}
321+
`;
322+
}
323+
324+
// Append styles to shadow root
325+
shadow.appendChild(style);
326+
}
327+
328+
// Method to return the Zuplo SVG logo
329+
getZuploLogoSVG() {
330+
return `
331+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" aria-hidden="true" viewBox="0 0 147 33" alt="Zuplo logo" class="w-auto h-8"><path fill="#FFF" d="M27.142 19.978H16.62L27.83 8.746a.758.758 0 0 0-.534-1.293H9.488V0h19.534a7.57 7.57 0 0 1 4.065 1.125 7.6 7.6 0 0 1 2.836 3.126 7.4 7.4 0 0 1-1.461 8.398l-7.32 7.328z"></path><path fill="#FFF" d="M9.489 11.042h10.524l-11.19 11.21a.772.772 0 0 0 .543 1.316h17.759v7.452H7.61a7.57 7.57 0 0 1-4.065-1.125A7.6 7.6 0 0 1 .71 26.768a7.4 7.4 0 0 1 1.462-8.397zm73.297 5.728c0 2.657-1.034 4.283-3.46 4.244-2.227-.04-3.38-1.666-3.38-4.283V6.696h-5.488v10.43c0 5.038 3.142 8.607 8.868 8.647 5.25.04 8.948-3.807 8.948-8.606V6.697h-5.488zm53.306-10.512c-5.925 0-10.098 4.204-10.098 9.757 0 5.552 4.175 9.756 10.098 9.756s10.099-4.204 10.099-9.756-4.173-9.757-10.099-9.757m0 14.794c-2.744 0-4.69-2.063-4.69-5.037 0-2.975 1.948-5.038 4.69-5.038s4.691 2.063 4.691 5.038-1.947 5.037-4.691 5.037M101.966 6.258c-5.926 0-10.099 4.204-10.099 9.757 0 .073.009.144.01.22h-.01v15.772h5.408V24.75a10.9 10.9 0 0 0 4.691 1.02c5.926 0 10.099-4.204 10.099-9.756s-4.173-9.756-10.099-9.756m0 14.794c-2.744 0-4.69-2.063-4.69-5.037 0-2.975 1.948-5.038 4.69-5.038s4.691 2.063 4.691 5.038-1.947 5.037-4.691 5.037M49.868 11.41h10.814l-10.814 8.452v5.473h17.514v-4.716h-10.84l10.84-8.473V6.694H49.868zm74.501 13.925h-1.831a7.46 7.46 0 0 1-5.262-2.177 7.42 7.42 0 0 1-2.183-5.248V.005h5.518V17.91a1.927 1.927 0 0 0 1.927 1.921h1.831z"></path></svg>
332+
`;
333+
}
334+
}
335+
336+
// Define the custom element
337+
customElements.define("zuplo-banner", ZuploBanner);

0 commit comments

Comments
 (0)