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$/] }, + }, + }); +};