From ed27506e9fbeca109cab1c329bc84e85663d93f6 Mon Sep 17 00:00:00 2001 From: Adrian Machado Date: Wed, 5 Feb 2025 16:06:45 -0800 Subject: [PATCH] Redo header --- apps/api/src/lib/rating.worker.ts | 2 + apps/api/src/services/slack.ts | 2 +- apps/web/public/zuplo-banner.js | 359 +++++++++++++++++++++++ apps/web/src/app/global.d.ts | 10 + apps/web/src/app/layout.tsx | 26 +- apps/web/src/components/Header/index.tsx | 33 ++- apps/web/tsconfig.json | 8 +- 7 files changed, 426 insertions(+), 14 deletions(-) create mode 100644 apps/web/public/zuplo-banner.js create mode 100644 apps/web/src/app/global.d.ts diff --git a/apps/api/src/lib/rating.worker.ts b/apps/api/src/lib/rating.worker.ts index 742e6120..7cba91a0 100644 --- a/apps/api/src/lib/rating.worker.ts +++ b/apps/api/src/lib/rating.worker.ts @@ -20,6 +20,7 @@ if (email && process.env.NODE_ENV === "production") { distinctId: email, properties: { reportId, + email, }, event: "report_uploaded", }); @@ -77,6 +78,7 @@ if (email) { distinctId: email, properties: { reportId, + email, }, event: "report_generated_successfully", }); diff --git a/apps/api/src/services/slack.ts b/apps/api/src/services/slack.ts index 57570933..f29dedb6 100644 --- a/apps/api/src/services/slack.ts +++ b/apps/api/src/services/slack.ts @@ -57,7 +57,7 @@ export async function postSuccessMessage({ }, { type: "mrkdwn", - text: `*Scopre:*\n${score}`, + text: `*Score:*\n${score}`, }, { type: "mrkdwn", diff --git a/apps/web/public/zuplo-banner.js b/apps/web/public/zuplo-banner.js new file mode 100644 index 00000000..2e0974d1 --- /dev/null +++ b/apps/web/public/zuplo-banner.js @@ -0,0 +1,359 @@ +class ZuploBanner extends HTMLElement { + constructor() { + super(); + + // Attach a shadow root + const shadow = this.attachShadow({ mode: "open" }); + + // JSON data for the tools + const toolsData = { + zudoku: { + name: "Zudoku", + logo: "https://cdn.zuplo.com/uploads/zudoku-logo-only.svg", + description: "API documentation should be free.", + url: "https://zudoku.dev?utm_source=ratemyopenapi&utm_medium=web&utm_campaign=header&ref=ratemyopenapi", + }, + ratemyopenapi: { + name: "Rate My OpenAPI", + logo: "https://cdn.zuplo.com/uploads/rmoa-logo-only.svg", + description: "Get feedback and a rating on your OpenAPI spec", + url: "https://ratemyopenapi.com", + }, + mockbin: { + name: "Mockbin", + logo: "https://cdn.zuplo.com/uploads/mockbin-logo-only.svg", + description: "Mock an API from OpenAPI in seconds", + url: "https://mockbin.io?utm_source=ratemyopenapi&utm_medium=web&utm_campaign=header&ref=ratemyopenapi", + }, + }; + + // Create wrapper + const wrapper = document.createElement("div"); + wrapper.setAttribute("class", "zuplo-banner"); + + // Left side: Text and logo + const leftDiv = document.createElement("div"); + leftDiv.setAttribute("class", "left"); + + const openSourceText = document.createElement("span"); + openSourceText.setAttribute("class", "open-source-text"); + openSourceText.textContent = "Open source by "; + + // Zuplo Logo using the provided SVG + const zuploLogoContainer = document.createElement("a"); + zuploLogoContainer.setAttribute( + "href", + "https://zuplo.com?utm_source=ratemyopenapi", + ); + zuploLogoContainer.setAttribute("target", "_blank"); + zuploLogoContainer.setAttribute("class", "zuplo-logo"); + zuploLogoContainer.innerHTML = ` + + ${this.getZuploLogoSVG()} + `; + + leftDiv.appendChild(openSourceText); + leftDiv.appendChild(zuploLogoContainer); + + // Right side: Button with grip icon and "View Tools" text + const rightDiv = document.createElement("div"); + rightDiv.setAttribute("class", "right"); + + const menuButton = document.createElement("button"); + menuButton.setAttribute("class", "menu-button"); + + // Grip icon SVG + const gripIconSVG = ` + + + + + + + + + + + + `; + + // Add "View Tools" text and icon to the button + const buttonContent = document.createElement("span"); + buttonContent.setAttribute("class", "button-content"); + buttonContent.innerHTML = `${gripIconSVG}More Tools`; + + menuButton.appendChild(buttonContent); + + rightDiv.appendChild(menuButton); + + // Append left and right divs to wrapper + wrapper.appendChild(leftDiv); + wrapper.appendChild(rightDiv); + + // Append wrapper to shadow root + shadow.appendChild(wrapper); + + // Create the menu (initially hidden) + const menu = document.createElement("div"); + menu.setAttribute("class", "menu"); + + // Create menu items based on toolsData + for (const key in toolsData) { + const tool = toolsData[key]; + + const menuItem = document.createElement("a"); + menuItem.setAttribute("href", tool.url); + menuItem.setAttribute("class", "menu-item"); + menuItem.setAttribute("target", "_blank"); + + const logo = document.createElement("img"); + logo.setAttribute("src", tool.logo); + logo.setAttribute("alt", tool.name); + + const textContainer = document.createElement("div"); + textContainer.setAttribute("class", "text-container"); + + const name = document.createElement("div"); + name.setAttribute("class", "tool-name"); + name.textContent = tool.name; + + const description = document.createElement("div"); + description.setAttribute("class", "tool-description"); + description.textContent = tool.description; + + textContainer.appendChild(name); + textContainer.appendChild(description); + menuItem.appendChild(logo); + menuItem.appendChild(textContainer); + + menu.appendChild(menuItem); + } + + // Add the footer to the menu + const menuFooter = document.createElement("div"); + menuFooter.setAttribute("class", "menu-footer"); + menuFooter.textContent = "Created with ❤️ by Zuplo"; + + menu.appendChild(menuFooter); + + // Append menu to the rightDiv instead of shadow root + rightDiv.appendChild(menu); + + // Handle button click to toggle menu visibility + menuButton.addEventListener("click", (event) => { + event.stopPropagation(); + menu.classList.toggle("visible"); + }); + + // Close menu when clicking outside + document.addEventListener("click", (event) => { + if (!this.contains(event.target)) { + menu.classList.remove("visible"); + } + }); + + // Styles + const style = document.createElement("style"); + style.textContent = ` + /* Set font family to Helvetica throughout */ + * { + font-family: 'Helvetica', sans-serif; + box-sizing: border-box; + } + .zuplo-banner { + display: flex; + justify-content: space-between; + align-items: center; + background-color: white; + color: black; + padding: 10px 20px; + width: 100%; + flex-wrap: nowrap; /* Prevent wrapping */ + } + .left { + display: flex; + align-items: center; + } + .left .zuplo-logo { + height: 20px; + margin-left: 10px; + display: flex; + align-items: center; /* Center vertically */ + position: relative; + top: 1px; /* Move down slightly */ + } + + .left .zuplo-logo:hover svg path { + fill: #FF00BD; + } + .left .zuplo-logo svg { + height: 100%; + width: auto; + } + .right { + position: relative; + display: flex; + align-items: center; + } + .menu-button { + display: flex; + align-items: center; + background-color: #ff00bd; + color: white; + border: none; + padding: 10px 16px; + border-radius: 5px; + cursor: pointer; + font-size: 16px; + line-height: 1; + white-space: nowrap; /* Prevent text wrapping */ + } + .menu-button .button-content { + display: flex; + align-items: center; + justify-content: center; + } + .menu-button svg { + width: 20px; + height: 20px; + margin-right: 8px; + } + .menu-button .button-text { + display: inline-block; + } + .menu { + display: none; + position: absolute; + right: 0; + top: calc(100% + 5px); /* Place below the button with 5px margin */ + background-color: white; + color: black; + box-shadow: 0 4px 10px rgba(0,0,0,0.1); /* Add shadow */ + z-index: 9999; /* High z-index */ + width: auto; /* Adjust width */ + min-width: 200px; /* Optional: set a minimum width */ + padding: 10px; + } + .menu.visible { + display: block; + } + .menu-item { + display: flex; + align-items: center; + text-decoration: none; + color: black; + padding: 10px 0; + border-bottom: 1px solid #eee; + } + .menu-item:last-child { + border-bottom: none; + } + .menu-item img { + width: 40px; + height: 40px; + margin-right: 10px; + } + .text-container { + display: flex; + flex-direction: column; + } + .tool-name { + font-weight: bold; + white-space: nowrap; /* Prevent wrapping */ + } + .menu-item:hover { + color: #FF00BD; + } + .tool-description { + font-size: 12px; + color: #666; + } + .menu-footer { + text-align: center; + margin-top: 10px; + font-size: 12px; + color: #666; + } + .open-source-text { + font-size: 16px; + } + /* Responsive */ + @media (max-width: 600px) { + .zuplo-banner { + flex-direction: column; /* Stack left content below right */ + align-items: center; /* Center horizontally */ + } + .left { + margin-top: 0; + } + .right { + margin-top: 10px; + width: auto; + justify-content: center; + } + .menu { + right: 0; + width: auto; /* Ensure menu is as wide as needed */ + } + } + `; + + // Handle mode attribute + const mode = this.getAttribute("mode") || "light"; + + if (mode === "dark") { + style.textContent += ` + .zuplo-banner { + background-color: black; + color: white; + } + .menu { + background-color: black; + color: white; + } + .menu-item { + color: white; + } + .menu-footer { + color: #ccc; + } + .menu-button { + background-color: #ff00bd; + color: white; + } + /* Invert Zuplo logo for dark mode */ + .left .zuplo-logo svg path { + fill: white; + :hover { + fill: #FF00BD; + } + } + `; + } else { + /* Set Zuplo logo color for light mode */ + style.textContent += ` + .left .zuplo-logo svg path { + fill: black; + :hover { + fill: #FF00BD; + } + } + `; + } + + // Append styles to shadow root + shadow.appendChild(style); + } + + // Method to return the Zuplo SVG logo + getZuploLogoSVG() { + return ` + + `; + } +} + +// Define the custom element +customElements.define("zuplo-banner", ZuploBanner); diff --git a/apps/web/src/app/global.d.ts b/apps/web/src/app/global.d.ts new file mode 100644 index 00000000..912aad60 --- /dev/null +++ b/apps/web/src/app/global.d.ts @@ -0,0 +1,10 @@ +declare namespace JSX { + interface IntrinsicElements { + "zuplo-banner": React.DetailedHTMLProps< + React.HTMLAttributes, + HTMLElement + > & { + mode?: "dark" | "light"; + }; + } +} diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index ca7d2378..901cc93c 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -44,20 +44,30 @@ const RootLayout = ({ children }: { children: React.ReactNode }) => ( lang="en" className={`${roboto.variable} ${robotoMono.variable} ${ibmPlexSans.variable}`} > - {process.env.NEXT_PUBLIC_ANALYTICS_URL ? ( - + + - -
-
- {children} + +
+ +
+
+
+
+ {children} +
+
-
diff --git a/apps/web/src/components/Header/index.tsx b/apps/web/src/components/Header/index.tsx index e554bdc3..6a64e24e 100644 --- a/apps/web/src/components/Header/index.tsx +++ b/apps/web/src/components/Header/index.tsx @@ -1,4 +1,5 @@ -import LogoIcon from "../LogoIcon"; +import Link from "next/link"; +import Image from "next/image"; const Header = () => (
@@ -10,9 +11,33 @@ const Header = () => (
- - - +
); diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index 0cf56e1f..a00dd608 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -24,7 +24,13 @@ "@/*": ["./src/*"] } }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + "src/components/ZuploBanner/zuplo-banner.js" + ], "exclude": ["node_modules"], "references": [{ "path": "../../packages/core" }] }