diff --git a/.vscode/settings.json b/.vscode/settings.json index 26f6b71b9..0ab3f19f4 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,9 @@ { "typescript.tsdk": "node_modules/typescript/lib", + "tailwindCSS.experimental.classRegex": [ + ["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"], + ["cn\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"] + ], "yaml.schemas": { "https://json.schemastore.org/github-workflow.json": ".github/workflows/*.yml", "https://raw.githubusercontent.com/compose-spec/compose-spec/master/schema/compose-spec.json": [ diff --git a/apps/artboard/.eslintrc.json b/apps/artboard/.eslintrc.json new file mode 100644 index 000000000..b58826f62 --- /dev/null +++ b/apps/artboard/.eslintrc.json @@ -0,0 +1,31 @@ +{ + "extends": ["plugin:@nx/react", "../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "extends": ["plugin:tailwindcss/recommended"], + "settings": { + "tailwindcss": { + "callees": ["cn", "clsx", "cva"], + "config": "tailwind.config.js" + } + }, + "rules": { + // react-hooks + "react-hooks/exhaustive-deps": "off", + + // tailwindcss + "tailwindcss/no-custom-classname": "off" + } + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/apps/artboard/index.html b/apps/artboard/index.html new file mode 100644 index 000000000..fa6451afe --- /dev/null +++ b/apps/artboard/index.html @@ -0,0 +1,37 @@ + + + + + Artboard | Reactive Resume + + + + + + + + + + + + + + +
+ + + + + + + diff --git a/apps/artboard/postcss.config.js b/apps/artboard/postcss.config.js new file mode 100644 index 000000000..a9649690d --- /dev/null +++ b/apps/artboard/postcss.config.js @@ -0,0 +1,10 @@ +const { join } = require("path"); + +module.exports = { + plugins: { + tailwindcss: { + config: join(__dirname, "tailwind.config.js"), + }, + autoprefixer: {}, + }, +}; diff --git a/apps/artboard/project.json b/apps/artboard/project.json new file mode 100644 index 000000000..008ee754e --- /dev/null +++ b/apps/artboard/project.json @@ -0,0 +1,64 @@ +{ + "name": "artboard", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "apps/artboard/src", + "projectType": "application", + "targets": { + "build": { + "executor": "@nx/vite:build", + "outputs": ["{options.outputPath}"], + "defaultConfiguration": "production", + "options": { + "outputPath": "dist/apps/artboard" + }, + "configurations": { + "development": { + "mode": "development" + }, + "production": { + "mode": "production" + } + } + }, + "serve": { + "executor": "@nx/vite:dev-server", + "defaultConfiguration": "development", + "options": { + "buildTarget": "artboard:build" + }, + "configurations": { + "development": { + "buildTarget": "artboard:build:development", + "hmr": true + }, + "production": { + "buildTarget": "artboard:build:production", + "hmr": false + } + } + }, + "preview": { + "executor": "@nx/vite:preview-server", + "defaultConfiguration": "development", + "options": { + "buildTarget": "artboard:build" + }, + "configurations": { + "development": { + "buildTarget": "artboard:build:development" + }, + "production": { + "buildTarget": "artboard:build:production" + } + } + }, + "lint": { + "executor": "@nx/eslint:lint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": ["apps/artboard/**/*.{ts,tsx,js,jsx}"] + } + } + }, + "tags": ["frontend"] +} diff --git a/apps/artboard/public/favicon.ico b/apps/artboard/public/favicon.ico new file mode 100644 index 000000000..ef0a8a374 Binary files /dev/null and b/apps/artboard/public/favicon.ico differ diff --git a/apps/artboard/public/icon/dark.svg b/apps/artboard/public/icon/dark.svg new file mode 100644 index 000000000..1709463fd --- /dev/null +++ b/apps/artboard/public/icon/dark.svg @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/apps/artboard/public/icon/light.svg b/apps/artboard/public/icon/light.svg new file mode 100644 index 000000000..8208f4eb1 --- /dev/null +++ b/apps/artboard/public/icon/light.svg @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/apps/artboard/src/assets/.gitkeep b/apps/artboard/src/assets/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/apps/artboard/src/components/page.tsx b/apps/artboard/src/components/page.tsx new file mode 100644 index 000000000..f4e48f1ee --- /dev/null +++ b/apps/artboard/src/components/page.tsx @@ -0,0 +1,49 @@ +import { useTheme } from "@reactive-resume/hooks"; +import { cn, pageSizeMap } from "@reactive-resume/utils"; + +import { useArtboardStore } from "../store/artboard"; + +type Props = { + mode?: "preview" | "builder"; + pageNumber: number; + children: React.ReactNode; +}; + +export const MM_TO_PX = 3.78; + +export const Page = ({ mode = "preview", pageNumber, children }: Props) => { + const { isDarkMode } = useTheme(); + + const page = useArtboardStore((state) => state.resume.metadata.page); + const fontFamily = useArtboardStore((state) => state.resume.metadata.typography.font.family); + + return ( +
+ {mode === "builder" && page.options.pageNumbers && ( +
+ Page {pageNumber} +
+ )} + + {children} + + {mode === "builder" && page.options.breakLine && ( +
+ )} +
+ ); +}; diff --git a/apps/artboard/src/components/picture.tsx b/apps/artboard/src/components/picture.tsx new file mode 100644 index 000000000..c69246b04 --- /dev/null +++ b/apps/artboard/src/components/picture.tsx @@ -0,0 +1,22 @@ +import { isUrl } from "@reactive-resume/utils"; + +import { useArtboardStore } from "../store/artboard"; + +export const Picture = () => { + const picture = useArtboardStore((state) => state.resume.basics.picture); + + if (!isUrl(picture.url) || picture.effects.hidden) return null; + + return ( + Profile + ); +}; diff --git a/apps/artboard/src/main.tsx b/apps/artboard/src/main.tsx new file mode 100644 index 000000000..44424378c --- /dev/null +++ b/apps/artboard/src/main.tsx @@ -0,0 +1,13 @@ +import { StrictMode } from "react"; +import * as ReactDOM from "react-dom/client"; +import { RouterProvider } from "react-router-dom"; + +import { router } from "./router"; + +const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement); + +root.render( + + + , +); diff --git a/apps/artboard/src/pages/artboard.tsx b/apps/artboard/src/pages/artboard.tsx new file mode 100644 index 000000000..7e14578b6 --- /dev/null +++ b/apps/artboard/src/pages/artboard.tsx @@ -0,0 +1,45 @@ +import { useEffect, useMemo } from "react"; +import { Outlet } from "react-router-dom"; +import webfontloader from "webfontloader"; + +import { useArtboardStore } from "../store/artboard"; + +export const ArtboardPage = () => { + const metadata = useArtboardStore((state) => state.resume.metadata); + + const fontString = useMemo(() => { + const family = metadata.typography.font.family; + const variants = metadata.typography.font.variants.join(","); + const subset = metadata.typography.font.subset; + + return `${family}:${variants}:${subset}`; + }, [metadata.typography.font]); + + useEffect(() => { + webfontloader.load({ + google: { families: [fontString] }, + active: () => { + const height = window.document.body.offsetHeight; + const message = { type: "PAGE_LOADED", payload: { height } }; + window.postMessage(message, "*"); + }, + }); + }, [fontString]); + + // Font Size & Line Height + useEffect(() => { + document.documentElement.style.setProperty("font-size", `${metadata.typography.font.size}px`); + document.documentElement.style.setProperty("line-height", `${metadata.typography.lineHeight}`); + }, [metadata]); + + // Underline Links + useEffect(() => { + if (metadata.typography.underlineLinks) { + document.querySelector("#root")!.classList.add("underline-links"); + } else { + document.querySelector("#root")!.classList.remove("underline-links"); + } + }, [metadata]); + + return ; +}; diff --git a/apps/artboard/src/pages/builder.tsx b/apps/artboard/src/pages/builder.tsx new file mode 100644 index 000000000..692d5c590 --- /dev/null +++ b/apps/artboard/src/pages/builder.tsx @@ -0,0 +1,63 @@ +import { SectionKey } from "@reactive-resume/schema"; +import { pageSizeMap } from "@reactive-resume/utils"; +import { useEffect, useRef } from "react"; +import { ReactZoomPanPinchRef, TransformComponent, TransformWrapper } from "react-zoom-pan-pinch"; + +import { MM_TO_PX, Page } from "../components/page"; +import { useArtboardStore } from "../store/artboard"; +import { Rhyhorn } from "../templates/rhyhorn"; + +export const BuilderLayout = () => { + const transformRef = useRef(null); + const format = useArtboardStore((state) => state.resume.metadata.page.format); + const layout = useArtboardStore((state) => state.resume.metadata.layout); + const template = useArtboardStore((state) => state.resume.metadata.template); + + useEffect(() => { + const handleMessage = (event: MessageEvent) => { + if (event.origin !== window.location.origin) return; + + if (event.data.type === "ZOOM_IN") transformRef.current?.zoomIn(0.2); + if (event.data.type === "ZOOM_OUT") transformRef.current?.zoomOut(0.2); + if (event.data.type === "CENTER_VIEW") transformRef.current?.centerView(); + if (event.data.type === "RESET_VIEW") { + transformRef.current?.resetTransform(0); + setTimeout(() => transformRef.current?.centerView(0.8, 0), 10); + } + }; + + window.addEventListener("message", handleMessage); + + return () => { + window.removeEventListener("message", handleMessage); + }; + }, [transformRef]); + + return ( + + + {layout.map((columns, pageIndex) => ( + + {template === "rhyhorn" && ( + + )} + + ))} + + + ); +}; diff --git a/apps/artboard/src/pages/preview.tsx b/apps/artboard/src/pages/preview.tsx new file mode 100644 index 000000000..87ec269ce --- /dev/null +++ b/apps/artboard/src/pages/preview.tsx @@ -0,0 +1,22 @@ +import { SectionKey } from "@reactive-resume/schema"; + +import { Page } from "../components/page"; +import { useArtboardStore } from "../store/artboard"; +import { Rhyhorn } from "../templates/rhyhorn"; + +export const PreviewLayout = () => { + const layout = useArtboardStore((state) => state.resume.metadata.layout); + const template = useArtboardStore((state) => state.resume.metadata.template); + + return ( + <> + {layout.map((columns, pageIndex) => ( + + {template === "rhyhorn" && ( + + )} + + ))} + + ); +}; diff --git a/apps/artboard/src/providers/index.tsx b/apps/artboard/src/providers/index.tsx new file mode 100644 index 000000000..e95e602b5 --- /dev/null +++ b/apps/artboard/src/providers/index.tsx @@ -0,0 +1,40 @@ +import { useEffect } from "react"; +import { Outlet } from "react-router-dom"; + +import { useArtboardStore } from "../store/artboard"; + +export const Providers = () => { + const resume = useArtboardStore((state) => state.resume); + const setResume = useArtboardStore((state) => state.setResume); + + 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.sessionStorage.getItem("resume"); + if (resumeData) return setResume(JSON.parse(resumeData)); + + window.addEventListener("message", handleMessage); + + return () => { + window.removeEventListener("message", handleMessage); + }; + }, [setResume]); + + // Only for testing, in production this will be fetched from window.postMessage + // useEffect(() => { + // setResume(sampleResume); + // }, [setResume]); + + if (!resume) return null; + + return ; +}; diff --git a/apps/artboard/src/router/index.tsx b/apps/artboard/src/router/index.tsx new file mode 100644 index 000000000..5b83fc668 --- /dev/null +++ b/apps/artboard/src/router/index.tsx @@ -0,0 +1,17 @@ +import { createBrowserRouter, createRoutesFromChildren, Route } from "react-router-dom"; + +import { ArtboardPage } from "../pages/artboard"; +import { BuilderLayout } from "../pages/builder"; +import { PreviewLayout } from "../pages/preview"; +import { Providers } from "../providers"; + +export const routes = createRoutesFromChildren( + }> + }> + } /> + } /> + + , +); + +export const router = createBrowserRouter(routes); diff --git a/apps/artboard/src/store/artboard.ts b/apps/artboard/src/store/artboard.ts new file mode 100644 index 000000000..8d03cd2fc --- /dev/null +++ b/apps/artboard/src/store/artboard.ts @@ -0,0 +1,12 @@ +import { ResumeData } from "@reactive-resume/schema"; +import { create } from "zustand"; + +export type ArtboardStore = { + resume: ResumeData; + setResume: (resume: ResumeData) => void; +}; + +export const useArtboardStore = create()((set) => ({ + resume: null as unknown as ResumeData, + setResume: (resume) => set({ resume }), +})); diff --git a/apps/artboard/src/styles/main.css b/apps/artboard/src/styles/main.css new file mode 100644 index 000000000..ffa30648e --- /dev/null +++ b/apps/artboard/src/styles/main.css @@ -0,0 +1,19 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +* { + @apply border-current; +} + +#root { + @apply antialiased; +} + +#root.underline-links a { + @apply underline underline-offset-2; +} + +.wysiwyg { + @apply prose max-w-none text-current prose-headings:my-1.5 prose-p:my-1.5 prose-ul:my-1.5 prose-li:my-1.5 prose-ol:my-1.5 prose-img:my-1.5 prose-hr:my-1.5; +} diff --git a/apps/artboard/src/templates/rhyhorn.tsx b/apps/artboard/src/templates/rhyhorn.tsx new file mode 100644 index 000000000..b24bf3c23 --- /dev/null +++ b/apps/artboard/src/templates/rhyhorn.tsx @@ -0,0 +1,695 @@ +import { SectionKey } from "@reactive-resume/schema"; +import { cn, isEmptyString, isUrl } from "@reactive-resume/utils"; +import { Fragment } from "react"; + +import { Picture } from "../components/picture"; +import { useArtboardStore } from "../store/artboard"; +import { TemplateProps } from "../types/template"; + +const fieldDisplay = cn("flex items-center gap-x-1.5 border-r pr-2 last:border-r-0 last:pr-0"); + +const Header = () => { + const basics = useArtboardStore((state) => state.resume.basics); + + return ( +
+ + +
+
{basics.name}
+
{basics.headline}
+ +
+ {basics.location && ( +
+ +
{basics.location}
+
+ )} + {basics.phone && ( + + )} + {basics.email && ( + + )} + {isUrl(basics.url.href) && ( + + )} + {basics.customFields.map((item) => ( +
+ + {[item.name, item.value].filter(Boolean).join(": ")} +
+ ))} +
+
+
+ ); +}; + +const sectionHeading = cn("mb-1.5 mt-3 border-b pb-0.5 text-sm font-bold uppercase"); + +const Profiles = () => { + const section = useArtboardStore((state) => state.resume.sections.profiles); + + if (!section.visible || !section.items.length) return null; + + return ( +
+

{section.name}

+ +
+ {section.items.map((item) => ( +
+ {item.network} + +
+ {isUrl(item.url.href) ? ( + + {item.url.label || item.username} + + ) : ( + {item.username} + )} + +

{item.network}

+
+
+ ))} +
+
+ ); +}; + +const Summary = () => { + const section = useArtboardStore((state) => state.resume.sections.summary); + + if (!section.visible || !section.content) return null; + + return ( +
+

{section.name}

+ + {!isEmptyString(section.content) && ( +
+
+
+ )} +
+ ); +}; + +const Experience = () => { + const section = useArtboardStore((state) => state.resume.sections.experience); + + if (!section.visible || !section.items.length) return null; + + return ( +
+

{section.name}

+ +
+ {section.items.map((item) => ( +
+
+
+
{item.company}
+
{item.position}
+ {isUrl(item.url.href) && ( + + )} +
+ +
+
{item.date}
+
{item.location}
+
+
+ + {!isEmptyString(item.summary) && ( +
+
+
+ )} +
+ ))} +
+
+ ); +}; + +const Education = () => { + const section = useArtboardStore((state) => state.resume.sections.education); + + if (!section.visible || !section.items.length) return null; + + return ( +
+

{section.name}

+ +
+ {section.items.map((item) => ( +
+
+
+
{item.institution}
+
{item.area}
+ {isUrl(item.url.href) && ( + + )} +
+ +
+
{item.date}
+
{item.studyType}
+
{item.score}
+
+
+ + {!isEmptyString(item.summary) && ( +
+
+
+ )} +
+ ))} +
+
+ ); +}; + +const Awards = () => { + const section = useArtboardStore((state) => state.resume.sections.awards); + + if (!section.visible || !section.items.length) return null; + + return ( +
+

{section.name}

+ +
+ {section.items.map((item) => ( +
+
+
+
{item.title}
+
{item.awarder}
+
+ +
+
{item.date}
+ {isUrl(item.url.href) && ( + + )} +
+
+ + {!isEmptyString(item.summary) && ( +
+
+
+ )} +
+ ))} +
+
+ ); +}; + +const Certifications = () => { + const section = useArtboardStore((state) => state.resume.sections.certifications); + + if (!section.visible || !section.items.length) return null; + + return ( +
+

{section.name}

+ +
+ {section.items.map((item) => ( +
+
+
+
{item.name}
+
{item.issuer}
+
+ +
+
{item.date}
+ {isUrl(item.url.href) && ( + + )} +
+
+ + {!isEmptyString(item.summary) && ( +
+
+
+ )} +
+ ))} +
+
+ ); +}; + +const Skills = () => { + const section = useArtboardStore((state) => state.resume.sections.skills); + + if (!section.visible || !section.items.length) return null; + + return ( +
+

{section.name}

+ +
+ {section.items.map((item) => ( +
+
+
+
{item.name}
+
{item.description}
+
+
+ + {item.keywords.length > 0 && ( +
+

{item.keywords.join(", ")}

+
+ )} +
+ ))} +
+
+ ); +}; + +const Interests = () => { + const section = useArtboardStore((state) => state.resume.sections.interests); + + if (!section.visible || !section.items.length) return null; + + return ( +
+

{section.name}

+ +
+ {section.items.map((item) => ( +
+
+
+
{item.name}
+
+
+ + {item.keywords.length > 0 && ( +
+

{item.keywords.join(", ")}

+
+ )} +
+ ))} +
+
+ ); +}; + +const Publications = () => { + const section = useArtboardStore((state) => state.resume.sections.publications); + + if (!section.visible || !section.items.length) return null; + + return ( +
+

{section.name}

+ +
+ {section.items.map((item) => ( +
+
+
+
{item.name}
+
{item.publisher}
+
+ +
+
{item.date}
+ {isUrl(item.url.href) && ( + + )} +
+
+ + {!isEmptyString(item.summary) && ( +
+
+
+ )} +
+ ))} +
+
+ ); +}; + +const Volunteer = () => { + const section = useArtboardStore((state) => state.resume.sections.volunteer); + + if (!section.visible || !section.items.length) return null; + + return ( +
+

{section.name}

+ +
+ {section.items.map((item) => ( +
+
+
+
{item.organization}
+
{item.position}
+ {isUrl(item.url.href) && ( + + )} +
+ +
+
{item.date}
+
{item.location}
+
+
+ + {!isEmptyString(item.summary) && ( +
+
+
+ )} +
+ ))} +
+
+ ); +}; + +const Languages = () => { + const section = useArtboardStore((state) => state.resume.sections.languages); + + if (!section.visible || !section.items.length) return null; + + return ( +
+

{section.name}

+ +
+ {section.items.map((item) => ( +
+
+
+
{item.name}
+
{item.fluency}
+
+
+
+ ))} +
+
+ ); +}; + +const Projects = () => { + const section = useArtboardStore((state) => state.resume.sections.projects); + + if (!section.visible || !section.items.length) return null; + + return ( +
+

{section.name}

+ +
+ {section.items.map((item) => ( +
+
+
+
{item.name}
+
{item.description}
+
+ +
+
{item.date}
+ {isUrl(item.url.href) && ( + + )} +
+
+ + {!isEmptyString(item.summary) && ( +
+
+
+ )} + + {item.keywords.length > 0 && ( +
+

{item.keywords.join(", ")}

+
+ )} +
+ ))} +
+
+ ); +}; + +const References = () => { + const section = useArtboardStore((state) => state.resume.sections.references); + + if (!section.visible || !section.items.length) return null; + + return ( +
+

{section.name}

+ +
+ {section.items.map((item) => ( +
+
+
+
{item.name}
+
{item.description}
+ {isUrl(item.url.href) && ( + + )} +
+
+ + {!isEmptyString(item.summary) && ( +
+
+
+ )} +
+ ))} +
+
+ ); +}; + +const Custom = ({ id }: { id: string }) => { + const section = useArtboardStore((state) => state.resume.sections.custom[id]); + + if (!section || !section.visible || !section.items.length) return null; + + return ( +
+

{section.name}

+ +
+ {section.items.map((item) => ( +
+
+
+
{item.name}
+
{item.description}
+ {isUrl(item.url.href) && ( + + )} +
+ +
+
{item.date}
+
{item.location}
+
+
+ + {!isEmptyString(item.summary) && ( +
+
+
+ )} + + {item.keywords.length > 0 && ( +
+

{item.keywords.join(", ")}

+
+ )} +
+ ))} +
+
+ ); +}; + +const mapSectionToComponent = (section: SectionKey) => { + switch (section) { + case "profiles": + return ; + case "summary": + return ; + case "experience": + return ; + case "education": + return ; + case "awards": + return ; + case "certifications": + return ; + case "skills": + return ; + case "interests": + return ; + case "publications": + return ; + case "volunteer": + return ; + case "languages": + return ; + case "projects": + return ; + case "references": + return ; + default: + if (section.startsWith("custom.")) return ; + + return

{section}

; + } +}; + +export const Rhyhorn = ({ columns, isFirstPage = false }: TemplateProps) => { + const [main, sidebar] = columns; + + return ( +
+ {isFirstPage &&
} + +
+ {main.map((section) => ( + {mapSectionToComponent(section)} + ))} + + {sidebar.map((section) => ( + {mapSectionToComponent(section)} + ))} +
+
+ ); +}; diff --git a/libs/templates/src/shared/templates.ts b/apps/artboard/src/types/template.ts similarity index 63% rename from libs/templates/src/shared/templates.ts rename to apps/artboard/src/types/template.ts index f66bcaeae..e87347b5b 100644 --- a/libs/templates/src/shared/templates.ts +++ b/apps/artboard/src/types/template.ts @@ -1,6 +1,11 @@ import { SectionKey } from "@reactive-resume/schema"; export type TemplateProps = { - isFirstPage?: boolean; columns: SectionKey[][]; + isFirstPage?: boolean; +}; + +export type BaseProps = { + children?: React.ReactNode; + className?: string; }; diff --git a/apps/artboard/tailwind.config.js b/apps/artboard/tailwind.config.js new file mode 100644 index 000000000..78e6efe03 --- /dev/null +++ b/apps/artboard/tailwind.config.js @@ -0,0 +1,13 @@ +const { createGlobPatternsForDependencies } = require("@nx/react/tailwind"); +const { join } = require("path"); + +/** @type {import('tailwindcss').Config} */ +module.exports = { + darkMode: "class", + content: [ + join(__dirname, "{src,pages,components,app}/**/*!(*.stories|*.spec).{ts,tsx,html}"), + ...createGlobPatternsForDependencies(__dirname), + ], + theme: {}, + plugins: [require("@tailwindcss/typography")], +}; diff --git a/libs/templates/tsconfig.lib.json b/apps/artboard/tsconfig.app.json similarity index 62% rename from libs/templates/tsconfig.lib.json rename to apps/artboard/tsconfig.app.json index 8d6bbf779..cd44a1e78 100644 --- a/libs/templates/tsconfig.lib.json +++ b/apps/artboard/tsconfig.app.json @@ -10,14 +10,14 @@ ] }, "exclude": [ - "**/*.spec.ts", - "**/*.test.ts", - "**/*.spec.tsx", - "**/*.test.tsx", - "**/*.spec.js", - "**/*.test.js", - "**/*.spec.jsx", - "**/*.test.jsx" + "src/**/*.spec.ts", + "src/**/*.test.ts", + "src/**/*.spec.tsx", + "src/**/*.test.tsx", + "src/**/*.spec.js", + "src/**/*.test.js", + "src/**/*.spec.jsx", + "src/**/*.test.jsx" ], "include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"] } diff --git a/libs/templates/tsconfig.json b/apps/artboard/tsconfig.json similarity index 68% rename from libs/templates/tsconfig.json rename to apps/artboard/tsconfig.json index cc9638116..fe609d7af 100644 --- a/libs/templates/tsconfig.json +++ b/apps/artboard/tsconfig.json @@ -5,16 +5,13 @@ "esModuleInterop": false, "allowSyntheticDefaultImports": true, "strict": true, - "types": ["vite/client", "vitest"] + "types": ["vite/client"] }, "files": [], "include": [], "references": [ { - "path": "./tsconfig.lib.json" - }, - { - "path": "./tsconfig.spec.json" + "path": "./tsconfig.app.json" } ], "extends": "../../tsconfig.base.json" diff --git a/apps/artboard/vite.config.ts b/apps/artboard/vite.config.ts new file mode 100644 index 000000000..35301c9ff --- /dev/null +++ b/apps/artboard/vite.config.ts @@ -0,0 +1,25 @@ +/// + +import { nxViteTsPaths } from "@nx/vite/plugins/nx-tsconfig-paths.plugin"; +import react from "@vitejs/plugin-react-swc"; +import { defineConfig, searchForWorkspaceRoot } from "vite"; + +export default defineConfig({ + base: "/artboard/", + + cacheDir: "../../node_modules/.vite/artboard", + + server: { + host: true, + port: 6173, + fs: { allow: [searchForWorkspaceRoot(process.cwd())] }, + }, + + plugins: [react(), nxViteTsPaths()], + + resolve: { + alias: { + "@/artboard/": `${searchForWorkspaceRoot(process.cwd())}/apps/artboard/src/`, + }, + }, +}); diff --git a/apps/client/proxy.conf.json b/apps/client/proxy.conf.json index 63dd62750..447417b8d 100644 --- a/apps/client/proxy.conf.json +++ b/apps/client/proxy.conf.json @@ -2,5 +2,9 @@ "/api": { "target": "http://localhost:3000", "secure": false + }, + "/artboard": { + "target": "http://localhost:6173", + "secure": false } } diff --git a/apps/client/src/pages/builder/_components/toolbar.tsx b/apps/client/src/pages/builder/_components/toolbar.tsx index 858603664..25ff3ebc8 100644 --- a/apps/client/src/pages/builder/_components/toolbar.tsx +++ b/apps/client/src/pages/builder/_components/toolbar.tsx @@ -22,7 +22,7 @@ export const BuilderToolbar = () => { const setValue = useResumeStore((state) => state.setValue); const undo = useTemporalResumeStore((state) => state.undo); const redo = useTemporalResumeStore((state) => state.redo); - const transformRef = useBuilderStore((state) => state.transform.ref); + const frameRef = useBuilderStore((state) => state.frame.ref); const id = useResumeStore((state) => state.resume.id); const isPublic = useResumeStore((state) => state.resume.visibility === "public"); @@ -41,6 +41,11 @@ export const BuilderToolbar = () => { openInNewTab(url); }; + const onZoomIn = () => frameRef?.contentWindow?.postMessage({ type: "ZOOM_IN" }, "*"); + const onZoomOut = () => frameRef?.contentWindow?.postMessage({ type: "ZOOM_OUT" }, "*"); + const onResetView = () => frameRef?.contentWindow?.postMessage({ type: "RESET_VIEW" }, "*"); + const onCenterView = () => frameRef?.contentWindow?.postMessage({ type: "CENTER_VIEW" }, "*"); + return ( { - {/* Zoom In */} - - {/* Zoom Out */} - - - {/* Center Artboard */} - diff --git a/apps/client/src/pages/builder/layout.tsx b/apps/client/src/pages/builder/layout.tsx index 6925e4db2..1711f3ac6 100644 --- a/apps/client/src/pages/builder/layout.tsx +++ b/apps/client/src/pages/builder/layout.tsx @@ -33,7 +33,7 @@ export const BuilderLayout = () => { if (isDesktop) { return (
- + { - const title = useResumeStore((state) => state.resume.title); - const resume = useResumeStore((state) => state.resume.data); - const setTransformRef = useBuilderStore((state) => state.transform.setRef); - - const { pageHeight, showBreakLine, showPageNumbers } = useMemo(() => { - const { format, options } = resume.metadata.page; + const frameRef = useBuilderStore((state) => state.frame.ref); + const setFrameRef = useBuilderStore((state) => state.frame.setRef); - return { - pageHeight: pageSizeMap[format].height, - showBreakLine: options.breakLine, - showPageNumbers: options.pageNumbers, - }; - }, [resume.metadata.page]); + const resume = useResumeStore((state) => state.resume); + const title = useResumeStore((state) => state.resume.title); - const Template = useMemo(() => { - const Component = templatesList.find((template) => template.id === resume.metadata.template) - ?.Component; + const updateResumeInFrame = useCallback(() => { + if (!frameRef || !frameRef.contentWindow) return; + const message = { type: "SET_RESUME", payload: resume.data }; + (() => frameRef.contentWindow.postMessage(message, "*"))(); + }, [frameRef, resume.data]); - if (!Component) return null; + // Send resume data to iframe on initial load + useEffect(() => { + if (!frameRef) return; + frameRef.addEventListener("load", updateResumeInFrame); + return () => frameRef.removeEventListener("load", updateResumeInFrame); + }, [frameRef]); - return Component; - }, [resume.metadata.template]); + // Send resume data to iframe on change of resume data + useEffect(updateResumeInFrame, [resume.data]); return ( <> @@ -50,45 +37,13 @@ export const BuilderPage = () => { {title} - Reactive Resume - setTransformRef(ref)} - > - - - - {resume.metadata.layout.map((columns, pageIndex) => ( - - - - {showPageNumbers && Page {pageIndex + 1}} - - {Template !== null && ( -