Merge pull request #282 from presenton/fix/nextjs-import-issues
fix: adds missing modules in nextjs
This commit is contained in:
commit
3dc5a22a62
1 changed files with 204 additions and 0 deletions
204
servers/nextjs/app/hooks/useRemoteSvgIcon.tsx
Normal file
204
servers/nextjs/app/hooks/useRemoteSvgIcon.tsx
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
import React from "react";
|
||||
|
||||
export type RemoteSvgOptions = {
|
||||
strokeColor?: string;
|
||||
fillColor?: string;
|
||||
className?: string;
|
||||
title?: string;
|
||||
color?: string;
|
||||
|
||||
};
|
||||
|
||||
function transformSvg(svgText: string, options: RemoteSvgOptions): string {
|
||||
try {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(svgText, "image/svg+xml");
|
||||
const svgEl = doc.querySelector("svg");
|
||||
if (!svgEl) return svgText;
|
||||
svgEl.style.outline = "none";
|
||||
svgEl.style.border = "none";
|
||||
svgEl.style.margin = "0";
|
||||
svgEl.style.padding = "0";
|
||||
svgEl.style.display = "inline-block";
|
||||
svgEl.style.verticalAlign = "middle";
|
||||
svgEl.style.overflow = "visible";
|
||||
svgEl.style.position = "relative";
|
||||
// Set only provided attributes to avoid clobbering inner shapes
|
||||
if (options.className) svgEl.setAttribute("class", options.className);
|
||||
if (options.strokeColor) svgEl.setAttribute("stroke", options.strokeColor);
|
||||
if (options.fillColor !== undefined) svgEl.setAttribute("fill", options.fillColor);
|
||||
|
||||
|
||||
const viewBox = svgEl.getAttribute("viewBox");
|
||||
let vbX = 0, vbY = 0, vbW = 0, vbH = 0;
|
||||
if (viewBox) {
|
||||
const parts = viewBox.split(/\s+/).map((n) => Number(n));
|
||||
if (parts.length === 4 && parts.every((n) => !Number.isNaN(n))) {
|
||||
vbX = parts[0];
|
||||
vbY = parts[1];
|
||||
vbW = parts[2];
|
||||
vbH = parts[3];
|
||||
}
|
||||
}
|
||||
// Only consider direct child rects; safer heuristic
|
||||
const rects = Array.from(svgEl.querySelectorAll("rect")).filter((r) => r.parentNode === svgEl);
|
||||
rects.forEach((r) => {
|
||||
const xAttr = r.getAttribute("x") || "0";
|
||||
const yAttr = r.getAttribute("y") || "0";
|
||||
const wAttr = r.getAttribute("width") || "";
|
||||
const hAttr = r.getAttribute("height") || "";
|
||||
const fill = r.getAttribute("fill");
|
||||
|
||||
const x = Number(xAttr);
|
||||
const y = Number(yAttr);
|
||||
const w = Number(wAttr);
|
||||
const h = Number(hAttr);
|
||||
|
||||
const isExactHundredPercent = wAttr === "100%" && hAttr === "100%" && (xAttr === "0" || xAttr === "0%") && (yAttr === "0" || yAttr === "0%");
|
||||
const approximatelyCoversViewBox = (
|
||||
vbW > 0 && vbH > 0 &&
|
||||
!Number.isNaN(w) && !Number.isNaN(h) &&
|
||||
Math.abs(w - vbW) <= Math.max(1, vbW * 0.02) &&
|
||||
Math.abs(h - vbH) <= Math.max(1, vbH * 0.02) &&
|
||||
Math.abs(x - vbX) <= Math.max(1, vbW * 0.02) &&
|
||||
Math.abs(y - vbY) <= Math.max(1, vbH * 0.02)
|
||||
);
|
||||
const noFill = (fill === null || fill === "none" || fill === "transparent");
|
||||
|
||||
const looksLikeFrame = noFill && (isExactHundredPercent || approximatelyCoversViewBox);
|
||||
if (looksLikeFrame) {
|
||||
r.parentElement?.removeChild(r);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
return svgEl.outerHTML;
|
||||
} catch {
|
||||
return svgText;
|
||||
}
|
||||
}
|
||||
|
||||
// Simple in-memory LRU cache for transformed SVG markup
|
||||
const SVG_CACHE_LIMIT = 15;
|
||||
const svgCache: Map<string, string> = new Map();
|
||||
|
||||
function makeCacheKey(url: string, options: RemoteSvgOptions): string {
|
||||
return [
|
||||
url,
|
||||
`sc=${options.strokeColor || ""}`,
|
||||
`fc=${options.fillColor || ""}`,
|
||||
`cls=${options.className || ""}`,
|
||||
|
||||
].join("|");
|
||||
}
|
||||
|
||||
function cacheGet(key: string): string | undefined {
|
||||
const value = svgCache.get(key);
|
||||
if (value !== undefined) {
|
||||
// refresh LRU order
|
||||
svgCache.delete(key);
|
||||
svgCache.set(key, value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function cacheSet(key: string, value: string) {
|
||||
if (svgCache.has(key)) svgCache.delete(key);
|
||||
svgCache.set(key, value);
|
||||
if (svgCache.size > SVG_CACHE_LIMIT) {
|
||||
const oldestKey = svgCache.keys().next().value as string | undefined;
|
||||
if (oldestKey !== undefined) svgCache.delete(oldestKey);
|
||||
}
|
||||
}
|
||||
|
||||
export function useRemoteSvgIcon(url?: string, options: RemoteSvgOptions = {}) {
|
||||
const [svgMarkup, setSvgMarkup] = React.useState<string | null>(null);
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
let cancelled = false;
|
||||
async function run() {
|
||||
if (!url) {
|
||||
// build simple fallback svg
|
||||
const stroke = options.strokeColor || "currentColor";
|
||||
const fill = options.fillColor ?? "none";
|
||||
const cls = options.className ? ` class=\"${options.className}\"` : "";
|
||||
setSvgMarkup(`<svg${cls} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' stroke='${stroke}' fill='${fill}' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><circle cx='12' cy='12' r='10' fill='currentColor' opacity='0.12'></circle><path d='M8 12l3 3 5-6' fill='none'></path></svg>`);
|
||||
return;
|
||||
}
|
||||
// non-svg extensions fallback
|
||||
if (/\.(png|jpe?g|gif|webp)(\?.*)?$/i.test(url)) {
|
||||
const stroke = options.strokeColor || "currentColor";
|
||||
const fill = options.fillColor ?? "none";
|
||||
const cls = options.className ? ` class=\"${options.className}\"` : "";
|
||||
setSvgMarkup(`<svg${cls} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' stroke='${stroke}' fill='${fill}' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><circle cx='12' cy='12' r='10' fill='currentColor' opacity='0.12'></circle><path d='M8 12l3 3 5-6' fill='none'></path></svg>`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Cache lookup
|
||||
const cacheKey = makeCacheKey(url, options);
|
||||
const cached = cacheGet(cacheKey);
|
||||
if (cached) {
|
||||
setSvgMarkup(cached);
|
||||
setError(null);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) {
|
||||
throw new Error(`HTTP ${res.status}`);
|
||||
}
|
||||
const ct = res.headers.get("content-type") || "";
|
||||
if (ct && !ct.includes("svg")) {
|
||||
throw new Error(`Non-SVG content: ${ct}`);
|
||||
}
|
||||
const text = await res.text();
|
||||
if (cancelled) return;
|
||||
const transformed = transformSvg(text, options);
|
||||
cacheSet(cacheKey, transformed);
|
||||
setSvgMarkup(transformed);
|
||||
setError(null);
|
||||
} catch (e: any) {
|
||||
if (cancelled) return;
|
||||
setError(e?.message || "Failed to load SVG");
|
||||
const stroke = options.strokeColor || "currentColor";
|
||||
const fill = options.fillColor ?? "none";
|
||||
const cls = options.className ? ` class=\"${options.className}\"` : "";
|
||||
setSvgMarkup(`<svg${cls} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' stroke='${stroke}' fill='${fill}' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><circle cx='12' cy='12' r='10' fill='currentColor' opacity='0.12'></circle><path d='M8 12l3 3 5-6' fill='none'></path></svg>`);
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn("RemoteSvgIcon fetch error", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
run();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [url, options.strokeColor, options.fillColor, options.className]);
|
||||
|
||||
return { svgMarkup, error };
|
||||
}
|
||||
|
||||
export const RemoteSvgIcon: React.FC<{
|
||||
url?: string;
|
||||
strokeColor?: string;
|
||||
fillColor?: string;
|
||||
className?: string;
|
||||
title?: string;
|
||||
color?: string;
|
||||
}> = ({ url, strokeColor, fillColor, className, title, color }) => {
|
||||
const { svgMarkup } = useRemoteSvgIcon(url, { strokeColor, fillColor, className, title, color });
|
||||
if (!svgMarkup) return null;
|
||||
return (
|
||||
<span
|
||||
data-path={url}
|
||||
role={title ? "img" : undefined}
|
||||
aria-label={title}
|
||||
dangerouslySetInnerHTML={{ __html: svgMarkup }}
|
||||
style={{ display: "inline-block", color: color }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Loading…
Add table
Reference in a new issue