/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
/**
* Tiny utilities shared by every landing-page variant:
*
* • useFonts(href) — injects a Google Fonts only while a /mcp page
* is mounted, so the global app stays unbothered.
* • useCopyToClipboard() — returns [copy, isJustCopied] for the install
* snippets and recipe cards.
* • useDocumentMeta(title, themeColor)
* — keeps
/ theme-color in sync per variant.
*
* None of these touch React Suspense / global state — they’re plain
* useEffect plumbing so the variants can be lifted out of the chooser
* without surprises.
*/
import { useEffect, useState } from 'react';
/** Inject a stylesheet while this hook is mounted. Idempotent. */
export function useFonts(...hrefs: string[]): void {
useEffect(() => {
const tags: HTMLLinkElement[] = [];
for (const href of hrefs) {
const existing = document.head.querySelector(`link[data-mcp-font="${href}"]`);
if (existing) {
// Already injected by another variant — refcount via a data attribute.
const refs = Number(existing.getAttribute('data-refs') ?? '1') + 1;
existing.setAttribute('data-refs', String(refs));
tags.push(existing as HTMLLinkElement);
continue;
}
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = href;
link.setAttribute('data-mcp-font', href);
link.setAttribute('data-refs', '1');
document.head.appendChild(link);
tags.push(link);
}
return () => {
for (const tag of tags) {
const refs = Number(tag.getAttribute('data-refs') ?? '1') - 1;
if (refs <= 0) tag.parentNode?.removeChild(tag);
else tag.setAttribute('data-refs', String(refs));
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [hrefs.join('|')]);
}
/** Browser-safe clipboard with a 1.4s "just copied" indicator. */
export function useCopyToClipboard(): {
copy: (text: string, key?: string) => Promise;
copiedKey: string | null;
} {
const [copiedKey, setCopiedKey] = useState(null);
return {
copiedKey,
copy: async (text: string, key?: string) => {
try {
await navigator.clipboard.writeText(text);
setCopiedKey(key ?? text);
setTimeout(() => setCopiedKey((curr) => (curr === (key ?? text) ? null : curr)), 1400);
} catch {
// Older browsers — fall through silently. The install dialogs always
// show the snippet anyway so the user can manually select+copy.
}
},
};
}
/** Set + theme-color while mounted; restore on unmount. */
export function useDocumentMeta(title: string, themeColor?: string): void {
useEffect(() => {
const prevTitle = document.title;
document.title = title;
let prevTheme: string | null = null;
let prevThemeExisted = false;
let themeMeta: HTMLMetaElement | null = null;
if (themeColor) {
themeMeta = document.querySelector('meta[name="theme-color"]');
if (themeMeta) {
// Track existence separately so a meta tag without `content`
// doesn't permanently keep our temporary color after unmount.
prevThemeExisted = themeMeta.hasAttribute('content');
prevTheme = themeMeta.getAttribute('content');
themeMeta.setAttribute('content', themeColor);
}
}
return () => {
document.title = prevTitle;
if (themeMeta) {
if (prevThemeExisted && prevTheme !== null) themeMeta.setAttribute('content', prevTheme);
else themeMeta.removeAttribute('content');
}
};
}, [title, themeColor]);
}
/** Smooth-scroll to an in-page anchor; updates the URL hash. */
export function scrollToAnchor(id: string): void {
const el = document.getElementById(id);
if (!el) return;
el.scrollIntoView({ behavior: 'smooth', block: 'start' });
history.replaceState(null, '', `#${id}`);
}