@zapstra/core-router
Version:
Query-preserving router for Zapstra platform apps
216 lines (214 loc) • 6.79 kB
JavaScript
import { useNavigate, useLocation, useNavigation } from '@remix-run/react';
import { createContext, useState, useRef, useEffect, useContext } from 'react';
import { jsxs, jsx } from 'react/jsx-runtime';
// src/navigation-utils.ts
function navigateWithQuery(navigate, path, search) {
navigate(path + (search || ""));
}
function useNavigateWithQuery() {
const navigate = useNavigate();
const { search } = useLocation();
return (path) => navigateWithQuery(navigate, path, search);
}
function useQueryParams() {
const { search } = useLocation();
const params = new URLSearchParams(search);
const result = {};
for (const [key, value] of params.entries()) {
result[key] = value;
}
return result;
}
function useIsPathActive() {
const location = useLocation();
return (path, exact = false) => {
if (exact) {
return location.pathname === path;
}
if (path === "/app") {
return location.pathname === "/app";
}
return location.pathname.startsWith(path);
};
}
function GradientProgressSpinner({
isLoading,
size = 80,
strokeWidth = 4
}) {
const [progress, setProgress] = useState(0);
const intervalRef = useRef(null);
const progressRef = useRef(0);
const radius = (size - strokeWidth * 2) / 2;
const circumference = radius * 2 * Math.PI;
const center = size / 2;
useEffect(() => {
if (isLoading) {
setProgress(0);
progressRef.current = 0;
let currentProgress = 0;
intervalRef.current = window.setInterval(() => {
currentProgress += (100 - currentProgress) * 0.1;
if (currentProgress > 95) {
currentProgress = 95;
}
progressRef.current = Math.floor(currentProgress);
setProgress(progressRef.current);
}, 100);
} else if (!isLoading) {
if (progressRef.current > 0 && progressRef.current < 100) {
setProgress(100);
progressRef.current = 100;
setTimeout(() => {
setProgress(0);
progressRef.current = 0;
}, 300);
}
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
}
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
};
}, [isLoading]);
if (!isLoading && progress === 0) return null;
const strokeDashoffset = circumference - progress / 100 * circumference;
return /* @__PURE__ */ jsxs(
"div",
{
style: {
position: "fixed",
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: "rgba(255, 255, 255, 0.9)",
backdropFilter: "blur(4px)",
zIndex: 999,
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
transition: "opacity 0.3s ease",
opacity: isLoading || progress > 0 ? 1 : 0,
pointerEvents: isLoading ? "auto" : "none"
},
children: [
/* @__PURE__ */ jsxs("div", { style: { position: "relative" }, children: [
/* @__PURE__ */ jsxs("svg", { width: size, height: size, style: { transform: "rotate(-90deg)" }, children: [
/* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsxs("linearGradient", { id: "progress-gradient", x1: "0%", y1: "0%", x2: "100%", y2: "100%", children: [
/* @__PURE__ */ jsx("stop", { offset: "0%", stopColor: "#218dfd" }),
/* @__PURE__ */ jsx("stop", { offset: "50%", stopColor: "#4a9fff" }),
/* @__PURE__ */ jsx("stop", { offset: "100%", stopColor: "#20e3b2" })
] }) }),
/* @__PURE__ */ jsx(
"circle",
{
cx: center,
cy: center,
r: radius,
fill: "none",
stroke: "#e5e7eb",
strokeWidth
}
),
/* @__PURE__ */ jsx(
"circle",
{
cx: center,
cy: center,
r: radius,
fill: "none",
stroke: "url(#progress-gradient)",
strokeWidth,
strokeLinecap: "round",
strokeDasharray: circumference,
strokeDashoffset,
style: {
transition: "stroke-dashoffset 0.2s ease",
filter: "drop-shadow(0 0 3px rgba(33, 141, 253, 0.4))"
}
}
)
] }),
/* @__PURE__ */ jsx(
"div",
{
style: {
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
textAlign: "center"
},
children: /* @__PURE__ */ jsxs(
"div",
{
style: {
fontSize: "20px",
fontWeight: 600,
background: "linear-gradient(135deg, #218dfd 0%, #20e3b2 100%)",
WebkitBackgroundClip: "text",
WebkitTextFillColor: "transparent",
backgroundClip: "text"
},
children: [
progress,
"%"
]
}
)
}
)
] }),
/* @__PURE__ */ jsx("div", { style: {
marginTop: "20px",
fontSize: "14px",
color: "#6b7280",
fontWeight: 500
}, children: "Loading..." })
]
}
);
}
var RouterCtx = createContext(null);
function AppRouterProvider({
children,
showLoadingSpinner = true,
spinnerProps
}) {
const navigation = useNavigation();
const navWithQuery = useNavigateWithQuery();
const ctx = {
route: navWithQuery,
isNavigating: navigation.state !== "idle"
};
return /* @__PURE__ */ jsxs(RouterCtx.Provider, { value: ctx, children: [
showLoadingSpinner && /* @__PURE__ */ jsx(
GradientProgressSpinner,
{
isLoading: ctx.isNavigating,
...spinnerProps
}
),
children
] });
}
function useAppRouter() {
const ctx = useContext(RouterCtx);
if (!ctx) {
throw new Error("useAppRouter must be used inside <AppRouterProvider>");
}
return ctx;
}
function withAppRouter(Component, options) {
return function WrappedComponent(props) {
return /* @__PURE__ */ jsx(AppRouterProvider, { ...options, children: /* @__PURE__ */ jsx(Component, { ...props }) });
};
}
export { AppRouterProvider, GradientProgressSpinner, navigateWithQuery, useAppRouter, useIsPathActive, useNavigateWithQuery, useQueryParams, withAppRouter };
//# sourceMappingURL=index.js.map
//# sourceMappingURL=index.js.map