-
-
Notifications
You must be signed in to change notification settings - Fork 66
docs: add cross-site announcement banner #857
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
Changes from all commits
7792edc
8be55d1
1015829
9b7a45d
240c4d7
dea56f9
741a9db
6851fce
93d7361
622e8db
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| <template> | ||
| <footer class="EndevFooter"> | ||
| <span>MIT License</span> | ||
| <span aria-hidden="true">·</span> | ||
| <span>Copyright © {{ year }}</span> | ||
| <span aria-hidden="true">·</span> | ||
| <a class="EndevFooterLink" href="https://en.dev"> | ||
| <img | ||
| alt="" | ||
| class="EndevFooterLogo" | ||
| height="28" | ||
| src="https://github.com/endevco.png?size=96" | ||
| width="28" | ||
| /> | ||
| <span>en.dev</span> | ||
| </a> | ||
| </footer> | ||
| </template> | ||
|
|
||
| <script setup> | ||
| const year = new Date().getFullYear(); | ||
| </script> | ||
|
|
||
| <style scoped> | ||
| .EndevFooter { | ||
| align-items: center; | ||
| border-top: 1px solid var(--vp-c-divider); | ||
| color: var(--vp-c-text-2); | ||
| display: flex; | ||
| flex-wrap: wrap; | ||
| font-size: 14px; | ||
| gap: 8px; | ||
| justify-content: center; | ||
| line-height: 28px; | ||
| margin-top: auto; | ||
| padding: 22px 24px 26px; | ||
| text-align: center; | ||
| } | ||
| .EndevFooterLink { | ||
| align-items: center; | ||
| color: inherit; | ||
| display: inline-flex; | ||
| gap: 8px; | ||
| line-height: 28px; | ||
| text-decoration: none; | ||
| transition: color 0.2s ease; | ||
| } | ||
| .EndevFooterLink:hover { | ||
| color: var(--vp-c-brand-1); | ||
| } | ||
| .EndevFooterLogo { | ||
| border-radius: 8px; | ||
| display: inline-block; | ||
| height: 28px; | ||
| vertical-align: middle; | ||
| width: 28px; | ||
| } | ||
| </style> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| .jdx-banner { | ||
| position: fixed; | ||
| top: 0; | ||
| left: 0; | ||
| right: 0; | ||
| z-index: 1001; | ||
| display: flex; | ||
| gap: 0.75rem; | ||
| align-items: center; | ||
| justify-content: center; | ||
| padding: 0.5rem 2.75rem; | ||
| background: var(--vp-c-brand-1, #3abff8); | ||
| color: #000; | ||
| font-size: 0.9rem; | ||
| line-height: 1.4; | ||
| text-align: center; | ||
| } | ||
| .jdx-banner a { | ||
| color: #000; | ||
| text-decoration: underline; | ||
| font-weight: 600; | ||
| } | ||
| .jdx-banner button { | ||
| position: absolute; | ||
| right: 0.5rem; | ||
| top: 50%; | ||
| transform: translateY(-50%); | ||
| background: transparent; | ||
| border: 0; | ||
| color: inherit; | ||
| font-size: 1.25rem; | ||
| cursor: pointer; | ||
| line-height: 1; | ||
| padding: 0 0.5rem; | ||
| opacity: 0.7; | ||
| } | ||
| .jdx-banner button:hover { | ||
| opacity: 1; | ||
| } | ||
| @media (max-width: 640px) { | ||
| .jdx-banner { | ||
| font-size: 0.85rem; | ||
| padding: 0.4rem 2.5rem; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| import "./banner.css"; | ||
|
|
||
| interface BannerData { | ||
| id: string; | ||
| enabled: boolean; | ||
| message: string; | ||
| link?: string; | ||
| linkText?: string; | ||
| } | ||
|
|
||
| const ENDPOINT = "https://jdx.dev/banner.json"; | ||
| const STORAGE_KEY = "jdx-banner-dismissed"; | ||
|
|
||
| export function initBanner(): void { | ||
| if (typeof window === "undefined") return; | ||
| fetch(ENDPOINT) | ||
| .then((r) => (r.ok ? (r.json() as Promise<BannerData>) : null)) | ||
| .then((b) => { | ||
| if (!b || !b.enabled) return; | ||
| if (localStorage.getItem(STORAGE_KEY) === b.id) return; | ||
| render(b); | ||
| }) | ||
| .catch(() => {}); | ||
| } | ||
|
|
||
| function isHttpUrl(value: string): boolean { | ||
| try { | ||
| const u = new URL(value, window.location.href); | ||
| return u.protocol === "http:" || u.protocol === "https:"; | ||
| } catch { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| function render(b: BannerData): void { | ||
| const el = document.createElement("div"); | ||
| el.className = "jdx-banner"; | ||
| el.setAttribute("role", "region"); | ||
| el.setAttribute("aria-label", "Site announcement"); | ||
|
|
||
| const msg = document.createElement("span"); | ||
| msg.textContent = b.message; | ||
| el.appendChild(msg); | ||
|
|
||
| if (b.link && isHttpUrl(b.link)) { | ||
| const a = document.createElement("a"); | ||
| a.href = b.link; | ||
| a.target = "_blank"; | ||
| a.rel = "noopener"; | ||
| a.textContent = b.linkText || "Learn more"; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Similar to the retrieval, setting an item in try {
localStorage.setItem(STORAGE_KEY, b.id)
} catch (e) {
// ignore storage errors
} |
||
| el.appendChild(a); | ||
| } | ||
|
|
||
| const syncHeight = () => { | ||
| document.documentElement.style.setProperty( | ||
| "--vp-layout-top-height", | ||
| `${el.offsetHeight}px`, | ||
| ); | ||
| }; | ||
|
|
||
| const observer = | ||
| typeof ResizeObserver !== "undefined" | ||
| ? new ResizeObserver(syncHeight) | ||
| : null; | ||
|
|
||
| const btn = document.createElement("button"); | ||
| btn.type = "button"; | ||
| btn.setAttribute("aria-label", "Dismiss"); | ||
| btn.textContent = "\u00d7"; | ||
| btn.addEventListener("click", () => { | ||
| localStorage.setItem(STORAGE_KEY, b.id); | ||
| observer?.disconnect(); | ||
| el.remove(); | ||
| document.documentElement.style.removeProperty("--vp-layout-top-height"); | ||
| }); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unhandled localStorage.setItem exception blocks banner dismissalLow Severity The dismiss button's click handler calls Reviewed by Cursor Bugbot for commit 622e8db. Configure here. |
||
| el.appendChild(btn); | ||
|
|
||
| document.body.prepend(el); | ||
|
|
||
| requestAnimationFrame(syncHeight); | ||
| observer?.observe(el); | ||
| } | ||


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Accessing
localStoragecan throw an error in some environments (e.g., if cookies/storage are blocked or in certain private browsing modes). It is safer to wrap this check in a try-catch block to ensure the banner logic doesn't crash the application initialization.