mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-10-04 02:33:47 +10:00
refactor(web): drop server-rendering guards from the client-only app
This commit is contained in:
@@ -59,8 +59,6 @@ function ChipDragOverlay({ activeChip }: ChipDragOverlayProps) {
|
||||
<DragOverlay dropAnimation={null}>{activeChip ? <ChipDragPreview chip={activeChip} /> : null}</DragOverlay>
|
||||
);
|
||||
|
||||
if (typeof document === "undefined") return overlay;
|
||||
|
||||
return createPortal(overlay, document.body);
|
||||
}
|
||||
|
||||
|
||||
@@ -359,7 +359,6 @@ const parsedTablesMatchSource = (sourceTables: readonly string[], tables: readon
|
||||
};
|
||||
|
||||
export const hasUnsupportedTableMarkup = (html: string) => {
|
||||
if (typeof DOMParser === "undefined") return false;
|
||||
const tableMarkers = tableMarkersFrom(html);
|
||||
if (tableMarkers.length === 0) return false;
|
||||
const document = new DOMParser().parseFromString(html, "text/html");
|
||||
|
||||
@@ -95,7 +95,6 @@ export function LoginPage({ disableEmailAuth, disableSignups }: Props) {
|
||||
|
||||
useEffect(() => {
|
||||
if (!("passkey" in providers)) return;
|
||||
if (typeof window === "undefined") return;
|
||||
if (!("PublicKeyCredential" in window)) return;
|
||||
if (!PublicKeyCredential.isConditionalMediationAvailable) return;
|
||||
if (hasStartedConditionalPasskeyRef.current) return;
|
||||
|
||||
@@ -182,7 +182,6 @@ function getResumeQueryKey(id: string): QueryKey {
|
||||
}
|
||||
|
||||
export function isEditableElementFocused(): boolean {
|
||||
if (typeof document === "undefined") return false;
|
||||
const element = document.activeElement as HTMLElement | null;
|
||||
if (!element) return false;
|
||||
return (
|
||||
@@ -211,7 +210,7 @@ function applyDeferredRemoteResume(id: string) {
|
||||
|
||||
const resume = runtime.deferredRemoteResume;
|
||||
runtime.deferredRemoteResume = undefined;
|
||||
if (runtime.deferredFocusHandler && typeof document !== "undefined") {
|
||||
if (runtime.deferredFocusHandler) {
|
||||
document.removeEventListener("focusout", runtime.deferredFocusHandler, true);
|
||||
runtime.deferredFocusHandler = undefined;
|
||||
}
|
||||
@@ -228,7 +227,7 @@ function deferRemoteResumeUntilBlur(id: string, resume: Resume) {
|
||||
const runtime = getRuntime(id);
|
||||
runtime.deferredRemoteResume = resume;
|
||||
|
||||
if (runtime.deferredFocusHandler || typeof document === "undefined") return;
|
||||
if (runtime.deferredFocusHandler) return;
|
||||
|
||||
const handler = () => {
|
||||
// Let focus settle (e.g. tabbing between fields) before deciding editing has ended.
|
||||
@@ -337,17 +336,15 @@ function createRuntime(): Runtime {
|
||||
sessionId: crypto.randomUUID(),
|
||||
};
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
runtime.beforeUnloadHandler = () => runtime.syncResume.flush();
|
||||
window.addEventListener("beforeunload", runtime.beforeUnloadHandler);
|
||||
// Changes made offline are sent as soon as the connection comes back.
|
||||
runtime.onlineHandler = () => {
|
||||
const current = useResumeStore.getState().resume;
|
||||
if (!runtime.hasPendingLocalChanges || !current) return;
|
||||
queueResumeSave(current);
|
||||
};
|
||||
window.addEventListener("online", runtime.onlineHandler);
|
||||
}
|
||||
runtime.beforeUnloadHandler = () => runtime.syncResume.flush();
|
||||
window.addEventListener("beforeunload", runtime.beforeUnloadHandler);
|
||||
// Changes made offline are sent as soon as the connection comes back.
|
||||
runtime.onlineHandler = () => {
|
||||
const current = useResumeStore.getState().resume;
|
||||
if (!runtime.hasPendingLocalChanges || !current) return;
|
||||
queueResumeSave(current);
|
||||
};
|
||||
window.addEventListener("online", runtime.onlineHandler);
|
||||
|
||||
return runtime;
|
||||
}
|
||||
@@ -376,15 +373,15 @@ function cleanupRuntime(id: string) {
|
||||
runtime.syncResume.flush();
|
||||
runtime.abortController.abort();
|
||||
|
||||
if (runtime.beforeUnloadHandler && typeof window !== "undefined") {
|
||||
if (runtime.beforeUnloadHandler) {
|
||||
window.removeEventListener("beforeunload", runtime.beforeUnloadHandler);
|
||||
}
|
||||
|
||||
if (runtime.onlineHandler && typeof window !== "undefined") {
|
||||
if (runtime.onlineHandler) {
|
||||
window.removeEventListener("online", runtime.onlineHandler);
|
||||
}
|
||||
|
||||
if (runtime.deferredFocusHandler && typeof document !== "undefined") {
|
||||
if (runtime.deferredFocusHandler) {
|
||||
document.removeEventListener("focusout", runtime.deferredFocusHandler, true);
|
||||
}
|
||||
|
||||
|
||||
@@ -489,7 +489,7 @@ type CropState = {
|
||||
/** Every photo option: upload with crop, address, show or hide, delete, fit, size, rotation, shape, border and shadow. */
|
||||
export function PictureSettings() {
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const appOrigin = typeof window === "undefined" ? "" : window.location.origin;
|
||||
const appOrigin = window.location.origin;
|
||||
|
||||
const [cropState, setCropState] = useState<CropState | null>(null);
|
||||
const [crop, setCrop] = useState({ x: 0, y: 0 });
|
||||
|
||||
@@ -15,7 +15,7 @@ export const DEFAULT_PDF_PAGE_SIZE: PreviewPageSize = {
|
||||
};
|
||||
|
||||
export const getPreviewCanvasScale = (width: number, height: number) => {
|
||||
const devicePixelRatio = typeof window === "undefined" ? 1 : window.devicePixelRatio || 1;
|
||||
const devicePixelRatio = window.devicePixelRatio || 1;
|
||||
const desiredScale = Math.max(PDF_PAGE_RENDER_SCALE, devicePixelRatio);
|
||||
const desiredPixels = width * height * desiredScale * desiredScale;
|
||||
|
||||
|
||||
@@ -122,7 +122,6 @@ function renderRichTextNode(node: ChildNode, key: string): ReactNode {
|
||||
|
||||
function RichText({ html }: { html: string }) {
|
||||
if (!html.trim()) return null;
|
||||
if (typeof DOMParser === "undefined") return stripHtml(html);
|
||||
|
||||
const body = new DOMParser().parseFromString(html, "text/html").body;
|
||||
return Array.from(body.childNodes).map((node, index) => renderRichTextNode(node, `rich-text-${index}`));
|
||||
@@ -130,7 +129,6 @@ function RichText({ html }: { html: string }) {
|
||||
|
||||
function hasRenderableRichText(html: string): boolean {
|
||||
if (!html.trim()) return false;
|
||||
if (typeof DOMParser === "undefined") return stripHtml(html).trim().length > 0;
|
||||
|
||||
const body = new DOMParser().parseFromString(html, "text/html").body;
|
||||
const getText = (node: ChildNode): string => {
|
||||
|
||||
@@ -128,7 +128,7 @@ export function ProfileSection({ session }: ProfileSectionProps) {
|
||||
const context = useRouteContext({ strict: false });
|
||||
const smtpEnabled = context.flags?.smtpEnabled ?? false;
|
||||
const user = session.user;
|
||||
const host = typeof window === "undefined" ? "" : window.location.host;
|
||||
const host = window.location.host;
|
||||
|
||||
const updateUser = async (patch: Parameters<typeof authClient.updateUser>[0]) => {
|
||||
const { error } = await authClient.updateUser(patch);
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Button } from "@reactive-resume/ui/components/button";
|
||||
import { SettingsSection } from "../section";
|
||||
|
||||
export function McpSection() {
|
||||
const address = typeof window === "undefined" ? "/mcp" : `${window.location.origin}/mcp`;
|
||||
const address = `${window.location.origin}/mcp`;
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [, copy] = useCopyToClipboard();
|
||||
|
||||
|
||||
@@ -18,16 +18,14 @@ export const authClient = createAuthClient({
|
||||
twoFactorClient({
|
||||
onTwoFactorRedirect() {
|
||||
// Redirect to 2FA verification page
|
||||
if (typeof window !== "undefined") {
|
||||
const { callbackURL, reauthenticate } = authSearchSchema.parse({
|
||||
reauthenticate: new URLSearchParams(window.location.search).get("reauthenticate") === "true",
|
||||
callbackURL: new URLSearchParams(window.location.search).get("callbackURL"),
|
||||
});
|
||||
const search = callbackURL
|
||||
? `?${new URLSearchParams({ callbackURL, ...(reauthenticate ? { reauthenticate: "true" } : {}) })}`
|
||||
: "";
|
||||
window.location.href = `/auth/verify-2fa${search}`;
|
||||
}
|
||||
const { callbackURL, reauthenticate } = authSearchSchema.parse({
|
||||
reauthenticate: new URLSearchParams(window.location.search).get("reauthenticate") === "true",
|
||||
callbackURL: new URLSearchParams(window.location.search).get("callbackURL"),
|
||||
});
|
||||
const search = callbackURL
|
||||
? `?${new URLSearchParams({ callbackURL, ...(reauthenticate ? { reauthenticate: "true" } : {}) })}`
|
||||
: "";
|
||||
window.location.href = `/auth/verify-2fa${search}`;
|
||||
},
|
||||
}),
|
||||
oauthProviderClient(),
|
||||
|
||||
@@ -6,14 +6,11 @@ import { BatchLinkPlugin } from "@orpc/client/plugins";
|
||||
import { createTanstackQueryUtils } from "@orpc/tanstack-query";
|
||||
import { rpcFetch } from "./fetch";
|
||||
|
||||
const getRpcUrl = () => {
|
||||
if (typeof window === "undefined") return "http://localhost:3000/api/rpc";
|
||||
return `${window.location.origin}/api/rpc`;
|
||||
};
|
||||
const getRpcUrl = () => `${window.location.origin}/api/rpc`;
|
||||
|
||||
export const client: RouterClient<typeof router> = createORPCClient(
|
||||
new RPCLink({
|
||||
url: getRpcUrl(),
|
||||
url: getRpcUrl,
|
||||
fetch: rpcFetch,
|
||||
plugins: [
|
||||
new BatchLinkPlugin({
|
||||
@@ -32,7 +29,7 @@ export const client: RouterClient<typeof router> = createORPCClient(
|
||||
|
||||
export const streamClient: RouterClient<typeof router> = createORPCClient(
|
||||
new RPCLink({
|
||||
url: getRpcUrl(),
|
||||
url: getRpcUrl,
|
||||
fetch: rpcFetch,
|
||||
interceptors: [
|
||||
onError((error) => {
|
||||
|
||||
@@ -7,10 +7,6 @@ import {
|
||||
} from "./seo";
|
||||
|
||||
describe("getCanonicalRootUrl", () => {
|
||||
it("uses the production root when no origin is available", () => {
|
||||
expect(getCanonicalRootUrl()).toBe("https://rxresu.me/");
|
||||
});
|
||||
|
||||
it("normalizes an app origin to the root URL", () => {
|
||||
expect(getCanonicalRootUrl("http://localhost:3000")).toBe("http://localhost:3000/");
|
||||
expect(getCanonicalRootUrl("https://rxresu.me/")).toBe("https://rxresu.me/");
|
||||
|
||||
@@ -1,19 +1,9 @@
|
||||
const productionRootUrl = "https://rxresu.me/";
|
||||
const appName = "Reactive Resume";
|
||||
const repositoryUrl = "https://github.com/reactive-resume/reactive-resume";
|
||||
|
||||
type JsonLd = Record<string, unknown>;
|
||||
|
||||
export const getCanonicalRootUrl = (origin?: string): string => {
|
||||
if (!origin) return productionRootUrl;
|
||||
|
||||
const url = new URL(origin);
|
||||
url.pathname = "/";
|
||||
url.search = "";
|
||||
url.hash = "";
|
||||
|
||||
return url.toString();
|
||||
};
|
||||
export const getCanonicalRootUrl = (origin: string): string => new URL("/", origin).href;
|
||||
|
||||
export const createNoindexFollowMeta = () => ({ name: "robots", content: "noindex, follow" });
|
||||
|
||||
|
||||
@@ -21,8 +21,4 @@ document.querySelectorAll(serverSeoSelectors.join(",")).forEach((element) => {
|
||||
element.remove();
|
||||
});
|
||||
|
||||
if (!rootElement.innerHTML) {
|
||||
const root = ReactDOM.createRoot(rootElement);
|
||||
|
||||
root.render(<RouterProvider router={router} />);
|
||||
}
|
||||
ReactDOM.createRoot(rootElement).render(<RouterProvider router={router} />);
|
||||
|
||||
@@ -9,7 +9,6 @@ import { createNoindexFollowMeta, createResumeSocialMeta, getCanonicalRootUrl }
|
||||
type LoaderData = Omit<RouterOutput["resume"]["getBySlug"], "data"> & { data: ResumeData };
|
||||
|
||||
export const Route = createFileRoute("/$username/$slug")({
|
||||
ssr: "data-only",
|
||||
component: lazyRouteComponent(() => import("@/features/resume/public/public-resume"), "PublicResumeRoute"),
|
||||
notFoundComponent: lazyRouteComponent(
|
||||
() => import("@/features/resume/public/public-resume"),
|
||||
@@ -38,7 +37,7 @@ export const Route = createFileRoute("/$username/$slug")({
|
||||
|
||||
const social = getResumeSocialMeta(resume.data, resume.name || "Resume");
|
||||
|
||||
const base = getCanonicalRootUrl(typeof window === "undefined" ? undefined : window.location.origin);
|
||||
const base = getCanonicalRootUrl(window.location.origin);
|
||||
const canonicalUrl = `${base}${params.username}/${params.slug}`;
|
||||
const imageUrl = `${base}opengraph/banner.jpg`;
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ const iconContextValue: IconProps = { size: 16, weight: "regular" };
|
||||
export const Route = createRootRouteWithContext<RouterContext>()({
|
||||
component: RootComponent,
|
||||
head: () => {
|
||||
const appUrl = typeof window !== "undefined" ? window.location.origin : "https://rxresu.me";
|
||||
const appUrl = window.location.origin;
|
||||
|
||||
return {
|
||||
links: [
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { defaultResumeData } from "@reactive-resume/schema/resume/default";
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ export const Route = createFileRoute("/_home/ats-checker")({
|
||||
// Back from signing up with a checked file to import.
|
||||
validateSearch: z.object({ import: z.coerce.boolean().optional().catch(undefined) }),
|
||||
head: () => {
|
||||
const origin = typeof window === "undefined" ? "https://rxresu.me" : window.location.origin;
|
||||
const origin = window.location.origin;
|
||||
const canonicalUrl = new URL("/ats-checker", origin).toString();
|
||||
const imageUrl = new URL("/opengraph/ats-checker.png", origin).toString();
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { ORPCError } from "@orpc/client";
|
||||
import { ClientOnly, createFileRoute, lazyRouteComponent, redirect } from "@tanstack/react-router";
|
||||
import { createFileRoute, lazyRouteComponent, redirect } from "@tanstack/react-router";
|
||||
import { getResumeSocialMeta } from "@reactive-resume/resume/social-meta";
|
||||
import { LoadingScreen } from "@/components/layout/loading-screen";
|
||||
import { NotFoundScreen } from "@/components/layout/not-found-screen";
|
||||
import { Homepage } from "@/features/homepage/page";
|
||||
import { orpc } from "@/libs/orpc/client";
|
||||
@@ -50,7 +49,7 @@ export const Route = createFileRoute("/_home/")({
|
||||
links: [{ rel: "canonical", href: canonicalUrl }],
|
||||
};
|
||||
}
|
||||
const appUrl = typeof window !== "undefined" ? window.location.origin : "https://rxresu.me";
|
||||
const appUrl = window.location.origin;
|
||||
const canonicalUrl = getCanonicalRootUrl(appUrl);
|
||||
|
||||
return {
|
||||
@@ -70,11 +69,7 @@ function RouteComponent() {
|
||||
</main>
|
||||
);
|
||||
if (root.status === "public") {
|
||||
return (
|
||||
<ClientOnly fallback={<LoadingScreen />}>
|
||||
<PublicResumePage resume={root.resume} username={root.username} slug={root.slug} flags={flags} isRoot />
|
||||
</ClientOnly>
|
||||
);
|
||||
return <PublicResumePage resume={root.resume} username={root.username} slug={root.slug} flags={flags} isRoot />;
|
||||
}
|
||||
|
||||
return <Homepage />;
|
||||
|
||||
@@ -2,13 +2,12 @@ import { createFileRoute, redirect } from "@tanstack/react-router";
|
||||
import { OAuthConsentPage } from "@/features/auth/pages/consent";
|
||||
|
||||
export const Route = createFileRoute("/auth/consent")({
|
||||
ssr: false,
|
||||
beforeLoad: ({ context, location }) => {
|
||||
beforeLoad: ({ context }) => {
|
||||
if (!context.session) {
|
||||
throw redirect({
|
||||
to: "/auth/login",
|
||||
search: {
|
||||
callbackURL: `/api/auth/oauth${typeof window === "undefined" ? location.searchStr : window.location.search}`,
|
||||
callbackURL: `/api/auth/oauth${window.location.search}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user