mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2025-11-10 04:22:27 +10:00
feat(templates): replace library with microfrontend app for templates
This commit is contained in:
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@ -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": [
|
||||
|
||||
31
apps/artboard/.eslintrc.json
Normal file
31
apps/artboard/.eslintrc.json
Normal file
@ -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": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
37
apps/artboard/index.html
Normal file
37
apps/artboard/index.html
Normal file
@ -0,0 +1,37 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Artboard | Reactive Resume</title>
|
||||
<base href="/" />
|
||||
|
||||
<!-- Meta -->
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
<!-- Favicon -->
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/svg+xml"
|
||||
href="/icon/dark.svg"
|
||||
media="(prefers-color-scheme: light)"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/svg+xml"
|
||||
href="/icon/light.svg"
|
||||
media="(prefers-color-scheme: dark)"
|
||||
/>
|
||||
|
||||
<!-- Styles -->
|
||||
<link rel="stylesheet" href="/src/styles/main.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
|
||||
<!-- Phosphor Icons -->
|
||||
<script src="https://unpkg.com/@phosphor-icons/web"></script>
|
||||
</body>
|
||||
</html>
|
||||
10
apps/artboard/postcss.config.js
Normal file
10
apps/artboard/postcss.config.js
Normal file
@ -0,0 +1,10 @@
|
||||
const { join } = require("path");
|
||||
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {
|
||||
config: join(__dirname, "tailwind.config.js"),
|
||||
},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
64
apps/artboard/project.json
Normal file
64
apps/artboard/project.json
Normal file
@ -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"]
|
||||
}
|
||||
BIN
apps/artboard/public/favicon.ico
Normal file
BIN
apps/artboard/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 68 KiB |
8
apps/artboard/public/icon/dark.svg
Normal file
8
apps/artboard/public/icon/dark.svg
Normal file
@ -0,0 +1,8 @@
|
||||
<svg width="256" height="256" viewBox="0 0 256 256" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M173.611 166.311L132.877 219.804H173.524L193.973 191.813L213.183 219.804H256L215.673 165.707L215.15 165.046L207.461 155.332L195.329 140.004L195.258 139.915L193.813 138.089L193.923 138.001L176.286 112.861H134.061L173.611 166.311ZM199.89 133.554L214.959 112.861H254.619L219.874 158.8L199.89 133.554Z"
|
||||
fill="#09090B" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M0 36.1959V174.314H39.0678V137.614H60.3938L60.4323 137.671C60.8436 137.653 61.2518 137.634 61.6569 137.614C75.0665 136.968 85.1471 135.549 96.3849 131.385C96.7596 131.246 97.1355 131.104 97.5128 130.959L97.4591 130.881C105.816 126.86 112.331 121.344 117.006 114.331C122.005 106.702 124.504 97.6915 124.504 87.2997C124.504 76.7764 122.005 67.7 117.006 60.0706C112.007 52.3097 104.904 46.3903 95.6964 42.3125C86.62 38.2347 75.7678 36.1959 63.1399 36.1959H0ZM102.156 137.725L64.8705 144.175L85.4361 174.314H127.266L102.156 137.725ZM39.0678 107.426H60.7721C68.9277 107.426 74.9786 105.65 78.9248 102.098C83.0026 98.5465 85.0415 93.6137 85.0415 87.2997C85.0415 80.8542 83.0026 75.8556 78.9248 72.304C74.9786 68.7523 68.9277 66.9765 60.7721 66.9765H39.0678V107.426Z"
|
||||
fill="#09090B" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
8
apps/artboard/public/icon/light.svg
Normal file
8
apps/artboard/public/icon/light.svg
Normal file
@ -0,0 +1,8 @@
|
||||
<svg width="256" height="256" viewBox="0 0 256 256" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M173.611 166.311L132.877 219.804H173.524L193.973 191.813L213.183 219.804H256L215.673 165.707L215.15 165.046L207.461 155.332L195.329 140.004L195.258 139.915L193.813 138.089L193.923 138.001L176.286 112.861H134.061L173.611 166.311ZM199.89 133.554L214.959 112.861H254.619L219.874 158.8L199.89 133.554Z"
|
||||
fill="#FAFAFA" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M0 36.1959V174.314H39.0678V137.614H60.3938L60.4323 137.671C60.8436 137.653 61.2517 137.634 61.6567 137.614C75.0665 136.968 85.1471 135.549 96.385 131.385C96.7596 131.246 97.1355 131.104 97.5128 130.959L97.4591 130.881C105.816 126.86 112.331 121.344 117.006 114.331C122.005 106.702 124.504 97.6915 124.504 87.2997C124.504 76.7764 122.005 67.7 117.006 60.0706C112.007 52.3097 104.904 46.3903 95.6964 42.3125C86.62 38.2347 75.7679 36.1959 63.1399 36.1959H0ZM102.156 137.725L64.8705 144.175L85.4361 174.314H127.266L102.156 137.725ZM39.0678 107.426H60.7721C68.9277 107.426 74.9786 105.65 78.9248 102.098C83.0026 98.5465 85.0415 93.6137 85.0415 87.2997C85.0415 80.8542 83.0026 75.8556 78.9248 72.304C74.9786 68.7523 68.9277 66.9765 60.7721 66.9765H39.0678V107.426Z"
|
||||
fill="#FAFAFA" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
0
apps/artboard/src/assets/.gitkeep
Normal file
0
apps/artboard/src/assets/.gitkeep
Normal file
49
apps/artboard/src/components/page.tsx
Normal file
49
apps/artboard/src/components/page.tsx
Normal file
@ -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 (
|
||||
<div
|
||||
data-page={pageNumber}
|
||||
className={cn("relative bg-white", mode === "builder" && "shadow-2xl")}
|
||||
style={{
|
||||
fontFamily,
|
||||
padding: page.margin,
|
||||
width: `${pageSizeMap[page.format].width * MM_TO_PX * window.devicePixelRatio}px`,
|
||||
minHeight: `${pageSizeMap[page.format].height * MM_TO_PX * window.devicePixelRatio}px`,
|
||||
}}
|
||||
>
|
||||
{mode === "builder" && page.options.pageNumbers && (
|
||||
<div className={cn("absolute -top-7 left-0 font-bold", isDarkMode && "text-white")}>
|
||||
Page {pageNumber}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{children}
|
||||
|
||||
{mode === "builder" && page.options.breakLine && (
|
||||
<div
|
||||
className="absolute inset-x-0 border-b border-dashed"
|
||||
style={{
|
||||
top: `${pageSizeMap[page.format].height * MM_TO_PX * window.devicePixelRatio}px`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
22
apps/artboard/src/components/picture.tsx
Normal file
22
apps/artboard/src/components/picture.tsx
Normal file
@ -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 (
|
||||
<img
|
||||
src={picture.url}
|
||||
alt="Profile"
|
||||
className="object-cover"
|
||||
style={{
|
||||
maxWidth: `${picture.size}px`,
|
||||
aspectRatio: `${picture.aspectRatio}`,
|
||||
borderRadius: `${picture.borderRadius}px`,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
13
apps/artboard/src/main.tsx
Normal file
13
apps/artboard/src/main.tsx
Normal file
@ -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(
|
||||
<StrictMode>
|
||||
<RouterProvider router={router} />
|
||||
</StrictMode>,
|
||||
);
|
||||
45
apps/artboard/src/pages/artboard.tsx
Normal file
45
apps/artboard/src/pages/artboard.tsx
Normal file
@ -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 <Outlet />;
|
||||
};
|
||||
63
apps/artboard/src/pages/builder.tsx
Normal file
63
apps/artboard/src/pages/builder.tsx
Normal file
@ -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<ReactZoomPanPinchRef>(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 (
|
||||
<TransformWrapper
|
||||
centerOnInit
|
||||
maxScale={2}
|
||||
minScale={0.4}
|
||||
initialScale={0.8}
|
||||
ref={transformRef}
|
||||
limitToBounds={false}
|
||||
>
|
||||
<TransformComponent
|
||||
wrapperClass="!w-screen !h-screen"
|
||||
contentClass="grid items-start justify-center space-x-12 pointer-events-none"
|
||||
contentStyle={{
|
||||
width: `${layout.length * (pageSizeMap[format].width * MM_TO_PX + 42)}px`,
|
||||
gridTemplateColumns: `repeat(${layout.length}, 1fr)`,
|
||||
}}
|
||||
>
|
||||
{layout.map((columns, pageIndex) => (
|
||||
<Page key={pageIndex} mode="builder" pageNumber={pageIndex + 1}>
|
||||
{template === "rhyhorn" && (
|
||||
<Rhyhorn isFirstPage={pageIndex === 0} columns={columns as SectionKey[][]} />
|
||||
)}
|
||||
</Page>
|
||||
))}
|
||||
</TransformComponent>
|
||||
</TransformWrapper>
|
||||
);
|
||||
};
|
||||
22
apps/artboard/src/pages/preview.tsx
Normal file
22
apps/artboard/src/pages/preview.tsx
Normal file
@ -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) => (
|
||||
<Page key={pageIndex} mode="preview" pageNumber={pageIndex + 1}>
|
||||
{template === "rhyhorn" && (
|
||||
<Rhyhorn isFirstPage={pageIndex === 0} columns={columns as SectionKey[][]} />
|
||||
)}
|
||||
</Page>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
40
apps/artboard/src/providers/index.tsx
Normal file
40
apps/artboard/src/providers/index.tsx
Normal file
@ -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 <Outlet />;
|
||||
};
|
||||
17
apps/artboard/src/router/index.tsx
Normal file
17
apps/artboard/src/router/index.tsx
Normal file
@ -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(
|
||||
<Route element={<Providers />}>
|
||||
<Route path="artboard" element={<ArtboardPage />}>
|
||||
<Route path="builder" element={<BuilderLayout />} />
|
||||
<Route path="preview" element={<PreviewLayout />} />
|
||||
</Route>
|
||||
</Route>,
|
||||
);
|
||||
|
||||
export const router = createBrowserRouter(routes);
|
||||
12
apps/artboard/src/store/artboard.ts
Normal file
12
apps/artboard/src/store/artboard.ts
Normal file
@ -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<ArtboardStore>()((set) => ({
|
||||
resume: null as unknown as ResumeData,
|
||||
setResume: (resume) => set({ resume }),
|
||||
}));
|
||||
19
apps/artboard/src/styles/main.css
Normal file
19
apps/artboard/src/styles/main.css
Normal file
@ -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;
|
||||
}
|
||||
695
apps/artboard/src/templates/rhyhorn.tsx
Normal file
695
apps/artboard/src/templates/rhyhorn.tsx
Normal file
@ -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 (
|
||||
<div className="flex items-center space-x-4">
|
||||
<Picture />
|
||||
|
||||
<div className="space-y-0.5">
|
||||
<div className="text-2xl font-bold leading-tight">{basics.name}</div>
|
||||
<div className="text-base">{basics.headline}</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-x-2 gap-y-0.5 text-sm">
|
||||
{basics.location && (
|
||||
<div className={cn(fieldDisplay)}>
|
||||
<i className="ph ph-map-pin" />
|
||||
<div>{basics.location}</div>
|
||||
</div>
|
||||
)}
|
||||
{basics.phone && (
|
||||
<div className={cn(fieldDisplay)}>
|
||||
<i className="ph ph-phone" />
|
||||
<a href={`tel:${basics.phone}`} target="_blank" rel="noreferrer">
|
||||
{basics.phone}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
{basics.email && (
|
||||
<div className={cn(fieldDisplay)}>
|
||||
<i className="ph ph-envelope" />
|
||||
<a href={`mailto:${basics.email}`} target="_blank" rel="noreferrer">
|
||||
{basics.email}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
{isUrl(basics.url.href) && (
|
||||
<div className={cn(fieldDisplay)}>
|
||||
<i className="ph ph-globe" />
|
||||
<a href={basics.url.href} target="_blank" rel="noreferrer">
|
||||
{basics.url.label || basics.url.href}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
{basics.customFields.map((item) => (
|
||||
<div key={item.id} className={cn(fieldDisplay)}>
|
||||
<i className={`ph ph-${item.icon}`} />
|
||||
<span>{[item.name, item.value].filter(Boolean).join(": ")}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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 id={section.id}>
|
||||
<h4 className={cn(sectionHeading)}>{section.name}</h4>
|
||||
|
||||
<div
|
||||
className="mt-2 grid items-center"
|
||||
style={{ gridTemplateColumns: `repeat(${section.columns}, 1fr)` }}
|
||||
>
|
||||
{section.items.map((item) => (
|
||||
<div key={item.id} className="flex items-center gap-x-3">
|
||||
<img
|
||||
width="16"
|
||||
height="16"
|
||||
alt={item.network}
|
||||
src={`https://cdn.simpleicons.org/${item.icon}`}
|
||||
/>
|
||||
|
||||
<div className="leading-tight">
|
||||
{isUrl(item.url.href) ? (
|
||||
<a href={item.url.href} target="_blank" rel="noreferrer" className="font-medium">
|
||||
{item.url.label || item.username}
|
||||
</a>
|
||||
) : (
|
||||
<span className="font-medium">{item.username}</span>
|
||||
)}
|
||||
|
||||
<p className="text-sm">{item.network}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
const Summary = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.summary);
|
||||
|
||||
if (!section.visible || !section.content) return null;
|
||||
|
||||
return (
|
||||
<section id={section.id}>
|
||||
<h4 className={cn(sectionHeading)}>{section.name}</h4>
|
||||
|
||||
{!isEmptyString(section.content) && (
|
||||
<main>
|
||||
<div
|
||||
className="wysiwyg"
|
||||
style={{ columns: section.columns }}
|
||||
dangerouslySetInnerHTML={{ __html: section.content }}
|
||||
/>
|
||||
</main>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
const Experience = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.experience);
|
||||
|
||||
if (!section.visible || !section.items.length) return null;
|
||||
|
||||
return (
|
||||
<section id={section.id}>
|
||||
<h4 className={cn(sectionHeading)}>{section.name}</h4>
|
||||
|
||||
<div
|
||||
className="grid items-start gap-x-4 gap-y-2"
|
||||
style={{ gridTemplateColumns: `repeat(${section.columns}, 1fr)` }}
|
||||
>
|
||||
{section.items.map((item) => (
|
||||
<div key={item.id}>
|
||||
<header className="mb-2 flex items-center justify-between">
|
||||
<div className="text-left">
|
||||
<div className="font-bold">{item.company}</div>
|
||||
<div>{item.position}</div>
|
||||
{isUrl(item.url.href) && (
|
||||
<div>
|
||||
<a href={item.url.href} target="_blank" rel="noreferrer">
|
||||
{item.url.label || item.url.href}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="shrink-0 text-right">
|
||||
<div className="font-bold">{item.date}</div>
|
||||
<div>{item.location}</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{!isEmptyString(item.summary) && (
|
||||
<main>
|
||||
<div className="wysiwyg" dangerouslySetInnerHTML={{ __html: item.summary }} />
|
||||
</main>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
const Education = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.education);
|
||||
|
||||
if (!section.visible || !section.items.length) return null;
|
||||
|
||||
return (
|
||||
<section id={section.id}>
|
||||
<h4 className={cn(sectionHeading)}>{section.name}</h4>
|
||||
|
||||
<div
|
||||
className="grid items-start gap-x-4 gap-y-2"
|
||||
style={{ gridTemplateColumns: `repeat(${section.columns}, 1fr)` }}
|
||||
>
|
||||
{section.items.map((item) => (
|
||||
<div key={item.id}>
|
||||
<header className="mb-2 flex items-center justify-between">
|
||||
<div className="text-left">
|
||||
<div className="font-bold">{item.institution}</div>
|
||||
<div>{item.area}</div>
|
||||
{isUrl(item.url.href) && (
|
||||
<div>
|
||||
<a href={item.url.href} target="_blank" rel="noreferrer">
|
||||
{item.url.label || item.url.href}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="shrink-0 text-right">
|
||||
<div className="font-bold">{item.date}</div>
|
||||
<div>{item.studyType}</div>
|
||||
<div>{item.score}</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{!isEmptyString(item.summary) && (
|
||||
<main>
|
||||
<div className="wysiwyg" dangerouslySetInnerHTML={{ __html: item.summary }} />
|
||||
</main>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
const Awards = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.awards);
|
||||
|
||||
if (!section.visible || !section.items.length) return null;
|
||||
|
||||
return (
|
||||
<section id={section.id}>
|
||||
<h4 className={cn(sectionHeading)}>{section.name}</h4>
|
||||
|
||||
<div
|
||||
className="grid items-start gap-x-4 gap-y-2"
|
||||
style={{ gridTemplateColumns: `repeat(${section.columns}, 1fr)` }}
|
||||
>
|
||||
{section.items.map((item) => (
|
||||
<div key={item.id} className="space-y-2">
|
||||
<header className="flex items-center justify-between">
|
||||
<div className="text-left">
|
||||
<div className="font-bold">{item.title}</div>
|
||||
<div>{item.awarder}</div>
|
||||
</div>
|
||||
|
||||
<div className="shrink-0 text-right">
|
||||
<div className="font-bold">{item.date}</div>
|
||||
{isUrl(item.url.href) && (
|
||||
<div>
|
||||
<a href={item.url.href} target="_blank" rel="noreferrer">
|
||||
{item.url.label || item.url.href}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{!isEmptyString(item.summary) && (
|
||||
<main>
|
||||
<div className="wysiwyg" dangerouslySetInnerHTML={{ __html: item.summary }} />
|
||||
</main>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
const Certifications = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.certifications);
|
||||
|
||||
if (!section.visible || !section.items.length) return null;
|
||||
|
||||
return (
|
||||
<section id={section.id}>
|
||||
<h4 className={cn(sectionHeading)}>{section.name}</h4>
|
||||
|
||||
<div
|
||||
className="grid items-start gap-x-4 gap-y-2"
|
||||
style={{ gridTemplateColumns: `repeat(${section.columns}, 1fr)` }}
|
||||
>
|
||||
{section.items.map((item) => (
|
||||
<div key={item.id} className="space-y-2">
|
||||
<header className="flex items-center justify-between">
|
||||
<div className="text-left">
|
||||
<div className="font-bold">{item.name}</div>
|
||||
<div>{item.issuer}</div>
|
||||
</div>
|
||||
|
||||
<div className="shrink-0 text-right">
|
||||
<div className="font-bold">{item.date}</div>
|
||||
{isUrl(item.url.href) && (
|
||||
<div>
|
||||
<a href={item.url.href} target="_blank" rel="noreferrer">
|
||||
{item.url.label || item.url.href}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{!isEmptyString(item.summary) && (
|
||||
<main>
|
||||
<div className="wysiwyg" dangerouslySetInnerHTML={{ __html: item.summary }} />
|
||||
</main>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
const Skills = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.skills);
|
||||
|
||||
if (!section.visible || !section.items.length) return null;
|
||||
|
||||
return (
|
||||
<section id={section.id}>
|
||||
<h4 className={cn(sectionHeading)}>{section.name}</h4>
|
||||
|
||||
<div
|
||||
className="grid items-start gap-x-4 gap-y-2"
|
||||
style={{ gridTemplateColumns: `repeat(${section.columns}, 1fr)` }}
|
||||
>
|
||||
{section.items.map((item) => (
|
||||
<div key={item.id} className="space-y-2">
|
||||
<header className="flex items-center justify-between">
|
||||
<div className="text-left">
|
||||
<div className="font-bold">{item.name}</div>
|
||||
<div>{item.description}</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{item.keywords.length > 0 && (
|
||||
<footer>
|
||||
<p className="text-sm">{item.keywords.join(", ")}</p>
|
||||
</footer>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
const Interests = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.interests);
|
||||
|
||||
if (!section.visible || !section.items.length) return null;
|
||||
|
||||
return (
|
||||
<section id={section.id}>
|
||||
<h4 className={cn(sectionHeading)}>{section.name}</h4>
|
||||
|
||||
<div
|
||||
className="grid items-start gap-x-4 gap-y-2"
|
||||
style={{ gridTemplateColumns: `repeat(${section.columns}, 1fr)` }}
|
||||
>
|
||||
{section.items.map((item) => (
|
||||
<div key={item.id}>
|
||||
<header className="flex items-center justify-between">
|
||||
<div className="text-left">
|
||||
<div className="font-bold">{item.name}</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{item.keywords.length > 0 && (
|
||||
<footer>
|
||||
<p className="text-sm">{item.keywords.join(", ")}</p>
|
||||
</footer>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
const Publications = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.publications);
|
||||
|
||||
if (!section.visible || !section.items.length) return null;
|
||||
|
||||
return (
|
||||
<section id={section.id}>
|
||||
<h4 className={cn(sectionHeading)}>{section.name}</h4>
|
||||
|
||||
<div
|
||||
className="grid items-start gap-x-4 gap-y-2"
|
||||
style={{ gridTemplateColumns: `repeat(${section.columns}, 1fr)` }}
|
||||
>
|
||||
{section.items.map((item) => (
|
||||
<div key={item.id} className="space-y-2">
|
||||
<header className="flex items-center justify-between">
|
||||
<div className="text-left">
|
||||
<div className="font-bold">{item.name}</div>
|
||||
<div>{item.publisher}</div>
|
||||
</div>
|
||||
|
||||
<div className="shrink-0 text-right">
|
||||
<div className="font-bold">{item.date}</div>
|
||||
{isUrl(item.url.href) && (
|
||||
<div>
|
||||
<a href={item.url.href} target="_blank" rel="noreferrer">
|
||||
{item.url.label || item.url.href}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{!isEmptyString(item.summary) && (
|
||||
<main>
|
||||
<div className="wysiwyg" dangerouslySetInnerHTML={{ __html: item.summary }} />
|
||||
</main>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
const Volunteer = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.volunteer);
|
||||
|
||||
if (!section.visible || !section.items.length) return null;
|
||||
|
||||
return (
|
||||
<section id={section.id}>
|
||||
<h4 className={cn(sectionHeading)}>{section.name}</h4>
|
||||
|
||||
<div
|
||||
className="grid items-start gap-x-4 gap-y-2"
|
||||
style={{ gridTemplateColumns: `repeat(${section.columns}, 1fr)` }}
|
||||
>
|
||||
{section.items.map((item) => (
|
||||
<div key={item.id}>
|
||||
<header className="mb-2 flex items-center justify-between">
|
||||
<div className="text-left">
|
||||
<div className="font-bold">{item.organization}</div>
|
||||
<div>{item.position}</div>
|
||||
{isUrl(item.url.href) && (
|
||||
<div>
|
||||
<a href={item.url.href} target="_blank" rel="noreferrer">
|
||||
{item.url.label || item.url.href}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="shrink-0 text-right">
|
||||
<div className="font-bold">{item.date}</div>
|
||||
<div>{item.location}</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{!isEmptyString(item.summary) && (
|
||||
<main>
|
||||
<div className="wysiwyg" dangerouslySetInnerHTML={{ __html: item.summary }} />
|
||||
</main>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
const Languages = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.languages);
|
||||
|
||||
if (!section.visible || !section.items.length) return null;
|
||||
|
||||
return (
|
||||
<section id={section.id}>
|
||||
<h4 className={cn(sectionHeading)}>{section.name}</h4>
|
||||
|
||||
<div
|
||||
className="grid items-start gap-x-4 gap-y-2"
|
||||
style={{ gridTemplateColumns: `repeat(${section.columns}, 1fr)` }}
|
||||
>
|
||||
{section.items.map((item) => (
|
||||
<div key={item.id} className="space-y-2">
|
||||
<header className="flex items-center justify-between">
|
||||
<div className="text-left">
|
||||
<div className="font-bold">{item.name}</div>
|
||||
<div>{item.fluency}</div>
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
const Projects = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.projects);
|
||||
|
||||
if (!section.visible || !section.items.length) return null;
|
||||
|
||||
return (
|
||||
<section id={section.id}>
|
||||
<h4 className={cn(sectionHeading)}>{section.name}</h4>
|
||||
|
||||
<div
|
||||
className="grid items-start gap-x-4 gap-y-2"
|
||||
style={{ gridTemplateColumns: `repeat(${section.columns}, 1fr)` }}
|
||||
>
|
||||
{section.items.map((item) => (
|
||||
<div key={item.id} className="space-y-2">
|
||||
<header className="flex items-center justify-between">
|
||||
<div className="text-left">
|
||||
<div className="font-bold">{item.name}</div>
|
||||
<div>{item.description}</div>
|
||||
</div>
|
||||
|
||||
<div className="shrink-0 text-right">
|
||||
<div className="font-bold">{item.date}</div>
|
||||
{isUrl(item.url.href) && (
|
||||
<div>
|
||||
<a href={item.url.href} target="_blank" rel="noreferrer">
|
||||
{item.url.label || item.url.href}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{!isEmptyString(item.summary) && (
|
||||
<main>
|
||||
<div className="wysiwyg" dangerouslySetInnerHTML={{ __html: item.summary }} />
|
||||
</main>
|
||||
)}
|
||||
|
||||
{item.keywords.length > 0 && (
|
||||
<footer>
|
||||
<p className="text-sm">{item.keywords.join(", ")}</p>
|
||||
</footer>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
const References = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.references);
|
||||
|
||||
if (!section.visible || !section.items.length) return null;
|
||||
|
||||
return (
|
||||
<section id={section.id}>
|
||||
<h4 className={cn(sectionHeading)}>{section.name}</h4>
|
||||
|
||||
<div
|
||||
className="grid items-start gap-x-4 gap-y-2"
|
||||
style={{ gridTemplateColumns: `repeat(${section.columns}, 1fr)` }}
|
||||
>
|
||||
{section.items.map((item) => (
|
||||
<div key={item.id} className="space-y-2">
|
||||
<header className="flex items-center justify-between">
|
||||
<div className="text-left">
|
||||
<div className="font-bold">{item.name}</div>
|
||||
<div>{item.description}</div>
|
||||
{isUrl(item.url.href) && (
|
||||
<div>
|
||||
<a href={item.url.href} target="_blank" rel="noreferrer">
|
||||
{item.url.label || item.url.href}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{!isEmptyString(item.summary) && (
|
||||
<main>
|
||||
<div className="wysiwyg" dangerouslySetInnerHTML={{ __html: item.summary }} />
|
||||
</main>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
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 id={section.id}>
|
||||
<h4 className={cn(sectionHeading)}>{section.name}</h4>
|
||||
|
||||
<div
|
||||
className="grid items-start gap-x-4 gap-y-2"
|
||||
style={{ gridTemplateColumns: `repeat(${section.columns}, 1fr)` }}
|
||||
>
|
||||
{section.items.map((item) => (
|
||||
<div key={item.id} className="space-y-2">
|
||||
<header className="flex items-center justify-between">
|
||||
<div className="text-left">
|
||||
<div className="font-bold">{item.name}</div>
|
||||
<div>{item.description}</div>
|
||||
{isUrl(item.url.href) && (
|
||||
<div>
|
||||
<a href={item.url.href} target="_blank" rel="noreferrer">
|
||||
{item.url.label || item.url.href}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="shrink-0 text-right">
|
||||
<div className="font-bold">{item.date}</div>
|
||||
<div>{item.location}</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{!isEmptyString(item.summary) && (
|
||||
<main>
|
||||
<div className="wysiwyg" dangerouslySetInnerHTML={{ __html: item.summary }} />
|
||||
</main>
|
||||
)}
|
||||
|
||||
{item.keywords.length > 0 && (
|
||||
<footer>
|
||||
<p className="text-sm">{item.keywords.join(", ")}</p>
|
||||
</footer>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
const mapSectionToComponent = (section: SectionKey) => {
|
||||
switch (section) {
|
||||
case "profiles":
|
||||
return <Profiles />;
|
||||
case "summary":
|
||||
return <Summary />;
|
||||
case "experience":
|
||||
return <Experience />;
|
||||
case "education":
|
||||
return <Education />;
|
||||
case "awards":
|
||||
return <Awards />;
|
||||
case "certifications":
|
||||
return <Certifications />;
|
||||
case "skills":
|
||||
return <Skills />;
|
||||
case "interests":
|
||||
return <Interests />;
|
||||
case "publications":
|
||||
return <Publications />;
|
||||
case "volunteer":
|
||||
return <Volunteer />;
|
||||
case "languages":
|
||||
return <Languages />;
|
||||
case "projects":
|
||||
return <Projects />;
|
||||
case "references":
|
||||
return <References />;
|
||||
default:
|
||||
if (section.startsWith("custom.")) return <Custom id={section.split(".")[1]} />;
|
||||
|
||||
return <p>{section}</p>;
|
||||
}
|
||||
};
|
||||
|
||||
export const Rhyhorn = ({ columns, isFirstPage = false }: TemplateProps) => {
|
||||
const [main, sidebar] = columns;
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{isFirstPage && <Header />}
|
||||
|
||||
<div className="space-y-2">
|
||||
{main.map((section) => (
|
||||
<Fragment key={section}>{mapSectionToComponent(section)}</Fragment>
|
||||
))}
|
||||
|
||||
{sidebar.map((section) => (
|
||||
<Fragment key={section}>{mapSectionToComponent(section)}</Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -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;
|
||||
};
|
||||
13
apps/artboard/tailwind.config.js
Normal file
13
apps/artboard/tailwind.config.js
Normal file
@ -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")],
|
||||
};
|
||||
@ -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"]
|
||||
}
|
||||
@ -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"
|
||||
25
apps/artboard/vite.config.ts
Normal file
25
apps/artboard/vite.config.ts
Normal file
@ -0,0 +1,25 @@
|
||||
/// <reference types='vitest' />
|
||||
|
||||
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/`,
|
||||
},
|
||||
},
|
||||
});
|
||||
@ -2,5 +2,9 @@
|
||||
"/api": {
|
||||
"target": "http://localhost:3000",
|
||||
"secure": false
|
||||
},
|
||||
"/artboard": {
|
||||
"target": "http://localhost:6173",
|
||||
"secure": false
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, bottom: -64 }}
|
||||
@ -65,49 +70,26 @@ export const BuilderToolbar = () => {
|
||||
|
||||
<Separator orientation="vertical" className="h-9" />
|
||||
|
||||
{/* Zoom In */}
|
||||
<Tooltip content="Zoom In">
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="rounded-none"
|
||||
onClick={() => transformRef?.zoomIn(0.2)}
|
||||
>
|
||||
<Button size="icon" variant="ghost" className="rounded-none" onClick={onZoomIn}>
|
||||
<MagnifyingGlassPlus />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
{/* Zoom Out */}
|
||||
<Tooltip content="Zoom Out">
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="rounded-none"
|
||||
onClick={() => transformRef?.zoomOut(0.2)}
|
||||
>
|
||||
<Button size="icon" variant="ghost" className="rounded-none" onClick={onZoomOut}>
|
||||
<MagnifyingGlassMinus />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content="Reset Zoom">
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="rounded-none"
|
||||
onClick={() => transformRef?.resetTransform()}
|
||||
>
|
||||
<Button size="icon" variant="ghost" className="rounded-none" onClick={onResetView}>
|
||||
<ClockClockwise />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
{/* Center Artboard */}
|
||||
<Tooltip content="Center Artboard">
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="rounded-none"
|
||||
onClick={() => transformRef?.centerView()}
|
||||
>
|
||||
<Button size="icon" variant="ghost" className="rounded-none" onClick={onCenterView}>
|
||||
<CubeFocus />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
@ -33,7 +33,7 @@ export const BuilderLayout = () => {
|
||||
if (isDesktop) {
|
||||
return (
|
||||
<div className="relative h-full w-full overflow-hidden">
|
||||
<PanelGroup direction="horizontal" autoSaveId="builder-layout">
|
||||
<PanelGroup direction="horizontal">
|
||||
<Panel
|
||||
collapsible
|
||||
minSize={20}
|
||||
|
||||
@ -1,19 +1,7 @@
|
||||
import { ResumeDto } from "@reactive-resume/dto";
|
||||
import { SectionKey } from "@reactive-resume/schema";
|
||||
import {
|
||||
Artboard,
|
||||
PageBreakLine,
|
||||
PageGrid,
|
||||
PageNumber,
|
||||
PageWrapper,
|
||||
templatesList,
|
||||
} from "@reactive-resume/templates";
|
||||
import { pageSizeMap } from "@reactive-resume/utils";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { useMemo } from "react";
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { Helmet } from "react-helmet-async";
|
||||
import { LoaderFunction, redirect } from "react-router-dom";
|
||||
import { ReactZoomPanPinchRef, TransformComponent, TransformWrapper } from "react-zoom-pan-pinch";
|
||||
|
||||
import { queryClient } from "@/client/libs/query-client";
|
||||
import { findResumeById } from "@/client/services/resume";
|
||||
@ -21,28 +9,27 @@ import { useBuilderStore } from "@/client/stores/builder";
|
||||
import { useResumeStore } from "@/client/stores/resume";
|
||||
|
||||
export const BuilderPage = () => {
|
||||
const frameRef = useBuilderStore((state) => state.frame.ref);
|
||||
const setFrameRef = useBuilderStore((state) => state.frame.setRef);
|
||||
|
||||
const resume = useResumeStore((state) => state.resume);
|
||||
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 updateResumeInFrame = useCallback(() => {
|
||||
if (!frameRef || !frameRef.contentWindow) return;
|
||||
const message = { type: "SET_RESUME", payload: resume.data };
|
||||
(() => frameRef.contentWindow.postMessage(message, "*"))();
|
||||
}, [frameRef, resume.data]);
|
||||
|
||||
return {
|
||||
pageHeight: pageSizeMap[format].height,
|
||||
showBreakLine: options.breakLine,
|
||||
showPageNumbers: options.pageNumbers,
|
||||
};
|
||||
}, [resume.metadata.page]);
|
||||
// Send resume data to iframe on initial load
|
||||
useEffect(() => {
|
||||
if (!frameRef) return;
|
||||
frameRef.addEventListener("load", updateResumeInFrame);
|
||||
return () => frameRef.removeEventListener("load", updateResumeInFrame);
|
||||
}, [frameRef]);
|
||||
|
||||
const Template = useMemo(() => {
|
||||
const Component = templatesList.find((template) => template.id === resume.metadata.template)
|
||||
?.Component;
|
||||
|
||||
if (!Component) return null;
|
||||
|
||||
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>{title} - Reactive Resume</title>
|
||||
</Helmet>
|
||||
|
||||
<TransformWrapper
|
||||
centerOnInit
|
||||
minScale={0.4}
|
||||
initialScale={1}
|
||||
limitToBounds={false}
|
||||
velocityAnimation={{ disabled: true }}
|
||||
ref={(ref: ReactZoomPanPinchRef) => setTransformRef(ref)}
|
||||
>
|
||||
<TransformComponent wrapperClass="!w-screen !h-screen">
|
||||
<PageGrid $offset={32}>
|
||||
<AnimatePresence presenceAffectsLayout>
|
||||
{resume.metadata.layout.map((columns, pageIndex) => (
|
||||
<motion.div
|
||||
layout
|
||||
key={pageIndex}
|
||||
initial={{ opacity: 0, x: -100 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -100 }}
|
||||
>
|
||||
<Artboard resume={resume}>
|
||||
<PageWrapper>
|
||||
{showPageNumbers && <PageNumber>Page {pageIndex + 1}</PageNumber>}
|
||||
|
||||
{Template !== null && (
|
||||
<Template
|
||||
isFirstPage={pageIndex === 0}
|
||||
columns={columns as SectionKey[][]}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showBreakLine && <PageBreakLine $pageHeight={pageHeight} />}
|
||||
</PageWrapper>
|
||||
</Artboard>
|
||||
</motion.div>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</PageGrid>
|
||||
</TransformComponent>
|
||||
</TransformWrapper>
|
||||
<iframe
|
||||
ref={setFrameRef}
|
||||
title={resume.id}
|
||||
src="/artboard/builder"
|
||||
className="mt-16 w-screen"
|
||||
style={{ height: `calc(100vh - 64px)` }}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import { templatesList } from "@reactive-resume/templates";
|
||||
import { Button, HoverCard, HoverCardContent, HoverCardTrigger } from "@reactive-resume/ui";
|
||||
import { cn } from "@reactive-resume/utils";
|
||||
import { cn, templatesList } from "@reactive-resume/utils";
|
||||
|
||||
import { useResumeStore } from "@/client/stores/resume";
|
||||
|
||||
@ -20,7 +19,7 @@ export const TemplateSection = () => {
|
||||
</header>
|
||||
|
||||
<main className="grid grid-cols-2 gap-4">
|
||||
{templatesList.map(({ id, name }) => (
|
||||
{templatesList.map(({ id, name, image }) => (
|
||||
<HoverCard key={id} openDelay={0} closeDelay={0}>
|
||||
<HoverCardTrigger asChild>
|
||||
<Button
|
||||
@ -36,12 +35,7 @@ export const TemplateSection = () => {
|
||||
</HoverCardTrigger>
|
||||
|
||||
<HoverCardContent className="max-w-xs overflow-hidden border-none bg-white p-0">
|
||||
<img
|
||||
alt={name}
|
||||
loading="lazy"
|
||||
src={`/templates/${id}.jpg`}
|
||||
className="aspect-[1/1.4142]"
|
||||
/>
|
||||
<img alt={name} src={image} loading="lazy" className="aspect-[1/1.4142]" />
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
))}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { CaretDown, Flask, MagicWand, Plus } from "@phosphor-icons/react";
|
||||
import { createResumeSchema, ResumeDto } from "@reactive-resume/dto";
|
||||
import { idSchema } from "@reactive-resume/schema";
|
||||
import { idSchema, sampleResume } from "@reactive-resume/schema";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
@ -38,7 +38,6 @@ import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
|
||||
import { sampleResume } from "@/client/constants/sample-resume";
|
||||
import { useToast } from "@/client/hooks/use-toast";
|
||||
import { useCreateResume, useDeleteResume, useUpdateResume } from "@/client/services/resume";
|
||||
import { useImportResume } from "@/client/services/resume/import";
|
||||
@ -258,7 +257,7 @@ export const ResumeDialog = () => {
|
||||
{isDuplicate && "Duplicate"}
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button type="button" size="icon" className="rounded-l-none border-l">
|
||||
<CaretDown />
|
||||
</Button>
|
||||
|
||||
@ -1,24 +0,0 @@
|
||||
import { useTemplate } from "@reactive-resume/hooks";
|
||||
import { ResumeData, SectionKey } from "@reactive-resume/schema";
|
||||
import { Artboard, PageWrapper } from "@reactive-resume/templates";
|
||||
import { Navigate } from "react-router-dom";
|
||||
import { useSessionStorage } from "usehooks-ts";
|
||||
|
||||
export const PrinterPage = () => {
|
||||
const [resume] = useSessionStorage<ResumeData | null>("resume", null);
|
||||
const template = useTemplate(resume?.metadata.template);
|
||||
|
||||
if (!resume) return <Navigate to="/" replace />;
|
||||
|
||||
return (
|
||||
<Artboard resume={resume} style={{ pointerEvents: "auto" }}>
|
||||
{resume.metadata.layout.map((columns, pageIndex) => (
|
||||
<PageWrapper key={pageIndex} data-page={pageIndex + 1}>
|
||||
{template !== null && (
|
||||
<template.Component isFirstPage={pageIndex === 0} columns={columns as SectionKey[][]} />
|
||||
)}
|
||||
</PageWrapper>
|
||||
))}
|
||||
</Artboard>
|
||||
);
|
||||
};
|
||||
@ -1,9 +1,7 @@
|
||||
import { ResumeDto } from "@reactive-resume/dto";
|
||||
import { useTemplate } from "@reactive-resume/hooks";
|
||||
import { SectionKey } from "@reactive-resume/schema";
|
||||
import { Artboard, PageWrapper } from "@reactive-resume/templates";
|
||||
import { Button } from "@reactive-resume/ui";
|
||||
import { pageSizeMap } from "@reactive-resume/utils";
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import { Helmet } from "react-helmet-async";
|
||||
import { Link, LoaderFunction, redirect, useLoaderData } from "react-router-dom";
|
||||
|
||||
@ -14,9 +12,42 @@ import { queryClient } from "@/client/libs/query-client";
|
||||
import { findResumeByUsernameSlug } from "@/client/services/resume";
|
||||
|
||||
export const PublicResumePage = () => {
|
||||
const frameRef = useRef<HTMLIFrameElement>(null);
|
||||
|
||||
const { title, data: resume } = useLoaderData() as ResumeDto;
|
||||
const format = resume.metadata.page.format;
|
||||
const template = useTemplate(resume.metadata.template);
|
||||
|
||||
const updateResumeInFrame = useCallback(() => {
|
||||
if (!frameRef.current || !frameRef.current.contentWindow) return;
|
||||
const message = { type: "SET_RESUME", payload: resume };
|
||||
(() => frameRef.current.contentWindow.postMessage(message, "*"))();
|
||||
}, [frameRef, resume]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!frameRef.current) return;
|
||||
frameRef.current.addEventListener("load", updateResumeInFrame);
|
||||
return () => frameRef.current?.removeEventListener("load", updateResumeInFrame);
|
||||
}, [frameRef]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!frameRef.current || !frameRef.current.contentWindow) return;
|
||||
|
||||
const handleMessage = (event: MessageEvent) => {
|
||||
if (!frameRef.current || !frameRef.current.contentWindow) return;
|
||||
if (event.origin !== window.location.origin) return;
|
||||
|
||||
if (event.data.type === "PAGE_LOADED") {
|
||||
frameRef.current.height = event.data.payload.height;
|
||||
frameRef.current.contentWindow.removeEventListener("message", handleMessage);
|
||||
}
|
||||
};
|
||||
|
||||
frameRef.current.contentWindow.addEventListener("message", handleMessage);
|
||||
|
||||
return () => {
|
||||
frameRef.current?.contentWindow?.removeEventListener("message", handleMessage);
|
||||
};
|
||||
}, [frameRef]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
@ -26,20 +57,14 @@ export const PublicResumePage = () => {
|
||||
|
||||
<div
|
||||
style={{ width: `${pageSizeMap[format].width}mm` }}
|
||||
className="mx-auto mb-6 mt-16 flex shadow-xl print:m-0 print:shadow-none"
|
||||
className="mx-auto mb-6 mt-16 overflow-hidden rounded shadow-xl print:m-0 print:shadow-none"
|
||||
>
|
||||
<Artboard resume={resume} style={{ pointerEvents: "auto" }}>
|
||||
{resume.metadata.layout.map((columns, pageIndex) => (
|
||||
<PageWrapper key={pageIndex} data-page={pageIndex + 1}>
|
||||
{template !== null && (
|
||||
<template.Component
|
||||
isFirstPage={pageIndex === 0}
|
||||
columns={columns as SectionKey[][]}
|
||||
/>
|
||||
)}
|
||||
</PageWrapper>
|
||||
))}
|
||||
</Artboard>
|
||||
<iframe
|
||||
title={title}
|
||||
ref={frameRef}
|
||||
src="/artboard/preview"
|
||||
style={{ width: `${pageSizeMap[format].width}mm` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center py-10 opacity-50 print:hidden">
|
||||
|
||||
@ -9,13 +9,13 @@ import { ResetPasswordPage } from "../pages/auth/reset-password/page";
|
||||
import { VerifyEmailPage } from "../pages/auth/verify-email/page";
|
||||
import { VerifyOtpPage } from "../pages/auth/verify-otp/page";
|
||||
import { BuilderLayout } from "../pages/builder/layout";
|
||||
import { builderLoader, BuilderPage } from "../pages/builder/page";
|
||||
import { BuilderPage } from "../pages/builder/page";
|
||||
import { builderLoader } from "../pages/builder/page";
|
||||
import { DashboardLayout } from "../pages/dashboard/layout";
|
||||
import { ResumesPage } from "../pages/dashboard/resumes/page";
|
||||
import { SettingsPage } from "../pages/dashboard/settings/page";
|
||||
import { HomeLayout } from "../pages/home/layout";
|
||||
import { HomePage } from "../pages/home/page";
|
||||
import { PrinterPage } from "../pages/printer/page";
|
||||
import { publicLoader, PublicResumePage } from "../pages/public/page";
|
||||
import { Providers } from "../providers";
|
||||
import { AuthGuard } from "./guards/auth";
|
||||
@ -80,8 +80,6 @@ export const routes = createRoutesFromElements(
|
||||
</Route>
|
||||
</Route>
|
||||
|
||||
<Route path="printer" element={<PrinterPage />} />
|
||||
|
||||
{/* Public Routes */}
|
||||
<Route path=":username">
|
||||
<Route path=":slug" loader={publicLoader} element={<PublicResumePage />} />
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import { ImperativePanelHandle } from "react-resizable-panels";
|
||||
import { ReactZoomPanPinchRef } from "react-zoom-pan-pinch";
|
||||
import { create } from "zustand";
|
||||
import { immer } from "zustand/middleware/immer";
|
||||
|
||||
@ -10,15 +9,15 @@ type Sheet = {
|
||||
|
||||
type Panel = {
|
||||
isDragging: boolean;
|
||||
setDragging: (dragging: boolean) => void;
|
||||
ref: ImperativePanelHandle | null;
|
||||
setRef: (ref: ImperativePanelHandle | null) => void;
|
||||
setDragging: (dragging: boolean) => void;
|
||||
};
|
||||
|
||||
interface BuilderState {
|
||||
transform: {
|
||||
ref: Omit<ReactZoomPanPinchRef, "instance"> | null;
|
||||
setRef: (ref: Omit<ReactZoomPanPinchRef, "instance"> | null) => void;
|
||||
frame: {
|
||||
ref: HTMLIFrameElement | null;
|
||||
setRef: (ref: HTMLIFrameElement | null) => void;
|
||||
};
|
||||
sheet: {
|
||||
left: Sheet;
|
||||
@ -36,11 +35,12 @@ interface BuilderActions {
|
||||
|
||||
export const useBuilderStore = create<BuilderState & BuilderActions>()(
|
||||
immer((set) => ({
|
||||
transform: {
|
||||
frame: {
|
||||
ref: null,
|
||||
setRef: (ref) => {
|
||||
set((state) => {
|
||||
state.transform.ref = ref;
|
||||
// @ts-expect-error Unable to set ref type
|
||||
state.frame.ref = ref;
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
@ -21,11 +21,6 @@ export default defineConfig({
|
||||
fs: { allow: [searchForWorkspaceRoot(process.cwd())] },
|
||||
},
|
||||
|
||||
preview: {
|
||||
host: true,
|
||||
port: +(process.env.__DEV__CLIENT_PORT ?? 5173),
|
||||
},
|
||||
|
||||
plugins: [react(), nxViteTsPaths(), splitVendorChunkPlugin()],
|
||||
|
||||
test: {
|
||||
|
||||
@ -27,10 +27,6 @@ import { UtilsModule } from "./utils/utils.module";
|
||||
CacheModule,
|
||||
UtilsModule,
|
||||
HealthModule,
|
||||
ServeStaticModule.forRoot({
|
||||
rootPath: join(__dirname, "..", "client"),
|
||||
exclude: ["/api*", "/docs*"],
|
||||
}),
|
||||
|
||||
// Feature Modules
|
||||
AuthModule.register(),
|
||||
@ -38,6 +34,16 @@ import { UtilsModule } from "./utils/utils.module";
|
||||
ResumeModule,
|
||||
StorageModule,
|
||||
PrinterModule,
|
||||
|
||||
// Static Assets
|
||||
ServeStaticModule.forRoot({
|
||||
serveRoot: "/artboard",
|
||||
rootPath: join(__dirname, "..", "artboard"),
|
||||
}),
|
||||
ServeStaticModule.forRoot({
|
||||
renderPath: "/*",
|
||||
rootPath: join(__dirname, "..", "client"),
|
||||
}),
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
|
||||
@ -4,7 +4,7 @@ import { Injectable } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import fontkit from "@pdf-lib/fontkit";
|
||||
import { ResumeDto } from "@reactive-resume/dto";
|
||||
import { getFontUrls, pageSizeMap, withTimeout } from "@reactive-resume/utils";
|
||||
import { getFontUrls, withTimeout } from "@reactive-resume/utils";
|
||||
import retry from "async-retry";
|
||||
import { readFile } from "fs/promises";
|
||||
import { join } from "path";
|
||||
@ -16,7 +16,6 @@ import { ErrorMessage } from "../constants/error-message";
|
||||
import { StorageService } from "../storage/storage.service";
|
||||
import { UtilsService } from "../utils/utils.service";
|
||||
|
||||
const MM_TO_PX = 3.78;
|
||||
const PRINTER_TIMEOUT = 10000; // 10 seconds
|
||||
|
||||
@Injectable()
|
||||
@ -114,59 +113,38 @@ export class PrinterService {
|
||||
}
|
||||
|
||||
// Set the data of the resume to be printed in the browser's session storage
|
||||
const format = resume.data.metadata.page.format;
|
||||
const numPages = resume.data.metadata.layout.length;
|
||||
|
||||
await page.evaluateOnNewDocument((data: string) => {
|
||||
sessionStorage.setItem("resume", data);
|
||||
}, JSON.stringify(resume.data));
|
||||
await page.evaluateOnNewDocument((data) => {
|
||||
sessionStorage.setItem("resume", JSON.stringify(data));
|
||||
}, resume.data);
|
||||
|
||||
await page.goto(`${url}/printer`, { waitUntil: "networkidle0" });
|
||||
await page.emulateMediaType("print");
|
||||
await page.goto(`${url}/artboard/preview`, { waitUntil: "networkidle0" });
|
||||
|
||||
const pagesBuffer: Buffer[] = [];
|
||||
|
||||
// Hide all the pages (elements with [data-page] attribute) using CSS
|
||||
const hidePages = () => {
|
||||
return page.$eval("iframe", (frame) => {
|
||||
frame.contentDocument?.documentElement.querySelectorAll("[data-page]").forEach((page) => {
|
||||
page.setAttribute("style", "display: none");
|
||||
});
|
||||
});
|
||||
};
|
||||
const processPage = async (index: number) => {
|
||||
const pageElement = await page.$(`[data-page="${index}"]`);
|
||||
const width = (await (await pageElement?.getProperty("scrollWidth"))?.jsonValue()) ?? 0;
|
||||
const height = (await (await pageElement?.getProperty("scrollHeight"))?.jsonValue()) ?? 0;
|
||||
|
||||
const processPage = (index: number) => {
|
||||
// Calculate the height of the page based on the format, convert mm to pixels
|
||||
const pageSize = {
|
||||
width: pageSizeMap[format].width * MM_TO_PX,
|
||||
height: pageSizeMap[format].height * MM_TO_PX,
|
||||
};
|
||||
const tempHtml = await page.evaluate((element: HTMLDivElement) => {
|
||||
const clonedElement = element.cloneNode(true) as HTMLDivElement;
|
||||
const tempHtml = document.body.innerHTML;
|
||||
document.body.innerHTML = `${clonedElement.outerHTML}`;
|
||||
return tempHtml;
|
||||
}, pageElement);
|
||||
|
||||
return page.$eval(
|
||||
"iframe",
|
||||
(frame, index, pageSize) => {
|
||||
const page = frame.contentDocument?.querySelector(`[data-page="${index}"]`);
|
||||
page?.setAttribute("style", "display: block");
|
||||
pagesBuffer.push(await page.pdf({ width, height }));
|
||||
|
||||
return {
|
||||
width: Math.max(pageSize.width, page?.scrollWidth ?? 0),
|
||||
height: Math.max(pageSize.height, page?.scrollHeight ?? 0),
|
||||
};
|
||||
},
|
||||
index,
|
||||
pageSize,
|
||||
);
|
||||
await page.evaluate((tempHtml: string) => {
|
||||
document.body.innerHTML = tempHtml;
|
||||
}, tempHtml);
|
||||
};
|
||||
|
||||
// Loop through all the pages and print them, by first displaying them, printing the PDF and then hiding them back
|
||||
for (let index = 1; index <= numPages; index++) {
|
||||
await hidePages();
|
||||
|
||||
const { width, height } = await processPage(index);
|
||||
const buffer = await page.pdf({ width, height });
|
||||
pagesBuffer.push(buffer);
|
||||
|
||||
await hidePages();
|
||||
await processPage(index);
|
||||
}
|
||||
|
||||
// Using 'pdf-lib', merge all the pages from their buffers into a single PDF
|
||||
@ -265,18 +243,13 @@ export class PrinterService {
|
||||
}
|
||||
|
||||
// Set the data of the resume to be printed in the browser's session storage
|
||||
const format = resume.data.metadata.page.format;
|
||||
|
||||
await page.evaluateOnNewDocument((data: string) => {
|
||||
sessionStorage.setItem("resume", data);
|
||||
}, JSON.stringify(resume.data));
|
||||
|
||||
await page.setViewport({
|
||||
width: Math.round(pageSizeMap[format].width * MM_TO_PX),
|
||||
height: Math.round(pageSizeMap[format].height * MM_TO_PX),
|
||||
});
|
||||
await page.setViewport({ width: 794, height: 1123 });
|
||||
|
||||
await page.goto(`${url}/printer`, { waitUntil: "networkidle0" });
|
||||
await page.goto(`${url}/artboard/preview`, { waitUntil: "networkidle0" });
|
||||
|
||||
// Save the JPEG to storage and return the URL
|
||||
// Store the URL in cache for future requests, under the previously generated hash digest
|
||||
|
||||
@ -110,8 +110,8 @@ export class ResumeController {
|
||||
|
||||
@Delete(":id")
|
||||
@UseGuards(TwoFactorGuard)
|
||||
async remove(@User() user: UserEntity, @Param("id") id: string) {
|
||||
await this.resumeService.remove(user.id, id);
|
||||
remove(@User() user: UserEntity, @Param("id") id: string) {
|
||||
return this.resumeService.remove(user.id, id);
|
||||
}
|
||||
|
||||
@Get("/print/:id")
|
||||
@ -122,6 +122,7 @@ export class ResumeController {
|
||||
|
||||
return { url };
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
Logger.error(error);
|
||||
throw new InternalServerErrorException(error);
|
||||
}
|
||||
|
||||
@ -179,10 +179,9 @@ export class ResumeService {
|
||||
// Remove files in storage, and their cached keys
|
||||
this.storageService.deleteObject(userId, "resumes", id),
|
||||
this.storageService.deleteObject(userId, "previews", id),
|
||||
|
||||
// Remove resume from database
|
||||
this.prisma.resume.delete({ where: { userId_id: { userId, id } } }),
|
||||
]);
|
||||
|
||||
return this.prisma.resume.delete({ where: { userId_id: { userId, id } } });
|
||||
}
|
||||
|
||||
async printResume(resume: ResumeDto) {
|
||||
|
||||
@ -1,12 +0,0 @@
|
||||
import { templatesList } from "@reactive-resume/templates";
|
||||
import { useMemo } from "react";
|
||||
|
||||
export const useTemplate = (templateId?: string) => {
|
||||
const template = useMemo(() => {
|
||||
return templatesList.find((template) => template.id === templateId);
|
||||
}, [templateId]);
|
||||
|
||||
if (!template || !template.Component) return null;
|
||||
|
||||
return template;
|
||||
};
|
||||
@ -1,5 +1,4 @@
|
||||
export * from "./hooks/use-breakpoint";
|
||||
export * from "./hooks/use-form-field";
|
||||
export * from "./hooks/use-password-toggle";
|
||||
export * from "./hooks/use-template";
|
||||
export * from "./hooks/use-theme";
|
||||
|
||||
@ -23,5 +23,6 @@ export const defaultResumeData: ResumeData = {
|
||||
|
||||
export * from "./basics";
|
||||
export * from "./metadata";
|
||||
export * from "./sample";
|
||||
export * from "./sections";
|
||||
export * from "./shared";
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { ResumeData } from "@reactive-resume/schema";
|
||||
import { ResumeData } from ".";
|
||||
|
||||
export const sampleResume: ResumeData = {
|
||||
basics: {
|
||||
@ -13,7 +13,7 @@ export const sampleResume: ResumeData = {
|
||||
},
|
||||
customFields: [],
|
||||
picture: {
|
||||
url: "http://localhost:9000/reactive-resume/clomqcm7b0004xi96goz2gqmt/pictures/clomqcm7b0004xi96goz2gqmt.jpg",
|
||||
url: "https://res.cloudinary.com/amruth-pillai/image/upload/v1699362669/reactive-resume/sample-resume/sample-picture_iitowc.jpg",
|
||||
size: 64,
|
||||
aspectRatio: 0.75,
|
||||
borderRadius: 6,
|
||||
@ -31,19 +31,19 @@ export const sampleResume: ResumeData = {
|
||||
visible: true,
|
||||
id: "summary",
|
||||
content:
|
||||
"<p>With over <strong><em>two decades of experience</em></strong> at the forefront of digital asset financial technology (FinTech) integration across the entire spectrum of financial service company operations, I bring a strategic and analytical approach, underpinned by a deep understanding of cryptocurrency markets, digital asset financial management, compliance, and regulatory frameworks.</p><h3>Career Highlights</h3><ul><li><p>I have a proven track record in the <strong>design and deployment of bespoke trading platforms</strong> tailored to the unique needs of clients and independent financial entities, exemplified by driving a remarkable <strong>1,200% increase in Assets Under Management (AUM)</strong> for Mesirow Financial Holdings, Inc.</p></li><li><p>Recognized for exceptional leadership abilities, I have consistently <strong>cultivated high-performance trading and software engineering teams</strong>, achieving retention rates that surpass industry standards.</p></li><li><p>Demonstrated innovation and thought leadership by co-creating and <strong>introducing Telcoin's stablecoin</strong> concept to key stakeholders within the banking industry.</p></li><li><p>Instrumental in <strong>advancing Telcoin’s strategic legislative initiatives</strong>, my direct involvement has been pivotal in the enactment of supportive financial regulations.</p></li></ul>",
|
||||
"<p>With over <strong><em>two decades of experience</em></strong> at the forefront of digital asset financial technology (FinTech) integration across the entire spectrum of financial service company operations, I bring a strategic and analytical approach, underpinned by a deep understanding of cryptocurrency markets, digital asset financial management, compliance, and regulatory frameworks.</p><h4>Career Highlights</h4><ul><li><p>I have a proven track record in the <strong>design and deployment of bespoke trading platforms</strong> tailored to the unique needs of clients and independent financial entities, exemplified by driving a remarkable <strong>1,200% increase in Assets Under Management (AUM)</strong> for Mesirow Financial Holdings, Inc.</p></li><li><p>Recognized for exceptional leadership abilities, I have consistently <strong>cultivated high-performance trading and software engineering teams</strong>, achieving retention rates that surpass industry standards.</p></li><li><p>Instrumental in <strong>advancing Telcoin’s strategic legislative initiatives</strong>, my direct involvement has been pivotal in the enactment of supportive financial regulations.</p></li></ul>",
|
||||
},
|
||||
awards: {
|
||||
name: "Awards",
|
||||
columns: 1,
|
||||
columns: 2,
|
||||
visible: true,
|
||||
id: "awards",
|
||||
items: [
|
||||
{
|
||||
id: "xk8q4bsp3vdlixehddifqsej",
|
||||
visible: true,
|
||||
title: 'Presented "Current Landscape of Digital Assets and Cryptocurrency"',
|
||||
awarder: "Meeting of the Midwest International Trade Association",
|
||||
title: "Current Landscape of Digital Assets",
|
||||
awarder: "Midwest International Trade Association",
|
||||
date: "Jul 2022",
|
||||
summary: "",
|
||||
url: {
|
||||
@ -204,7 +204,7 @@ export const sampleResume: ResumeData = {
|
||||
},
|
||||
languages: {
|
||||
name: "Languages",
|
||||
columns: 4,
|
||||
columns: 2,
|
||||
visible: true,
|
||||
id: "languages",
|
||||
items: [
|
||||
@ -361,7 +361,7 @@ export const sampleResume: ResumeData = {
|
||||
},
|
||||
skills: {
|
||||
name: "Skills",
|
||||
columns: 2,
|
||||
columns: 1,
|
||||
visible: true,
|
||||
id: "skills",
|
||||
items: [
|
||||
@ -440,7 +440,7 @@ export const sampleResume: ResumeData = {
|
||||
date: "Jan 2018 - Jul 2020",
|
||||
location: "Nassau, Bahamas",
|
||||
summary:
|
||||
"<p>Proprietary quantitative trading firm and privately-held quantitative futures fund with $1B in AUM and a focus on futures and OTC FX opportunities. 25+ year track record of trading global derivatives markets.</p><h3>Execution Team Manager</h3><p>Led international OTC FX and futures trading desk, software development, compliance, and market data for fund averaging $5B in monthly FX transactions and generating 500K+ futures contracts per month.</p><p>Steered a 12-member team of 4 traders and 8 software engineers. Oversaw co-located trade execution and managed direct market access in 4 data centers globally for exchanges not co-located. Built partnerships with trading technology providers. Point of contact with banks, brokerages, FXSpotStream, and 9 other liquidity providers. Led market data feed agreements with exchanges. Maintained CME and Eurex exchange memberships. Headed fulfillment of regulatory compliance and due diligence requests.</p><ul><li><p><strong>Decreased HFT FX trading costs</strong> by integrating streaming of 1 million NDF prices into co-located servers, coordinating with engineers of the FX pricing engine.</p></li></ul><ul><li><p><strong>Streamlined market data contracts and reduced costs by improving data quality and availability.</strong> Consolidated technology providers (Bloomberg and Reuters) and enhanced data collection for time periods needed, data types, and breadth of data.</p></li></ul><ul><li><p><strong>Scaled capabilities by recruiting to enlarge the team</strong>, hiring 3 traders, 2 software engineers, and a consultant with 85% retention for 2+ years. Sourced top talent by posting jobs, leveraging network of contacts, and outsourcing with recruiters.</p></li></ul><ul><li><p><strong>Reduced bank, vendor, and futures broker fees, expanded access to services</strong>, and maintained open lines of communication by strengthening existing relationships. Contacted sales representatives to facilitate introductions.</p></li></ul>",
|
||||
"<p>Proprietary quantitative trading firm and privately-held quantitative futures fund with $1B in AUM and a focus on futures and OTC FX opportunities. 25+ year track record of trading global derivatives markets.</p><h4>Execution Team Manager</h4><p>Led international OTC FX and futures trading desk, software development, compliance, and market data for fund averaging $5B in monthly FX transactions and generating 500K+ futures contracts per month.</p><p>Steered a 12-member team of 4 traders and 8 software engineers. Oversaw co-located trade execution and managed direct market access in 4 data centers globally for exchanges not co-located. Built partnerships with trading technology providers. Point of contact with banks, brokerages, FXSpotStream, and 9 other liquidity providers. Led market data feed agreements with exchanges. Maintained CME and Eurex exchange memberships. Headed fulfillment of regulatory compliance and due diligence requests.</p><ul><li><p><strong>Decreased HFT FX trading costs</strong> by integrating streaming of 1 million NDF prices into co-located servers, coordinating with engineers of the FX pricing engine.</p></li></ul><ul><li><p><strong>Streamlined market data contracts and reduced costs by improving data quality and availability.</strong> Consolidated technology providers (Bloomberg and Reuters) and enhanced data collection for time periods needed, data types, and breadth of data.</p></li></ul><ul><li><p><strong>Scaled capabilities by recruiting to enlarge the team</strong>, hiring 3 traders, 2 software engineers, and a consultant with 85% retention for 2+ years. Sourced top talent by posting jobs, leveraging network of contacts, and outsourcing with recruiters.</p></li></ul><ul><li><p><strong>Reduced bank, vendor, and futures broker fees, expanded access to services</strong>, and maintained open lines of communication by strengthening existing relationships. Contacted sales representatives to facilitate introductions.</p></li></ul>",
|
||||
keywords: [],
|
||||
url: {
|
||||
label: "",
|
||||
@ -456,7 +456,7 @@ export const sampleResume: ResumeData = {
|
||||
template: "rhyhorn",
|
||||
layout: [
|
||||
[["profiles", "summary", "experience"], []],
|
||||
[["custom.juryi0w9w9jabsgorks0bixq", "education", "awards", "certifications"], []],
|
||||
[["custom.juryi0w9w9jabsgorks0bixq", "education", "certifications", "awards"], []],
|
||||
[
|
||||
["skills", "interests", "publications", "volunteer", "languages", "projects", "references"],
|
||||
[],
|
||||
@ -477,7 +477,7 @@ export const sampleResume: ResumeData = {
|
||||
theme: {
|
||||
background: "#ffffff",
|
||||
text: "#000000",
|
||||
primary: "#222222",
|
||||
primary: "#78716c",
|
||||
},
|
||||
typography: {
|
||||
font: {
|
||||
@ -1,4 +0,0 @@
|
||||
{
|
||||
"presets": [["@nx/react/babel", { "runtime": "automatic", "useBuiltIns": "usage" }]],
|
||||
"plugins": [["styled-components", { "pure": true, "ssr": false }]]
|
||||
}
|
||||
@ -1,18 +0,0 @@
|
||||
{
|
||||
"extends": ["plugin:@nx/react", "../../.eslintrc.json"],
|
||||
"ignorePatterns": ["!**/*"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
|
||||
"rules": {}
|
||||
},
|
||||
{
|
||||
"files": ["*.ts", "*.tsx"],
|
||||
"rules": {}
|
||||
},
|
||||
{
|
||||
"files": ["*.js", "*.jsx"],
|
||||
"rules": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -1,16 +0,0 @@
|
||||
{
|
||||
"name": "@reactive-resume/templates",
|
||||
"version": "0.0.1",
|
||||
"private": false,
|
||||
"main": "./index.js",
|
||||
"types": "./index.d.ts",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./index.mjs",
|
||||
"require": "./index.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,40 +0,0 @@
|
||||
{
|
||||
"name": "templates",
|
||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||
"sourceRoot": "libs/templates/src",
|
||||
"projectType": "library",
|
||||
"tags": ["frontend"],
|
||||
"targets": {
|
||||
"lint": {
|
||||
"executor": "@nx/eslint:lint",
|
||||
"outputs": ["{options.outputFile}"],
|
||||
"options": {
|
||||
"lintFilePatterns": ["libs/templates/**/*.{ts,tsx,js,jsx}"]
|
||||
}
|
||||
},
|
||||
"build": {
|
||||
"executor": "@nx/vite:build",
|
||||
"outputs": ["{options.outputPath}"],
|
||||
"defaultConfiguration": "production",
|
||||
"options": {
|
||||
"outputPath": "dist/libs/templates"
|
||||
},
|
||||
"configurations": {
|
||||
"development": {
|
||||
"mode": "development"
|
||||
},
|
||||
"production": {
|
||||
"mode": "production"
|
||||
}
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"executor": "@nx/vite:test",
|
||||
"outputs": ["{options.reportsDirectory}"],
|
||||
"options": {
|
||||
"passWithNoTests": true,
|
||||
"reportsDirectory": "../../coverage/libs/templates"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,3 +0,0 @@
|
||||
export * from "./shared";
|
||||
export * from "./styles";
|
||||
export * from "./templates";
|
||||
@ -1,49 +0,0 @@
|
||||
import { ResumeData } from "@reactive-resume/schema";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { FrameContextConsumer } from "react-frame-component";
|
||||
import { StyleSheetManager } from "styled-components";
|
||||
|
||||
import { GlobalStyles } from "../styles";
|
||||
import { GlobalStyleProps } from "../styles/shared";
|
||||
import { FrameWrapper } from "./frame";
|
||||
import { useStore } from "./store";
|
||||
|
||||
type Props = {
|
||||
resume: ResumeData;
|
||||
children: React.ReactNode;
|
||||
style?: React.CSSProperties;
|
||||
};
|
||||
|
||||
export const Artboard = ({ resume, style, children }: Props) => {
|
||||
const store = useStore();
|
||||
const metadata = useStore((state) => state.metadata);
|
||||
|
||||
const styles: GlobalStyleProps | null = useMemo(() => {
|
||||
if (!metadata) return null;
|
||||
|
||||
return {
|
||||
$css: metadata.css,
|
||||
$page: metadata.page,
|
||||
$theme: metadata.theme,
|
||||
$typography: metadata.typography,
|
||||
};
|
||||
}, [metadata]);
|
||||
|
||||
useEffect(() => useStore.setState(resume), [resume]);
|
||||
|
||||
if (Object.keys(store).length === 0 || !styles) return;
|
||||
|
||||
return (
|
||||
<FrameWrapper style={style}>
|
||||
<FrameContextConsumer>
|
||||
{({ document }) => (
|
||||
<StyleSheetManager target={document?.head}>
|
||||
<GlobalStyles {...styles} />
|
||||
|
||||
{children}
|
||||
</StyleSheetManager>
|
||||
)}
|
||||
</FrameContextConsumer>
|
||||
</FrameWrapper>
|
||||
);
|
||||
};
|
||||
@ -1,97 +0,0 @@
|
||||
import { pageSizeMap } from "@reactive-resume/utils";
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import Frame from "react-frame-component";
|
||||
import webfontloader from "webfontloader";
|
||||
|
||||
import { useStore } from "./store";
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
style?: React.CSSProperties;
|
||||
};
|
||||
|
||||
export const FrameWrapper = ({ children, style }: Props) => {
|
||||
const resume = useStore();
|
||||
const format = resume.metadata.page.format;
|
||||
const font = resume.metadata.typography.font;
|
||||
|
||||
const fontString = useMemo(() => {
|
||||
const family = font.family;
|
||||
const variants = font.variants.join(",");
|
||||
const subset = font.subset;
|
||||
|
||||
return `${family}:${variants}:${subset}`;
|
||||
}, [font]);
|
||||
|
||||
const frameRef = useRef<HTMLIFrameElement | null>(null);
|
||||
|
||||
const width = useMemo(() => `${pageSizeMap[format].width}mm`, [format]);
|
||||
const [height, setHeight] = useState(`${pageSizeMap[format].height}mm`);
|
||||
|
||||
const handleResize = useCallback((frame: HTMLIFrameElement) => {
|
||||
const height = frame.contentDocument?.body?.scrollHeight ?? 0;
|
||||
setHeight(`${height}px`);
|
||||
}, []);
|
||||
|
||||
const loadFonts = useCallback(
|
||||
(frame: HTMLIFrameElement) => {
|
||||
if (font.family === "CMU Serif") {
|
||||
return webfontloader.load({
|
||||
classes: false,
|
||||
custom: {
|
||||
families: ["CMU Serif"],
|
||||
urls: ["https://cdn.jsdelivr.net/npm/computer-modern/cmu-serif.min.css"],
|
||||
},
|
||||
context: frame.contentWindow!,
|
||||
fontactive: () => handleResize(frame!),
|
||||
});
|
||||
}
|
||||
|
||||
webfontloader.load({
|
||||
classes: false,
|
||||
google: { families: [fontString] },
|
||||
context: frame.contentWindow!,
|
||||
fontactive: () => handleResize(frame!),
|
||||
});
|
||||
},
|
||||
[font, fontString, handleResize],
|
||||
);
|
||||
|
||||
const loadIconFonts = useCallback((frame: HTMLIFrameElement) => {
|
||||
const document = frame.contentDocument!;
|
||||
|
||||
const link = document.createElement("link");
|
||||
link.type = "text/css";
|
||||
link.rel = "stylesheet";
|
||||
link.href = "https://unpkg.com/@phosphor-icons/web@2.0.3/src/regular/style.css";
|
||||
|
||||
document.head.appendChild(link);
|
||||
}, []);
|
||||
|
||||
const onLoad = useCallback(() => {
|
||||
if (!frameRef.current) return;
|
||||
|
||||
handleResize(frameRef.current);
|
||||
loadFonts(frameRef.current);
|
||||
loadIconFonts(frameRef.current);
|
||||
}, [frameRef, handleResize, loadFonts, loadIconFonts]);
|
||||
|
||||
useEffect(() => {
|
||||
onLoad();
|
||||
}, [resume, onLoad]);
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(onLoad, 250);
|
||||
}, [onLoad]);
|
||||
|
||||
return (
|
||||
<Frame
|
||||
ref={frameRef}
|
||||
onLoad={onLoad}
|
||||
onLoadedData={onLoad}
|
||||
style={{ width, height, pointerEvents: "none", ...style }}
|
||||
>
|
||||
{children}
|
||||
</Frame>
|
||||
);
|
||||
};
|
||||
@ -1,4 +0,0 @@
|
||||
export * from "./artboard";
|
||||
export * from "./frame";
|
||||
export * from "./store";
|
||||
export * from "./templates";
|
||||
@ -1,4 +0,0 @@
|
||||
import { ResumeData } from "@reactive-resume/schema";
|
||||
import { create } from "zustand";
|
||||
|
||||
export const useStore = create<ResumeData>()(() => ({}) as ResumeData);
|
||||
@ -1,14 +0,0 @@
|
||||
import styled from "styled-components";
|
||||
|
||||
export const ItemGrid = styled.div<{ $columns?: number }>`
|
||||
display: grid;
|
||||
grid-gap: 16px;
|
||||
grid-template-columns: repeat(${({ $columns }) => $columns ?? 1}, 1fr);
|
||||
`;
|
||||
|
||||
export const PageGrid = styled.div<{ $offset: number }>`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
column-gap: ${({ $offset }) => $offset}px;
|
||||
`;
|
||||
@ -1,13 +0,0 @@
|
||||
import { createGlobalStyle } from "styled-components";
|
||||
|
||||
import { Reset } from "./reset";
|
||||
import { GlobalStyleProps, Shared } from "./shared";
|
||||
|
||||
export const GlobalStyles = createGlobalStyle<GlobalStyleProps>`
|
||||
${Reset}
|
||||
${Shared}
|
||||
`;
|
||||
|
||||
export * from "./grid";
|
||||
export * from "./page";
|
||||
export * from "./picture";
|
||||
@ -1,59 +0,0 @@
|
||||
import styled from "styled-components";
|
||||
|
||||
export const PageWrapper = styled.div`
|
||||
position: relative;
|
||||
|
||||
width: var(--page-width);
|
||||
padding: var(--page-margin);
|
||||
min-height: var(--page-height);
|
||||
|
||||
/* Theme */
|
||||
color: var(--color-text);
|
||||
background-color: var(--color-background);
|
||||
|
||||
@media print {
|
||||
margin: 0 auto;
|
||||
|
||||
&:not(:last-child) {
|
||||
height: var(--page-height);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const PageNumber = styled.p`
|
||||
top: 0;
|
||||
right: 0;
|
||||
color: black;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
padding: 0 0.5rem;
|
||||
position: absolute;
|
||||
outline: 1px solid black;
|
||||
background-color: white;
|
||||
`;
|
||||
|
||||
export const PageBreakLine = styled.div<{ $pageHeight: number }>`
|
||||
position: absolute;
|
||||
top: ${({ $pageHeight }) => $pageHeight}mm;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 10;
|
||||
border: 1px dashed var(--color-text);
|
||||
|
||||
/* Text */
|
||||
&:before {
|
||||
content: "End of Page";
|
||||
background: white;
|
||||
color: black;
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
height: auto;
|
||||
line-height: 0rem;
|
||||
padding: 12px 16px;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
text-align: right;
|
||||
top: -25px;
|
||||
}
|
||||
`;
|
||||
@ -1,11 +0,0 @@
|
||||
import { Basics } from "@reactive-resume/schema";
|
||||
import styled from "styled-components";
|
||||
|
||||
export const Picture = styled.img<{ $picture: Basics["picture"] }>`
|
||||
width: ${({ $picture }) => $picture.size}px;
|
||||
aspect-ratio: ${({ $picture }) => $picture.aspectRatio};
|
||||
border-radius: ${({ $picture }) => $picture.borderRadius}px;
|
||||
|
||||
${({ $picture }) => $picture.effects.grayscale && `filter: grayscale(1);`}
|
||||
${({ $picture }) => $picture.effects.border && `border: 2px solid var(--color-primary);`}
|
||||
`;
|
||||
@ -1,122 +0,0 @@
|
||||
import { css } from "styled-components";
|
||||
|
||||
export const Reset = css`
|
||||
/***
|
||||
The new CSS reset - version 1.11.1 (last updated 24.10.2023)
|
||||
GitHub page: https://github.com/elad2412/the-new-css-reset
|
||||
***/
|
||||
|
||||
/*
|
||||
Remove all the styles of the "User-Agent-Stylesheet", except for the 'display' property
|
||||
- The "symbol *" part is to solve Firefox SVG sprite bug
|
||||
- The "html" element is excluded, otherwise a bug in Chrome breaks the CSS hyphens property (https://github.com/elad2412/the-new-css-reset/issues/36)
|
||||
*/
|
||||
*:where(:not(html, iframe, canvas, img, svg, video, audio):not(svg *, symbol *)) {
|
||||
all: unset;
|
||||
display: revert;
|
||||
}
|
||||
|
||||
/* Preferred box-sizing value */
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Fix mobile Safari increase font-size on landscape mode */
|
||||
html {
|
||||
-moz-text-size-adjust: none;
|
||||
-webkit-text-size-adjust: none;
|
||||
text-size-adjust: none;
|
||||
font-size: var(--font-size);
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
body {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Reapply the pointer cursor for anchor tags */
|
||||
a,
|
||||
button {
|
||||
cursor: revert;
|
||||
}
|
||||
|
||||
/* Remove list styles (bullets/numbers) */
|
||||
ol,
|
||||
ul,
|
||||
menu,
|
||||
summary {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
/* For images to not be able to exceed their container */
|
||||
img {
|
||||
max-inline-size: 100%;
|
||||
max-block-size: 100%;
|
||||
}
|
||||
|
||||
/* removes spacing between cells in tables */
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
/* Safari - solving issue when using user-select:none on the <body> text input doesn't working */
|
||||
input,
|
||||
textarea {
|
||||
user-select: auto;
|
||||
-webkit-user-select: auto;
|
||||
}
|
||||
|
||||
/* revert the 'white-space' property for textarea elements on Safari */
|
||||
textarea {
|
||||
white-space: revert;
|
||||
}
|
||||
|
||||
/* minimum style to allow to style meter element */
|
||||
meter {
|
||||
-webkit-appearance: revert;
|
||||
appearance: revert;
|
||||
}
|
||||
|
||||
/* preformatted text - use only for this feature */
|
||||
:where(pre) {
|
||||
all: revert;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* reset default text opacity of input placeholder */
|
||||
::placeholder {
|
||||
color: unset;
|
||||
}
|
||||
|
||||
/* fix the feature of 'hidden' attribute.
|
||||
display:revert; revert to element instead of attribute */
|
||||
:where([hidden]) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* revert for bug in Chromium browsers
|
||||
- fix for the content editable attribute will work properly.
|
||||
- webkit-user-select: auto; added for Safari in case of using user-select:none on wrapper element*/
|
||||
:where([contenteditable]:not([contenteditable="false"])) {
|
||||
-moz-user-modify: read-write;
|
||||
-webkit-user-modify: read-write;
|
||||
overflow-wrap: break-word;
|
||||
line-break: after-white-space;
|
||||
-webkit-line-break: after-white-space;
|
||||
user-select: auto;
|
||||
-webkit-user-select: auto;
|
||||
}
|
||||
|
||||
/* apply back the draggable feature - exist only in Chromium and Safari */
|
||||
:where([draggable="true"]) {
|
||||
-webkit-user-drag: element;
|
||||
}
|
||||
|
||||
/* Revert Modal native behavior */
|
||||
:where(dialog:modal) {
|
||||
all: revert;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
`;
|
||||
@ -1,147 +0,0 @@
|
||||
import { Metadata } from "@reactive-resume/schema";
|
||||
import { pageSizeMap } from "@reactive-resume/utils";
|
||||
import { css } from "styled-components";
|
||||
|
||||
export type GlobalStyleProps = {
|
||||
$css: Metadata["css"];
|
||||
$page: Metadata["page"];
|
||||
$theme: Metadata["theme"];
|
||||
$typography: Metadata["typography"];
|
||||
};
|
||||
|
||||
export const Shared = css<GlobalStyleProps>`
|
||||
/* CSS Variables */
|
||||
:root {
|
||||
/* Theme */
|
||||
--color-text: ${({ $theme }) => $theme.text};
|
||||
--color-primary: ${({ $theme }) => $theme.primary};
|
||||
--color-background: ${({ $theme }) => $theme.background};
|
||||
|
||||
/* Page */
|
||||
--page-width: ${({ $page }) => pageSizeMap[$page.format].width}mm;
|
||||
--page-height: ${({ $page }) => pageSizeMap[$page.format].height}mm;
|
||||
--page-margin: ${({ $page }) => $page.margin}px;
|
||||
|
||||
/* Typography */
|
||||
--font-size: ${({ $typography }) => $typography.font.size}px;
|
||||
--font-family: ${({ $typography }) => $typography.font.family};
|
||||
--line-height: ${({ $typography }) => $typography.lineHeight}rem;
|
||||
}
|
||||
|
||||
/* Headings */
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-weight: bold;
|
||||
line-height: var(--line-height);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* Paragraphs */
|
||||
p {
|
||||
font-size: var(--font-size);
|
||||
line-height: var(--line-height);
|
||||
}
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
small {
|
||||
font-size: calc(var(--font-size) - 2px);
|
||||
line-height: calc(var(--line-height) - 2px);
|
||||
}
|
||||
|
||||
i,
|
||||
em {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
u {
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 1.5px;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: ${({ $typography }) => ($typography.underlineLinks ? "underline" : "none")};
|
||||
text-underline-offset: 1.5px;
|
||||
}
|
||||
|
||||
s,
|
||||
del {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
pre,
|
||||
code {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
pre code {
|
||||
display: block;
|
||||
padding: 1rem;
|
||||
border-radius: 4px;
|
||||
color: white;
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
mark {
|
||||
color: black;
|
||||
background-color: #fcd34d;
|
||||
}
|
||||
|
||||
/* Lists */
|
||||
menu,
|
||||
ol,
|
||||
ul {
|
||||
list-style: disc inside;
|
||||
|
||||
li {
|
||||
margin: 0.25rem 0;
|
||||
line-height: var(--line-height);
|
||||
|
||||
p {
|
||||
display: inline;
|
||||
line-height: var(--line-height);
|
||||
}
|
||||
}
|
||||
|
||||
menu,
|
||||
ol,
|
||||
ul {
|
||||
list-style: circle inside;
|
||||
|
||||
li {
|
||||
padding-left: 32px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Horizontal Rules */
|
||||
hr {
|
||||
margin: 0.5rem 0;
|
||||
border: 0.5px solid currentColor;
|
||||
}
|
||||
|
||||
/* Images */
|
||||
img {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
`;
|
||||
@ -1,16 +0,0 @@
|
||||
import { TemplateProps } from "../shared";
|
||||
import { Rhyhorn } from "./rhyhorn";
|
||||
|
||||
type Template = {
|
||||
id: string;
|
||||
name: string;
|
||||
Component: (props: TemplateProps) => JSX.Element;
|
||||
};
|
||||
|
||||
export const templatesList: Template[] = [
|
||||
{
|
||||
id: "rhyhorn",
|
||||
name: "Rhyhorn",
|
||||
Component: Rhyhorn,
|
||||
},
|
||||
];
|
||||
@ -1,56 +0,0 @@
|
||||
import { SectionKey } from "@reactive-resume/schema";
|
||||
|
||||
import { TemplateProps } from "../../shared";
|
||||
import { Awards } from "./sections/awards";
|
||||
import { Certifications } from "./sections/certifications";
|
||||
import { CustomSection } from "./sections/custom";
|
||||
import { Education } from "./sections/education";
|
||||
import { Experience } from "./sections/experience";
|
||||
import { Header } from "./sections/header";
|
||||
import { Interests } from "./sections/interests";
|
||||
import { Languages } from "./sections/languages";
|
||||
import { Profiles } from "./sections/profiles";
|
||||
import { Projects } from "./sections/projects";
|
||||
import { Publications } from "./sections/publications";
|
||||
import { References } from "./sections/references";
|
||||
import { Skills } from "./sections/skills";
|
||||
import { Summary } from "./sections/summary";
|
||||
import { Volunteer } from "./sections/volunteer";
|
||||
import { RhyhornWrapper } from "./style";
|
||||
|
||||
const sectionMap: Partial<Record<SectionKey, () => React.ReactNode>> = {
|
||||
summary: Summary,
|
||||
profiles: Profiles,
|
||||
experience: Experience,
|
||||
education: Education,
|
||||
awards: Awards,
|
||||
skills: Skills,
|
||||
certifications: Certifications,
|
||||
interests: Interests,
|
||||
languages: Languages,
|
||||
volunteer: Volunteer,
|
||||
projects: Projects,
|
||||
publications: Publications,
|
||||
references: References,
|
||||
};
|
||||
|
||||
const getSection = (id: SectionKey) => {
|
||||
const Section = sectionMap[id];
|
||||
|
||||
// Custom Section
|
||||
if (!Section) return <CustomSection key={id} id={id} />;
|
||||
|
||||
return <Section key={id} />;
|
||||
};
|
||||
|
||||
export const Rhyhorn = ({ isFirstPage, columns }: TemplateProps) => (
|
||||
<RhyhornWrapper>
|
||||
{isFirstPage && <Header />}
|
||||
|
||||
{/* Main */}
|
||||
{columns[0].map(getSection)}
|
||||
|
||||
{/* Sidebar */}
|
||||
{columns[1].map(getSection)}
|
||||
</RhyhornWrapper>
|
||||
);
|
||||
@ -1,37 +0,0 @@
|
||||
import { Award as IAward } from "@reactive-resume/schema";
|
||||
import { useStore } from "@reactive-resume/templates";
|
||||
import { isUrl } from "@reactive-resume/utils";
|
||||
|
||||
import { SectionBase } from "../shared/section-base";
|
||||
|
||||
export const Awards = () => {
|
||||
const section = useStore((state) => state.sections.awards);
|
||||
|
||||
return (
|
||||
<SectionBase<IAward>
|
||||
section={section}
|
||||
header={(item) => (
|
||||
<>
|
||||
<div>
|
||||
<h6>{item.title}</h6>
|
||||
<p>{item.awarder}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h6>{item.date}</h6>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
main={(item) => <div dangerouslySetInnerHTML={{ __html: item.summary }} />}
|
||||
footer={(item) => (
|
||||
<div>
|
||||
{isUrl(item.url.href) && (
|
||||
<a href={item.url.href} target="_blank" rel="noopener noreferrer nofollow">
|
||||
<h6>{item.url.label || item.url.href}</h6>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -1,37 +0,0 @@
|
||||
import { Certification as ICertification } from "@reactive-resume/schema";
|
||||
import { useStore } from "@reactive-resume/templates";
|
||||
import { isUrl } from "@reactive-resume/utils";
|
||||
|
||||
import { SectionBase } from "../shared/section-base";
|
||||
|
||||
export const Certifications = () => {
|
||||
const section = useStore((state) => state.sections.certifications);
|
||||
|
||||
return (
|
||||
<SectionBase<ICertification>
|
||||
section={section}
|
||||
header={(item) => (
|
||||
<>
|
||||
<div>
|
||||
<h6>{item.name}</h6>
|
||||
<p>{item.issuer}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h6>{item.date}</h6>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
main={(item) => <div dangerouslySetInnerHTML={{ __html: item.summary }} />}
|
||||
footer={(item) => (
|
||||
<div>
|
||||
{isUrl(item.url.href) && (
|
||||
<a href={item.url.href} target="_blank" rel="noopener noreferrer nofollow">
|
||||
<h6>{item.url.label || item.url.href}</h6>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -1,54 +0,0 @@
|
||||
import {
|
||||
CustomSection as ICustomSection,
|
||||
CustomSectionItem,
|
||||
SectionKey,
|
||||
} from "@reactive-resume/schema";
|
||||
import { useStore } from "@reactive-resume/templates";
|
||||
import { isUrl } from "@reactive-resume/utils";
|
||||
import get from "lodash.get";
|
||||
|
||||
import { SectionBase } from "../shared/section-base";
|
||||
|
||||
type Props = {
|
||||
id: SectionKey;
|
||||
};
|
||||
|
||||
export const CustomSection = ({ id }: Props) => {
|
||||
const section = useStore((state) => get(state.sections, id));
|
||||
|
||||
if (!section) return null;
|
||||
|
||||
return (
|
||||
// @ts-expect-error Unable to infer type of Custom Section accurately, ignoring for now
|
||||
<SectionBase<ICustomSection>
|
||||
section={section}
|
||||
header={(item: CustomSectionItem) => (
|
||||
<>
|
||||
<div>
|
||||
<h6>{item.name}</h6>
|
||||
<p>{item.description}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h6>{item.date}</h6>
|
||||
<p>{item.location}</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
main={(item: CustomSectionItem) => <div dangerouslySetInnerHTML={{ __html: item.summary }} />}
|
||||
footer={(item: CustomSectionItem) => (
|
||||
<>
|
||||
<small>{item.keywords.join(", ")}</small>
|
||||
|
||||
<div>
|
||||
{isUrl(item.url.href) && (
|
||||
<a href={item.url.href} target="_blank" rel="noopener noreferrer nofollow">
|
||||
<h6>{item.url.label || item.url.href}</h6>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -1,41 +0,0 @@
|
||||
import { Education as IEducation } from "@reactive-resume/schema";
|
||||
import { useStore } from "@reactive-resume/templates";
|
||||
import { isUrl } from "@reactive-resume/utils";
|
||||
|
||||
import { SectionBase } from "../shared/section-base";
|
||||
|
||||
export const Education = () => {
|
||||
const section = useStore((state) => state.sections.education);
|
||||
|
||||
return (
|
||||
<SectionBase<IEducation>
|
||||
section={section}
|
||||
header={(item) => (
|
||||
<>
|
||||
<div>
|
||||
<h6>{item.institution}</h6>
|
||||
<p>{item.area}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h6>{item.date}</h6>
|
||||
<p>
|
||||
{item.studyType}
|
||||
{item.score ? ` | ${item.score}` : ""}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
main={(item) => <div dangerouslySetInnerHTML={{ __html: item.summary }} />}
|
||||
footer={(item) => (
|
||||
<div>
|
||||
{isUrl(item.url.href) && (
|
||||
<a href={item.url.href} target="_blank" rel="noopener noreferrer nofollow">
|
||||
<h6>{item.url.label || item.url.href}</h6>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -1,38 +0,0 @@
|
||||
import { Experience as IExperience } from "@reactive-resume/schema";
|
||||
import { useStore } from "@reactive-resume/templates";
|
||||
import { isUrl } from "@reactive-resume/utils";
|
||||
|
||||
import { SectionBase } from "../shared/section-base";
|
||||
|
||||
export const Experience = () => {
|
||||
const section = useStore((state) => state.sections.experience);
|
||||
|
||||
return (
|
||||
<SectionBase<IExperience>
|
||||
section={section}
|
||||
header={(item) => (
|
||||
<>
|
||||
<div>
|
||||
<h6>{item.company}</h6>
|
||||
<p>{item.position}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h6>{item.date}</h6>
|
||||
<p>{item.location}</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
main={(item) => <div dangerouslySetInnerHTML={{ __html: item.summary }} />}
|
||||
footer={(item) => (
|
||||
<div>
|
||||
{isUrl(item.url.href) && (
|
||||
<a href={item.url.href} target="_blank" rel="noopener noreferrer nofollow">
|
||||
<h6>{item.url.label || item.url.href}</h6>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -1,57 +0,0 @@
|
||||
import { Picture, useStore } from "@reactive-resume/templates";
|
||||
import { isUrl } from "@reactive-resume/utils";
|
||||
|
||||
export const Header = () => {
|
||||
const basics = useStore((state) => state.basics);
|
||||
|
||||
return (
|
||||
<div className="header">
|
||||
{isUrl(basics.picture.url) && !basics.picture.effects.hidden && (
|
||||
<Picture
|
||||
alt={basics.name}
|
||||
src={basics.picture.url}
|
||||
$picture={basics.picture}
|
||||
className="header__picture"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="header__basics">
|
||||
<h1 className="header__name">{basics.name}</h1>
|
||||
<p className="header__headline">{basics.headline}</p>
|
||||
|
||||
<div className="header__meta">
|
||||
{basics.location && <span className="header__meta-location">{basics.location}</span>}
|
||||
{basics.phone && (
|
||||
<span className="header__meta-phone">
|
||||
<a href={`tel:${basics.phone}`} target="_blank" rel="noopener noreferrer nofollow">
|
||||
{basics.phone}
|
||||
</a>
|
||||
</span>
|
||||
)}
|
||||
{basics.email && (
|
||||
<span className="header__meta-email">
|
||||
<a href={`mailto:${basics.email}`} target="_blank" rel="noopener noreferrer nofollow">
|
||||
{basics.email}
|
||||
</a>
|
||||
</span>
|
||||
)}
|
||||
{basics.url.href && (
|
||||
<span className="header__meta-url">
|
||||
<a href={basics.url.href} target="_blank" rel="noopener noreferrer nofollow">
|
||||
{basics.url.label || basics.url.href}
|
||||
</a>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="header__meta custom-fields">
|
||||
{basics.customFields.map((field) => (
|
||||
<span key={field.id} className="header__meta custom-field">
|
||||
{[field.name, field.value].filter(Boolean).join(": ")}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -1,24 +0,0 @@
|
||||
import { Interest as IInterest } from "@reactive-resume/schema";
|
||||
import { useStore } from "@reactive-resume/templates";
|
||||
|
||||
import { SectionBase } from "../shared/section-base";
|
||||
|
||||
export const Interests = () => {
|
||||
const section = useStore((state) => state.sections.interests);
|
||||
|
||||
return (
|
||||
<SectionBase<IInterest>
|
||||
section={section}
|
||||
header={(item) => (
|
||||
<>
|
||||
<div>
|
||||
<h6>{item.name}</h6>
|
||||
<p>{item.keywords.join(", ")}</p>
|
||||
</div>
|
||||
|
||||
<div />
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -1,25 +0,0 @@
|
||||
import { Language as ILanguage } from "@reactive-resume/schema";
|
||||
import { useStore } from "@reactive-resume/templates";
|
||||
import { getCEFRLevel } from "@reactive-resume/utils";
|
||||
|
||||
import { SectionBase } from "../shared/section-base";
|
||||
|
||||
export const Languages = () => {
|
||||
const section = useStore((state) => state.sections.languages);
|
||||
|
||||
return (
|
||||
<SectionBase<ILanguage>
|
||||
section={section}
|
||||
header={(item) => (
|
||||
<>
|
||||
<div>
|
||||
<h6>{item.name}</h6>
|
||||
<p>{item.fluency || getCEFRLevel(item.fluencyLevel)}</p>
|
||||
</div>
|
||||
|
||||
<div />
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -1,48 +0,0 @@
|
||||
import { Profile as IProfile } from "@reactive-resume/schema";
|
||||
import { useStore } from "@reactive-resume/templates";
|
||||
import { isUrl } from "@reactive-resume/utils";
|
||||
import styled from "styled-components";
|
||||
|
||||
import { SectionBase } from "../shared/section-base";
|
||||
|
||||
const Username = styled.h6`
|
||||
line-height: 1;
|
||||
font-weight: 500;
|
||||
`;
|
||||
|
||||
export const Profiles = () => {
|
||||
const section = useStore((state) => state.sections.profiles);
|
||||
|
||||
return (
|
||||
<SectionBase<IProfile>
|
||||
section={section}
|
||||
header={(item) => (
|
||||
<>
|
||||
<div>
|
||||
{item.icon && (
|
||||
<i>
|
||||
<img
|
||||
width="16"
|
||||
height="16"
|
||||
alt={item.network}
|
||||
src={`https://cdn.simpleicons.org/${item.icon}`}
|
||||
/>
|
||||
</i>
|
||||
)}
|
||||
|
||||
{isUrl(item.url.href) ? (
|
||||
<a href={item.url.href} target="_blank" rel="noopener noreferrer nofollow">
|
||||
<Username>{item.username}</Username>
|
||||
</a>
|
||||
) : (
|
||||
<Username>{item.username}</Username>
|
||||
)}
|
||||
<small>{item.network}</small>
|
||||
</div>
|
||||
|
||||
<div />
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -1,41 +0,0 @@
|
||||
import { Project as IProject } from "@reactive-resume/schema";
|
||||
import { useStore } from "@reactive-resume/templates";
|
||||
import { isUrl } from "@reactive-resume/utils";
|
||||
|
||||
import { SectionBase } from "../shared/section-base";
|
||||
|
||||
export const Projects = () => {
|
||||
const section = useStore((state) => state.sections.projects);
|
||||
|
||||
return (
|
||||
<SectionBase<IProject>
|
||||
section={section}
|
||||
header={(item) => (
|
||||
<>
|
||||
<div>
|
||||
<h6>{item.name}</h6>
|
||||
<p>{item.description}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h6>{item.date}</h6>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
main={(item) => <div dangerouslySetInnerHTML={{ __html: item.summary }} />}
|
||||
footer={(item) => (
|
||||
<>
|
||||
<small>{item.keywords.join(", ")}</small>
|
||||
|
||||
<div>
|
||||
{isUrl(item.url.href) && (
|
||||
<a href={item.url.href} target="_blank" rel="noopener noreferrer nofollow">
|
||||
<h6>{item.url.label || item.url.href}</h6>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -1,37 +0,0 @@
|
||||
import { Publication as IPublication } from "@reactive-resume/schema";
|
||||
import { useStore } from "@reactive-resume/templates";
|
||||
import { isUrl } from "@reactive-resume/utils";
|
||||
|
||||
import { SectionBase } from "../shared/section-base";
|
||||
|
||||
export const Publications = () => {
|
||||
const section = useStore((state) => state.sections.publications);
|
||||
|
||||
return (
|
||||
<SectionBase<IPublication>
|
||||
section={section}
|
||||
header={(item) => (
|
||||
<>
|
||||
<div>
|
||||
<h6>{item.name}</h6>
|
||||
<p>{item.publisher}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h6>{item.date}</h6>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
main={(item) => <div dangerouslySetInnerHTML={{ __html: item.summary }} />}
|
||||
footer={(item) => (
|
||||
<div>
|
||||
{isUrl(item.url.href) && (
|
||||
<a href={item.url.href} target="_blank" rel="noopener noreferrer nofollow">
|
||||
<h6>{item.url.label || item.url.href}</h6>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -1,35 +0,0 @@
|
||||
import { Reference as IReference } from "@reactive-resume/schema";
|
||||
import { useStore } from "@reactive-resume/templates";
|
||||
import { isUrl } from "@reactive-resume/utils";
|
||||
|
||||
import { SectionBase } from "../shared/section-base";
|
||||
|
||||
export const References = () => {
|
||||
const section = useStore((state) => state.sections.references);
|
||||
|
||||
return (
|
||||
<SectionBase<IReference>
|
||||
section={section}
|
||||
header={(item) => (
|
||||
<>
|
||||
<div>
|
||||
<h6>{item.name}</h6>
|
||||
<p>{item.description}</p>
|
||||
</div>
|
||||
|
||||
<div />
|
||||
</>
|
||||
)}
|
||||
main={(item) => <div dangerouslySetInnerHTML={{ __html: item.summary }} />}
|
||||
footer={(item) => (
|
||||
<div>
|
||||
{isUrl(item.url.href) && (
|
||||
<a href={item.url.href} target="_blank" rel="noopener noreferrer nofollow">
|
||||
<h6>{item.url.label || item.url.href}</h6>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -1,25 +0,0 @@
|
||||
import { Skill as ISkill } from "@reactive-resume/schema";
|
||||
import { useStore } from "@reactive-resume/templates";
|
||||
|
||||
import { SectionBase } from "../shared/section-base";
|
||||
|
||||
export const Skills = () => {
|
||||
const section = useStore((state) => state.sections.skills);
|
||||
|
||||
return (
|
||||
<SectionBase<ISkill>
|
||||
section={section}
|
||||
header={(item) => (
|
||||
<>
|
||||
<div>
|
||||
<h6>{item.name}</h6>
|
||||
<p>{item.description}</p>
|
||||
</div>
|
||||
|
||||
<div />
|
||||
</>
|
||||
)}
|
||||
footer={(item) => <small>{item.keywords.join(", ")}</small>}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -1,20 +0,0 @@
|
||||
import { useStore } from "@reactive-resume/templates";
|
||||
|
||||
export const Summary = () => {
|
||||
const section = useStore((state) => state.sections.summary);
|
||||
|
||||
if (!section.visible || !section.content) return null;
|
||||
|
||||
return (
|
||||
<section id={section.id} className={`section section__${section.id}`}>
|
||||
<h4 className="section__heading">{section.name}</h4>
|
||||
|
||||
<main className="section__item-content">
|
||||
<div
|
||||
style={{ columns: section.columns }}
|
||||
dangerouslySetInnerHTML={{ __html: section.content }}
|
||||
/>
|
||||
</main>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
@ -1,38 +0,0 @@
|
||||
import { Volunteer as IVolunteer } from "@reactive-resume/schema";
|
||||
import { useStore } from "@reactive-resume/templates";
|
||||
import { isUrl } from "@reactive-resume/utils";
|
||||
|
||||
import { SectionBase } from "../shared/section-base";
|
||||
|
||||
export const Volunteer = () => {
|
||||
const section = useStore((state) => state.sections.volunteer);
|
||||
|
||||
return (
|
||||
<SectionBase<IVolunteer>
|
||||
section={section}
|
||||
header={(item) => (
|
||||
<>
|
||||
<div>
|
||||
<h6>{item.organization}</h6>
|
||||
<p>{item.position}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h6>{item.date}</h6>
|
||||
<p>{item.location}</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
main={(item) => <div dangerouslySetInnerHTML={{ __html: item.summary }} />}
|
||||
footer={(item) => (
|
||||
<div>
|
||||
{isUrl(item.url.href) && (
|
||||
<a href={item.url.href} target="_blank" rel="noopener noreferrer nofollow">
|
||||
<h6>{item.url.label || item.url.href}</h6>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -1,31 +0,0 @@
|
||||
import { Item, SectionItem, SectionWithItem } from "@reactive-resume/schema";
|
||||
import { ItemGrid } from "@reactive-resume/templates";
|
||||
|
||||
type Props<T extends Item> = {
|
||||
section: SectionWithItem<T>;
|
||||
header?: (item: T) => React.ReactNode;
|
||||
main?: (item: T) => React.ReactNode;
|
||||
footer?: (item: T) => React.ReactNode;
|
||||
};
|
||||
|
||||
export const SectionBase = <T extends SectionItem>({ section, header, main, footer }: Props<T>) => {
|
||||
if (!section.visible || !section.items.length) return null;
|
||||
|
||||
return (
|
||||
<section id={section.id} className={`section section__${section.id}`}>
|
||||
<h4 className="section__heading">{section.name}</h4>
|
||||
|
||||
<ItemGrid className="section__items" $columns={section.columns}>
|
||||
{section.items
|
||||
.filter((item) => !!item.visible)
|
||||
.map((item) => (
|
||||
<div key={item.id} className="section__item">
|
||||
{header && <header className="section__item-header">{header(item as T)}</header>}
|
||||
{main && <main className="section__item-main">{main(item as T)}</main>}
|
||||
{footer && <footer className="section__item-footer">{footer(item as T)}</footer>}
|
||||
</div>
|
||||
))}
|
||||
</ItemGrid>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
@ -1,105 +0,0 @@
|
||||
import styled from "styled-components";
|
||||
|
||||
export const RhyhornWrapper = styled.div`
|
||||
display: grid;
|
||||
row-gap: 16px;
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
|
||||
&__picture {
|
||||
align-self: center;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
&__basics {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
&__name {
|
||||
font-size: 1.5rem;
|
||||
line-height: calc(var(--line-height) + 0.5rem);
|
||||
}
|
||||
|
||||
&__headline {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
&__meta {
|
||||
font-size: 0.875rem;
|
||||
line-height: var(--line-height);
|
||||
|
||||
span {
|
||||
padding: 0 6px;
|
||||
border-right: 1px solid currentColor;
|
||||
|
||||
&:first-child {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.section {
|
||||
&__heading {
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.2rem;
|
||||
padding-bottom: 2px;
|
||||
margin-bottom: 6px;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-primary);
|
||||
border-bottom: 1px solid var(--color-text);
|
||||
}
|
||||
|
||||
&__item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
|
||||
&-header {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 6px;
|
||||
|
||||
& > div:last-child {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
&:has(i) div {
|
||||
margin-left: 22px;
|
||||
}
|
||||
|
||||
& > div > i {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&-content p:not(:last-child),
|
||||
&-main p:not(:last-child) {
|
||||
padding-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
& .rating {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 4px;
|
||||
|
||||
& > span {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
margin-left: 4px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid currentColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
@ -1,19 +0,0 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../dist/out-tsc",
|
||||
"types": ["vitest/globals", "vitest/importMeta", "vite/client", "node", "vitest"]
|
||||
},
|
||||
"include": [
|
||||
"vite.config.ts",
|
||||
"src/**/*.test.ts",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.test.tsx",
|
||||
"src/**/*.spec.tsx",
|
||||
"src/**/*.test.js",
|
||||
"src/**/*.spec.js",
|
||||
"src/**/*.test.jsx",
|
||||
"src/**/*.spec.jsx",
|
||||
"src/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
@ -1,39 +0,0 @@
|
||||
/// <reference types='vitest' />
|
||||
|
||||
import { nxViteTsPaths } from "@nx/vite/plugins/nx-tsconfig-paths.plugin";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import * as path from "path";
|
||||
import { defineConfig } from "vite";
|
||||
import dts from "vite-plugin-dts";
|
||||
|
||||
export default defineConfig({
|
||||
cacheDir: "../../node_modules/.vite/templates",
|
||||
|
||||
plugins: [
|
||||
react(),
|
||||
nxViteTsPaths(),
|
||||
dts({
|
||||
entryRoot: "src",
|
||||
tsconfigPath: path.join(__dirname, "tsconfig.lib.json"),
|
||||
}),
|
||||
],
|
||||
|
||||
build: {
|
||||
lib: {
|
||||
entry: "src/index.ts",
|
||||
name: "templates",
|
||||
fileName: "index",
|
||||
formats: ["es", "cjs"],
|
||||
},
|
||||
rollupOptions: {
|
||||
external: ["react", "react-dom", "react/jsx-runtime"],
|
||||
},
|
||||
},
|
||||
|
||||
test: {
|
||||
globals: true,
|
||||
environment: "jsdom",
|
||||
cache: { dir: "../../node_modules/.vitest" },
|
||||
include: ["src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
|
||||
},
|
||||
});
|
||||
@ -1,11 +1,7 @@
|
||||
import { cva } from "class-variance-authority";
|
||||
|
||||
export const buttonVariants = cva(
|
||||
[
|
||||
"inline-flex items-center justify-center rounded-sm text-sm font-medium ring-offset-background disabled:pointer-events-none disabled:opacity-50",
|
||||
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary focus-visible:ring-offset-2",
|
||||
"scale-100 transition-[transform,background-color] active:scale-95",
|
||||
],
|
||||
"inline-flex scale-100 items-center justify-center rounded-sm text-sm font-medium ring-offset-background transition-[transform,background-color] focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary focus-visible:ring-offset-2 active:scale-95 disabled:pointer-events-none disabled:opacity-50",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { Template } from "./types";
|
||||
|
||||
export const pageSizeMap = {
|
||||
a4: {
|
||||
width: 210,
|
||||
@ -8,3 +10,12 @@ export const pageSizeMap = {
|
||||
height: 279,
|
||||
},
|
||||
};
|
||||
|
||||
export const templatesList: Template[] = [
|
||||
{
|
||||
id: "rhyhorn",
|
||||
name: "Rhyhorn",
|
||||
image:
|
||||
"https://res.cloudinary.com/amruth-pillai/image/upload/v1699370067/reactive-resume/templates/482-2480x3508_vuf5ev.jpg",
|
||||
},
|
||||
];
|
||||
|
||||
@ -15,6 +15,11 @@ export const isUrl = (string: string) => {
|
||||
return urlRegex.test(string);
|
||||
};
|
||||
|
||||
export const isEmptyString = (string: string) => {
|
||||
if (string === "<p></p>") return true;
|
||||
return string.trim().length === 0;
|
||||
};
|
||||
|
||||
export const extractUrl = (string: string) => {
|
||||
const urlRegex = /https?:\/\/[^ \n]+/i;
|
||||
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
export type Json = Record<string, unknown>;
|
||||
|
||||
export type Template = { id: string; name: string; image: string };
|
||||
|
||||
export type LayoutLocator = { page: number; column: number; section: number };
|
||||
|
||||
export type DeepPartial<T> = T extends object ? { [P in keyof T]?: DeepPartial<T[P]> } : T;
|
||||
|
||||
47
package.json
47
package.json
@ -51,28 +51,27 @@
|
||||
"@tailwindcss/typography": "^0.5.10",
|
||||
"@tanstack/eslint-plugin-query": "^5.6.0",
|
||||
"@testing-library/react": "14.0.0",
|
||||
"@types/async-retry": "^1.4.7",
|
||||
"@types/bcryptjs": "^2.4.5",
|
||||
"@types/cookie-parser": "^1.4.5",
|
||||
"@types/express": "^4.17.20",
|
||||
"@types/file-saver": "^2.0.6",
|
||||
"@types/async-retry": "^1.4.8",
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"@types/cookie-parser": "^1.4.6",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@types/jest": "^29.5.7",
|
||||
"@types/lodash.debounce": "^4.0.8",
|
||||
"@types/lodash.get": "^4.4.8",
|
||||
"@types/lodash.set": "^4.3.8",
|
||||
"@types/multer": "^1.4.9",
|
||||
"@types/lodash.debounce": "^4.0.9",
|
||||
"@types/lodash.get": "^4.4.9",
|
||||
"@types/lodash.set": "^4.3.9",
|
||||
"@types/multer": "^1.4.10",
|
||||
"@types/node": "20.8.10",
|
||||
"@types/nodemailer": "^6.4.13",
|
||||
"@types/papaparse": "^5.3.10",
|
||||
"@types/passport": "^1.0.14",
|
||||
"@types/passport-github2": "^1.2.8",
|
||||
"@types/passport-google-oauth20": "^2.0.13",
|
||||
"@types/passport-local": "^1.0.37",
|
||||
"@types/papaparse": "^5.3.11",
|
||||
"@types/passport": "^1.0.15",
|
||||
"@types/passport-github2": "^1.2.9",
|
||||
"@types/passport-google-oauth20": "^2.0.14",
|
||||
"@types/passport-local": "^1.0.38",
|
||||
"@types/react": "18.2.36",
|
||||
"@types/react-dom": "18.2.14",
|
||||
"@types/react-is": "18.2.3",
|
||||
"@types/retry": "^0.12.4",
|
||||
"@types/styled-components": "5.1.29",
|
||||
"@types/webfontloader": "^1.6.36",
|
||||
"@typescript-eslint/eslint-plugin": "^6.10.0",
|
||||
"@typescript-eslint/parser": "^6.10.0",
|
||||
@ -81,7 +80,6 @@
|
||||
"@vitest/coverage-v8": "^0.34.6",
|
||||
"@vitest/ui": "~0.34.6",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"babel-plugin-styled-components": "2.1.4",
|
||||
"cypress": "^13.4.0",
|
||||
"cz-conventional-changelog": "^3.3.0",
|
||||
"eslint": "~8.53.0",
|
||||
@ -115,10 +113,10 @@
|
||||
"vitest": "~0.34.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.0.8",
|
||||
"@dnd-kit/modifiers": "^6.0.1",
|
||||
"@dnd-kit/sortable": "^7.0.2",
|
||||
"@dnd-kit/utilities": "^3.2.1",
|
||||
"@dnd-kit/core": "^6.1.0",
|
||||
"@dnd-kit/modifiers": "^7.0.0",
|
||||
"@dnd-kit/sortable": "^8.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@fontsource/ibm-plex-sans": "^5.0.17",
|
||||
"@hookform/resolvers": "^3.3.2",
|
||||
"@nestjs-modules/mailer": "^1.9.1",
|
||||
@ -172,7 +170,7 @@
|
||||
"@tiptap/extension-underline": "^2.1.12",
|
||||
"@tiptap/react": "^2.1.12",
|
||||
"@tiptap/starter-kit": "^2.1.12",
|
||||
"@types/passport-jwt": "^3.0.12",
|
||||
"@types/passport-jwt": "^3.0.13",
|
||||
"async-retry": "^1.3.3",
|
||||
"await-to-js": "^3.0.0",
|
||||
"axios": "^1.6.0",
|
||||
@ -188,7 +186,7 @@
|
||||
"deepmerge": "^4.3.1",
|
||||
"file-saver": "^2.0.5",
|
||||
"framer-motion": "^10.16.4",
|
||||
"helmet": "^7.0.0",
|
||||
"helmet": "^7.1.0",
|
||||
"immer": "^10.0.3",
|
||||
"ioredis": "^5.3.2",
|
||||
"jszip": "^3.10.1",
|
||||
@ -201,7 +199,7 @@
|
||||
"nestjs-prisma": "^0.22.0",
|
||||
"nestjs-zod": "^3.0.0",
|
||||
"nodemailer": "^6.9.7",
|
||||
"openai": "^4.16.0",
|
||||
"openai": "^4.16.1",
|
||||
"otplib": "^12.0.1",
|
||||
"papaparse": "^5.4.1",
|
||||
"passport": "^0.6.0",
|
||||
@ -215,10 +213,8 @@
|
||||
"react": "18.2.0",
|
||||
"react-colorful": "^5.6.1",
|
||||
"react-dom": "18.2.0",
|
||||
"react-frame-component": "^5.2.6",
|
||||
"react-helmet-async": "^1.3.0",
|
||||
"react-hook-form": "^7.48.2",
|
||||
"react-is": "18.2.0",
|
||||
"react-parallax-tilt": "^1.7.172",
|
||||
"react-resizable-panels": "^0.0.55",
|
||||
"react-router-dom": "6.18.0",
|
||||
@ -226,7 +222,6 @@
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rxjs": "^7.8.1",
|
||||
"sharp": "^0.32.6",
|
||||
"styled-components": "6.1.0",
|
||||
"tailwind-merge": "^2.0.0",
|
||||
"tslib": "^2.6.2",
|
||||
"unique-names-generator": "^4.7.1",
|
||||
|
||||
226
pnpm-lock.yaml
generated
226
pnpm-lock.yaml
generated
@ -6,17 +6,17 @@ settings:
|
||||
|
||||
dependencies:
|
||||
'@dnd-kit/core':
|
||||
specifier: ^6.0.8
|
||||
version: 6.0.8(react-dom@18.2.0)(react@18.2.0)
|
||||
specifier: ^6.1.0
|
||||
version: 6.1.0(react-dom@18.2.0)(react@18.2.0)
|
||||
'@dnd-kit/modifiers':
|
||||
specifier: ^6.0.1
|
||||
version: 6.0.1(@dnd-kit/core@6.0.8)(react@18.2.0)
|
||||
specifier: ^7.0.0
|
||||
version: 7.0.0(@dnd-kit/core@6.1.0)(react@18.2.0)
|
||||
'@dnd-kit/sortable':
|
||||
specifier: ^7.0.2
|
||||
version: 7.0.2(@dnd-kit/core@6.0.8)(react@18.2.0)
|
||||
specifier: ^8.0.0
|
||||
version: 8.0.0(@dnd-kit/core@6.1.0)(react@18.2.0)
|
||||
'@dnd-kit/utilities':
|
||||
specifier: ^3.2.1
|
||||
version: 3.2.1(react@18.2.0)
|
||||
specifier: ^3.2.2
|
||||
version: 3.2.2(react@18.2.0)
|
||||
'@fontsource/ibm-plex-sans':
|
||||
specifier: ^5.0.17
|
||||
version: 5.0.17
|
||||
@ -177,8 +177,8 @@ dependencies:
|
||||
specifier: ^2.1.12
|
||||
version: 2.1.12(@tiptap/pm@2.1.12)
|
||||
'@types/passport-jwt':
|
||||
specifier: ^3.0.12
|
||||
version: 3.0.12
|
||||
specifier: ^3.0.13
|
||||
version: 3.0.13
|
||||
async-retry:
|
||||
specifier: ^1.3.3
|
||||
version: 1.3.3
|
||||
@ -225,8 +225,8 @@ dependencies:
|
||||
specifier: ^10.16.4
|
||||
version: 10.16.4(react-dom@18.2.0)(react@18.2.0)
|
||||
helmet:
|
||||
specifier: ^7.0.0
|
||||
version: 7.0.0
|
||||
specifier: ^7.1.0
|
||||
version: 7.1.0
|
||||
immer:
|
||||
specifier: ^10.0.3
|
||||
version: 10.0.3
|
||||
@ -264,8 +264,8 @@ dependencies:
|
||||
specifier: ^6.9.7
|
||||
version: 6.9.7
|
||||
openai:
|
||||
specifier: ^4.16.0
|
||||
version: 4.16.0
|
||||
specifier: ^4.16.1
|
||||
version: 4.16.1
|
||||
otplib:
|
||||
specifier: ^12.0.1
|
||||
version: 12.0.1
|
||||
@ -455,35 +455,35 @@ devDependencies:
|
||||
specifier: 14.0.0
|
||||
version: 14.0.0(react-dom@18.2.0)(react@18.2.0)
|
||||
'@types/async-retry':
|
||||
specifier: ^1.4.7
|
||||
version: 1.4.7
|
||||
specifier: ^1.4.8
|
||||
version: 1.4.8
|
||||
'@types/bcryptjs':
|
||||
specifier: ^2.4.5
|
||||
version: 2.4.5
|
||||
specifier: ^2.4.6
|
||||
version: 2.4.6
|
||||
'@types/cookie-parser':
|
||||
specifier: ^1.4.5
|
||||
version: 1.4.5
|
||||
specifier: ^1.4.6
|
||||
version: 1.4.6
|
||||
'@types/express':
|
||||
specifier: ^4.17.20
|
||||
version: 4.17.20
|
||||
specifier: ^4.17.21
|
||||
version: 4.17.21
|
||||
'@types/file-saver':
|
||||
specifier: ^2.0.6
|
||||
version: 2.0.6
|
||||
specifier: ^2.0.7
|
||||
version: 2.0.7
|
||||
'@types/jest':
|
||||
specifier: ^29.5.7
|
||||
version: 29.5.7
|
||||
'@types/lodash.debounce':
|
||||
specifier: ^4.0.8
|
||||
version: 4.0.8
|
||||
specifier: ^4.0.9
|
||||
version: 4.0.9
|
||||
'@types/lodash.get':
|
||||
specifier: ^4.4.8
|
||||
version: 4.4.8
|
||||
specifier: ^4.4.9
|
||||
version: 4.4.9
|
||||
'@types/lodash.set':
|
||||
specifier: ^4.3.8
|
||||
version: 4.3.8
|
||||
specifier: ^4.3.9
|
||||
version: 4.3.9
|
||||
'@types/multer':
|
||||
specifier: ^1.4.9
|
||||
version: 1.4.9
|
||||
specifier: ^1.4.10
|
||||
version: 1.4.10
|
||||
'@types/node':
|
||||
specifier: 20.8.10
|
||||
version: 20.8.10
|
||||
@ -491,20 +491,20 @@ devDependencies:
|
||||
specifier: ^6.4.13
|
||||
version: 6.4.13
|
||||
'@types/papaparse':
|
||||
specifier: ^5.3.10
|
||||
version: 5.3.10
|
||||
specifier: ^5.3.11
|
||||
version: 5.3.11
|
||||
'@types/passport':
|
||||
specifier: ^1.0.14
|
||||
version: 1.0.14
|
||||
specifier: ^1.0.15
|
||||
version: 1.0.15
|
||||
'@types/passport-github2':
|
||||
specifier: ^1.2.8
|
||||
version: 1.2.8
|
||||
specifier: ^1.2.9
|
||||
version: 1.2.9
|
||||
'@types/passport-google-oauth20':
|
||||
specifier: ^2.0.13
|
||||
version: 2.0.13
|
||||
specifier: ^2.0.14
|
||||
version: 2.0.14
|
||||
'@types/passport-local':
|
||||
specifier: ^1.0.37
|
||||
version: 1.0.37
|
||||
specifier: ^1.0.38
|
||||
version: 1.0.38
|
||||
'@types/react':
|
||||
specifier: 18.2.36
|
||||
version: 18.2.36
|
||||
@ -2303,8 +2303,8 @@ packages:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/@dnd-kit/accessibility@3.0.1(react@18.2.0):
|
||||
resolution: {integrity: sha512-HXRrwS9YUYQO9lFRc/49uO/VICbM+O+ZRpFDe9Pd1rwVv2PCNkRiTZRdxrDgng/UkvdC3Re9r2vwPpXXrWeFzg==}
|
||||
/@dnd-kit/accessibility@3.1.0(react@18.2.0):
|
||||
resolution: {integrity: sha512-ea7IkhKvlJUv9iSHJOnxinBcoOI3ppGnnL+VDJ75O45Nss6HtZd8IdN8touXPDtASfeI2T2LImb8VOZcL47wjQ==}
|
||||
peerDependencies:
|
||||
react: '>=16.8.0'
|
||||
dependencies:
|
||||
@ -2312,45 +2312,45 @@ packages:
|
||||
tslib: 2.6.2
|
||||
dev: false
|
||||
|
||||
/@dnd-kit/core@6.0.8(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-lYaoP8yHTQSLlZe6Rr9qogouGUz9oRUj4AHhDQGQzq/hqaJRpFo65X+JKsdHf8oUFBzx5A+SJPUvxAwTF2OabA==}
|
||||
/@dnd-kit/core@6.1.0(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-J3cQBClB4TVxwGo3KEjssGEXNJqGVWx17aRTZ1ob0FliR5IjYgTxl5YJbKTzA6IzrtelotH19v6y7uoIRUZPSg==}
|
||||
peerDependencies:
|
||||
react: '>=16.8.0'
|
||||
react-dom: '>=16.8.0'
|
||||
dependencies:
|
||||
'@dnd-kit/accessibility': 3.0.1(react@18.2.0)
|
||||
'@dnd-kit/utilities': 3.2.1(react@18.2.0)
|
||||
'@dnd-kit/accessibility': 3.1.0(react@18.2.0)
|
||||
'@dnd-kit/utilities': 3.2.2(react@18.2.0)
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
tslib: 2.6.2
|
||||
dev: false
|
||||
|
||||
/@dnd-kit/modifiers@6.0.1(@dnd-kit/core@6.0.8)(react@18.2.0):
|
||||
resolution: {integrity: sha512-rbxcsg3HhzlcMHVHWDuh9LCjpOVAgqbV78wLGI8tziXY3+qcMQ61qVXIvNKQFuhj75dSfD+o+PYZQ/NUk2A23A==}
|
||||
/@dnd-kit/modifiers@7.0.0(@dnd-kit/core@6.1.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-BG/ETy3eBjFap7+zIti53f0PCLGDzNXyTmn6fSdrudORf+OH04MxrW4p5+mPu4mgMk9kM41iYONjc3DOUWTcfg==}
|
||||
peerDependencies:
|
||||
'@dnd-kit/core': ^6.0.6
|
||||
'@dnd-kit/core': ^6.1.0
|
||||
react: '>=16.8.0'
|
||||
dependencies:
|
||||
'@dnd-kit/core': 6.0.8(react-dom@18.2.0)(react@18.2.0)
|
||||
'@dnd-kit/utilities': 3.2.1(react@18.2.0)
|
||||
'@dnd-kit/core': 6.1.0(react-dom@18.2.0)(react@18.2.0)
|
||||
'@dnd-kit/utilities': 3.2.2(react@18.2.0)
|
||||
react: 18.2.0
|
||||
tslib: 2.6.2
|
||||
dev: false
|
||||
|
||||
/@dnd-kit/sortable@7.0.2(@dnd-kit/core@6.0.8)(react@18.2.0):
|
||||
resolution: {integrity: sha512-wDkBHHf9iCi1veM834Gbk1429bd4lHX4RpAwT0y2cHLf246GAvU2sVw/oxWNpPKQNQRQaeGXhAVgrOl1IT+iyA==}
|
||||
/@dnd-kit/sortable@8.0.0(@dnd-kit/core@6.1.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-U3jk5ebVXe1Lr7c2wU7SBZjcWdQP+j7peHJfCspnA81enlu88Mgd7CC8Q+pub9ubP7eKVETzJW+IBAhsqbSu/g==}
|
||||
peerDependencies:
|
||||
'@dnd-kit/core': ^6.0.7
|
||||
'@dnd-kit/core': ^6.1.0
|
||||
react: '>=16.8.0'
|
||||
dependencies:
|
||||
'@dnd-kit/core': 6.0.8(react-dom@18.2.0)(react@18.2.0)
|
||||
'@dnd-kit/utilities': 3.2.1(react@18.2.0)
|
||||
'@dnd-kit/core': 6.1.0(react-dom@18.2.0)(react@18.2.0)
|
||||
'@dnd-kit/utilities': 3.2.2(react@18.2.0)
|
||||
react: 18.2.0
|
||||
tslib: 2.6.2
|
||||
dev: false
|
||||
|
||||
/@dnd-kit/utilities@3.2.1(react@18.2.0):
|
||||
resolution: {integrity: sha512-OOXqISfvBw/1REtkSK2N3Fi2EQiLMlWUlqnOK/UpOISqBZPWpE6TqL+jcPtMOkE8TqYGiURvRdPSI9hltNUjEA==}
|
||||
/@dnd-kit/utilities@3.2.2(react@18.2.0):
|
||||
resolution: {integrity: sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==}
|
||||
peerDependencies:
|
||||
react: '>=16.8.0'
|
||||
dependencies:
|
||||
@ -6769,8 +6769,8 @@ packages:
|
||||
resolution: {integrity: sha512-0Z6Tr7wjKJIk4OUEjVUQMtyunLDy339vcMaj38Kpj6jM2OE1p3S4kXExKZ7a3uXQAPCoy3sbrP1wibDKaf39oA==}
|
||||
dev: true
|
||||
|
||||
/@types/async-retry@1.4.7:
|
||||
resolution: {integrity: sha512-4NH5wuf9x7LZWD23/knI6RBywD1qCmLm7wNaqq0riy7hdDrCGGfkPOUvx0Cb78lVrFrEnCvocoL8+UcvSQlBDw==}
|
||||
/@types/async-retry@1.4.8:
|
||||
resolution: {integrity: sha512-Qup/B5PWLe86yI5I3av6ePGaeQrIHNKCwbsQotD6aHQ6YkHsMUxVZkZsmx/Ry3VZQ6uysHwTjQ7666+k6UjVJA==}
|
||||
dependencies:
|
||||
'@types/retry': 0.12.4
|
||||
dev: true
|
||||
@ -6804,8 +6804,8 @@ packages:
|
||||
'@babel/types': 7.23.0
|
||||
dev: true
|
||||
|
||||
/@types/bcryptjs@2.4.5:
|
||||
resolution: {integrity: sha512-tOF6TivOIvq+TWQm78335CMdyVJhpBG3NUdWQDAp95ax4E2rSKbws/ELHLk5EBoucwx/tHt3/hhLOHwWJgVrSw==}
|
||||
/@types/bcryptjs@2.4.6:
|
||||
resolution: {integrity: sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==}
|
||||
dev: true
|
||||
|
||||
/@types/body-parser@1.19.4:
|
||||
@ -6851,10 +6851,10 @@ packages:
|
||||
dependencies:
|
||||
'@types/node': 20.8.10
|
||||
|
||||
/@types/cookie-parser@1.4.5:
|
||||
resolution: {integrity: sha512-cbpH1NldYslPt7WRHXZFm+G7DTfUg57dQSCf1qrHwT8wtGX41JHLYf3Cieiqg7waPWjorVgcSSllZov+A1PJbg==}
|
||||
/@types/cookie-parser@1.4.6:
|
||||
resolution: {integrity: sha512-KoooCrD56qlLskXPLGUiJxOMnv5l/8m7cQD2OxJ73NPMhuSz9PmvwRD6EpjDyKBVrdJDdQ4bQK7JFNHnNmax0w==}
|
||||
dependencies:
|
||||
'@types/express': 4.17.20
|
||||
'@types/express': 4.17.21
|
||||
dev: true
|
||||
|
||||
/@types/ejs@3.1.4:
|
||||
@ -6888,16 +6888,16 @@ packages:
|
||||
'@types/range-parser': 1.2.6
|
||||
'@types/send': 0.17.3
|
||||
|
||||
/@types/express@4.17.20:
|
||||
resolution: {integrity: sha512-rOaqlkgEvOW495xErXMsmyX3WKBInbhG5eqojXYi3cGUaLoRDlXa5d52fkfWZT963AZ3v2eZ4MbKE6WpDAGVsw==}
|
||||
/@types/express@4.17.21:
|
||||
resolution: {integrity: sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==}
|
||||
dependencies:
|
||||
'@types/body-parser': 1.19.4
|
||||
'@types/express-serve-static-core': 4.17.39
|
||||
'@types/qs': 6.9.9
|
||||
'@types/serve-static': 1.15.4
|
||||
|
||||
/@types/file-saver@2.0.6:
|
||||
resolution: {integrity: sha512-Mw671DVqoMHbjw0w4v2iiOro01dlT/WhWp5uwecBa0Wg8c+bcZOjgF1ndBnlaxhtvFCgTRBtsGivSVhrK/vnag==}
|
||||
/@types/file-saver@2.0.7:
|
||||
resolution: {integrity: sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==}
|
||||
dev: true
|
||||
|
||||
/@types/graceful-fs@4.1.8:
|
||||
@ -6975,20 +6975,20 @@ packages:
|
||||
'@types/node': 20.8.10
|
||||
dev: true
|
||||
|
||||
/@types/lodash.debounce@4.0.8:
|
||||
resolution: {integrity: sha512-REumepIJjQFSOaBUoj81U5ZzF9YIhovzE2Lm6ejUbycmwx597k2ivG1cVfPtAj4eVuSbGoZDkJR0sRIahsE6/Q==}
|
||||
/@types/lodash.debounce@4.0.9:
|
||||
resolution: {integrity: sha512-Ma5JcgTREwpLRwMM+XwBR7DaWe96nC38uCBDFKZWbNKD+osjVzdpnUSwBcqCptrp16sSOLBAUb50Car5I0TCsQ==}
|
||||
dependencies:
|
||||
'@types/lodash': 4.14.200
|
||||
dev: true
|
||||
|
||||
/@types/lodash.get@4.4.8:
|
||||
resolution: {integrity: sha512-XK+co6sBkJxh1vaVP8al6cAA17dX//RNCknGG8JhpHFJfxq/GXKAYB9NKheG22pu2xpWpxfFd65W08EhH2IFlg==}
|
||||
/@types/lodash.get@4.4.9:
|
||||
resolution: {integrity: sha512-J5dvW98sxmGnamqf+/aLP87PYXyrha9xIgc2ZlHl6OHMFR2Ejdxep50QfU0abO1+CH6+ugx+8wEUN1toImAinA==}
|
||||
dependencies:
|
||||
'@types/lodash': 4.14.200
|
||||
dev: true
|
||||
|
||||
/@types/lodash.set@4.3.8:
|
||||
resolution: {integrity: sha512-WYIWnVO5xkcEKehhZf0Whrf9wj9D1AuaGTpwT/mCEJXKgdC2UWcMpvRqJahKQNhnOjmGEhpUqbYNJ6gUgdGSQw==}
|
||||
/@types/lodash.set@4.3.9:
|
||||
resolution: {integrity: sha512-KOxyNkZpbaggVmqbpr82N2tDVTx05/3/j0f50Es1prxrWB0XYf9p3QNxqcbWb7P1Q9wlvsUSlCFnwlPCIJ46PQ==}
|
||||
dependencies:
|
||||
'@types/lodash': 4.14.200
|
||||
dev: true
|
||||
@ -7007,10 +7007,10 @@ packages:
|
||||
resolution: {integrity: sha512-Kfe/D3hxHTusnPNRbycJE1N77WHDsdS4AjUYIzlDzhDrS47NrwuL3YW4VITxwR7KCVpzwgy4Rbj829KSSQmwXQ==}
|
||||
dev: true
|
||||
|
||||
/@types/multer@1.4.9:
|
||||
resolution: {integrity: sha512-9NSvPJ2E8bNTc8XtJq1Cimx2Wrn2Ah48F15B2Du/hM8a8CHLhVbJMlF3ZCqhvMdht7Sa+YdP0aKP7N4fxDcrrg==}
|
||||
/@types/multer@1.4.10:
|
||||
resolution: {integrity: sha512-6l9mYMhUe8wbnz/67YIjc7ZJyQNZoKq7fRXVf7nMdgWgalD0KyzJ2ywI7hoATUSXSbTu9q2HBiEwzy0tNN1v2w==}
|
||||
dependencies:
|
||||
'@types/express': 4.17.20
|
||||
'@types/express': 4.17.21
|
||||
dev: true
|
||||
|
||||
/@types/node-fetch@2.6.8:
|
||||
@ -7060,8 +7060,8 @@ packages:
|
||||
resolution: {integrity: sha512-qZqHmdGEALeSATMB1djT1S5szv6Wtpb7DKpHrt2XG4iyKlV7C2Xk8GmDXr1KXakOqUfX6ohw7ceruYt4NVmB1Q==}
|
||||
dev: false
|
||||
|
||||
/@types/papaparse@5.3.10:
|
||||
resolution: {integrity: sha512-mS1Fta/xJ9EDYmAvpeWzcV9Gr0cOl1ClpW7di9+wSUNDIDO55tBtyXg97O7K+Syrd9rDEmuejM2iqmJIJ1SO5g==}
|
||||
/@types/papaparse@5.3.11:
|
||||
resolution: {integrity: sha512-ISil0lMkpRDrBTKRPnUgVb5IqxWwj19gWBrX/ROk3pbkkslBN3URa713r/BSfAUj+w9gTPg3S3f45aMToVfh1w==}
|
||||
dependencies:
|
||||
'@types/node': 20.8.10
|
||||
dev: true
|
||||
@ -7070,56 +7070,56 @@ packages:
|
||||
resolution: {integrity: sha512-3YmXzzPAdOTVljVMkTMBdBEvlOLg2cDQaDhnnhT3nT9uDbnJzjWhKlzb+desT12Y7tGqaN6d+AbozcKzyL36Ng==}
|
||||
dev: true
|
||||
|
||||
/@types/passport-github2@1.2.8:
|
||||
resolution: {integrity: sha512-OD1WQ5AOIyTHgBWYJxG/yudHhojAfZXLpidzyoohjwFfu5sjJf/A5J23sm8YCQpnDjcWwaPFQ/NB7XC3QZjNJg==}
|
||||
/@types/passport-github2@1.2.9:
|
||||
resolution: {integrity: sha512-/nMfiPK2E6GKttwBzwj0Wjaot8eHrM57hnWxu52o6becr5/kXlH/4yE2v2rh234WGvSgEEzIII02Nc5oC5xEHA==}
|
||||
dependencies:
|
||||
'@types/express': 4.17.20
|
||||
'@types/passport': 1.0.14
|
||||
'@types/express': 4.17.21
|
||||
'@types/passport': 1.0.15
|
||||
'@types/passport-oauth2': 1.4.14
|
||||
dev: true
|
||||
|
||||
/@types/passport-google-oauth20@2.0.13:
|
||||
resolution: {integrity: sha512-idIhUp1RyBqk8cgApCHvqIvk09QVZv83hQJ/39VonIHYZkBps8p0AfB9INtPee3iuittdFx9J+i35pdZBgCqUQ==}
|
||||
/@types/passport-google-oauth20@2.0.14:
|
||||
resolution: {integrity: sha512-ZaZpRUAeMl3vy298ulKO1wGLn9SQtj/CyIfZL/Px5xU9pybMiQU3mhXDCBiWSbg0EK9uXT4ZoWC3ktuWY+5fwQ==}
|
||||
dependencies:
|
||||
'@types/express': 4.17.20
|
||||
'@types/passport': 1.0.14
|
||||
'@types/express': 4.17.21
|
||||
'@types/passport': 1.0.15
|
||||
'@types/passport-oauth2': 1.4.14
|
||||
dev: true
|
||||
|
||||
/@types/passport-jwt@3.0.12:
|
||||
resolution: {integrity: sha512-nXCd1lu20rw//nZ5AnK1FnlVZdSC4R5xksquev9oAJlXwJw0irMdZ7dRAE4KDlalptKObiaoam6BQ8lpujeZog==}
|
||||
/@types/passport-jwt@3.0.13:
|
||||
resolution: {integrity: sha512-fjHaC6Bv8EpMMqzTnHP32SXlZGaNfBPC/Po5dmRGYi2Ky7ljXPbGnOy+SxZqa6iZvFgVhoJ1915Re3m93zmcfA==}
|
||||
dependencies:
|
||||
'@types/express': 4.17.20
|
||||
'@types/express': 4.17.21
|
||||
'@types/jsonwebtoken': 9.0.4
|
||||
'@types/passport-strategy': 0.2.37
|
||||
dev: false
|
||||
|
||||
/@types/passport-local@1.0.37:
|
||||
resolution: {integrity: sha512-c57CwMHhMP2BBiOLyQZGRP43F8JtC84H976YVJdiU4EIWvqRCZ3F7QtsEgksOEIgMOk1Kz3EEKGA93OiDPQtRQ==}
|
||||
/@types/passport-local@1.0.38:
|
||||
resolution: {integrity: sha512-nsrW4A963lYE7lNTv9cr5WmiUD1ibYJvWrpE13oxApFsRt77b0RdtZvKbCdNIY4v/QZ6TRQWaDDEwV1kCTmcXg==}
|
||||
dependencies:
|
||||
'@types/express': 4.17.20
|
||||
'@types/passport': 1.0.14
|
||||
'@types/express': 4.17.21
|
||||
'@types/passport': 1.0.15
|
||||
'@types/passport-strategy': 0.2.37
|
||||
dev: true
|
||||
|
||||
/@types/passport-oauth2@1.4.14:
|
||||
resolution: {integrity: sha512-wZBvnRwqdvm35l1Jn9ebYm2Q7UtxYIdBu1PjoKXMoxJytniVjXxYJmrlDXn5fMZROWbJbnEnp1XSDANqtvMdGQ==}
|
||||
dependencies:
|
||||
'@types/express': 4.17.20
|
||||
'@types/express': 4.17.21
|
||||
'@types/oauth': 0.9.3
|
||||
'@types/passport': 1.0.14
|
||||
'@types/passport': 1.0.15
|
||||
dev: true
|
||||
|
||||
/@types/passport-strategy@0.2.37:
|
||||
resolution: {integrity: sha512-ltgwLnwHVfpjK7/66lpv43hiz90nIVb36JmeB0iF3FAZoHX6+LbkY5Ey97Bm8Jr0uGhQyDFEsSOOfejp5PJehg==}
|
||||
dependencies:
|
||||
'@types/express': 4.17.20
|
||||
'@types/passport': 1.0.14
|
||||
'@types/express': 4.17.21
|
||||
'@types/passport': 1.0.15
|
||||
|
||||
/@types/passport@1.0.14:
|
||||
resolution: {integrity: sha512-D6p2ygR2S7Cq5PO7iUaEIQu/5WrM0tONu6Lxgk0C9r3lafQIlVpWCo3V/KI9To3OqHBxcfQaOeK+8AvwW5RYmw==}
|
||||
/@types/passport@1.0.15:
|
||||
resolution: {integrity: sha512-oHOgzPBp5eLI1U/7421qYV/ZySQXMYCBSfRkDe1tQ0YrIbLY/M/76qIXE7Bs7lFyvw1x5QqiNQ9imvh0fQHe9Q==}
|
||||
dependencies:
|
||||
'@types/express': 4.17.20
|
||||
'@types/express': 4.17.21
|
||||
|
||||
/@types/prop-types@15.7.9:
|
||||
resolution: {integrity: sha512-n1yyPsugYNSmHgxDFjicaI2+gCNjsBck8UX9kuofAKlc0h1bL+20oSF72KeNaW2DUlesbEVCFgyV2dPGTiY42g==}
|
||||
@ -7183,7 +7183,7 @@ packages:
|
||||
/@types/serve-index@1.9.3:
|
||||
resolution: {integrity: sha512-4KG+yMEuvDPRrYq5fyVm/I2uqAJSAwZK9VSa+Zf+zUq9/oxSSvy3kkIqyL+jjStv6UCVi8/Aho0NHtB1Fwosrg==}
|
||||
dependencies:
|
||||
'@types/express': 4.17.20
|
||||
'@types/express': 4.17.21
|
||||
dev: true
|
||||
|
||||
/@types/serve-static@1.15.4:
|
||||
@ -12073,8 +12073,8 @@ packages:
|
||||
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
|
||||
hasBin: true
|
||||
|
||||
/helmet@7.0.0:
|
||||
resolution: {integrity: sha512-MsIgYmdBh460ZZ8cJC81q4XJknjG567wzEmv46WOBblDb6TUd3z8/GhgmsM9pn8g2B80tAJ4m5/d3Bi1KrSUBQ==}
|
||||
/helmet@7.1.0:
|
||||
resolution: {integrity: sha512-g+HZqgfbpXdCkme/Cd/mZkV0aV3BZZZSugecH03kl38m/Kmdx8jKjBikpDj2cr+Iynv4KpYEviojNdTJActJAg==}
|
||||
engines: {node: '>=16.0.0'}
|
||||
dev: false
|
||||
|
||||
@ -12253,7 +12253,7 @@ packages:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/http-proxy-middleware@2.0.6(@types/express@4.17.20):
|
||||
/http-proxy-middleware@2.0.6(@types/express@4.17.21):
|
||||
resolution: {integrity: sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
peerDependencies:
|
||||
@ -12262,7 +12262,7 @@ packages:
|
||||
'@types/express':
|
||||
optional: true
|
||||
dependencies:
|
||||
'@types/express': 4.17.20
|
||||
'@types/express': 4.17.21
|
||||
'@types/http-proxy': 1.17.13
|
||||
http-proxy: 1.18.1
|
||||
is-glob: 4.0.3
|
||||
@ -15397,8 +15397,8 @@ packages:
|
||||
is-wsl: 2.2.0
|
||||
dev: true
|
||||
|
||||
/openai@4.16.0:
|
||||
resolution: {integrity: sha512-P7IWqvaP0EuD3cQvzmU24KprnS0tHavrTuMiChEOa7pugqrKUfcmTnHcr+w01sJHny3vYoTvydCZ77cQolYj+g==}
|
||||
/openai@4.16.1:
|
||||
resolution: {integrity: sha512-Gr+uqUN1ICSk6VhrX64E+zL7skjI1TgPr/XUN+ZQuNLLOvx15+XZulx/lSW4wFEAQzgjBDlMBbBeikguGIjiMg==}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
'@types/node': 18.18.8
|
||||
@ -19675,7 +19675,7 @@ packages:
|
||||
dependencies:
|
||||
'@types/bonjour': 3.5.12
|
||||
'@types/connect-history-api-fallback': 1.5.2
|
||||
'@types/express': 4.17.20
|
||||
'@types/express': 4.17.21
|
||||
'@types/serve-index': 1.9.3
|
||||
'@types/serve-static': 1.15.4
|
||||
'@types/sockjs': 0.3.35
|
||||
@ -19690,7 +19690,7 @@ packages:
|
||||
express: 4.18.2
|
||||
graceful-fs: 4.2.11
|
||||
html-entities: 2.4.0
|
||||
http-proxy-middleware: 2.0.6(@types/express@4.17.20)
|
||||
http-proxy-middleware: 2.0.6(@types/express@4.17.21)
|
||||
ipaddr.js: 2.1.0
|
||||
launch-editor: 2.6.1
|
||||
open: 8.4.2
|
||||
|
||||
@ -15,15 +15,13 @@
|
||||
"skipDefaultLibCheck": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
// App Paths
|
||||
"@/client/*": ["apps/client/src/*"],
|
||||
"@/server/*": ["apps/server/src/*"],
|
||||
// Library Paths
|
||||
"@/artboard/*": ["apps/artboard/src/*"],
|
||||
"@reactive-resume/dto": ["libs/dto/src/index.ts"],
|
||||
"@reactive-resume/hooks": ["libs/hooks/src/index.ts"],
|
||||
"@reactive-resume/parser": ["libs/parser/src/index.ts"],
|
||||
"@reactive-resume/schema": ["libs/schema/src/index.ts"],
|
||||
"@reactive-resume/templates": ["libs/templates/src/index.ts"],
|
||||
"@reactive-resume/ui": ["libs/ui/src/index.ts"],
|
||||
"@reactive-resume/utils": ["libs/utils/src/index.ts"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user