base
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Ensures that a URL has a trailing slash while preserving query parameters and fragments
|
||||
* @param url - The URL to process
|
||||
* @returns The URL with a trailing slash added to the pathname (if not already present)
|
||||
*/
|
||||
export function ensureTrailingSlash(url: string): string {
|
||||
try {
|
||||
const fallbackBaseUrl =
|
||||
typeof window !== "undefined" && window.location.origin ? window.location.origin : "http://dummy.com";
|
||||
// Handle relative URLs by creating a URL object with a fallback base URL
|
||||
const urlObj = new URL(url, fallbackBaseUrl);
|
||||
|
||||
// Don't modify root path
|
||||
if (urlObj.pathname === "/") {
|
||||
return url;
|
||||
}
|
||||
|
||||
// Add trailing slash if it doesn't exist
|
||||
if (!urlObj.pathname.endsWith("/")) {
|
||||
urlObj.pathname += "/";
|
||||
}
|
||||
|
||||
// For relative URLs, return just the path + search + hash
|
||||
if (url.startsWith("/")) {
|
||||
return urlObj.pathname + urlObj.search + urlObj.hash;
|
||||
}
|
||||
|
||||
// For absolute URLs, return the full URL
|
||||
return urlObj.toString();
|
||||
} catch (error) {
|
||||
// If URL parsing fails, return the original URL
|
||||
console.warn("Failed to parse URL for trailing slash enforcement:", url, error);
|
||||
return url;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
|
||||
// Minimal shim so code using next/image compiles under React Router + Vite
|
||||
// without changing call sites. It just renders a native img.
|
||||
|
||||
type NextImageProps = React.ImgHTMLAttributes<HTMLImageElement> & {
|
||||
src: string;
|
||||
fill?: boolean;
|
||||
priority?: boolean;
|
||||
quality?: number;
|
||||
placeholder?: "blur" | "empty";
|
||||
blurDataURL?: string;
|
||||
};
|
||||
|
||||
function Image({
|
||||
src,
|
||||
alt = "",
|
||||
fill,
|
||||
priority: _priority,
|
||||
quality: _quality,
|
||||
placeholder: _placeholder,
|
||||
blurDataURL: _blurDataURL,
|
||||
...rest
|
||||
}: NextImageProps) {
|
||||
// If fill is true, apply object-fit styles
|
||||
const style = fill ? { objectFit: "cover" as const, width: "100%", height: "100%" } : rest.style;
|
||||
|
||||
return <img src={src} alt={alt} {...rest} style={style} />;
|
||||
}
|
||||
|
||||
export default Image;
|
||||
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { Link as RRLink } from "react-router";
|
||||
import { ensureTrailingSlash } from "./helper";
|
||||
|
||||
type NextLinkProps = React.ComponentProps<"a"> & {
|
||||
href: string;
|
||||
replace?: boolean;
|
||||
prefetch?: boolean; // next.js prop, ignored
|
||||
scroll?: boolean; // next.js prop, ignored
|
||||
shallow?: boolean; // next.js prop, ignored
|
||||
};
|
||||
|
||||
function Link({ href, replace, prefetch: _prefetch, scroll: _scroll, shallow: _shallow, ...rest }: NextLinkProps) {
|
||||
return <RRLink to={ensureTrailingSlash(href)} replace={replace} {...rest} />;
|
||||
}
|
||||
|
||||
export default Link;
|
||||
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { useLocation, useNavigate, useParams as useParamsRR, useSearchParams as useSearchParamsRR } from "react-router";
|
||||
import { ensureTrailingSlash } from "./helper";
|
||||
|
||||
export function useRouter() {
|
||||
const navigate = useNavigate();
|
||||
return useMemo(
|
||||
() => ({
|
||||
push: (to: string) => {
|
||||
// Defer navigation to avoid state updates during render
|
||||
setTimeout(() => navigate(ensureTrailingSlash(to)), 0);
|
||||
},
|
||||
replace: (to: string) => {
|
||||
// Defer navigation to avoid state updates during render
|
||||
setTimeout(() => navigate(ensureTrailingSlash(to), { replace: true }), 0);
|
||||
},
|
||||
back: () => {
|
||||
setTimeout(() => navigate(-1), 0);
|
||||
},
|
||||
forward: () => {
|
||||
setTimeout(() => navigate(1), 0);
|
||||
},
|
||||
refresh: () => {
|
||||
location.reload();
|
||||
},
|
||||
prefetch: async (_to: string) => {
|
||||
// no-op in this shim
|
||||
},
|
||||
}),
|
||||
[navigate]
|
||||
);
|
||||
}
|
||||
|
||||
export function usePathname(): string {
|
||||
const { pathname } = useLocation();
|
||||
return pathname;
|
||||
}
|
||||
|
||||
export function useSearchParams(): URLSearchParams {
|
||||
const [searchParams] = useSearchParamsRR();
|
||||
return searchParams;
|
||||
}
|
||||
|
||||
export function useParams() {
|
||||
return useParamsRR();
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import { useEffect } from "react";
|
||||
|
||||
type ScriptProps = {
|
||||
src?: string;
|
||||
id?: string;
|
||||
strategy?: "beforeInteractive" | "afterInteractive" | "lazyOnload" | "worker";
|
||||
onLoad?: () => void;
|
||||
onError?: () => void;
|
||||
children?: string;
|
||||
defer?: boolean;
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
// Minimal shim for next/script that creates a script tag
|
||||
function Script({ src, id, strategy: _strategy, onLoad, onError, children, ...rest }: ScriptProps) {
|
||||
useEffect(() => {
|
||||
if (src) {
|
||||
const script = document.createElement("script");
|
||||
if (id) script.id = id;
|
||||
script.src = src;
|
||||
if (onLoad) script.onload = onLoad;
|
||||
if (onError) script.onerror = onError;
|
||||
Object.keys(rest).forEach((key) => {
|
||||
script.setAttribute(key, rest[key]);
|
||||
});
|
||||
document.body.appendChild(script);
|
||||
|
||||
return () => {
|
||||
if (script.parentNode) {
|
||||
document.body.removeChild(script);
|
||||
}
|
||||
};
|
||||
} else if (children) {
|
||||
const script = document.createElement("script");
|
||||
if (id) script.id = id;
|
||||
script.textContent = children;
|
||||
Object.keys(rest).forEach((key) => {
|
||||
script.setAttribute(key, rest[key]);
|
||||
});
|
||||
document.body.appendChild(script);
|
||||
|
||||
return () => {
|
||||
if (script.parentNode) {
|
||||
document.body.removeChild(script);
|
||||
}
|
||||
};
|
||||
}
|
||||
}, [src, id, children, onLoad, onError, rest]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export default Script;
|
||||
Reference in New Issue
Block a user