diff --git a/apps/artboard/src/pages/artboard.tsx b/apps/artboard/src/pages/artboard.tsx
index a6aa8c70..c4809109 100644
--- a/apps/artboard/src/pages/artboard.tsx
+++ b/apps/artboard/src/pages/artboard.tsx
@@ -1,7 +1,7 @@
+import { sanitize } from "@reactive-resume/utils";
import { useEffect, useMemo } from "react";
import { Helmet } from "react-helmet-async";
import { Outlet } from "react-router";
-import sanitizeHtml from "sanitize-html";
import webfontloader from "webfontloader";
import { useArtboardStore } from "../store/artboard";
@@ -64,7 +64,7 @@ export const ArtboardPage = () => {
{name} | Reactive Resume
{metadata.css.visible && (
)}
diff --git a/apps/artboard/src/providers/index.tsx b/apps/artboard/src/providers/index.tsx
index 2fb8edaa..0b056baf 100644
--- a/apps/artboard/src/providers/index.tsx
+++ b/apps/artboard/src/providers/index.tsx
@@ -12,27 +12,21 @@ export const Providers = () => {
useEffect(() => {
const handleMessage = (event: MessageEvent) => {
if (event.origin !== window.location.origin) return;
-
if (event.data.type === "SET_RESUME") setResume(event.data.payload);
- if (event.data.type === "SET_THEME") {
- event.data.payload === "dark"
- ? document.documentElement.classList.add("dark")
- : document.documentElement.classList.remove("dark");
- }
};
- const resumeData = window.localStorage.getItem("resume");
- if (resumeData) {
- setResume(JSON.parse(resumeData));
- return;
- }
-
- window.addEventListener("message", handleMessage);
+ window.addEventListener("message", handleMessage, false);
return () => {
- window.removeEventListener("message", handleMessage);
+ window.removeEventListener("message", handleMessage, false);
};
- }, [setResume]);
+ }, []);
+
+ useEffect(() => {
+ const resumeData = window.localStorage.getItem("resume");
+
+ if (resumeData) setResume(JSON.parse(resumeData));
+ }, [window.localStorage.getItem("resume")]);
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (!resume) return null;
diff --git a/apps/artboard/src/templates/azurill.tsx b/apps/artboard/src/templates/azurill.tsx
index 8e0068fa..e7040c1a 100644
--- a/apps/artboard/src/templates/azurill.tsx
+++ b/apps/artboard/src/templates/azurill.tsx
@@ -15,10 +15,9 @@ import type {
URL,
} from "@reactive-resume/schema";
import { Education, Experience, Volunteer } from "@reactive-resume/schema";
-import { cn, isEmptyString, isUrl, linearTransform } from "@reactive-resume/utils";
+import { cn, isEmptyString, isUrl, linearTransform, sanitize } from "@reactive-resume/utils";
import get from "lodash.get";
import React, { Fragment } from "react";
-import sanitizeHtml from "sanitize-html";
import { BrandIcon } from "../components/brand-icon";
import { Picture } from "../components/picture";
@@ -99,7 +98,7 @@ const Summary = () => {
@@ -226,7 +225,7 @@ const Section = ({
{summary !== undefined && !isEmptyString(summary) && (
)}
diff --git a/apps/artboard/src/templates/bronzor.tsx b/apps/artboard/src/templates/bronzor.tsx
index b5e30e40..237b6347 100644
--- a/apps/artboard/src/templates/bronzor.tsx
+++ b/apps/artboard/src/templates/bronzor.tsx
@@ -15,10 +15,9 @@ import type {
URL,
} from "@reactive-resume/schema";
import { Education, Experience, Volunteer } from "@reactive-resume/schema";
-import { cn, isEmptyString, isUrl } from "@reactive-resume/utils";
+import { cn, isEmptyString, isUrl, sanitize } from "@reactive-resume/utils";
import get from "lodash.get";
import { Fragment } from "react";
-import sanitizeHtml from "sanitize-html";
import { BrandIcon } from "../components/brand-icon";
import { Picture } from "../components/picture";
@@ -90,7 +89,7 @@ const Summary = () => {
@@ -207,7 +206,7 @@ const Section = ({
{summary !== undefined && !isEmptyString(summary) && (
)}
diff --git a/apps/artboard/src/templates/chikorita.tsx b/apps/artboard/src/templates/chikorita.tsx
index 4edd0394..e55fbfb0 100644
--- a/apps/artboard/src/templates/chikorita.tsx
+++ b/apps/artboard/src/templates/chikorita.tsx
@@ -15,10 +15,9 @@ import type {
URL,
} from "@reactive-resume/schema";
import { Education, Experience, Volunteer } from "@reactive-resume/schema";
-import { cn, isEmptyString, isUrl } from "@reactive-resume/utils";
+import { cn, isEmptyString, isUrl, sanitize } from "@reactive-resume/utils";
import get from "lodash.get";
import { Fragment } from "react";
-import sanitizeHtml from "sanitize-html";
import { BrandIcon } from "../components/brand-icon";
import { Picture } from "../components/picture";
@@ -90,7 +89,7 @@ const Summary = () => {
{section.name}
@@ -210,7 +209,7 @@ const Section = ({
{summary !== undefined && !isEmptyString(summary) && (
)}
diff --git a/apps/artboard/src/templates/ditto.tsx b/apps/artboard/src/templates/ditto.tsx
index a347380e..fd1c35dc 100644
--- a/apps/artboard/src/templates/ditto.tsx
+++ b/apps/artboard/src/templates/ditto.tsx
@@ -15,10 +15,9 @@ import type {
URL,
} from "@reactive-resume/schema";
import { Education, Experience, Volunteer } from "@reactive-resume/schema";
-import { cn, isEmptyString, isUrl } from "@reactive-resume/utils";
+import { cn, isEmptyString, isUrl, sanitize } from "@reactive-resume/utils";
import get from "lodash.get";
import { Fragment } from "react";
-import sanitizeHtml from "sanitize-html";
import { BrandIcon } from "../components/brand-icon";
import { Picture } from "../components/picture";
@@ -110,7 +109,7 @@ const Summary = () => {
{section.name}
@@ -232,7 +231,7 @@ const Section = ({
{summary !== undefined && !isEmptyString(summary) && (
)}
diff --git a/apps/artboard/src/templates/gengar.tsx b/apps/artboard/src/templates/gengar.tsx
index 8bc57bbe..3f7ffecb 100644
--- a/apps/artboard/src/templates/gengar.tsx
+++ b/apps/artboard/src/templates/gengar.tsx
@@ -15,10 +15,9 @@ import type {
URL,
} from "@reactive-resume/schema";
import { Education, Experience, Volunteer } from "@reactive-resume/schema";
-import { cn, hexToRgb, isEmptyString, isUrl } from "@reactive-resume/utils";
+import { cn, hexToRgb, isEmptyString, isUrl, sanitize } from "@reactive-resume/utils";
import get from "lodash.get";
import { Fragment } from "react";
-import sanitizeHtml from "sanitize-html";
import { BrandIcon } from "../components/brand-icon";
import { Picture } from "../components/picture";
@@ -90,7 +89,7 @@ const Summary = () => {
@@ -212,7 +211,7 @@ const Section = ({
{summary !== undefined && !isEmptyString(summary) && (
)}
diff --git a/apps/artboard/src/templates/glalie.tsx b/apps/artboard/src/templates/glalie.tsx
index 648469bc..557fa84f 100644
--- a/apps/artboard/src/templates/glalie.tsx
+++ b/apps/artboard/src/templates/glalie.tsx
@@ -15,10 +15,16 @@ import type {
URL,
} from "@reactive-resume/schema";
import { Education, Experience, Volunteer } from "@reactive-resume/schema";
-import { cn, hexToRgb, isEmptyString, isUrl, linearTransform } from "@reactive-resume/utils";
+import {
+ cn,
+ hexToRgb,
+ isEmptyString,
+ isUrl,
+ linearTransform,
+ sanitize,
+} from "@reactive-resume/utils";
import get from "lodash.get";
import { Fragment } from "react";
-import sanitizeHtml from "sanitize-html";
import { BrandIcon } from "../components/brand-icon";
import { Picture } from "../components/picture";
@@ -90,7 +96,7 @@ const Summary = () => {
{section.name}
@@ -215,7 +221,7 @@ const Section = ({
{summary !== undefined && !isEmptyString(summary) && (
)}
diff --git a/apps/artboard/src/templates/kakuna.tsx b/apps/artboard/src/templates/kakuna.tsx
index 25d357bb..a0a06466 100644
--- a/apps/artboard/src/templates/kakuna.tsx
+++ b/apps/artboard/src/templates/kakuna.tsx
@@ -14,10 +14,9 @@ import type {
URL,
} from "@reactive-resume/schema";
import { Education, Experience, Volunteer } from "@reactive-resume/schema";
-import { cn, isEmptyString, isUrl } from "@reactive-resume/utils";
+import { cn, isEmptyString, isUrl, sanitize } from "@reactive-resume/utils";
import get from "lodash.get";
import React, { Fragment } from "react";
-import sanitizeHtml from "sanitize-html";
import { BrandIcon } from "../components/brand-icon";
import { Picture } from "../components/picture";
@@ -109,7 +108,7 @@ const Summary = () => {
@@ -223,7 +222,7 @@ const Section = ({
{summary !== undefined && !isEmptyString(summary) && (
)}
diff --git a/apps/artboard/src/templates/leafish.tsx b/apps/artboard/src/templates/leafish.tsx
index c96af55e..16302fc5 100644
--- a/apps/artboard/src/templates/leafish.tsx
+++ b/apps/artboard/src/templates/leafish.tsx
@@ -14,10 +14,9 @@ import type {
URL,
} from "@reactive-resume/schema";
import { Education, Experience, Volunteer } from "@reactive-resume/schema";
-import { cn, hexToRgb, isEmptyString, isUrl } from "@reactive-resume/utils";
+import { cn, hexToRgb, isEmptyString, isUrl, sanitize } from "@reactive-resume/utils";
import get from "lodash.get";
import React, { Fragment } from "react";
-import sanitizeHtml from "sanitize-html";
import { BrandIcon } from "../components/brand-icon";
import { Picture } from "../components/picture";
@@ -43,7 +42,7 @@ const Header = () => {
@@ -218,7 +217,7 @@ const Section = ({
{summary !== undefined && !isEmptyString(summary) && (
)}
diff --git a/apps/artboard/src/templates/nosepass.tsx b/apps/artboard/src/templates/nosepass.tsx
index 8a028358..b2a1150b 100644
--- a/apps/artboard/src/templates/nosepass.tsx
+++ b/apps/artboard/src/templates/nosepass.tsx
@@ -15,10 +15,9 @@ import type {
URL,
} from "@reactive-resume/schema";
import { Education, Experience, Volunteer } from "@reactive-resume/schema";
-import { cn, isEmptyString, isUrl } from "@reactive-resume/utils";
+import { cn, isEmptyString, isUrl, sanitize } from "@reactive-resume/utils";
import get from "lodash.get";
import { Fragment } from "react";
-import sanitizeHtml from "sanitize-html";
import { BrandIcon } from "../components/brand-icon";
import { Picture } from "../components/picture";
@@ -106,7 +105,7 @@ const Summary = () => {
@@ -219,7 +218,7 @@ const Section = ({
{summary !== undefined && !isEmptyString(summary) && (
)}
@@ -257,7 +256,7 @@ const Section = ({
{summary !== undefined && !isEmptyString(summary) && (
)}
diff --git a/apps/artboard/src/templates/onyx.tsx b/apps/artboard/src/templates/onyx.tsx
index ba0c4da4..72aa1736 100644
--- a/apps/artboard/src/templates/onyx.tsx
+++ b/apps/artboard/src/templates/onyx.tsx
@@ -14,10 +14,9 @@ import type {
URL,
} from "@reactive-resume/schema";
import { Education, Experience, Volunteer } from "@reactive-resume/schema";
-import { cn, isEmptyString, isUrl } from "@reactive-resume/utils";
+import { cn, isEmptyString, isUrl, sanitize } from "@reactive-resume/utils";
import get from "lodash.get";
import React, { Fragment } from "react";
-import sanitizeHtml from "sanitize-html";
import { BrandIcon } from "../components/brand-icon";
import { Picture } from "../components/picture";
@@ -110,7 +109,7 @@ const Summary = () => {
{section.name}
@@ -225,7 +224,7 @@ const Section = ({
{summary !== undefined && !isEmptyString(summary) && (
)}
diff --git a/apps/artboard/src/templates/pikachu.tsx b/apps/artboard/src/templates/pikachu.tsx
index c27aad9e..46d3ab26 100644
--- a/apps/artboard/src/templates/pikachu.tsx
+++ b/apps/artboard/src/templates/pikachu.tsx
@@ -15,10 +15,9 @@ import type {
URL,
} from "@reactive-resume/schema";
import { Education, Experience, Volunteer } from "@reactive-resume/schema";
-import { cn, isEmptyString, isUrl } from "@reactive-resume/utils";
+import { cn, isEmptyString, isUrl, sanitize } from "@reactive-resume/utils";
import get from "lodash.get";
import { Fragment } from "react";
-import sanitizeHtml from "sanitize-html";
import { BrandIcon } from "../components/brand-icon";
import { Picture } from "../components/picture";
@@ -111,7 +110,7 @@ const Summary = () => {
{section.name}
@@ -240,7 +239,7 @@ const Section = ({
{summary !== undefined && !isEmptyString(summary) && (
)}
diff --git a/apps/artboard/src/templates/rhyhorn.tsx b/apps/artboard/src/templates/rhyhorn.tsx
index 8b251417..480756d1 100644
--- a/apps/artboard/src/templates/rhyhorn.tsx
+++ b/apps/artboard/src/templates/rhyhorn.tsx
@@ -15,10 +15,9 @@ import type {
URL,
} from "@reactive-resume/schema";
import { Education, Experience, Volunteer } from "@reactive-resume/schema";
-import { cn, isEmptyString, isUrl } from "@reactive-resume/utils";
+import { cn, isEmptyString, isUrl, sanitize } from "@reactive-resume/utils";
import get from "lodash.get";
import { Fragment } from "react";
-import sanitizeHtml from "sanitize-html";
import { BrandIcon } from "../components/brand-icon";
import { Picture } from "../components/picture";
@@ -91,7 +90,7 @@ const Summary = () => {
{section.name}
@@ -206,7 +205,7 @@ const Section = ({
{summary !== undefined && !isEmptyString(summary) && (
)}
diff --git a/apps/client/src/pages/builder/page.tsx b/apps/client/src/pages/builder/page.tsx
index 30af1a0c..06532480 100644
--- a/apps/client/src/pages/builder/page.tsx
+++ b/apps/client/src/pages/builder/page.tsx
@@ -17,25 +17,41 @@ export const BuilderPage = () => {
const resume = useResumeStore((state) => state.resume);
const title = useResumeStore((state) => state.resume.title);
- const updateResumeInFrame = useCallback(() => {
- const message = { type: "SET_RESUME", payload: resume.data };
-
+ const syncResumeToArtboard = useCallback(() => {
setImmediate(() => {
- frameRef?.contentWindow?.postMessage(message, "*");
+ if (!frameRef?.contentWindow) return;
+ const message = { type: "SET_RESUME", payload: resume.data };
+ frameRef.contentWindow.postMessage(message, "*");
});
}, [frameRef?.contentWindow, resume.data]);
// Send resume data to iframe on initial load
useEffect(() => {
if (!frameRef) return;
- frameRef.addEventListener("load", updateResumeInFrame);
+
+ frameRef.addEventListener("load", syncResumeToArtboard);
+
return () => {
- frameRef.removeEventListener("load", updateResumeInFrame);
+ frameRef.removeEventListener("load", syncResumeToArtboard);
+ };
+ }, [frameRef]);
+
+ // Persistently check if iframe has loaded using setInterval
+ useEffect(() => {
+ const interval = setInterval(() => {
+ if (frameRef?.contentWindow?.document.readyState === "complete") {
+ syncResumeToArtboard();
+ clearInterval(interval);
+ }
+ }, 100);
+
+ return () => {
+ clearInterval(interval);
};
}, [frameRef]);
// Send resume data to iframe on change of resume data
- useEffect(updateResumeInFrame, [resume.data]);
+ useEffect(syncResumeToArtboard, [resume.data]);
return (
<>
diff --git a/apps/client/src/pages/builder/sidebars/right/sections/typography.tsx b/apps/client/src/pages/builder/sidebars/right/sections/typography.tsx
index 8c27d140..bb891672 100644
--- a/apps/client/src/pages/builder/sidebars/right/sections/typography.tsx
+++ b/apps/client/src/pages/builder/sidebars/right/sections/typography.tsx
@@ -43,6 +43,8 @@ export const TypographySection = () => {
const loadFontSuggestions = useCallback(() => {
for (const font of fontSuggestions) {
+ if (localFonts.includes(font)) continue;
+
webfontloader.load({
events: false,
classes: false,
diff --git a/libs/utils/package.json b/libs/utils/package.json
index e7c48e2c..563dffd8 100644
--- a/libs/utils/package.json
+++ b/libs/utils/package.json
@@ -9,12 +9,13 @@
"access": "public"
},
"dependencies": {
- "papaparse": "^5.4.1",
- "dayjs": "^1.11.11",
- "unique-names-generator": "^4.7.1",
- "clsx": "^2.1.1",
- "tailwind-merge": "^2.3.0",
"@swc/helpers": "~0.5.11",
+ "clsx": "^2.1.1",
+ "dayjs": "^1.11.11",
+ "papaparse": "^5.4.1",
+ "sanitize-html": "^2.14.0",
+ "tailwind-merge": "^2.3.0",
+ "unique-names-generator": "^4.7.1",
"zod": "^3.24.1"
}
}
diff --git a/libs/utils/src/namespaces/string.ts b/libs/utils/src/namespaces/string.ts
index f566528e..cbcf3882 100644
--- a/libs/utils/src/namespaces/string.ts
+++ b/libs/utils/src/namespaces/string.ts
@@ -1,3 +1,4 @@
+import sanitizeHtml from "sanitize-html";
import type { Config as UniqueNamesConfig } from "unique-names-generator";
import { adjectives, animals, uniqueNamesGenerator } from "unique-names-generator";
@@ -55,3 +56,17 @@ export const parseLayoutLocator = (payload: SortablePayload | null): LayoutLocat
return { page, column, section };
};
+
+export const sanitize = (html: string, options?: sanitizeHtml.IOptions) => {
+ return sanitizeHtml(html, {
+ ...options,
+ allowedAttributes: {
+ ...options?.allowedAttributes,
+ "*": ["class", "style"],
+ },
+ allowedStyles: {
+ ...options?.allowedStyles,
+ "*": { "text-align": [/^left$/, /^right$/, /^center$/, /^justify$/] },
+ },
+ });
+};