Compare commits
133 Commits
v4.3.3
...
6fcb7a4845
| Author | SHA1 | Date | |
|---|---|---|---|
| 6fcb7a4845 | |||
| 213f96b189 | |||
| e8d6d4ad3a | |||
| 87370cfdf0 | |||
| 112a644927 | |||
| 2977e3528a | |||
| 21fd079f94 | |||
| 9bdc61b50d | |||
| 2de24d5b55 | |||
| 1cba9d0fb9 | |||
| 8f532bf4a6 | |||
| 4efdabd475 | |||
| 8803101dcd | |||
| a4a9381b65 | |||
| 618d42019e | |||
| d9b56cfb5b | |||
| 5624dc4f83 | |||
| 313fe47050 | |||
| 772332661f | |||
| 4c6b512455 | |||
| cc576f5220 | |||
| b193329c23 | |||
| 8e1459f092 | |||
| e19b55ff65 | |||
| 687161df98 | |||
| c54cfebf5f | |||
| e8d0ed1370 | |||
| 87a17a5196 | |||
| d19df3389c | |||
| faa4c606e5 | |||
| 210fbd18ac | |||
| 3ba0141ab8 | |||
| 279a2ddaeb | |||
| ea7ee3dd7a | |||
| a55bea1e07 | |||
| ed5cb7f17b | |||
| c7f8daaff3 | |||
| ac48ad78db | |||
| c18e18cce8 | |||
| 65ba13e503 | |||
| 44ec9e1d43 | |||
| 7c53949741 | |||
| 6d37769e38 | |||
| c525f8d2cc | |||
| 04dfcae898 | |||
| 9b5a99a8ca | |||
| c44f1c5282 | |||
| 24dfa99034 | |||
| 35c0177729 | |||
| 098d67cd8c | |||
| d8e0ced54c | |||
| 8e9b409bae | |||
| b995a6b6c0 | |||
| 3e76a52306 | |||
| 4e91a2e2ef | |||
| 4314912d5a | |||
| 93da5157ff | |||
| cc7bc4ffb8 | |||
| 78eefe9da1 | |||
| 7f7c4acdcb | |||
| a23dbfa3df | |||
| 249104e7a3 | |||
| 76bbe7de6b | |||
| 1994dde1f2 | |||
| a8626e400d | |||
| f5136da681 | |||
| 8efc243e43 | |||
| cd21860535 | |||
| 7054623678 | |||
| 37781d51f3 | |||
| ec4e43d4fc | |||
| 1d4529128f | |||
| 60ed3e2a8d | |||
| 5b67e7c0b4 | |||
| 1399d3c44b | |||
| eb543cf32d | |||
| 9c6d9833d6 | |||
| f50cbd71b7 | |||
| 3d5b3db321 | |||
| e438602773 | |||
| f42b29e4ac | |||
| 92995d9c2b | |||
| b21f1648c4 | |||
| 94c04b44df | |||
| 54ed0678bf | |||
| 817ec96963 | |||
| 8ad5458e2a | |||
| a87c5edd60 | |||
| e4327736bd | |||
| 1fddbe5f92 | |||
| 2c482e7df8 | |||
| c8edcd3dad | |||
| a82c25c7cb | |||
| 73b423030f | |||
| 809551d0f8 | |||
| e795ec64d6 | |||
| f8373e4798 | |||
| a31c434fbc | |||
| 1fa8aae80a | |||
| 27b60a4df9 | |||
| d21983aab4 | |||
| 9406d78653 | |||
| c7ae0e94d7 | |||
| 308a8e3ae3 | |||
| 4c90cc1838 | |||
| 460a40711e | |||
| 18cf814779 | |||
| a9656afbbf | |||
| 385fe008ce | |||
| 7e25e853d7 | |||
| de5adbe3d2 | |||
| c239ae3f49 | |||
| 1bfdff5b30 | |||
| 63db927924 | |||
| 9a34e4af27 | |||
| b5589338ec | |||
| a19059aa76 | |||
| 15f962310b | |||
| a32def2086 | |||
| 21af624096 | |||
| 227870ac78 | |||
| 33cb3dbd6a | |||
| eb7813ac6f | |||
| 0f8f2fe560 | |||
| 51f38f0884 | |||
| 6b93fd179d | |||
| 9385f36832 | |||
| a5dc15dc08 | |||
| eab996f7e7 | |||
| 43c5a33773 | |||
| 7fb0226ddc | |||
| db6e7a7480 | |||
| 6335ad1571 |
11
.env.example
@ -68,3 +68,14 @@ STORAGE_SKIP_BUCKET_CHECK=false
|
||||
# GOOGLE_CLIENT_ID=
|
||||
# GOOGLE_CLIENT_SECRET=
|
||||
# GOOGLE_CALLBACK_URL=http://localhost:5173/api/auth/google/callback
|
||||
|
||||
# OpenID (Optional)
|
||||
# VITE_OPENID_NAME=
|
||||
# OPENID_AUTHORIZATION_URL=
|
||||
# OPENID_CALLBACK_URL=http://localhost:5173/api/auth/openid/callback
|
||||
# OPENID_CLIENT_ID=
|
||||
# OPENID_CLIENT_SECRET=
|
||||
# OPENID_ISSUER=
|
||||
# OPENID_SCOPE=openid profile email
|
||||
# OPENID_TOKEN_URL=
|
||||
# OPENID_USER_INFO_URL=
|
||||
|
||||
@ -78,6 +78,7 @@
|
||||
"@typescript-eslint/no-misused-promises": "off",
|
||||
"@typescript-eslint/no-unsafe-assignment": "off",
|
||||
"@typescript-eslint/no-unsafe-member-access": "off",
|
||||
"@typescript-eslint/consistent-type-imports": "error",
|
||||
"@typescript-eslint/restrict-template-expressions": "off",
|
||||
"@typescript-eslint/no-redundant-type-constituents": "off",
|
||||
"@typescript-eslint/consistent-type-definitions": ["error", "type"],
|
||||
|
||||
2
.github/workflows/lint-test-build.yml
vendored
@ -39,7 +39,7 @@ jobs:
|
||||
run: pnpm run lint
|
||||
|
||||
- name: Format
|
||||
run: pnpm run format:check
|
||||
run: pnpm run format
|
||||
|
||||
- name: Test
|
||||
run: pnpm run test
|
||||
|
||||
2
.github/workflows/publish-docker-image.yml
vendored
@ -153,7 +153,7 @@ jobs:
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
|
||||
- name: Deploy the latest image on rxresu.me
|
||||
run: curl -Xk POST ${{ secrets.SERVICE_WEBHOOK }}
|
||||
run: curl -kX POST ${{ secrets.SERVICE_WEBHOOK }}
|
||||
|
||||
- name: Inform about the release on Discord
|
||||
uses: sarisia/actions-status-discord@v1.14.3
|
||||
|
||||
1
.gitignore
vendored
@ -40,6 +40,7 @@ Thumbs.db
|
||||
# Generated Files
|
||||
.nx
|
||||
.swc
|
||||
.turbo
|
||||
fly.toml
|
||||
stats.html
|
||||
|
||||
|
||||
@ -5,7 +5,9 @@
|
||||
"install": "always",
|
||||
"packageManager": "pnpm",
|
||||
"reject": [
|
||||
"nx",
|
||||
"eslint",
|
||||
"@nx/*",
|
||||
"@swc/*",
|
||||
"@swc-node/*",
|
||||
"@reactive-resume/*",
|
||||
|
||||
2
.vscode/extensions.json
vendored
@ -1,3 +1,3 @@
|
||||
{
|
||||
"recommendations": ["nrwl.angular-console", "esbenp.prettier-vscode", "dbaeumer.vscode-eslint"]
|
||||
"recommendations": ["esbenp.prettier-vscode", "dbaeumer.vscode-eslint"]
|
||||
}
|
||||
|
||||
6
.vscode/settings.json
vendored
@ -1,9 +1,9 @@
|
||||
{
|
||||
"css.validate": false,
|
||||
"vitest.disableWorkspaceWarning": true,
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"i18n-ally.localesPaths": ["apps/client/src/locales"],
|
||||
"tailwindCSS.experimental.classRegex": [
|
||||
["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"],
|
||||
["cn\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"]
|
||||
]
|
||||
],
|
||||
"typescript.tsdk": "node_modules/typescript/lib"
|
||||
}
|
||||
|
||||
@ -7,7 +7,7 @@ ARG NX_CLOUD_ACCESS_TOKEN
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
|
||||
RUN corepack enable pnpm && corepack prepare pnpm --activate
|
||||
RUN corepack enable
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
||||
39
README.md
@ -1,4 +1,15 @@
|
||||

|
||||
<div align="center" markdown="1">
|
||||
<a href="https://go.warp.dev/Reactive-Resume">
|
||||
<img alt="Warp Sponsorship" width="400" src="https://github.com/warpdotdev/brand-assets/blob/main/Github/Sponsor/Warp-Github-LG-03.png?raw=true" />
|
||||
</a>
|
||||
|
||||
### [Warp, built for coding with multiple AI agents.](https://go.warp.dev/Reactive-Resume)
|
||||
|
||||
[Available for MacOS, Linux, & Windows](https://go.warp.dev/Reactive-Resume)<br>
|
||||
|
||||
---
|
||||
|
||||
<img alt="Reactive Resume" width="800" src="https://i.imgur.com/FFc4nyZ.jpg" />
|
||||
|
||||

|
||||
[](https://hub.docker.com/repository/docker/amruthpillai/reactive-resume)
|
||||
@ -12,6 +23,8 @@ A free and open-source resume builder that simplifies the process of creating, u
|
||||
|
||||
### [Go to App](https://rxresu.me/) | [Docs](https://docs.rxresu.me/)
|
||||
|
||||
</div>
|
||||
|
||||
## Description
|
||||
|
||||
Reactive Resume is a free and open-source resume builder that simplifies the process of creating, updating, and sharing your resume. With zero user tracking or advertising, your privacy is a top priority. The platform is extremely user-friendly and can be self-hosted in less than 30 seconds if you wish to own your data completely.
|
||||
@ -24,17 +37,21 @@ Start creating your standout resume with Reactive Resume today!
|
||||
|
||||
## Templates
|
||||
|
||||
| Azurill | Bronzor | Chikorita |
|
||||
| ------------------------------------------------------------ | ----------------------------------------------------------- | ----------------------------------------------------------- |
|
||||
| <img src="https://i.imgur.com/jKgo04C.jpeg" width="200px" /> | <img src="https://i.imgur.com/DFNQZP2.jpg" width="200px" /> | <img src="https://i.imgur.com/Dwv8Y7f.jpg" width="200px" /> |
|
||||
| Azurill | Bronzor | Chikorita |
|
||||
| ----------------------------------------------------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------- |
|
||||
| <img src="./apps/client/public/templates/jpg/azurill.jpg" width="200px" /> | <img src="./apps/client/public/templates/jpg/bronzor.jpg" width="200px" /> | <img src="./apps/client/public/templates/jpg/chikorita.jpg" width="200px" /> |
|
||||
|
||||
| Ditto | Kakuna | Nosepass |
|
||||
| ----------------------------------------------------------- | ----------------------------------------------------------- | ----------------------------------------------------------- |
|
||||
| <img src="https://i.imgur.com/6c5lASL.jpg" width="200px" /> | <img src="https://i.imgur.com/268ML3t.jpg" width="200px" /> | <img src="https://i.imgur.com/npRLsPS.jpg" width="200px" /> |
|
||||
| Ditto | Gengar | Glalie |
|
||||
| -------------------------------------------------------------------- | ---------------------------------------------------------------------- | ---------------------------------------------------------------------- |
|
||||
| <img src="./apps/client/public/templates/jpg/ditto.jpg" width="200px" /> | <img src="./apps/client/public/templates/jpg/gengar.jpg" width="200px" /> | <img src="./apps/client/public/templates/jpg/glalie.jpg" width="200px" /> |
|
||||
|
||||
| Onyx | Pikachu | Rhyhorn |
|
||||
| ----------------------------------------------------------- | ----------------------------------------------------------- | ----------------------------------------------------------- |
|
||||
| <img src="https://i.imgur.com/cxplXOW.jpg" width="200px" /> | <img src="https://i.imgur.com/Y9f7qsh.jpg" width="200px" /> | <img src="https://i.imgur.com/h4kQxy2.jpg" width="200px" /> |
|
||||
| Kakuna | Leafish | Nosepass |
|
||||
| ---------------------------------------------------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------ |
|
||||
| <img src="./apps/client/public/templates/jpg/kakuna.jpg" width="200px" /> | <img src="./apps/client/public/templates/jpg/leafish.jpg" width="200px" /> | <img src="./apps/client/public/templates/jpg/nosepass.jpg" width="200px" /> |
|
||||
|
||||
| Onyx | Pikachu | Rhyhorn |
|
||||
| ------------------------------------------------------------------- | ----------------------------------------------------------------------- | ----------------------------------------------------------------------- |
|
||||
| <img src="./apps/client/public/templates/jpg/onyx.jpg" width="200px" /> | <img src="./apps/client/public/templates/jpg/pikachu.jpg" width="200px" /> | <img src="./apps/client/public/templates/jpg/rhyhorn.jpg" width="200px" /> |
|
||||
|
||||
## Features
|
||||
|
||||
@ -93,7 +110,7 @@ _By the community, for the community._
|
||||
A passion project by [Amruth Pillai](https://www.amruthpillai.com/)
|
||||
|
||||
<p>
|
||||
<a href="https://www.digitalocean.com/?utm_medium=opensource&utm_source=Reactive-Resume">
|
||||
<a href="https://m.do.co/c/ceae1fff245e">
|
||||
<img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/PoweredByDO/DO_Powered_by_Badge_blue.svg" width="200px">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
@ -39,13 +39,6 @@
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
|
||||
<!-- Phosphor Icons -->
|
||||
<script src="https://unpkg.com/@phosphor-icons/web"></script>
|
||||
|
||||
<!-- Simple Icons -->
|
||||
<link
|
||||
type="text/css"
|
||||
rel="stylesheet"
|
||||
href="https://unpkg.com/simple-icons-font@v14/font/simple-icons.min.css"
|
||||
/>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@phosphor-icons/web"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -1,13 +1,14 @@
|
||||
import { cn } from "@reactive-resume/utils";
|
||||
import { forwardRef } from "react";
|
||||
|
||||
type BrandIconProps = {
|
||||
slug: string;
|
||||
};
|
||||
|
||||
export const BrandIcon = ({ slug }: BrandIconProps) => {
|
||||
if (slug === "linkedin") {
|
||||
export const BrandIcon = forwardRef<HTMLImageElement, BrandIconProps>(({ slug }, ref) => {
|
||||
if (slug.toLowerCase() === "linkedin") {
|
||||
return (
|
||||
<img
|
||||
ref={ref}
|
||||
alt="LinkedIn"
|
||||
className="size-4"
|
||||
src={`${window.location.origin}/support-logos/linkedin.svg`}
|
||||
@ -15,5 +16,9 @@ export const BrandIcon = ({ slug }: BrandIconProps) => {
|
||||
);
|
||||
}
|
||||
|
||||
return <i className={cn("si si--color text-[1rem]", `si-${slug}`)} />;
|
||||
};
|
||||
return (
|
||||
<img ref={ref} alt={slug} className="size-4" src={`https://cdn.simpleicons.org/${slug}`} />
|
||||
);
|
||||
});
|
||||
|
||||
BrandIcon.displayName = "BrandIcon";
|
||||
|
||||
@ -14,8 +14,8 @@ export const Picture = ({ className }: PictureProps) => {
|
||||
|
||||
return (
|
||||
<img
|
||||
src={picture.url}
|
||||
alt="Profile"
|
||||
src={picture.url}
|
||||
className={cn(
|
||||
"relative z-20 object-cover",
|
||||
picture.effects.border && "border-primary",
|
||||
|
||||
5
apps/artboard/src/constants/helmet.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { HelmetData } from "react-helmet-async";
|
||||
|
||||
export const helmetData = new HelmetData({});
|
||||
|
||||
export const helmetContext = helmetData.context;
|
||||
@ -1,6 +1,6 @@
|
||||
import { StrictMode } from "react";
|
||||
import * as ReactDOM from "react-dom/client";
|
||||
import { RouterProvider } from "react-router-dom";
|
||||
import { RouterProvider } from "react-router";
|
||||
|
||||
import { router } from "./router";
|
||||
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { Outlet } from "react-router-dom";
|
||||
import { Helmet } from "react-helmet-async";
|
||||
import { Outlet } from "react-router";
|
||||
import webfontloader from "webfontloader";
|
||||
|
||||
import { useArtboardStore } from "../store/artboard";
|
||||
|
||||
export const ArtboardPage = () => {
|
||||
const name = useArtboardStore((state) => state.resume.basics.name);
|
||||
const metadata = useArtboardStore((state) => state.resume.metadata);
|
||||
|
||||
const fontString = useMemo(() => {
|
||||
@ -55,5 +57,18 @@ export const ArtboardPage = () => {
|
||||
}
|
||||
}, [metadata]);
|
||||
|
||||
return <Outlet />;
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{name} | Reactive Resume</title>
|
||||
{metadata.css.visible && (
|
||||
<style id="custom-css" lang="css">
|
||||
{metadata.css.value}
|
||||
</style>
|
||||
)}
|
||||
</Helmet>
|
||||
|
||||
<Outlet />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
import { SectionKey } from "@reactive-resume/schema";
|
||||
import { pageSizeMap, Template } from "@reactive-resume/utils";
|
||||
import type { SectionKey } from "@reactive-resume/schema";
|
||||
import type { Template } from "@reactive-resume/utils";
|
||||
import { pageSizeMap } from "@reactive-resume/utils";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { ReactZoomPanPinchRef, TransformComponent, TransformWrapper } from "react-zoom-pan-pinch";
|
||||
import type { ReactZoomPanPinchRef } from "react-zoom-pan-pinch";
|
||||
import { TransformComponent, TransformWrapper } from "react-zoom-pan-pinch";
|
||||
|
||||
import { MM_TO_PX, Page } from "../components/page";
|
||||
import { useArtboardStore } from "../store/artboard";
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { SectionKey } from "@reactive-resume/schema";
|
||||
import { Template } from "@reactive-resume/utils";
|
||||
import type { SectionKey } from "@reactive-resume/schema";
|
||||
import type { Template } from "@reactive-resume/utils";
|
||||
import { useMemo } from "react";
|
||||
|
||||
import { Page } from "../components/page";
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import { useEffect } from "react";
|
||||
import { Outlet } from "react-router-dom";
|
||||
import { HelmetProvider } from "react-helmet-async";
|
||||
import { Outlet } from "react-router";
|
||||
|
||||
import { helmetContext } from "../constants/helmet";
|
||||
import { useArtboardStore } from "../store/artboard";
|
||||
|
||||
export const Providers = () => {
|
||||
@ -10,35 +12,28 @@ export const Providers = () => {
|
||||
useEffect(() => {
|
||||
const handleMessage = (event: MessageEvent) => {
|
||||
if (event.origin !== window.location.origin) return;
|
||||
|
||||
if (event.data.type === "SET_RESUME") setResume(event.data.payload);
|
||||
if (event.data.type === "SET_THEME") {
|
||||
event.data.payload === "dark"
|
||||
? document.documentElement.classList.add("dark")
|
||||
: document.documentElement.classList.remove("dark");
|
||||
}
|
||||
};
|
||||
|
||||
const resumeData = window.localStorage.getItem("resume");
|
||||
if (resumeData) {
|
||||
setResume(JSON.parse(resumeData));
|
||||
return;
|
||||
}
|
||||
|
||||
window.addEventListener("message", handleMessage);
|
||||
window.addEventListener("message", handleMessage, false);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("message", handleMessage);
|
||||
window.removeEventListener("message", handleMessage, false);
|
||||
};
|
||||
}, [setResume]);
|
||||
}, []);
|
||||
|
||||
// Only for testing, in production this will be fetched from window.postMessage
|
||||
// useEffect(() => {
|
||||
// setResume(sampleResume);
|
||||
// }, [setResume]);
|
||||
useEffect(() => {
|
||||
const resumeData = window.localStorage.getItem("resume");
|
||||
|
||||
if (resumeData) setResume(JSON.parse(resumeData));
|
||||
}, [window.localStorage.getItem("resume")]);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (!resume) return null;
|
||||
|
||||
return <Outlet />;
|
||||
return (
|
||||
<HelmetProvider context={helmetContext}>
|
||||
<Outlet />
|
||||
</HelmetProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { createBrowserRouter, createRoutesFromChildren, Route } from "react-router-dom";
|
||||
import { createBrowserRouter, createRoutesFromChildren, Route } from "react-router";
|
||||
|
||||
import { ArtboardPage } from "../pages/artboard";
|
||||
import { BuilderLayout } from "../pages/builder";
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { ResumeData } from "@reactive-resume/schema";
|
||||
import type { ResumeData } from "@reactive-resume/schema";
|
||||
import { create } from "zustand";
|
||||
|
||||
export type ArtboardStore = {
|
||||
|
||||
@ -8,6 +8,10 @@
|
||||
@apply border-current;
|
||||
}
|
||||
|
||||
body {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#root {
|
||||
@apply antialiased;
|
||||
}
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
import {
|
||||
import type {
|
||||
Award,
|
||||
Certification,
|
||||
CustomSection,
|
||||
CustomSectionGroup,
|
||||
Education,
|
||||
Experience,
|
||||
Interest,
|
||||
Language,
|
||||
Profile,
|
||||
@ -15,16 +13,16 @@ import {
|
||||
SectionWithItem,
|
||||
Skill,
|
||||
URL,
|
||||
Volunteer,
|
||||
} from "@reactive-resume/schema";
|
||||
import { cn, isEmptyString, isUrl, linearTransform } from "@reactive-resume/utils";
|
||||
import { Education, Experience, Volunteer } from "@reactive-resume/schema";
|
||||
import { cn, isEmptyString, isUrl, linearTransform, sanitize } from "@reactive-resume/utils";
|
||||
import get from "lodash.get";
|
||||
import React, { Fragment } from "react";
|
||||
|
||||
import { BrandIcon } from "../components/brand-icon";
|
||||
import { Picture } from "../components/picture";
|
||||
import { useArtboardStore } from "../store/artboard";
|
||||
import { TemplateProps } from "../types/template";
|
||||
import type { TemplateProps } from "../types/template";
|
||||
|
||||
const Header = () => {
|
||||
const basics = useArtboardStore((state) => state.resume.basics);
|
||||
@ -100,9 +98,9 @@ const Summary = () => {
|
||||
<div className="absolute left-[-4.5px] top-[8px] hidden size-[8px] rounded-full bg-primary group-[.main]:block" />
|
||||
|
||||
<div
|
||||
dangerouslySetInnerHTML={{ __html: section.content }}
|
||||
className="wysiwyg"
|
||||
dangerouslySetInnerHTML={{ __html: sanitize(section.content) }}
|
||||
style={{ columns: section.columns }}
|
||||
className="wysiwyg"
|
||||
/>
|
||||
</main>
|
||||
</section>
|
||||
@ -188,7 +186,7 @@ const Section = <T,>({
|
||||
summaryKey,
|
||||
keywordsKey,
|
||||
}: SectionProps<T>) => {
|
||||
if (!section.visible || section.items.length === 0) return null;
|
||||
if (!section.visible || section.items.filter((item) => item.visible).length === 0) return null;
|
||||
|
||||
return (
|
||||
<section id={section.id} className="grid">
|
||||
@ -226,7 +224,10 @@ const Section = <T,>({
|
||||
<div>{children?.(item as T)}</div>
|
||||
|
||||
{summary !== undefined && !isEmptyString(summary) && (
|
||||
<div dangerouslySetInnerHTML={{ __html: summary }} className="wysiwyg" />
|
||||
<div
|
||||
dangerouslySetInnerHTML={{ __html: sanitize(summary) }}
|
||||
className="wysiwyg"
|
||||
/>
|
||||
)}
|
||||
|
||||
{level !== undefined && level > 0 && <Rating level={level} />}
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
import {
|
||||
import type {
|
||||
Award,
|
||||
Certification,
|
||||
CustomSection,
|
||||
CustomSectionGroup,
|
||||
Education,
|
||||
Experience,
|
||||
Interest,
|
||||
Language,
|
||||
Profile,
|
||||
@ -15,16 +13,16 @@ import {
|
||||
SectionWithItem,
|
||||
Skill,
|
||||
URL,
|
||||
Volunteer,
|
||||
} from "@reactive-resume/schema";
|
||||
import { cn, isEmptyString, isUrl } from "@reactive-resume/utils";
|
||||
import { Education, Experience, Volunteer } from "@reactive-resume/schema";
|
||||
import { cn, isEmptyString, isUrl, sanitize } from "@reactive-resume/utils";
|
||||
import get from "lodash.get";
|
||||
import { Fragment } from "react";
|
||||
|
||||
import { BrandIcon } from "../components/brand-icon";
|
||||
import { Picture } from "../components/picture";
|
||||
import { useArtboardStore } from "../store/artboard";
|
||||
import { TemplateProps } from "../types/template";
|
||||
import type { TemplateProps } from "../types/template";
|
||||
|
||||
const Header = () => {
|
||||
const basics = useArtboardStore((state) => state.resume.basics);
|
||||
@ -91,9 +89,9 @@ const Summary = () => {
|
||||
</div>
|
||||
|
||||
<div
|
||||
dangerouslySetInnerHTML={{ __html: section.content }}
|
||||
className="wysiwyg col-span-4"
|
||||
dangerouslySetInnerHTML={{ __html: sanitize(section.content) }}
|
||||
style={{ columns: section.columns }}
|
||||
className="wysiwyg col-span-4"
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
@ -179,7 +177,7 @@ const Section = <T,>({
|
||||
summaryKey,
|
||||
keywordsKey,
|
||||
}: SectionProps<T>) => {
|
||||
if (!section.visible || section.items.length === 0) return null;
|
||||
if (!section.visible || section.items.filter((item) => item.visible).length === 0) return null;
|
||||
|
||||
return (
|
||||
<section id={section.id} className="grid grid-cols-5 border-t pt-2.5">
|
||||
@ -207,7 +205,10 @@ const Section = <T,>({
|
||||
</div>
|
||||
|
||||
{summary !== undefined && !isEmptyString(summary) && (
|
||||
<div dangerouslySetInnerHTML={{ __html: summary }} className="wysiwyg" />
|
||||
<div
|
||||
dangerouslySetInnerHTML={{ __html: sanitize(summary) }}
|
||||
className="wysiwyg"
|
||||
/>
|
||||
)}
|
||||
|
||||
{level !== undefined && level > 0 && <Rating level={level} />}
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
import {
|
||||
import type {
|
||||
Award,
|
||||
Certification,
|
||||
CustomSection,
|
||||
CustomSectionGroup,
|
||||
Education,
|
||||
Experience,
|
||||
Interest,
|
||||
Language,
|
||||
Profile,
|
||||
@ -15,16 +13,16 @@ import {
|
||||
SectionWithItem,
|
||||
Skill,
|
||||
URL,
|
||||
Volunteer,
|
||||
} from "@reactive-resume/schema";
|
||||
import { cn, isEmptyString, isUrl } from "@reactive-resume/utils";
|
||||
import { Education, Experience, Volunteer } from "@reactive-resume/schema";
|
||||
import { cn, isEmptyString, isUrl, sanitize } from "@reactive-resume/utils";
|
||||
import get from "lodash.get";
|
||||
import { Fragment } from "react";
|
||||
|
||||
import { BrandIcon } from "../components/brand-icon";
|
||||
import { Picture } from "../components/picture";
|
||||
import { useArtboardStore } from "../store/artboard";
|
||||
import { TemplateProps } from "../types/template";
|
||||
import type { TemplateProps } from "../types/template";
|
||||
|
||||
const Header = () => {
|
||||
const basics = useArtboardStore((state) => state.resume.basics);
|
||||
@ -91,9 +89,9 @@ const Summary = () => {
|
||||
<h4 className="mb-2 border-b pb-0.5 text-sm font-bold">{section.name}</h4>
|
||||
|
||||
<div
|
||||
dangerouslySetInnerHTML={{ __html: section.content }}
|
||||
className="wysiwyg"
|
||||
dangerouslySetInnerHTML={{ __html: sanitize(section.content) }}
|
||||
style={{ columns: section.columns }}
|
||||
className="wysiwyg"
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
@ -184,7 +182,7 @@ const Section = <T,>({
|
||||
summaryKey,
|
||||
keywordsKey,
|
||||
}: SectionProps<T>) => {
|
||||
if (!section.visible || section.items.length === 0) return null;
|
||||
if (!section.visible || section.items.filter((item) => item.visible).length === 0) return null;
|
||||
|
||||
return (
|
||||
<section id={section.id} className="grid">
|
||||
@ -211,7 +209,7 @@ const Section = <T,>({
|
||||
|
||||
{summary !== undefined && !isEmptyString(summary) && (
|
||||
<div
|
||||
dangerouslySetInnerHTML={{ __html: summary }}
|
||||
dangerouslySetInnerHTML={{ __html: sanitize(summary) }}
|
||||
className="wysiwyg group-[.sidebar]:prose-invert"
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
import {
|
||||
import type {
|
||||
Award,
|
||||
Certification,
|
||||
CustomSection,
|
||||
CustomSectionGroup,
|
||||
Education,
|
||||
Experience,
|
||||
Interest,
|
||||
Language,
|
||||
Profile,
|
||||
@ -15,16 +13,16 @@ import {
|
||||
SectionWithItem,
|
||||
Skill,
|
||||
URL,
|
||||
Volunteer,
|
||||
} from "@reactive-resume/schema";
|
||||
import { cn, isEmptyString, isUrl } from "@reactive-resume/utils";
|
||||
import { Education, Experience, Volunteer } from "@reactive-resume/schema";
|
||||
import { cn, isEmptyString, isUrl, sanitize } from "@reactive-resume/utils";
|
||||
import get from "lodash.get";
|
||||
import { Fragment } from "react";
|
||||
|
||||
import { BrandIcon } from "../components/brand-icon";
|
||||
import { Picture } from "../components/picture";
|
||||
import { useArtboardStore } from "../store/artboard";
|
||||
import { TemplateProps } from "../types/template";
|
||||
import type { TemplateProps } from "../types/template";
|
||||
|
||||
const Header = () => {
|
||||
const basics = useArtboardStore((state) => state.resume.basics);
|
||||
@ -111,9 +109,9 @@ const Summary = () => {
|
||||
<h4 className="mb-2 text-base font-bold">{section.name}</h4>
|
||||
|
||||
<div
|
||||
dangerouslySetInnerHTML={{ __html: section.content }}
|
||||
className="wysiwyg"
|
||||
dangerouslySetInnerHTML={{ __html: sanitize(section.content) }}
|
||||
style={{ columns: section.columns }}
|
||||
className="wysiwyg"
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
@ -199,7 +197,7 @@ const Section = <T,>({
|
||||
summaryKey,
|
||||
keywordsKey,
|
||||
}: SectionProps<T>) => {
|
||||
if (!section.visible || section.items.length === 0) return null;
|
||||
if (!section.visible || section.items.filter((item) => item.visible).length === 0) return null;
|
||||
|
||||
return (
|
||||
<section id={section.id} className="grid">
|
||||
@ -232,7 +230,10 @@ const Section = <T,>({
|
||||
</div>
|
||||
|
||||
{summary !== undefined && !isEmptyString(summary) && (
|
||||
<div dangerouslySetInnerHTML={{ __html: summary }} className="wysiwyg" />
|
||||
<div
|
||||
dangerouslySetInnerHTML={{ __html: sanitize(summary) }}
|
||||
className="wysiwyg"
|
||||
/>
|
||||
)}
|
||||
|
||||
{level !== undefined && level > 0 && <Rating level={level} />}
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
import {
|
||||
import type {
|
||||
Award,
|
||||
Certification,
|
||||
CustomSection,
|
||||
CustomSectionGroup,
|
||||
Education,
|
||||
Experience,
|
||||
Interest,
|
||||
Language,
|
||||
Profile,
|
||||
@ -15,16 +13,16 @@ import {
|
||||
SectionWithItem,
|
||||
Skill,
|
||||
URL,
|
||||
Volunteer,
|
||||
} from "@reactive-resume/schema";
|
||||
import { cn, hexToRgb, isEmptyString, isUrl } from "@reactive-resume/utils";
|
||||
import { Education, Experience, Volunteer } from "@reactive-resume/schema";
|
||||
import { cn, hexToRgb, isEmptyString, isUrl, sanitize } from "@reactive-resume/utils";
|
||||
import get from "lodash.get";
|
||||
import { Fragment } from "react";
|
||||
|
||||
import { BrandIcon } from "../components/brand-icon";
|
||||
import { Picture } from "../components/picture";
|
||||
import { useArtboardStore } from "../store/artboard";
|
||||
import { TemplateProps } from "../types/template";
|
||||
import type { TemplateProps } from "../types/template";
|
||||
|
||||
const Header = () => {
|
||||
const basics = useArtboardStore((state) => state.resume.basics);
|
||||
@ -91,9 +89,9 @@ const Summary = () => {
|
||||
<div className="p-custom space-y-4" style={{ backgroundColor: hexToRgb(primaryColor, 0.2) }}>
|
||||
<section id={section.id}>
|
||||
<div
|
||||
dangerouslySetInnerHTML={{ __html: section.content }}
|
||||
className="wysiwyg"
|
||||
dangerouslySetInnerHTML={{ __html: sanitize(section.content) }}
|
||||
style={{ columns: section.columns }}
|
||||
className="wysiwyg"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
@ -186,7 +184,7 @@ const Section = <T,>({
|
||||
summaryKey,
|
||||
keywordsKey,
|
||||
}: SectionProps<T>) => {
|
||||
if (!section.visible || section.items.length === 0) return null;
|
||||
if (!section.visible || section.items.filter((item) => item.visible).length === 0) return null;
|
||||
|
||||
return (
|
||||
<section id={section.id} className="grid">
|
||||
@ -212,7 +210,10 @@ const Section = <T,>({
|
||||
</div>
|
||||
|
||||
{summary !== undefined && !isEmptyString(summary) && (
|
||||
<div dangerouslySetInnerHTML={{ __html: summary }} className="wysiwyg" />
|
||||
<div
|
||||
dangerouslySetInnerHTML={{ __html: sanitize(summary) }}
|
||||
className="wysiwyg"
|
||||
/>
|
||||
)}
|
||||
|
||||
{level !== undefined && level > 0 && <Rating level={level} />}
|
||||
@ -530,6 +531,11 @@ const mapSectionToComponent = (section: SectionKey) => {
|
||||
case "education": {
|
||||
return <Education />;
|
||||
}
|
||||
|
||||
case "summary": {
|
||||
return <Summary />;
|
||||
}
|
||||
|
||||
case "awards": {
|
||||
return <Awards />;
|
||||
}
|
||||
@ -591,8 +597,6 @@ export const Gengar = ({ columns, isFirstPage = false }: TemplateProps) => {
|
||||
</div>
|
||||
|
||||
<div className={cn("main group", sidebar.length > 0 ? "col-span-2" : "col-span-3")}>
|
||||
{isFirstPage && <Summary />}
|
||||
|
||||
<div className="p-custom space-y-4">
|
||||
{main.map((section) => (
|
||||
<Fragment key={section}>{mapSectionToComponent(section)}</Fragment>
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
import {
|
||||
import type {
|
||||
Award,
|
||||
Certification,
|
||||
CustomSection,
|
||||
CustomSectionGroup,
|
||||
Education,
|
||||
Experience,
|
||||
Interest,
|
||||
Language,
|
||||
Profile,
|
||||
@ -15,16 +13,23 @@ import {
|
||||
SectionWithItem,
|
||||
Skill,
|
||||
URL,
|
||||
Volunteer,
|
||||
} from "@reactive-resume/schema";
|
||||
import { cn, hexToRgb, isEmptyString, isUrl, linearTransform } from "@reactive-resume/utils";
|
||||
import { Education, Experience, Volunteer } from "@reactive-resume/schema";
|
||||
import {
|
||||
cn,
|
||||
hexToRgb,
|
||||
isEmptyString,
|
||||
isUrl,
|
||||
linearTransform,
|
||||
sanitize,
|
||||
} from "@reactive-resume/utils";
|
||||
import get from "lodash.get";
|
||||
import { Fragment } from "react";
|
||||
|
||||
import { BrandIcon } from "../components/brand-icon";
|
||||
import { Picture } from "../components/picture";
|
||||
import { useArtboardStore } from "../store/artboard";
|
||||
import { TemplateProps } from "../types/template";
|
||||
import type { TemplateProps } from "../types/template";
|
||||
|
||||
const Header = () => {
|
||||
const basics = useArtboardStore((state) => state.resume.basics);
|
||||
@ -91,9 +96,9 @@ const Summary = () => {
|
||||
<h4 className="mb-2 border-b pb-0.5 text-sm font-bold">{section.name}</h4>
|
||||
|
||||
<div
|
||||
dangerouslySetInnerHTML={{ __html: section.content }}
|
||||
className="wysiwyg"
|
||||
dangerouslySetInnerHTML={{ __html: sanitize(section.content) }}
|
||||
style={{ columns: section.columns }}
|
||||
className="wysiwyg"
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
@ -187,7 +192,7 @@ const Section = <T,>({
|
||||
summaryKey,
|
||||
keywordsKey,
|
||||
}: SectionProps<T>) => {
|
||||
if (!section.visible || section.items.length === 0) return null;
|
||||
if (!section.visible || section.items.filter((item) => item.visible).length === 0) return null;
|
||||
|
||||
return (
|
||||
<section id={section.id} className="grid">
|
||||
@ -215,7 +220,10 @@ const Section = <T,>({
|
||||
</div>
|
||||
|
||||
{summary !== undefined && !isEmptyString(summary) && (
|
||||
<div dangerouslySetInnerHTML={{ __html: summary }} className="wysiwyg" />
|
||||
<div
|
||||
dangerouslySetInnerHTML={{ __html: sanitize(summary) }}
|
||||
className="wysiwyg"
|
||||
/>
|
||||
)}
|
||||
|
||||
{level !== undefined && level > 0 && <Rating level={level} />}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Template } from "@reactive-resume/utils";
|
||||
import type { Template } from "@reactive-resume/utils";
|
||||
|
||||
import { Azurill } from "./azurill";
|
||||
import { Bronzor } from "./bronzor";
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
import {
|
||||
import type {
|
||||
Award,
|
||||
Certification,
|
||||
CustomSection,
|
||||
CustomSectionGroup,
|
||||
Education,
|
||||
Experience,
|
||||
Interest,
|
||||
Language,
|
||||
Project,
|
||||
@ -14,16 +12,16 @@ import {
|
||||
SectionWithItem,
|
||||
Skill,
|
||||
URL,
|
||||
Volunteer,
|
||||
} from "@reactive-resume/schema";
|
||||
import { cn, isEmptyString, isUrl } from "@reactive-resume/utils";
|
||||
import { Education, Experience, Volunteer } from "@reactive-resume/schema";
|
||||
import { cn, isEmptyString, isUrl, sanitize } from "@reactive-resume/utils";
|
||||
import get from "lodash.get";
|
||||
import React, { Fragment } from "react";
|
||||
|
||||
import { BrandIcon } from "../components/brand-icon";
|
||||
import { Picture } from "../components/picture";
|
||||
import { useArtboardStore } from "../store/artboard";
|
||||
import { TemplateProps } from "../types/template";
|
||||
import type { TemplateProps } from "../types/template";
|
||||
|
||||
const Header = () => {
|
||||
const basics = useArtboardStore((state) => state.resume.basics);
|
||||
@ -110,9 +108,9 @@ const Summary = () => {
|
||||
</h4>
|
||||
|
||||
<div
|
||||
dangerouslySetInnerHTML={{ __html: section.content }}
|
||||
className="wysiwyg"
|
||||
dangerouslySetInnerHTML={{ __html: sanitize(section.content) }}
|
||||
style={{ columns: section.columns }}
|
||||
className="wysiwyg"
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
@ -198,7 +196,7 @@ const Section = <T,>({
|
||||
summaryKey,
|
||||
keywordsKey,
|
||||
}: SectionProps<T>) => {
|
||||
if (!section.visible || section.items.length === 0) return null;
|
||||
if (!section.visible || section.items.filter((item) => item.visible).length === 0) return null;
|
||||
|
||||
return (
|
||||
<section id={section.id} className="grid">
|
||||
@ -223,7 +221,10 @@ const Section = <T,>({
|
||||
<div>{children?.(item as T)}</div>
|
||||
|
||||
{summary !== undefined && !isEmptyString(summary) && (
|
||||
<div dangerouslySetInnerHTML={{ __html: summary }} className="wysiwyg" />
|
||||
<div
|
||||
dangerouslySetInnerHTML={{ __html: sanitize(summary) }}
|
||||
className="wysiwyg"
|
||||
/>
|
||||
)}
|
||||
|
||||
{level !== undefined && level > 0 && <Rating level={level} />}
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
import {
|
||||
import type {
|
||||
Award,
|
||||
Certification,
|
||||
CustomSection,
|
||||
CustomSectionGroup,
|
||||
Education,
|
||||
Experience,
|
||||
Interest,
|
||||
Language,
|
||||
Project,
|
||||
@ -14,16 +12,16 @@ import {
|
||||
SectionWithItem,
|
||||
Skill,
|
||||
URL,
|
||||
Volunteer,
|
||||
} from "@reactive-resume/schema";
|
||||
import { cn, hexToRgb, isEmptyString, isUrl } from "@reactive-resume/utils";
|
||||
import { Education, Experience, Volunteer } from "@reactive-resume/schema";
|
||||
import { cn, hexToRgb, isEmptyString, isUrl, sanitize } from "@reactive-resume/utils";
|
||||
import get from "lodash.get";
|
||||
import React, { Fragment } from "react";
|
||||
|
||||
import { BrandIcon } from "../components/brand-icon";
|
||||
import { Picture } from "../components/picture";
|
||||
import { useArtboardStore } from "../store/artboard";
|
||||
import { TemplateProps } from "../types/template";
|
||||
import type { TemplateProps } from "../types/template";
|
||||
|
||||
const Header = () => {
|
||||
const basics = useArtboardStore((state) => state.resume.basics);
|
||||
@ -44,9 +42,9 @@ const Header = () => {
|
||||
</div>
|
||||
|
||||
<div
|
||||
dangerouslySetInnerHTML={{ __html: section.content }}
|
||||
className="wysiwyg"
|
||||
dangerouslySetInnerHTML={{ __html: sanitize(section.content) }}
|
||||
style={{ columns: section.columns }}
|
||||
className="wysiwyg"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -193,7 +191,7 @@ const Section = <T,>({
|
||||
summaryKey,
|
||||
keywordsKey,
|
||||
}: SectionProps<T>) => {
|
||||
if (!section.visible || section.items.length === 0) return null;
|
||||
if (!section.visible || section.items.filter((item) => item.visible).length === 0) return null;
|
||||
|
||||
return (
|
||||
<section id={section.id} className="grid">
|
||||
@ -218,7 +216,10 @@ const Section = <T,>({
|
||||
<div>{children?.(item as T)}</div>
|
||||
|
||||
{summary !== undefined && !isEmptyString(summary) && (
|
||||
<div dangerouslySetInnerHTML={{ __html: summary }} className="wysiwyg" />
|
||||
<div
|
||||
dangerouslySetInnerHTML={{ __html: sanitize(summary) }}
|
||||
className="wysiwyg"
|
||||
/>
|
||||
)}
|
||||
|
||||
{level !== undefined && level > 0 && <Rating level={level} />}
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
import {
|
||||
import type {
|
||||
Award,
|
||||
Certification,
|
||||
CustomSection,
|
||||
CustomSectionGroup,
|
||||
Education,
|
||||
Experience,
|
||||
Interest,
|
||||
Language,
|
||||
Profile,
|
||||
@ -15,16 +13,16 @@ import {
|
||||
SectionWithItem,
|
||||
Skill,
|
||||
URL,
|
||||
Volunteer,
|
||||
} from "@reactive-resume/schema";
|
||||
import { cn, isEmptyString, isUrl } from "@reactive-resume/utils";
|
||||
import { Education, Experience, Volunteer } from "@reactive-resume/schema";
|
||||
import { cn, isEmptyString, isUrl, sanitize } from "@reactive-resume/utils";
|
||||
import get from "lodash.get";
|
||||
import { Fragment } from "react";
|
||||
|
||||
import { BrandIcon } from "../components/brand-icon";
|
||||
import { Picture } from "../components/picture";
|
||||
import { useArtboardStore } from "../store/artboard";
|
||||
import { TemplateProps } from "../types/template";
|
||||
import type { TemplateProps } from "../types/template";
|
||||
|
||||
const Header = () => {
|
||||
const basics = useArtboardStore((state) => state.resume.basics);
|
||||
@ -107,9 +105,9 @@ const Summary = () => {
|
||||
</div>
|
||||
|
||||
<div
|
||||
dangerouslySetInnerHTML={{ __html: section.content }}
|
||||
className="wysiwyg"
|
||||
dangerouslySetInnerHTML={{ __html: sanitize(section.content) }}
|
||||
style={{ columns: section.columns }}
|
||||
className="wysiwyg"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
@ -182,7 +180,7 @@ const Section = <T,>({
|
||||
summaryKey,
|
||||
keywordsKey,
|
||||
}: SectionProps<T>) => {
|
||||
if (!section.visible || section.items.length === 0) return null;
|
||||
if (!section.visible || section.items.filter((item) => item.visible).length === 0) return null;
|
||||
|
||||
return (
|
||||
<section id={section.id} className={cn("grid", dateKey !== undefined && "gap-y-4")}>
|
||||
@ -219,7 +217,10 @@ const Section = <T,>({
|
||||
{url !== undefined && section.separateLinks && <Link url={url} />}
|
||||
|
||||
{summary !== undefined && !isEmptyString(summary) && (
|
||||
<div dangerouslySetInnerHTML={{ __html: summary }} className="wysiwyg" />
|
||||
<div
|
||||
dangerouslySetInnerHTML={{ __html: sanitize(summary) }}
|
||||
className="wysiwyg"
|
||||
/>
|
||||
)}
|
||||
|
||||
{keywords !== undefined && keywords.length > 0 && (
|
||||
@ -254,7 +255,10 @@ const Section = <T,>({
|
||||
{url !== undefined && section.separateLinks && <Link url={url} />}
|
||||
|
||||
{summary !== undefined && !isEmptyString(summary) && (
|
||||
<div dangerouslySetInnerHTML={{ __html: summary }} className="wysiwyg" />
|
||||
<div
|
||||
dangerouslySetInnerHTML={{ __html: sanitize(summary) }}
|
||||
className="wysiwyg"
|
||||
/>
|
||||
)}
|
||||
|
||||
{keywords !== undefined && keywords.length > 0 && (
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
import {
|
||||
import type {
|
||||
Award,
|
||||
Certification,
|
||||
CustomSection,
|
||||
CustomSectionGroup,
|
||||
Education,
|
||||
Experience,
|
||||
Interest,
|
||||
Language,
|
||||
Project,
|
||||
@ -14,16 +12,16 @@ import {
|
||||
SectionWithItem,
|
||||
Skill,
|
||||
URL,
|
||||
Volunteer,
|
||||
} from "@reactive-resume/schema";
|
||||
import { cn, isEmptyString, isUrl } from "@reactive-resume/utils";
|
||||
import { Education, Experience, Volunteer } from "@reactive-resume/schema";
|
||||
import { cn, isEmptyString, isUrl, sanitize } from "@reactive-resume/utils";
|
||||
import get from "lodash.get";
|
||||
import React, { Fragment } from "react";
|
||||
|
||||
import { BrandIcon } from "../components/brand-icon";
|
||||
import { Picture } from "../components/picture";
|
||||
import { useArtboardStore } from "../store/artboard";
|
||||
import { TemplateProps } from "../types/template";
|
||||
import type { TemplateProps } from "../types/template";
|
||||
|
||||
const Header = () => {
|
||||
const basics = useArtboardStore((state) => state.resume.basics);
|
||||
@ -111,9 +109,9 @@ const Summary = () => {
|
||||
<h4 className="font-bold text-primary">{section.name}</h4>
|
||||
|
||||
<div
|
||||
dangerouslySetInnerHTML={{ __html: section.content }}
|
||||
className="wysiwyg"
|
||||
dangerouslySetInnerHTML={{ __html: sanitize(section.content) }}
|
||||
style={{ columns: section.columns }}
|
||||
className="wysiwyg"
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
@ -199,7 +197,7 @@ const Section = <T,>({
|
||||
summaryKey,
|
||||
keywordsKey,
|
||||
}: SectionProps<T>) => {
|
||||
if (!section.visible || section.items.length === 0) return null;
|
||||
if (!section.visible || section.items.filter((item) => item.visible).length === 0) return null;
|
||||
|
||||
return (
|
||||
<section id={section.id} className="grid">
|
||||
@ -225,7 +223,10 @@ const Section = <T,>({
|
||||
</div>
|
||||
|
||||
{summary !== undefined && !isEmptyString(summary) && (
|
||||
<div dangerouslySetInnerHTML={{ __html: summary }} className="wysiwyg" />
|
||||
<div
|
||||
dangerouslySetInnerHTML={{ __html: sanitize(summary) }}
|
||||
className="wysiwyg"
|
||||
/>
|
||||
)}
|
||||
|
||||
{level !== undefined && level > 0 && <Rating level={level} />}
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
import {
|
||||
import type {
|
||||
Award,
|
||||
Certification,
|
||||
CustomSection,
|
||||
CustomSectionGroup,
|
||||
Education,
|
||||
Experience,
|
||||
Interest,
|
||||
Language,
|
||||
Profile,
|
||||
@ -15,16 +13,16 @@ import {
|
||||
SectionWithItem,
|
||||
Skill,
|
||||
URL,
|
||||
Volunteer,
|
||||
} from "@reactive-resume/schema";
|
||||
import { cn, isEmptyString, isUrl } from "@reactive-resume/utils";
|
||||
import { Education, Experience, Volunteer } from "@reactive-resume/schema";
|
||||
import { cn, isEmptyString, isUrl, sanitize } from "@reactive-resume/utils";
|
||||
import get from "lodash.get";
|
||||
import { Fragment } from "react";
|
||||
|
||||
import { BrandIcon } from "../components/brand-icon";
|
||||
import { Picture } from "../components/picture";
|
||||
import { useArtboardStore } from "../store/artboard";
|
||||
import { TemplateProps } from "../types/template";
|
||||
import type { TemplateProps } from "../types/template";
|
||||
|
||||
const Header = () => {
|
||||
const basics = useArtboardStore((state) => state.resume.basics);
|
||||
@ -112,9 +110,9 @@ const Summary = () => {
|
||||
<h4 className="mb-2 border-b border-primary text-base font-bold">{section.name}</h4>
|
||||
|
||||
<div
|
||||
dangerouslySetInnerHTML={{ __html: section.content }}
|
||||
className="wysiwyg"
|
||||
dangerouslySetInnerHTML={{ __html: sanitize(section.content) }}
|
||||
style={{ columns: section.columns }}
|
||||
className="wysiwyg"
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
@ -214,7 +212,7 @@ const Section = <T,>({
|
||||
summaryKey,
|
||||
keywordsKey,
|
||||
}: SectionProps<T>) => {
|
||||
if (!section.visible || section.items.length === 0) return null;
|
||||
if (!section.visible || section.items.filter((item) => item.visible).length === 0) return null;
|
||||
|
||||
return (
|
||||
<section id={section.id} className="grid">
|
||||
@ -240,7 +238,10 @@ const Section = <T,>({
|
||||
</div>
|
||||
|
||||
{summary !== undefined && !isEmptyString(summary) && (
|
||||
<div dangerouslySetInnerHTML={{ __html: summary }} className="wysiwyg" />
|
||||
<div
|
||||
dangerouslySetInnerHTML={{ __html: sanitize(summary) }}
|
||||
className="wysiwyg"
|
||||
/>
|
||||
)}
|
||||
|
||||
{level !== undefined && level > 0 && <Rating level={level} />}
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
import {
|
||||
import type {
|
||||
Award,
|
||||
Certification,
|
||||
CustomSection,
|
||||
CustomSectionGroup,
|
||||
Education,
|
||||
Experience,
|
||||
Interest,
|
||||
Language,
|
||||
Profile,
|
||||
@ -15,16 +13,16 @@ import {
|
||||
SectionWithItem,
|
||||
Skill,
|
||||
URL,
|
||||
Volunteer,
|
||||
} from "@reactive-resume/schema";
|
||||
import { cn, isEmptyString, isUrl } from "@reactive-resume/utils";
|
||||
import { Education, Experience, Volunteer } from "@reactive-resume/schema";
|
||||
import { cn, isEmptyString, isUrl, sanitize } from "@reactive-resume/utils";
|
||||
import get from "lodash.get";
|
||||
import { Fragment } from "react";
|
||||
|
||||
import { BrandIcon } from "../components/brand-icon";
|
||||
import { Picture } from "../components/picture";
|
||||
import { useArtboardStore } from "../store/artboard";
|
||||
import { TemplateProps } from "../types/template";
|
||||
import type { TemplateProps } from "../types/template";
|
||||
|
||||
const Header = () => {
|
||||
const basics = useArtboardStore((state) => state.resume.basics);
|
||||
@ -92,9 +90,9 @@ const Summary = () => {
|
||||
<h4 className="mb-2 border-b pb-0.5 text-sm font-bold">{section.name}</h4>
|
||||
|
||||
<div
|
||||
dangerouslySetInnerHTML={{ __html: section.content }}
|
||||
className="wysiwyg"
|
||||
dangerouslySetInnerHTML={{ __html: sanitize(section.content) }}
|
||||
style={{ columns: section.columns }}
|
||||
className="wysiwyg"
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
@ -180,7 +178,7 @@ const Section = <T,>({
|
||||
summaryKey,
|
||||
keywordsKey,
|
||||
}: SectionProps<T>) => {
|
||||
if (!section.visible || section.items.length === 0) return null;
|
||||
if (!section.visible || section.items.filter((item) => item.visible).length === 0) return null;
|
||||
|
||||
return (
|
||||
<section id={section.id} className="grid">
|
||||
@ -206,7 +204,10 @@ const Section = <T,>({
|
||||
</div>
|
||||
|
||||
{summary !== undefined && !isEmptyString(summary) && (
|
||||
<div dangerouslySetInnerHTML={{ __html: summary }} className="wysiwyg" />
|
||||
<div
|
||||
dangerouslySetInnerHTML={{ __html: sanitize(summary) }}
|
||||
className="wysiwyg"
|
||||
/>
|
||||
)}
|
||||
|
||||
{level !== undefined && level > 0 && <Rating level={level} />}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { SectionKey } from "@reactive-resume/schema";
|
||||
import type { SectionKey } from "@reactive-resume/schema";
|
||||
|
||||
export type TemplateProps = {
|
||||
columns: SectionKey[][];
|
||||
|
||||
@ -42,13 +42,6 @@
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
|
||||
<!-- Phosphor Icons -->
|
||||
<script src="https://unpkg.com/@phosphor-icons/web"></script>
|
||||
|
||||
<!-- Simple Icons -->
|
||||
<link
|
||||
type="text/css"
|
||||
rel="stylesheet"
|
||||
href="https://unpkg.com/simple-icons-font@v14/font/simple-icons.min.css"
|
||||
/>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@phosphor-icons/web"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
155
apps/client/public/styles/prism-dark.css
Normal file
@ -0,0 +1,155 @@
|
||||
code[class*="language-"],
|
||||
pre[class*="language-"] {
|
||||
color: #f8f8f2;
|
||||
background: none;
|
||||
font-family: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace;
|
||||
text-align: left;
|
||||
white-space: pre;
|
||||
word-spacing: normal;
|
||||
word-break: normal;
|
||||
word-wrap: normal;
|
||||
line-height: 1.5;
|
||||
|
||||
-moz-tab-size: 4;
|
||||
-o-tab-size: 4;
|
||||
tab-size: 4;
|
||||
|
||||
-webkit-hyphens: none;
|
||||
-moz-hyphens: none;
|
||||
-ms-hyphens: none;
|
||||
hyphens: none;
|
||||
}
|
||||
|
||||
/* Code blocks */
|
||||
pre[class*="language-"] {
|
||||
padding: 1em;
|
||||
margin: 0.5em 0;
|
||||
overflow: auto;
|
||||
border-radius: 0.3em;
|
||||
}
|
||||
|
||||
:not(pre) > code[class*="language-"],
|
||||
pre[class*="language-"] {
|
||||
background: #2b2b2b;
|
||||
}
|
||||
|
||||
/* Inline code */
|
||||
:not(pre) > code[class*="language-"] {
|
||||
padding: 0.1em;
|
||||
border-radius: 0.3em;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.token.comment,
|
||||
.token.prolog,
|
||||
.token.doctype,
|
||||
.token.cdata {
|
||||
color: #d4d0ab;
|
||||
}
|
||||
|
||||
.token.punctuation {
|
||||
color: #fefefe;
|
||||
}
|
||||
|
||||
.token.property,
|
||||
.token.tag,
|
||||
.token.constant,
|
||||
.token.symbol,
|
||||
.token.deleted {
|
||||
color: #ffa07a;
|
||||
}
|
||||
|
||||
.token.boolean,
|
||||
.token.number {
|
||||
color: #00e0e0;
|
||||
}
|
||||
|
||||
.token.selector,
|
||||
.token.attr-name,
|
||||
.token.string,
|
||||
.token.char,
|
||||
.token.builtin,
|
||||
.token.inserted {
|
||||
color: #abe338;
|
||||
}
|
||||
|
||||
.token.operator,
|
||||
.token.entity,
|
||||
.token.url,
|
||||
.language-css .token.string,
|
||||
.style .token.string,
|
||||
.token.variable {
|
||||
color: #00e0e0;
|
||||
}
|
||||
|
||||
.token.atrule,
|
||||
.token.attr-value,
|
||||
.token.function {
|
||||
color: #ffd700;
|
||||
}
|
||||
|
||||
.token.keyword {
|
||||
color: #00e0e0;
|
||||
}
|
||||
|
||||
.token.regex,
|
||||
.token.important {
|
||||
color: #ffd700;
|
||||
}
|
||||
|
||||
.token.important,
|
||||
.token.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.token.italic {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.token.entity {
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
@media screen and (forced-colors: active) {
|
||||
code[class*="language-"],
|
||||
pre[class*="language-"] {
|
||||
color: windowText;
|
||||
background: window;
|
||||
}
|
||||
|
||||
:not(pre) > code[class*="language-"],
|
||||
pre[class*="language-"] {
|
||||
background: window;
|
||||
}
|
||||
|
||||
.token.important {
|
||||
background: highlight;
|
||||
color: window;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.token.atrule,
|
||||
.token.attr-value,
|
||||
.token.function,
|
||||
.token.keyword,
|
||||
.token.operator,
|
||||
.token.selector {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.token.attr-value,
|
||||
.token.comment,
|
||||
.token.doctype,
|
||||
.token.function,
|
||||
.token.keyword,
|
||||
.token.operator,
|
||||
.token.property,
|
||||
.token.string {
|
||||
color: highlight;
|
||||
}
|
||||
|
||||
.token.attr-value,
|
||||
.token.url {
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
167
apps/client/public/styles/prism-light.css
Normal file
@ -0,0 +1,167 @@
|
||||
code[class*="language-"],
|
||||
pre[class*="language-"] {
|
||||
color: #393a34;
|
||||
font-family: "Consolas", "Bitstream Vera Sans Mono", "Courier New", Courier, monospace;
|
||||
direction: ltr;
|
||||
text-align: left;
|
||||
white-space: pre;
|
||||
word-spacing: normal;
|
||||
word-break: normal;
|
||||
font-size: 0.9em;
|
||||
line-height: 1.2em;
|
||||
|
||||
-moz-tab-size: 4;
|
||||
-o-tab-size: 4;
|
||||
tab-size: 4;
|
||||
|
||||
-webkit-hyphens: none;
|
||||
-moz-hyphens: none;
|
||||
-ms-hyphens: none;
|
||||
hyphens: none;
|
||||
}
|
||||
|
||||
pre > code[class*="language-"] {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
pre[class*="language-"]::-moz-selection,
|
||||
pre[class*="language-"] ::-moz-selection,
|
||||
code[class*="language-"]::-moz-selection,
|
||||
code[class*="language-"] ::-moz-selection {
|
||||
background: #c1def1;
|
||||
}
|
||||
|
||||
pre[class*="language-"]::selection,
|
||||
pre[class*="language-"] ::selection,
|
||||
code[class*="language-"]::selection,
|
||||
code[class*="language-"] ::selection {
|
||||
background: #c1def1;
|
||||
}
|
||||
|
||||
/* Code blocks */
|
||||
pre[class*="language-"] {
|
||||
padding: 1em;
|
||||
margin: 0.5em 0;
|
||||
overflow: auto;
|
||||
border: 1px solid #dddddd;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
/* Inline code */
|
||||
:not(pre) > code[class*="language-"] {
|
||||
padding: 0.2em;
|
||||
padding-top: 1px;
|
||||
padding-bottom: 1px;
|
||||
background: #f8f8f8;
|
||||
border: 1px solid #dddddd;
|
||||
}
|
||||
|
||||
.token.comment,
|
||||
.token.prolog,
|
||||
.token.doctype,
|
||||
.token.cdata {
|
||||
color: #008000;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.token.namespace {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.token.string {
|
||||
color: #a31515;
|
||||
}
|
||||
|
||||
.token.punctuation,
|
||||
.token.operator {
|
||||
color: #393a34; /* no highlight */
|
||||
}
|
||||
|
||||
.token.url,
|
||||
.token.symbol,
|
||||
.token.number,
|
||||
.token.boolean,
|
||||
.token.variable,
|
||||
.token.constant,
|
||||
.token.inserted {
|
||||
color: #36acaa;
|
||||
}
|
||||
|
||||
.token.atrule,
|
||||
.token.keyword,
|
||||
.token.attr-value,
|
||||
.language-autohotkey .token.selector,
|
||||
.language-json .token.boolean,
|
||||
.language-json .token.number,
|
||||
code[class*="language-css"] {
|
||||
color: #0000ff;
|
||||
}
|
||||
|
||||
.token.function {
|
||||
color: #393a34;
|
||||
}
|
||||
|
||||
.token.deleted,
|
||||
.language-autohotkey .token.tag {
|
||||
color: #9a050f;
|
||||
}
|
||||
|
||||
.token.selector,
|
||||
.language-autohotkey .token.keyword {
|
||||
color: #00009f;
|
||||
}
|
||||
|
||||
.token.important {
|
||||
color: #e90;
|
||||
}
|
||||
|
||||
.token.important,
|
||||
.token.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.token.italic {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.token.class-name,
|
||||
.language-json .token.property {
|
||||
color: #2b91af;
|
||||
}
|
||||
|
||||
.token.tag,
|
||||
.token.selector {
|
||||
color: #800000;
|
||||
}
|
||||
|
||||
.token.attr-name,
|
||||
.token.property,
|
||||
.token.regex,
|
||||
.token.entity {
|
||||
color: #ff0000;
|
||||
}
|
||||
|
||||
.token.directive.tag .tag {
|
||||
background: #ffff00;
|
||||
color: #393a34;
|
||||
}
|
||||
|
||||
/* overrides color-values for the Line Numbers plugin
|
||||
* http://prismjs.com/plugins/line-numbers/
|
||||
*/
|
||||
.line-numbers.line-numbers .line-numbers-rows {
|
||||
border-right-color: #a5a5a5;
|
||||
}
|
||||
|
||||
.line-numbers .line-numbers-rows > span:before {
|
||||
color: #2b91af;
|
||||
}
|
||||
|
||||
/* overrides color-values for the Line Highlight plugin
|
||||
* http://prismjs.com/plugins/line-highlight/
|
||||
*/
|
||||
.line-highlight.line-highlight {
|
||||
background: rgba(193, 222, 241, 0.2);
|
||||
background: -webkit-linear-gradient(left, rgba(193, 222, 241, 0.2) 70%, rgba(221, 222, 241, 0));
|
||||
background: linear-gradient(to right, rgba(193, 222, 241, 0.2) 70%, rgba(221, 222, 241, 0));
|
||||
}
|
||||
0
apps/client/public/templates/jpg/azurill.jpg
Executable file → Normal file
|
Before Width: | Height: | Size: 129 KiB After Width: | Height: | Size: 129 KiB |
0
apps/client/public/templates/jpg/bronzor.jpg
Executable file → Normal file
|
Before Width: | Height: | Size: 120 KiB After Width: | Height: | Size: 120 KiB |
0
apps/client/public/templates/jpg/chikorita.jpg
Executable file → Normal file
|
Before Width: | Height: | Size: 145 KiB After Width: | Height: | Size: 145 KiB |
0
apps/client/public/templates/jpg/ditto.jpg
Executable file → Normal file
|
Before Width: | Height: | Size: 138 KiB After Width: | Height: | Size: 138 KiB |
0
apps/client/public/templates/jpg/kakuna.jpg
Executable file → Normal file
|
Before Width: | Height: | Size: 119 KiB After Width: | Height: | Size: 119 KiB |
0
apps/client/public/templates/jpg/nosepass.jpg
Executable file → Normal file
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
0
apps/client/public/templates/jpg/onyx.jpg
Executable file → Normal file
|
Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 113 KiB |
0
apps/client/public/templates/jpg/pikachu.jpg
Executable file → Normal file
|
Before Width: | Height: | Size: 136 KiB After Width: | Height: | Size: 136 KiB |
0
apps/client/public/templates/jpg/rhyhorn.jpg
Executable file → Normal file
|
Before Width: | Height: | Size: 131 KiB After Width: | Height: | Size: 131 KiB |
@ -289,7 +289,7 @@
|
||||
]
|
||||
],
|
||||
"css": {
|
||||
"value": ".section {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
|
||||
"value": "* {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
|
||||
"visible": false
|
||||
},
|
||||
"page": {
|
||||
|
||||
@ -314,7 +314,7 @@
|
||||
]
|
||||
],
|
||||
"css": {
|
||||
"value": ".section {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
|
||||
"value": "* {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
|
||||
"visible": false
|
||||
},
|
||||
"page": {
|
||||
|
||||
@ -314,7 +314,7 @@
|
||||
]
|
||||
],
|
||||
"css": {
|
||||
"value": ".section {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
|
||||
"value": "* {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
|
||||
"visible": false
|
||||
},
|
||||
"page": {
|
||||
|
||||
@ -315,7 +315,7 @@
|
||||
]
|
||||
],
|
||||
"css": {
|
||||
"value": ".section {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
|
||||
"value": "* {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
|
||||
"visible": false
|
||||
},
|
||||
"page": {
|
||||
|
||||
@ -289,7 +289,7 @@
|
||||
[[], []]
|
||||
],
|
||||
"css": {
|
||||
"value": ".section {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
|
||||
"value": "* {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
|
||||
"visible": false
|
||||
},
|
||||
"page": {
|
||||
|
||||
@ -288,7 +288,7 @@
|
||||
]
|
||||
],
|
||||
"css": {
|
||||
"value": ".section {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
|
||||
"value": "* {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
|
||||
"visible": false
|
||||
},
|
||||
"page": {
|
||||
|
||||
@ -287,7 +287,7 @@
|
||||
]
|
||||
],
|
||||
"css": {
|
||||
"value": ".section {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
|
||||
"value": "* {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
|
||||
"visible": false
|
||||
},
|
||||
"page": {
|
||||
|
||||
@ -289,7 +289,7 @@
|
||||
]
|
||||
],
|
||||
"css": {
|
||||
"value": ".section {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
|
||||
"value": "* {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
|
||||
"visible": false
|
||||
},
|
||||
"page": {
|
||||
|
||||
@ -306,7 +306,7 @@
|
||||
[["projects", "certifications", "skills", "languages", "references"], []]
|
||||
],
|
||||
"css": {
|
||||
"value": ".section {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
|
||||
"value": "* {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
|
||||
"visible": false
|
||||
},
|
||||
"page": {
|
||||
|
||||
@ -287,7 +287,7 @@
|
||||
]
|
||||
],
|
||||
"css": {
|
||||
"value": ".section {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
|
||||
"value": "* {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
|
||||
"visible": false
|
||||
},
|
||||
"page": {
|
||||
|
||||
@ -315,7 +315,7 @@
|
||||
]
|
||||
],
|
||||
"css": {
|
||||
"value": ".section {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
|
||||
"value": "* {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
|
||||
"visible": false
|
||||
},
|
||||
"page": {
|
||||
|
||||
@ -288,7 +288,7 @@
|
||||
]
|
||||
],
|
||||
"css": {
|
||||
"value": ".section {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
|
||||
"value": "* {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
|
||||
"visible": false
|
||||
},
|
||||
"page": {
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import { t } from "@lingui/macro";
|
||||
import {
|
||||
CaretDown,
|
||||
ChatTeardropText,
|
||||
CircleNotch,
|
||||
Exam,
|
||||
MagicWand,
|
||||
PenNib,
|
||||
CaretDownIcon,
|
||||
ChatTeardropTextIcon,
|
||||
CircleNotchIcon,
|
||||
ExamIcon,
|
||||
MagicWandIcon,
|
||||
PenNibIcon,
|
||||
} from "@phosphor-icons/react";
|
||||
import {
|
||||
Badge,
|
||||
@ -75,27 +75,31 @@ export const AiActions = ({ value, onChange, className }: Props) => {
|
||||
variant="primary"
|
||||
className="-rotate-90 bg-background px-2 text-[10px] leading-[10px]"
|
||||
>
|
||||
<MagicWand size={10} className="mr-1" />
|
||||
<MagicWandIcon size={10} className="mr-1" />
|
||||
{t`AI`}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<Button size="sm" variant="outline" disabled={!!loading} onClick={() => onClick("improve")}>
|
||||
{loading === "improve" ? <CircleNotch className="animate-spin" /> : <PenNib />}
|
||||
{loading === "improve" ? <CircleNotchIcon className="animate-spin" /> : <PenNibIcon />}
|
||||
<span className="ml-2 text-xs">{t`Improve Writing`}</span>
|
||||
</Button>
|
||||
|
||||
<Button size="sm" variant="outline" disabled={!!loading} onClick={() => onClick("fix")}>
|
||||
{loading === "fix" ? <CircleNotch className="animate-spin" /> : <Exam />}
|
||||
{loading === "fix" ? <CircleNotchIcon className="animate-spin" /> : <ExamIcon />}
|
||||
<span className="ml-2 text-xs">{t`Fix Spelling & Grammar`}</span>
|
||||
</Button>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button size="sm" variant="outline" disabled={!!loading}>
|
||||
{loading === "tone" ? <CircleNotch className="animate-spin" /> : <ChatTeardropText />}
|
||||
{loading === "tone" ? (
|
||||
<CircleNotchIcon className="animate-spin" />
|
||||
) : (
|
||||
<ChatTeardropTextIcon />
|
||||
)}
|
||||
<span className="mx-2 text-xs">{t`Change Tone`}</span>
|
||||
<CaretDown />
|
||||
<CaretDownIcon />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
|
||||
@ -1,13 +1,23 @@
|
||||
import { cn } from "@reactive-resume/utils";
|
||||
import { forwardRef, useEffect } from "react";
|
||||
import { useDebounceValue } from "usehooks-ts";
|
||||
|
||||
type BrandIconProps = {
|
||||
slug: string;
|
||||
};
|
||||
|
||||
export const BrandIcon = ({ slug }: BrandIconProps) => {
|
||||
if (slug === "linkedin") {
|
||||
export const BrandIcon = forwardRef<HTMLImageElement, BrandIconProps>(({ slug }, ref) => {
|
||||
const [debouncedSlug, setValue] = useDebounceValue(slug, 600);
|
||||
|
||||
useEffect(() => {
|
||||
setValue(slug);
|
||||
}, [slug]);
|
||||
|
||||
if (!slug) return null;
|
||||
|
||||
if (debouncedSlug === "linkedin") {
|
||||
return (
|
||||
<img
|
||||
ref={ref}
|
||||
alt="LinkedIn"
|
||||
className="size-5"
|
||||
src={`${window.location.origin}/support-logos/linkedin.svg`}
|
||||
@ -15,5 +25,14 @@ export const BrandIcon = ({ slug }: BrandIconProps) => {
|
||||
);
|
||||
}
|
||||
|
||||
return <i className={cn("si si--color text-[1.25rem]", `si-${slug}`)} />;
|
||||
};
|
||||
return (
|
||||
<img
|
||||
ref={ref}
|
||||
alt={debouncedSlug}
|
||||
className="size-5"
|
||||
src={`https://cdn.simpleicons.org/${debouncedSlug}`}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
BrandIcon.displayName = "BrandIcon";
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { t } from "@lingui/macro";
|
||||
import { CaretDown, Check } from "@phosphor-icons/react";
|
||||
import { CaretDownIcon, CheckIcon } from "@phosphor-icons/react";
|
||||
import {
|
||||
Button,
|
||||
Command,
|
||||
@ -61,7 +61,7 @@ export const LocaleCombobox = ({ value, onValueChange }: Props) => {
|
||||
onValueChange(result.original.locale);
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
"mr-2 size-4 opacity-0",
|
||||
value === original.locale && "opacity-100",
|
||||
@ -104,7 +104,7 @@ export const LocaleComboboxPopover = ({ value, onValueChange }: Props) => {
|
||||
<span className="line-clamp-1 text-left font-normal">
|
||||
{selected?.name} <span className="ml-1 text-xs opacity-50">({selected?.locale})</span>
|
||||
</span>
|
||||
<CaretDown
|
||||
<CaretDownIcon
|
||||
className={cn(
|
||||
"ml-2 size-4 shrink-0 rotate-0 opacity-50 transition-transform",
|
||||
open && "rotate-180",
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { t } from "@lingui/macro";
|
||||
import { useLingui } from "@lingui/react";
|
||||
import { Translate } from "@phosphor-icons/react";
|
||||
import { TranslateIcon } from "@phosphor-icons/react";
|
||||
import { Button, Popover, PopoverContent, PopoverTrigger } from "@reactive-resume/ui";
|
||||
import { useState } from "react";
|
||||
|
||||
@ -13,8 +14,8 @@ export const LocaleSwitch = () => {
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button size="icon" variant="ghost">
|
||||
<Translate size={20} />
|
||||
<Button size="icon" variant="ghost" aria-label={t`Change Language`}>
|
||||
<TranslateIcon size={20} />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="end" className="p-0">
|
||||
|
||||
@ -1,14 +1,17 @@
|
||||
import { CloudSun, Moon, Sun } from "@phosphor-icons/react";
|
||||
import { t } from "@lingui/macro";
|
||||
import { CloudSunIcon, MoonIcon, SunIcon } from "@phosphor-icons/react";
|
||||
import { useTheme } from "@reactive-resume/hooks";
|
||||
import { Button } from "@reactive-resume/ui";
|
||||
import { motion, Variants } from "framer-motion";
|
||||
import type { Variants } from "framer-motion";
|
||||
import { motion } from "framer-motion";
|
||||
import { useMemo } from "react";
|
||||
|
||||
type Props = {
|
||||
size?: number;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const ThemeSwitch = ({ size = 20 }: Props) => {
|
||||
export const ThemeSwitch = ({ size = 20, className }: Props) => {
|
||||
const { theme, toggleTheme } = useTheme();
|
||||
|
||||
const variants: Variants = useMemo(() => {
|
||||
@ -20,12 +23,12 @@ export const ThemeSwitch = ({ size = 20 }: Props) => {
|
||||
}, [size]);
|
||||
|
||||
return (
|
||||
<Button size="icon" variant="ghost" onClick={toggleTheme}>
|
||||
<Button size="icon" variant="ghost" className={className} onClick={toggleTheme}>
|
||||
<div className="cursor-pointer overflow-hidden" style={{ width: size, height: size }}>
|
||||
<motion.div animate={theme} variants={variants} className="flex">
|
||||
<Sun size={size} className="shrink-0" />
|
||||
<CloudSun size={size} className="shrink-0" />
|
||||
<Moon size={size} className="shrink-0" />
|
||||
<SunIcon size={size} className="shrink-0" aria-label={t`Switch to Light Mode`} />
|
||||
<CloudSunIcon size={size} className="shrink-0" aria-label={t`Use System Theme`} />
|
||||
<MoonIcon size={size} className="shrink-0" aria-label={t`Switch to Dark Mode`} />
|
||||
</motion.div>
|
||||
</div>
|
||||
</Button>
|
||||
|
||||
@ -7,7 +7,7 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
KeyboardShortcut,
|
||||
} from "@reactive-resume/ui";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useNavigate } from "react-router";
|
||||
|
||||
import { useLogout } from "../services/auth";
|
||||
|
||||
@ -26,7 +26,7 @@ export const UserOptions = ({ children }: Props) => {
|
||||
<DropdownMenuContent side="top" align="start" className="w-48">
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
navigate("/dashboard/settings");
|
||||
void navigate("/dashboard/settings");
|
||||
}}
|
||||
>
|
||||
{t`Settings`}
|
||||
|
||||
@ -1,2 +1,3 @@
|
||||
export const DEFAULT_MODEL = "gpt-3.5-turbo";
|
||||
export const DEFAULT_MAX_TOKENS = 1024;
|
||||
export const DEFAULT_AZURE_API_VERSION = "2024-10-21";
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { ReactParallaxTiltProps } from "react-parallax-tilt";
|
||||
import type { ReactParallaxTiltProps } from "react-parallax-tilt";
|
||||
|
||||
export const defaultTiltProps: ReactParallaxTiltProps = {
|
||||
scale: 1.05,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { QueryKey } from "@tanstack/react-query";
|
||||
import type { QueryKey } from "@tanstack/react-query";
|
||||
|
||||
export const USER_KEY: QueryKey = ["user"];
|
||||
export const AUTH_PROVIDERS_KEY: QueryKey = ["auth", "providers"];
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import { ToastActionElement, ToastProps } from "@reactive-resume/ui";
|
||||
import type { ToastActionElement, ToastProps } from "@reactive-resume/ui";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
const TOAST_LIMIT = 1;
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import { t } from "@lingui/macro";
|
||||
import { deepSearchAndParseDates, ErrorMessage } from "@reactive-resume/utils";
|
||||
import type { ErrorMessage } from "@reactive-resume/utils";
|
||||
import { deepSearchAndParseDates } from "@reactive-resume/utils";
|
||||
import _axios from "axios";
|
||||
import createAuthRefreshInterceptor from "axios-auth-refresh";
|
||||
import { redirect } from "react-router-dom";
|
||||
import { redirect } from "react-router";
|
||||
|
||||
import { refreshToken } from "@/client/services/auth";
|
||||
|
||||
|
||||
@ -13,6 +13,7 @@ export const dayjsLocales: Record<string, () => Promise<ILocale>> = {
|
||||
"af-ZA": () => import("dayjs/locale/af"),
|
||||
"am-ET": () => import("dayjs/locale/am"),
|
||||
"ar-SA": () => import("dayjs/locale/ar-sa"),
|
||||
"az-AZ": () => import("dayjs/locale/az"),
|
||||
"bg-BG": () => import("dayjs/locale/bg"),
|
||||
"bn-BD": () => import("dayjs/locale/bn"),
|
||||
"ca-ES": () => import("dayjs/locale/ca"),
|
||||
@ -35,17 +36,21 @@ export const dayjsLocales: Record<string, () => Promise<ILocale>> = {
|
||||
"kn-IN": () => import("dayjs/locale/kn"),
|
||||
"ko-KR": () => import("dayjs/locale/ko"),
|
||||
"lt-LT": () => import("dayjs/locale/lt"),
|
||||
"lv-LV": () => import("dayjs/locale/lv"),
|
||||
"ml-IN": () => import("dayjs/locale/ml"),
|
||||
"mr-IN": () => import("dayjs/locale/mr"),
|
||||
"ms-MY": () => import("dayjs/locale/ms-my"),
|
||||
"ne-NP": () => import("dayjs/locale/ne"),
|
||||
"nl-NL": () => import("dayjs/locale/nl"),
|
||||
"no-NO": () => import("dayjs/locale/en"),
|
||||
"no-NO": () => import("dayjs/locale/nb"),
|
||||
"or-IN": () => import("dayjs/locale/en"),
|
||||
"pl-PL": () => import("dayjs/locale/pl"),
|
||||
"pt-BR": () => import("dayjs/locale/pt-br"),
|
||||
"pt-PT": () => import("dayjs/locale/pt"),
|
||||
"ro-RO": () => import("dayjs/locale/ro"),
|
||||
"ru-RU": () => import("dayjs/locale/ru"),
|
||||
"sk-SK": () => import("dayjs/locale/sk"),
|
||||
"sq-AL": () => import("dayjs/locale/sq"),
|
||||
"sr-SP": () => import("dayjs/locale/sr"),
|
||||
"sv-SE": () => import("dayjs/locale/sv"),
|
||||
"ta-IN": () => import("dayjs/locale/ta"),
|
||||
@ -53,6 +58,7 @@ export const dayjsLocales: Record<string, () => Promise<ILocale>> = {
|
||||
"th-TH": () => import("dayjs/locale/th"),
|
||||
"tr-TR": () => import("dayjs/locale/tr"),
|
||||
"uk-UA": () => import("dayjs/locale/uk"),
|
||||
"uz-UZ": () => import("dayjs/locale/uz"),
|
||||
"vi-VN": () => import("dayjs/locale/vi"),
|
||||
"zh-CN": () => import("dayjs/locale/zh-cn"),
|
||||
"zh-TW": () => import("dayjs/locale/zh-tw"),
|
||||
|
||||