Compare commits
156 Commits
v4.3.5
...
ecbd9a0480
| Author | SHA1 | Date | |
|---|---|---|---|
| ecbd9a0480 | |||
| 25021d1b20 | |||
| 53fdfdf8db | |||
| 8a45f2de4d | |||
| 4ccc7bae40 | |||
| 2585c47de8 | |||
| b49798950a | |||
| 03f15f91b3 | |||
| 8e13d9c3ac | |||
| 80932eb80c | |||
| b2ae2c05d8 | |||
| e349fc9bd1 | |||
| 53213dfb26 | |||
| 40f27a53b6 | |||
| 2d900600bf | |||
| 047e9c248f | |||
| 1cc1c39903 | |||
| f6f2a29a7a | |||
| 4368f6a887 | |||
| f6db9fb387 | |||
| afa3804bea | |||
| e2f43b4931 | |||
| 067fdd0921 | |||
| 1296e6bd45 | |||
| d883edb51f | |||
| 51fcf13d37 | |||
| 91ef87b4e0 | |||
| 4c0cc947a2 | |||
| 6fcb7a4845 | |||
| 213f96b189 | |||
| e8d6d4ad3a | |||
| 87370cfdf0 | |||
| 112a644927 | |||
| 2977e3528a | |||
| fd7b5e0dd4 | |||
| 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 |
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=
|
||||
|
||||
@ -5,7 +5,6 @@
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
|
||||
"extends": ["plugin:prettier/recommended"],
|
||||
"plugins": ["simple-import-sort", "unused-imports"],
|
||||
"rules": {
|
||||
// eslint
|
||||
@ -42,14 +41,6 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
// prettier
|
||||
"prettier/prettier": [
|
||||
"warn",
|
||||
{
|
||||
"endOfLine": "auto"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
@ -78,6 +69,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"],
|
||||
|
||||
1
.github/.well-known/funding-manifest-urls
vendored
Normal file
@ -0,0 +1 @@
|
||||
https://rxresu.me/funding.json
|
||||
3
.github/workflows/lint-test-build.yml
vendored
@ -38,9 +38,6 @@ jobs:
|
||||
- name: Lint
|
||||
run: pnpm run lint
|
||||
|
||||
- name: Format
|
||||
run: pnpm run format:check
|
||||
|
||||
- name: Test
|
||||
run: pnpm run test
|
||||
|
||||
|
||||
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>
|
||||
|
||||
@ -4,13 +4,6 @@
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
|
||||
"extends": ["plugin:tailwindcss/recommended"],
|
||||
"settings": {
|
||||
"tailwindcss": {
|
||||
"callees": ["cn", "clsx", "cva"],
|
||||
"config": "tailwind.config.js"
|
||||
}
|
||||
},
|
||||
"rules": {
|
||||
// eslint
|
||||
"@typescript-eslint/no-require-imports": "off",
|
||||
@ -28,10 +21,7 @@
|
||||
],
|
||||
|
||||
// react-hooks
|
||||
"react-hooks/exhaustive-deps": "off",
|
||||
|
||||
// tailwindcss
|
||||
"tailwindcss/no-custom-classname": "off"
|
||||
"react-hooks/exhaustive-deps": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@ -39,6 +39,6 @@
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
|
||||
<!-- Phosphor Icons -->
|
||||
<script src="https://unpkg.com/@phosphor-icons/web"></script>
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/@phosphor-icons/web"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -1,17 +1,24 @@
|
||||
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
|
||||
alt="linkedin"
|
||||
ref={ref}
|
||||
alt="LinkedIn"
|
||||
className="size-4"
|
||||
src={`${window.location.origin}/support-logos/linkedin.svg`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <img alt={slug} className="size-4" src={`https://cdn.simpleicons.org/${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,10 +1,13 @@
|
||||
import { isLocalFont } from "@reactive-resume/utils";
|
||||
import { useEffect, useMemo } from "react";
|
||||
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(() => {
|
||||
@ -16,6 +19,18 @@ export const ArtboardPage = () => {
|
||||
}, [metadata.typography.font]);
|
||||
|
||||
useEffect(() => {
|
||||
const family = metadata.typography.font.family;
|
||||
if (isLocalFont(family)) {
|
||||
let frame = 0;
|
||||
frame = requestAnimationFrame(() => {
|
||||
const width = window.document.body.offsetWidth;
|
||||
const height = window.document.body.offsetHeight;
|
||||
const message = { type: "PAGE_LOADED", payload: { width, height } };
|
||||
window.postMessage(message, "*");
|
||||
});
|
||||
return () => { cancelAnimationFrame(frame); };
|
||||
}
|
||||
|
||||
webfontloader.load({
|
||||
google: { families: [fontString] },
|
||||
active: () => {
|
||||
@ -57,7 +72,14 @@ export const ArtboardPage = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
{metadata.css.visible && <style lang="css">{`[data-page] { ${metadata.css.value} }`}</style>}
|
||||
<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 { 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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -6,7 +6,7 @@ import { PreviewLayout } from "../pages/preview";
|
||||
import { Providers } from "../providers";
|
||||
|
||||
export const routes = createRoutesFromChildren(
|
||||
<Route element={<Providers />} hydrateFallbackElement={<div>Loading...</div>}>
|
||||
<Route element={<Providers />}>
|
||||
<Route path="artboard" element={<ArtboardPage />}>
|
||||
<Route path="builder" element={<BuilderLayout />} />
|
||||
<Route path="preview" element={<PreviewLayout />} />
|
||||
|
||||
@ -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>
|
||||
@ -133,13 +131,13 @@ const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
|
||||
if (!isUrl(url.href)) return null;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-x-1.5">
|
||||
<div className="flex items-center gap-x-1.5 break-all">
|
||||
{!iconOnRight && (icon ?? <i className="ph ph-bold ph-link text-primary" />)}
|
||||
<a
|
||||
href={url.href}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener nofollow"
|
||||
className={cn("inline-block", className)}
|
||||
className={cn("line-clamp-1 max-w-fit", className)}
|
||||
>
|
||||
{label ?? (url.label || url.href)}
|
||||
</a>
|
||||
@ -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} />}
|
||||
@ -552,7 +553,7 @@ export const Azurill = ({ columns, isFirstPage = false }: TemplateProps) => {
|
||||
const [main, sidebar] = columns;
|
||||
|
||||
return (
|
||||
<div className="p-custom space-y-3">
|
||||
<div className="space-y-3 p-custom">
|
||||
{isFirstPage && <Header />}
|
||||
|
||||
<div className="grid grid-cols-3 gap-x-4">
|
||||
|
||||
@ -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>
|
||||
);
|
||||
@ -124,13 +122,13 @@ const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
|
||||
if (!isUrl(url.href)) return null;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-x-1.5">
|
||||
<div className="flex items-center gap-x-1.5 break-all">
|
||||
{!iconOnRight && (icon ?? <i className="ph ph-bold ph-link text-primary" />)}
|
||||
<a
|
||||
href={url.href}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener nofollow"
|
||||
className={cn("inline-block", className)}
|
||||
className={cn("line-clamp-1 max-w-fit", className)}
|
||||
>
|
||||
{label ?? (url.label || url.href)}
|
||||
</a>
|
||||
@ -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} />}
|
||||
@ -567,7 +568,7 @@ export const Bronzor = ({ columns, isFirstPage = false }: TemplateProps) => {
|
||||
const [main, sidebar] = columns;
|
||||
|
||||
return (
|
||||
<div className="p-custom space-y-4">
|
||||
<div className="space-y-4 p-custom">
|
||||
{isFirstPage && <Header />}
|
||||
|
||||
<div className="space-y-4">
|
||||
|
||||
@ -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>
|
||||
);
|
||||
@ -127,14 +125,14 @@ const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
|
||||
if (!isUrl(url.href)) return null;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-x-1.5">
|
||||
<div className="flex items-center gap-x-1.5 break-all">
|
||||
{!iconOnRight &&
|
||||
(icon ?? <i className="ph ph-bold ph-link text-primary group-[.sidebar]:text-white" />)}
|
||||
<a
|
||||
href={url.href}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener nofollow"
|
||||
className={cn("inline-block", className)}
|
||||
className={cn("line-clamp-1 max-w-fit", className)}
|
||||
>
|
||||
{label ?? (url.label || url.href)}
|
||||
</a>
|
||||
@ -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"
|
||||
/>
|
||||
)}
|
||||
@ -573,7 +571,7 @@ export const Chikorita = ({ columns, isFirstPage = false }: TemplateProps) => {
|
||||
<div className="grid min-h-[inherit] grid-cols-3">
|
||||
<div
|
||||
className={cn(
|
||||
"main p-custom group space-y-4",
|
||||
"main group space-y-4 p-custom",
|
||||
sidebar.length > 0 ? "col-span-2" : "col-span-3",
|
||||
)}
|
||||
>
|
||||
@ -586,7 +584,7 @@ export const Chikorita = ({ columns, isFirstPage = false }: TemplateProps) => {
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"sidebar p-custom group h-full space-y-4 bg-primary text-background",
|
||||
"sidebar group h-full space-y-4 bg-primary p-custom text-background",
|
||||
sidebar.length === 0 && "hidden",
|
||||
)}
|
||||
>
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
import {
|
||||
import type {
|
||||
Award,
|
||||
Certification,
|
||||
CustomSection,
|
||||
CustomSectionGroup,
|
||||
Education,
|
||||
Experience,
|
||||
Interest,
|
||||
Language,
|
||||
Profile,
|
||||
@ -15,22 +13,22 @@ 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);
|
||||
|
||||
return (
|
||||
<div className="p-custom relative grid grid-cols-3 space-x-4 pb-0">
|
||||
<div className="relative grid grid-cols-3 space-x-4 p-custom pb-0">
|
||||
<Picture className="mx-auto" />
|
||||
|
||||
<div className="relative z-10 col-span-2 text-background">
|
||||
@ -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>
|
||||
);
|
||||
@ -144,13 +142,13 @@ const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
|
||||
if (!isUrl(url.href)) return null;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-x-1.5">
|
||||
<div className="flex items-center gap-x-1.5 break-all">
|
||||
{!iconOnRight && (icon ?? <i className="ph ph-bold ph-link text-primary" />)}
|
||||
<a
|
||||
href={url.href}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener nofollow"
|
||||
className={cn("inline-block", className)}
|
||||
className={cn("line-clamp-1 max-w-fit", className)}
|
||||
>
|
||||
{label ?? (url.label || url.href)}
|
||||
</a>
|
||||
@ -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} />}
|
||||
@ -603,7 +604,7 @@ export const Ditto = ({ columns, isFirstPage = false }: TemplateProps) => {
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-3">
|
||||
<div className="sidebar p-custom group space-y-4">
|
||||
<div className="sidebar group space-y-4 p-custom">
|
||||
{sidebar.map((section) => (
|
||||
<Fragment key={section}>{mapSectionToComponent(section)}</Fragment>
|
||||
))}
|
||||
@ -611,7 +612,7 @@ export const Ditto = ({ columns, isFirstPage = false }: TemplateProps) => {
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"main p-custom group space-y-4",
|
||||
"main group space-y-4 p-custom",
|
||||
sidebar.length > 0 ? "col-span-2" : "col-span-3",
|
||||
)}
|
||||
>
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
import {
|
||||
import type {
|
||||
Award,
|
||||
Certification,
|
||||
CustomSection,
|
||||
CustomSectionGroup,
|
||||
Education,
|
||||
Experience,
|
||||
Interest,
|
||||
Language,
|
||||
Profile,
|
||||
@ -15,22 +13,22 @@ 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);
|
||||
|
||||
return (
|
||||
<div className="p-custom space-y-4 bg-primary text-background">
|
||||
<div className="space-y-4 bg-primary p-custom text-background">
|
||||
<Picture className="border-background" />
|
||||
|
||||
<div>
|
||||
@ -88,12 +86,12 @@ const Summary = () => {
|
||||
if (!section.visible || isEmptyString(section.content)) return null;
|
||||
|
||||
return (
|
||||
<div className="p-custom space-y-4" style={{ backgroundColor: hexToRgb(primaryColor, 0.2) }}>
|
||||
<div className="space-y-4 p-custom" 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>
|
||||
@ -125,7 +123,7 @@ const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
|
||||
if (!isUrl(url.href)) return null;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-x-1.5">
|
||||
<div className="flex items-center gap-x-1.5 break-all">
|
||||
{!iconOnRight &&
|
||||
(icon ?? (
|
||||
<i className="ph ph-bold ph-link text-primary group-[.sidebar]:text-background" />
|
||||
@ -134,7 +132,7 @@ const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
|
||||
href={url.href}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener nofollow"
|
||||
className={cn("inline-block", className)}
|
||||
className={cn("line-clamp-1 max-w-fit", className)}
|
||||
>
|
||||
{label ?? (url.label || url.href)}
|
||||
</a>
|
||||
@ -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 />;
|
||||
}
|
||||
@ -581,7 +587,7 @@ export const Gengar = ({ columns, isFirstPage = false }: TemplateProps) => {
|
||||
{isFirstPage && <Header />}
|
||||
|
||||
<div
|
||||
className="p-custom flex-1 space-y-4"
|
||||
className="flex-1 space-y-4 p-custom"
|
||||
style={{ backgroundColor: hexToRgb(primaryColor, 0.2) }}
|
||||
>
|
||||
{sidebar.map((section) => (
|
||||
@ -591,9 +597,7 @@ 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">
|
||||
<div className="space-y-4 p-custom">
|
||||
{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>
|
||||
);
|
||||
@ -130,14 +135,14 @@ const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
|
||||
if (!isUrl(url.href)) return null;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-x-1.5">
|
||||
<div className="flex items-center gap-x-1.5 break-all">
|
||||
{!iconOnRight &&
|
||||
(icon ?? <i className="ph ph-bold ph-link text-primary group-[.sidebar]:text-primary" />)}
|
||||
<a
|
||||
href={url.href}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener nofollow"
|
||||
className={cn("inline-block", className)}
|
||||
className={cn("line-clamp-1 max-w-fit", className)}
|
||||
>
|
||||
{label ?? (url.label || url.href)}
|
||||
</a>
|
||||
@ -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} />}
|
||||
@ -579,7 +587,7 @@ export const Glalie = ({ columns, isFirstPage = false }: TemplateProps) => {
|
||||
return (
|
||||
<div className="grid min-h-[inherit] grid-cols-3">
|
||||
<div
|
||||
className={cn("sidebar p-custom group space-y-4", sidebar.length === 0 && "hidden")}
|
||||
className={cn("sidebar group space-y-4 p-custom", sidebar.length === 0 && "hidden")}
|
||||
style={{ backgroundColor: hexToRgb(primaryColor, 0.2) }}
|
||||
>
|
||||
{isFirstPage && <Header />}
|
||||
@ -591,7 +599,7 @@ export const Glalie = ({ columns, isFirstPage = false }: TemplateProps) => {
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"main p-custom group space-y-4",
|
||||
"main group space-y-4 p-custom",
|
||||
sidebar.length > 0 ? "col-span-2" : "col-span-3",
|
||||
)}
|
||||
>
|
||||
|
||||
@ -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>
|
||||
);
|
||||
@ -143,13 +141,13 @@ const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
|
||||
if (!isUrl(url.href)) return null;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-x-1.5">
|
||||
<div className="flex items-center gap-x-1.5 break-all">
|
||||
{!iconOnRight && (icon ?? <i className="ph ph-bold ph-link text-primary" />)}
|
||||
<a
|
||||
href={url.href}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener nofollow"
|
||||
className={cn("inline-block", className)}
|
||||
className={cn("line-clamp-1 max-w-fit", className)}
|
||||
>
|
||||
{label ?? (url.label || url.href)}
|
||||
</a>
|
||||
@ -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} />}
|
||||
@ -523,7 +524,7 @@ export const Kakuna = ({ columns, isFirstPage = false }: TemplateProps) => {
|
||||
const [main, sidebar] = columns;
|
||||
|
||||
return (
|
||||
<div className="p-custom space-y-4">
|
||||
<div className="space-y-4 p-custom">
|
||||
{isFirstPage && <Header />}
|
||||
|
||||
<div className="space-y-4">
|
||||
|
||||
@ -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);
|
||||
@ -34,7 +32,7 @@ const Header = () => {
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
className="p-custom flex items-center space-x-8"
|
||||
className="flex items-center space-x-8 p-custom"
|
||||
style={{ backgroundColor: hexToRgb(primaryColor, 0.2) }}
|
||||
>
|
||||
<div className="space-y-3">
|
||||
@ -44,16 +42,16 @@ const Header = () => {
|
||||
</div>
|
||||
|
||||
<div
|
||||
dangerouslySetInnerHTML={{ __html: section.content }}
|
||||
className="wysiwyg"
|
||||
dangerouslySetInnerHTML={{ __html: sanitize(section.content) }}
|
||||
style={{ columns: section.columns }}
|
||||
className="wysiwyg"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Picture />
|
||||
</div>
|
||||
|
||||
<div className="p-custom space-y-3" style={{ backgroundColor: hexToRgb(primaryColor, 0.4) }}>
|
||||
<div className="space-y-3 p-custom" style={{ backgroundColor: hexToRgb(primaryColor, 0.4) }}>
|
||||
<div className="flex flex-wrap items-center gap-x-3 gap-y-0.5 text-sm">
|
||||
{basics.location && (
|
||||
<div className="flex items-center gap-x-1.5">
|
||||
@ -138,13 +136,13 @@ const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
|
||||
if (!isUrl(url.href)) return null;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-x-1.5">
|
||||
<div className="flex items-center gap-x-1.5 break-all">
|
||||
{!iconOnRight && (icon ?? <i className="ph ph-bold ph-link text-primary" />)}
|
||||
<a
|
||||
href={url.href}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener nofollow"
|
||||
className={cn("inline-block", className)}
|
||||
className={cn("line-clamp-1 max-w-fit", className)}
|
||||
>
|
||||
{label ?? (url.label || url.href)}
|
||||
</a>
|
||||
@ -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} />}
|
||||
@ -518,7 +519,7 @@ export const Leafish = ({ columns, isFirstPage = false }: TemplateProps) => {
|
||||
<div>
|
||||
{isFirstPage && <Header />}
|
||||
|
||||
<div className="p-custom grid grid-cols-2 items-start space-x-6">
|
||||
<div className="grid grid-cols-2 items-start space-x-6 p-custom">
|
||||
<div className={cn("grid gap-y-4", sidebar.length === 0 && "col-span-2")}>
|
||||
{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,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>
|
||||
@ -128,13 +126,13 @@ const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
|
||||
if (!isUrl(url.href)) return null;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-x-1.5">
|
||||
<div className="flex items-center gap-x-1.5 break-all">
|
||||
{!iconOnRight && (icon ?? <i className="ph ph-bold ph-link text-primary" />)}
|
||||
<a
|
||||
href={url.href}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener nofollow"
|
||||
className={cn("inline-block", className)}
|
||||
className={cn("line-clamp-1 max-w-fit", className)}
|
||||
>
|
||||
{label ?? (url.label || url.href)}
|
||||
</a>
|
||||
@ -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 && (
|
||||
@ -571,7 +575,7 @@ export const Nosepass = ({ columns, isFirstPage = false }: TemplateProps) => {
|
||||
const [main, sidebar] = columns;
|
||||
|
||||
return (
|
||||
<div className="p-custom space-y-6">
|
||||
<div className="space-y-6 p-custom">
|
||||
<div className="flex items-center justify-between">
|
||||
<img alt="Europass Logo" className="h-[42px]" src="/assets/europass.png" />
|
||||
|
||||
|
||||
@ -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>
|
||||
);
|
||||
@ -144,13 +142,13 @@ const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
|
||||
if (!isUrl(url.href)) return null;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-x-1.5">
|
||||
<div className="flex items-center gap-x-1.5 break-all">
|
||||
{!iconOnRight && (icon ?? <i className="ph ph-bold ph-link text-primary" />)}
|
||||
<a
|
||||
href={url.href}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener nofollow"
|
||||
className={cn("inline-block", className)}
|
||||
className={cn("line-clamp-1 max-w-fit", className)}
|
||||
>
|
||||
{label ?? (url.label || url.href)}
|
||||
</a>
|
||||
@ -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} />}
|
||||
@ -563,7 +564,7 @@ export const Onyx = ({ columns, isFirstPage = false }: TemplateProps) => {
|
||||
const [main, sidebar] = columns;
|
||||
|
||||
return (
|
||||
<div className="p-custom space-y-4">
|
||||
<div className="space-y-4 p-custom">
|
||||
{isFirstPage && <Header />}
|
||||
|
||||
{main.map((section) => (
|
||||
|
||||
@ -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>
|
||||
);
|
||||
@ -128,15 +126,11 @@ const Rating = ({ level }: RatingProps) => (
|
||||
<i
|
||||
key={index}
|
||||
className={cn(
|
||||
"ph ph-diamond text-primary",
|
||||
"ph ph-bold ph-diamond text-primary",
|
||||
level > index && "ph-fill",
|
||||
level <= index && "ph-bold",
|
||||
)}
|
||||
/>
|
||||
// <div
|
||||
// key={index}
|
||||
// className={cn("h-2 w-4 border border-primary", level > index && "bg-primary")}
|
||||
// />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
@ -153,7 +147,7 @@ const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
|
||||
if (!isUrl(url.href)) return null;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-x-1.5">
|
||||
<div className="flex items-center gap-x-1.5 break-all">
|
||||
{!iconOnRight &&
|
||||
(icon ?? (
|
||||
<i className="ph ph-bold ph-link text-primary group-[.summary]:text-background" />
|
||||
@ -162,7 +156,7 @@ const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
|
||||
href={url.href}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener nofollow"
|
||||
className={cn("inline-block", className)}
|
||||
className={cn("line-clamp-1 max-w-fit", className)}
|
||||
>
|
||||
{label ?? (url.label || url.href)}
|
||||
</a>
|
||||
@ -214,7 +208,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 +234,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} />}
|
||||
@ -600,7 +597,7 @@ export const Pikachu = ({ columns, isFirstPage = false }: TemplateProps) => {
|
||||
const [main, sidebar] = columns;
|
||||
|
||||
return (
|
||||
<div className="p-custom grid grid-cols-3 space-x-6">
|
||||
<div className="grid grid-cols-3 space-x-6 p-custom">
|
||||
<div className="sidebar group space-y-4">
|
||||
{isFirstPage && <Picture className="w-full !max-w-none" />}
|
||||
|
||||
|
||||
@ -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>
|
||||
);
|
||||
@ -125,13 +123,13 @@ const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
|
||||
if (!isUrl(url.href)) return null;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-x-1.5 border-r pr-2 last:border-r-0 last:pr-0">
|
||||
<div className="flex items-center gap-x-1.5 break-all border-r pr-2 last:border-r-0 last:pr-0">
|
||||
{!iconOnRight && (icon ?? <i className="ph ph-bold ph-link text-primary" />)}
|
||||
<a
|
||||
href={url.href}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener nofollow"
|
||||
className={cn("inline-block", className)}
|
||||
className={cn("line-clamp-1 max-w-fit", className)}
|
||||
>
|
||||
{label ?? (url.label || url.href)}
|
||||
</a>
|
||||
@ -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} />}
|
||||
@ -566,7 +567,7 @@ export const Rhyhorn = ({ columns, isFirstPage = false }: TemplateProps) => {
|
||||
const [main, sidebar] = columns;
|
||||
|
||||
return (
|
||||
<div className="p-custom space-y-4">
|
||||
<div className="space-y-4 p-custom">
|
||||
{isFirstPage && <Header />}
|
||||
|
||||
{main.map((section) => (
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { SectionKey } from "@reactive-resume/schema";
|
||||
import type { SectionKey } from "@reactive-resume/schema";
|
||||
|
||||
export type TemplateProps = {
|
||||
columns: SectionKey[][];
|
||||
|
||||
@ -4,20 +4,10 @@
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.ts", "*.tsx"],
|
||||
"extends": [
|
||||
"plugin:tailwindcss/recommended",
|
||||
"plugin:@tanstack/eslint-plugin-query/recommended"
|
||||
],
|
||||
"extends": ["plugin:@tanstack/eslint-plugin-query/recommended"],
|
||||
"parserOptions": {
|
||||
"projectService": "./apps/client/tsconfig.json"
|
||||
},
|
||||
"settings": {
|
||||
"tailwindcss": {
|
||||
"callees": ["cn", "clsx", "cva"],
|
||||
"config": "tailwind.config.js",
|
||||
"whitelist": ["ph", "ph\\-.*", "si", "si\\-.*"]
|
||||
}
|
||||
},
|
||||
"plugins": ["lingui"],
|
||||
"rules": {
|
||||
// eslint
|
||||
|
||||
@ -42,6 +42,6 @@
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
|
||||
<!-- Phosphor Icons -->
|
||||
<script src="https://unpkg.com/@phosphor-icons/web"></script>
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/@phosphor-icons/web"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
68
apps/client/public/funding.json
Normal file
@ -0,0 +1,68 @@
|
||||
{
|
||||
"$schema": "https://fundingjson.org/schema/v1.0.0/funding.schema.json",
|
||||
"version": "v1.0.0",
|
||||
"entity": {
|
||||
"type": "individual",
|
||||
"role": "maintainer",
|
||||
"name": "Amruth Pillai",
|
||||
"email": "im.amruth@gmail.com",
|
||||
"description": "Software Engineer",
|
||||
"webpageUrl": {
|
||||
"url": "https://rxresu.me/funding.json"
|
||||
}
|
||||
},
|
||||
"projects": [
|
||||
{
|
||||
"guid": "reactive-resume",
|
||||
"name": "Reactive Resume",
|
||||
"description": "A free and open-source resume builder that simplifies the process of creating, updating, and sharing your resume.",
|
||||
"webpageUrl": {
|
||||
"url": "https://rxresu.me"
|
||||
},
|
||||
"repositoryUrl": {
|
||||
"url": "https://github.com/AmruthPillai/Reactive-Resume",
|
||||
"wellKnown": "https://github.com/AmruthPillai/Reactive-Resume/blob/main/.github/.well-known/funding-manifest-urls"
|
||||
},
|
||||
"licenses": ["spdx:MIT"],
|
||||
"tags": ["data", "design", "productivity", "resume-builder"]
|
||||
}
|
||||
],
|
||||
"funding": {
|
||||
"plans": [
|
||||
{
|
||||
"guid": "sponsor",
|
||||
"status": "active",
|
||||
"name": "Sponsor",
|
||||
"description": "Support the project on a recurring basis by becoming a sponsor.",
|
||||
"amount": 10,
|
||||
"currency": "EUR",
|
||||
"frequency": "monthly",
|
||||
"channels": ["github", "open-collective"]
|
||||
},
|
||||
{
|
||||
"guid": "donation",
|
||||
"status": "active",
|
||||
"name": "Donation",
|
||||
"description": "Show your support for the project by making a one-time donation.",
|
||||
"amount": 0,
|
||||
"currency": "EUR",
|
||||
"frequency": "one-time",
|
||||
"channels": ["github", "open-collective"]
|
||||
}
|
||||
],
|
||||
"channels": [
|
||||
{
|
||||
"guid": "github",
|
||||
"type": "payment-provider",
|
||||
"description": "GitHub Sponsors",
|
||||
"address": "https://github.com/sponsors/AmruthPillai"
|
||||
},
|
||||
{
|
||||
"guid": "open-collective",
|
||||
"type": "payment-provider",
|
||||
"description": "Open Collective",
|
||||
"address": "https://opencollective.com/reactive-resume"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
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 |
@ -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>
|
||||
|
||||
@ -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,5 +1,6 @@
|
||||
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";
|
||||
|
||||
@ -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"),
|
||||
|
||||