mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2025-11-10 04:22:27 +10:00
release: v4.1.0
This commit is contained in:
@ -22,14 +22,8 @@ Dockerfile
|
|||||||
*.sublime-workspace
|
*.sublime-workspace
|
||||||
|
|
||||||
# IDE - VSCode
|
# IDE - VSCode
|
||||||
.vscode/*
|
|
||||||
!.vscode/settings.json
|
|
||||||
!.vscode/tasks.json
|
|
||||||
!.vscode/launch.json
|
|
||||||
!.vscode/extensions.json
|
|
||||||
|
|
||||||
# IDE - Visual Studio
|
|
||||||
.vs/*
|
.vs/*
|
||||||
|
.vscode/*
|
||||||
!.vscode/settings.json
|
!.vscode/settings.json
|
||||||
!.vscode/tasks.json
|
!.vscode/tasks.json
|
||||||
!.vscode/launch.json
|
!.vscode/launch.json
|
||||||
|
|||||||
21
.env.example
21
.env.example
@ -4,14 +4,6 @@ NODE_ENV=development
|
|||||||
# Ports
|
# Ports
|
||||||
PORT=3000
|
PORT=3000
|
||||||
|
|
||||||
# Client Port & URL (for development)
|
|
||||||
__DEV__CLIENT_PORT=5173 # Only used in development
|
|
||||||
__DEV__CLIENT_URL=http://localhost:5173 # Only used in development
|
|
||||||
|
|
||||||
# Artboard Port & URL (for development)
|
|
||||||
__DEV__ARTBOARD_PORT=6173 # Only used in development
|
|
||||||
__DEV__ARTBOARD_URL=http://localhost:6173 # Only used in development
|
|
||||||
|
|
||||||
# URLs
|
# URLs
|
||||||
# These URLs must reference a publicly accessible domain or IP address, not a docker container ID (depending on your compose setup)
|
# These URLs must reference a publicly accessible domain or IP address, not a docker container ID (depending on your compose setup)
|
||||||
PUBLIC_URL=http://localhost:3000
|
PUBLIC_URL=http://localhost:3000
|
||||||
@ -53,19 +45,18 @@ STORAGE_ACCESS_KEY=minioadmin
|
|||||||
STORAGE_SECRET_KEY=minioadmin
|
STORAGE_SECRET_KEY=minioadmin
|
||||||
STORAGE_USE_SSL=false
|
STORAGE_USE_SSL=false
|
||||||
|
|
||||||
# Redis (for cache & server session management)
|
|
||||||
REDIS_URL=redis://default:password@localhost:6379
|
|
||||||
|
|
||||||
# Sentry (for error reporting, Optional)
|
# Sentry (for error reporting, Optional)
|
||||||
# VITE_SENTRY_DSN=
|
# SERVER_SENTRY_DSN=
|
||||||
|
# VITE_CLIENT_SENTRY_DSN=
|
||||||
|
|
||||||
# Crowdin (Optional)
|
# Crowdin (Optional)
|
||||||
CROWDIN_PROJECT_ID=
|
# CROWDIN_PROJECT_ID=
|
||||||
CROWDIN_PERSONAL_TOKEN=
|
# CROWDIN_PERSONAL_TOKEN=
|
||||||
|
|
||||||
# Email (Optional)
|
# Flags (Optional)
|
||||||
# DISABLE_EMAIL_AUTH=true
|
# DISABLE_EMAIL_AUTH=true
|
||||||
# VITE_DISABLE_SIGNUPS=false
|
# VITE_DISABLE_SIGNUPS=false
|
||||||
|
# SKIP_STORAGE_BUCKET_CHECK=false
|
||||||
|
|
||||||
# GitHub (OAuth, Optional)
|
# GitHub (OAuth, Optional)
|
||||||
# GITHUB_CLIENT_ID=
|
# GITHUB_CLIENT_ID=
|
||||||
|
|||||||
@ -8,6 +8,9 @@
|
|||||||
"extends": ["plugin:prettier/recommended"],
|
"extends": ["plugin:prettier/recommended"],
|
||||||
"plugins": ["simple-import-sort", "unused-imports"],
|
"plugins": ["simple-import-sort", "unused-imports"],
|
||||||
"rules": {
|
"rules": {
|
||||||
|
// eslint
|
||||||
|
"no-return-await": "off",
|
||||||
|
|
||||||
// simple-import-sort
|
// simple-import-sort
|
||||||
"simple-import-sort/imports": "error",
|
"simple-import-sort/imports": "error",
|
||||||
"simple-import-sort/exports": "error",
|
"simple-import-sort/exports": "error",
|
||||||
@ -43,10 +46,36 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"files": ["*.ts", "*.tsx"],
|
"files": ["*.ts", "*.tsx"],
|
||||||
"extends": ["plugin:@nx/typescript"],
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"parserOptions": {
|
||||||
|
"sourceType": "module",
|
||||||
|
"ecmaVersion": "latest",
|
||||||
|
"project": ["tsconfig.*?.json"]
|
||||||
|
},
|
||||||
|
"extends": [
|
||||||
|
"plugin:@nx/typescript",
|
||||||
|
"plugin:@typescript-eslint/strict-type-checked",
|
||||||
|
"plugin:@typescript-eslint/stylistic-type-checked",
|
||||||
|
"plugin:unicorn/recommended"
|
||||||
|
],
|
||||||
|
"plugins": ["@typescript-eslint", "unicorn"],
|
||||||
"rules": {
|
"rules": {
|
||||||
// typescript-eslint
|
// typescript-eslint
|
||||||
"@typescript-eslint/no-unused-vars": "off"
|
"@typescript-eslint/no-unsafe-call": "off",
|
||||||
|
"@typescript-eslint/no-unused-vars": "off",
|
||||||
|
"@typescript-eslint/return-await": "error",
|
||||||
|
"@typescript-eslint/no-unsafe-return": "off",
|
||||||
|
"@typescript-eslint/no-unsafe-argument": "off",
|
||||||
|
"@typescript-eslint/no-misused-promises": "off",
|
||||||
|
"@typescript-eslint/no-unsafe-assignment": "off",
|
||||||
|
"@typescript-eslint/no-unsafe-member-access": "off",
|
||||||
|
"@typescript-eslint/restrict-template-expressions": "off",
|
||||||
|
"@typescript-eslint/consistent-type-definitions": ["error", "type"],
|
||||||
|
|
||||||
|
// unicorn
|
||||||
|
"unicorn/no-null": "off",
|
||||||
|
"unicorn/prevent-abbreviations": "off",
|
||||||
|
"unicorn/prefer-string-replace-all": "off"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
2
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
2
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
@ -21,7 +21,7 @@ body:
|
|||||||
label: Product Variant
|
label: Product Variant
|
||||||
description: What variant of Reactive Resume are you using?
|
description: What variant of Reactive Resume are you using?
|
||||||
options:
|
options:
|
||||||
- Cloud (http://rxresu.me)
|
- Cloud (https://rxresu.me)
|
||||||
- Self-Hosted
|
- Self-Hosted
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
20
.github/workflows/publish-docker-image.yml
vendored
20
.github/workflows/publish-docker-image.yml
vendored
@ -38,9 +38,6 @@ jobs:
|
|||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3.0.0
|
uses: docker/setup-qemu-action@v3.0.0
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3.3.0
|
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v3.1.0
|
uses: docker/login-action@v3.1.0
|
||||||
with:
|
with:
|
||||||
@ -54,6 +51,13 @@ jobs:
|
|||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ github.token }}
|
password: ${{ github.token }}
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3.3.0
|
||||||
|
with:
|
||||||
|
driver: cloud
|
||||||
|
version: "lab:latest"
|
||||||
|
endpoint: "amruthpillai/default"
|
||||||
|
|
||||||
- name: Extract Docker Metadata
|
- name: Extract Docker Metadata
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v5.5.1
|
uses: docker/metadata-action@v5.5.1
|
||||||
@ -111,9 +115,6 @@ jobs:
|
|||||||
pattern: digests-*
|
pattern: digests-*
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3.3.0
|
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v3.1.0
|
uses: docker/login-action@v3.1.0
|
||||||
with:
|
with:
|
||||||
@ -127,6 +128,13 @@ jobs:
|
|||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ github.token }}
|
password: ${{ github.token }}
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3.3.0
|
||||||
|
with:
|
||||||
|
driver: cloud
|
||||||
|
version: "lab:latest"
|
||||||
|
endpoint: "amruthpillai/default"
|
||||||
|
|
||||||
- name: Extract Docker Metadata
|
- name: Extract Docker Metadata
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v5.5.1
|
uses: docker/metadata-action@v5.5.1
|
||||||
|
|||||||
8
.gitignore
vendored
8
.gitignore
vendored
@ -16,14 +16,8 @@ node_modules
|
|||||||
*.sublime-workspace
|
*.sublime-workspace
|
||||||
|
|
||||||
# IDE - VSCode
|
# IDE - VSCode
|
||||||
.vscode/*
|
|
||||||
!.vscode/settings.json
|
|
||||||
!.vscode/tasks.json
|
|
||||||
!.vscode/launch.json
|
|
||||||
!.vscode/extensions.json
|
|
||||||
|
|
||||||
# IDE - Visual Studio
|
|
||||||
.vs/*
|
.vs/*
|
||||||
|
.vscode/*
|
||||||
!.vscode/settings.json
|
!.vscode/settings.json
|
||||||
!.vscode/tasks.json
|
!.vscode/tasks.json
|
||||||
!.vscode/launch.json
|
!.vscode/launch.json
|
||||||
|
|||||||
@ -14,6 +14,6 @@ When it comes to **security**, you now have the option to protect your account w
|
|||||||
|
|
||||||
From a **design** perspective, the motivation behind this is to ensure that Reactive Resume is taken more seriously and not perceived as just another subpar side-project, which is often associated with free software. My goal is to demonstrate that this is not the case, and that **free and open source software can be just as good**, if not better, than paid alternatives.
|
From a **design** perspective, the motivation behind this is to ensure that Reactive Resume is taken more seriously and not perceived as just another subpar side-project, which is often associated with free software. My goal is to demonstrate that this is not the case, and that **free and open source software can be just as good**, if not better, than paid alternatives.
|
||||||
|
|
||||||
From a **self-hosting perspective**, it has never been simpler. Instead of running two separate services on your Docker (one for the client and one for the server) and struggling to establish communication between them, now you only need to pull a single image. Additionally, there are a few dependent services available on Docker (such as Postgres, Redis, Minio etc.) that you can also pull and have them all working together seamlessly.
|
From a **self-hosting perspective**, it has never been simpler. Instead of running two separate services on your Docker (one for the client and one for the server) and struggling to establish communication between them, now you only need to pull a single image. Additionally, there are a few dependent services available on Docker (such as Postgres, Minio etc.) that you can also pull and have them all working together seamlessly.
|
||||||
|
|
||||||
I'm excited for you to try out the app, as I've spent months building it to perfection. If you enjoy the experience of building your resume using the app, please consider supporting by [becoming a GitHub Sponsor](https://github.com/sponsors/AmruthPillai).
|
I'm excited for you to try out the app, as I've spent months building it to perfection. If you enjoy the experience of building your resume using the app, please consider supporting by [becoming a GitHub Sponsor](https://github.com/sponsors/AmruthPillai).
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## Getting the project set up locally
|
## Getting the project set up locally
|
||||||
|
|
||||||
There are a number of Docker Compose examples that are suitable for a wide variety of deployment strategies depending on your use-case. All of the examples can be found in the `tools/compose` folder.
|
There are a number of Docker Compose examples that are suitable for a wide variety of deployment strategies depending on your use-case. All the examples can be found in the `tools/compose` folder.
|
||||||
|
|
||||||
To run the development environment of the application locally on your computer, please follow these steps:
|
To run the development environment of the application locally on your computer, please follow these steps:
|
||||||
|
|
||||||
@ -57,15 +57,13 @@ You can also visit `http://localhost:3000/api/health`, the health check endpoint
|
|||||||
"info": {
|
"info": {
|
||||||
"database": { "status": "up" },
|
"database": { "status": "up" },
|
||||||
"storage": { "status": "up" },
|
"storage": { "status": "up" },
|
||||||
"browser": { "status": "up", "version": "Chrome/119.0.6045.9" },
|
"browser": { "status": "up", "version": "Chrome/119.0.6045.9" }
|
||||||
"redis": { "status": "up" }
|
|
||||||
},
|
},
|
||||||
"error": {},
|
"error": {},
|
||||||
"details": {
|
"details": {
|
||||||
"database": { "status": "up" },
|
"database": { "status": "up" },
|
||||||
"storage": { "status": "up" },
|
"storage": { "status": "up" },
|
||||||
"browser": { "status": "up", "version": "Chrome/119.0.6045.9" },
|
"browser": { "status": "up", "version": "Chrome/119.0.6045.9" }
|
||||||
"redis": { "status": "up" }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@ -40,11 +40,11 @@ Start creating your standout resume with Reactive Resume today!
|
|||||||
|
|
||||||
- **Free, forever** and open-source
|
- **Free, forever** and open-source
|
||||||
- No telemetry, user tracking or advertising
|
- No telemetry, user tracking or advertising
|
||||||
- You can self-host the application in less then 30 seconds
|
- You can self-host the application in less than 30 seconds
|
||||||
- **Available in multiple languages** ([help add/improve your language here](https://translate.rxresu.me/))
|
- **Available in multiple languages** ([help add/improve your language here](https://translate.rxresu.me/))
|
||||||
- Use your email address (or a throw-away address, no problem) to create an account
|
- Use your email address (or a throw-away address, no problem) to create an account
|
||||||
- You can also sign in with your GitHub or Google account, and even set up two-factor authentication for extra security
|
- You can also sign in with your GitHub or Google account, and even set up two-factor authentication for extra security
|
||||||
- Create as many resumes as you like under a single account, optimising each resume for every job application based on it’s description for a higher ATS score
|
- Create as many resumes as you like under a single account, optimising each resume for every job application based on its description for a higher ATS score
|
||||||
- **Bring your own OpenAI API key** and unlock features such as improving your writing, fixing spelling and grammar or changing the tone of your text in one-click
|
- **Bring your own OpenAI API key** and unlock features such as improving your writing, fixing spelling and grammar or changing the tone of your text in one-click
|
||||||
- Translate your resume into any language using ChatGPT and import it back for easier editing
|
- Translate your resume into any language using ChatGPT and import it back for easier editing
|
||||||
- Create single page resumes or a resume that spans multiple pages easily
|
- Create single page resumes or a resume that spans multiple pages easily
|
||||||
@ -69,7 +69,6 @@ Start creating your standout resume with Reactive Resume today!
|
|||||||
- NestJS, for the backend
|
- NestJS, for the backend
|
||||||
- Postgres (primary database)
|
- Postgres (primary database)
|
||||||
- Prisma ORM, which frees you to switch to any other relational database with a few minor changes in the code
|
- Prisma ORM, which frees you to switch to any other relational database with a few minor changes in the code
|
||||||
- Redis (for caching, session storage and resume statistics)
|
|
||||||
- Minio (for object storage: to store avatars, resume PDFs and previews)
|
- Minio (for object storage: to store avatars, resume PDFs and previews)
|
||||||
- Browserless (for headless chrome, to print PDFs and generate previews)
|
- Browserless (for headless chrome, to print PDFs and generate previews)
|
||||||
- SMTP Server (to send password recovery emails)
|
- SMTP Server (to send password recovery emails)
|
||||||
|
|||||||
@ -11,4 +11,4 @@
|
|||||||
|
|
||||||
## Reporting a Vulnerability
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
Please raise an issue on GitHub to report any security vulnerabilities in the app. If the vulnerability is potentially lethal, send me an email about it on hello@amruthpillai.com.
|
Please raise an issue on GitHub to report any security vulnerabilities in the app. If the vulnerability is potentially lethal, email me about it on hello@amruthpillai.com.
|
||||||
|
|||||||
@ -12,6 +12,18 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"rules": {
|
"rules": {
|
||||||
|
// react
|
||||||
|
"react/no-unescaped-entities": "off",
|
||||||
|
"react/jsx-sort-props": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"reservedFirst": true,
|
||||||
|
"callbacksLast": true,
|
||||||
|
"shorthandFirst": true,
|
||||||
|
"noSortAlphabetically": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
// react-hooks
|
// react-hooks
|
||||||
"react-hooks/exhaustive-deps": "off",
|
"react-hooks/exhaustive-deps": "off",
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
const { join } = require("path");
|
const path = require("node:path");
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: {
|
plugins: {
|
||||||
tailwindcss: {
|
tailwindcss: {
|
||||||
config: join(__dirname, "tailwind.config.js"),
|
config: path.join(__dirname, "tailwind.config.js"),
|
||||||
},
|
},
|
||||||
autoprefixer: {},
|
autoprefixer: {},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -4,7 +4,8 @@ import { RouterProvider } from "react-router-dom";
|
|||||||
|
|
||||||
import { router } from "./router";
|
import { router } from "./router";
|
||||||
|
|
||||||
const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement);
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
const root = ReactDOM.createRoot(document.querySelector("#root")!);
|
||||||
|
|
||||||
root.render(
|
root.render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
|
|||||||
@ -39,20 +39,20 @@ export const ArtboardPage = () => {
|
|||||||
`${metadata.typography.lineHeight}`,
|
`${metadata.typography.lineHeight}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
document.documentElement.style.setProperty("--color-text", `${metadata.theme.text}`);
|
document.documentElement.style.setProperty("--color-text", metadata.theme.text);
|
||||||
document.documentElement.style.setProperty("--color-primary", `${metadata.theme.primary}`);
|
document.documentElement.style.setProperty("--color-primary", metadata.theme.primary);
|
||||||
document.documentElement.style.setProperty(
|
document.documentElement.style.setProperty("--color-background", metadata.theme.background);
|
||||||
"--color-background",
|
|
||||||
`${metadata.theme.background}`,
|
|
||||||
);
|
|
||||||
}, [metadata]);
|
}, [metadata]);
|
||||||
|
|
||||||
// Typography Options
|
// Typography Options
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.querySelectorAll(`[data-page]`).forEach((el) => {
|
// eslint-disable-next-line unicorn/prefer-spread
|
||||||
|
const elements = Array.from(document.querySelectorAll(`[data-page]`));
|
||||||
|
|
||||||
|
for (const el of elements) {
|
||||||
el.classList.toggle("hide-icons", metadata.typography.hideIcons);
|
el.classList.toggle("hide-icons", metadata.typography.hideIcons);
|
||||||
el.classList.toggle("underline-links", metadata.typography.underlineLinks);
|
el.classList.toggle("underline-links", metadata.typography.underlineLinks);
|
||||||
});
|
}
|
||||||
}, [metadata]);
|
}, [metadata]);
|
||||||
|
|
||||||
return <Outlet />;
|
return <Outlet />;
|
||||||
|
|||||||
@ -38,11 +38,11 @@ export const BuilderLayout = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<TransformWrapper
|
<TransformWrapper
|
||||||
|
ref={transformRef}
|
||||||
centerOnInit
|
centerOnInit
|
||||||
maxScale={2}
|
maxScale={2}
|
||||||
minScale={0.4}
|
minScale={0.4}
|
||||||
initialScale={0.8}
|
initialScale={0.8}
|
||||||
ref={transformRef}
|
|
||||||
limitToBounds={false}
|
limitToBounds={false}
|
||||||
>
|
>
|
||||||
<TransformComponent
|
<TransformComponent
|
||||||
@ -56,8 +56,8 @@ export const BuilderLayout = () => {
|
|||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{layout.map((columns, pageIndex) => (
|
{layout.map((columns, pageIndex) => (
|
||||||
<motion.div
|
<motion.div
|
||||||
layout
|
|
||||||
key={pageIndex}
|
key={pageIndex}
|
||||||
|
layout
|
||||||
initial={{ opacity: 0, x: -200, y: 0 }}
|
initial={{ opacity: 0, x: -200, y: 0 }}
|
||||||
animate={{ opacity: 1, x: 0, transition: { delay: pageIndex * 0.3 } }}
|
animate={{ opacity: 1, x: 0, transition: { delay: pageIndex * 0.3 } }}
|
||||||
exit={{ opacity: 0, x: -200 }}
|
exit={{ opacity: 0, x: -200 }}
|
||||||
|
|||||||
@ -20,7 +20,10 @@ export const Providers = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const resumeData = window.localStorage.getItem("resume");
|
const resumeData = window.localStorage.getItem("resume");
|
||||||
if (resumeData) return setResume(JSON.parse(resumeData));
|
if (resumeData) {
|
||||||
|
setResume(JSON.parse(resumeData));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
window.addEventListener("message", handleMessage);
|
window.addEventListener("message", handleMessage);
|
||||||
|
|
||||||
@ -34,6 +37,7 @@ export const Providers = () => {
|
|||||||
// setResume(sampleResume);
|
// setResume(sampleResume);
|
||||||
// }, [setResume]);
|
// }, [setResume]);
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||||
if (!resume) return null;
|
if (!resume) return null;
|
||||||
|
|
||||||
return <Outlet />;
|
return <Outlet />;
|
||||||
|
|||||||
@ -8,5 +8,7 @@ export type ArtboardStore = {
|
|||||||
|
|
||||||
export const useArtboardStore = create<ArtboardStore>()((set) => ({
|
export const useArtboardStore = create<ArtboardStore>()((set) => ({
|
||||||
resume: null as unknown as ResumeData,
|
resume: null as unknown as ResumeData,
|
||||||
setResume: (resume) => set({ resume }),
|
setResume: (resume) => {
|
||||||
|
set({ resume });
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
@ -93,9 +93,9 @@ const Summary = () => {
|
|||||||
<div className="absolute left-[-4.5px] top-[8px] hidden size-[8px] rounded-full bg-primary group-[.main]:block" />
|
<div className="absolute left-[-4.5px] top-[8px] hidden size-[8px] rounded-full bg-primary group-[.main]:block" />
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
dangerouslySetInnerHTML={{ __html: section.content }}
|
||||||
className="wysiwyg"
|
className="wysiwyg"
|
||||||
style={{ columns: section.columns }}
|
style={{ columns: section.columns }}
|
||||||
dangerouslySetInnerHTML={{ __html: section.content }}
|
|
||||||
/>
|
/>
|
||||||
</main>
|
</main>
|
||||||
</section>
|
</section>
|
||||||
@ -133,7 +133,7 @@ const Link = ({ url, icon, label, className }: LinkProps) => {
|
|||||||
rel="noreferrer noopener nofollow"
|
rel="noreferrer noopener nofollow"
|
||||||
className={cn("inline-block", className)}
|
className={cn("inline-block", className)}
|
||||||
>
|
>
|
||||||
{label || url.label || url.href}
|
{label ?? (url.label || url.href)}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -158,7 +158,7 @@ const Section = <T,>({
|
|||||||
summaryKey,
|
summaryKey,
|
||||||
keywordsKey,
|
keywordsKey,
|
||||||
}: SectionProps<T>) => {
|
}: SectionProps<T>) => {
|
||||||
if (!section.visible || !section.items.length) return null;
|
if (!section.visible || section.items.length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id={section.id} className="grid">
|
<section id={section.id} className="grid">
|
||||||
@ -196,7 +196,7 @@ const Section = <T,>({
|
|||||||
<div>{children?.(item as T)}</div>
|
<div>{children?.(item as T)}</div>
|
||||||
|
|
||||||
{summary !== undefined && !isEmptyString(summary) && (
|
{summary !== undefined && !isEmptyString(summary) && (
|
||||||
<div className="wysiwyg" dangerouslySetInnerHTML={{ __html: summary }} />
|
<div dangerouslySetInnerHTML={{ __html: summary }} className="wysiwyg" />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{level !== undefined && level > 0 && <Rating level={level} />}
|
{level !== undefined && level > 0 && <Rating level={level} />}
|
||||||
@ -449,37 +449,51 @@ const Custom = ({ id }: { id: string }) => {
|
|||||||
|
|
||||||
const mapSectionToComponent = (section: SectionKey) => {
|
const mapSectionToComponent = (section: SectionKey) => {
|
||||||
switch (section) {
|
switch (section) {
|
||||||
case "profiles":
|
case "profiles": {
|
||||||
return <Profiles />;
|
return <Profiles />;
|
||||||
case "summary":
|
}
|
||||||
|
case "summary": {
|
||||||
return <Summary />;
|
return <Summary />;
|
||||||
case "experience":
|
}
|
||||||
|
case "experience": {
|
||||||
return <Experience />;
|
return <Experience />;
|
||||||
case "education":
|
}
|
||||||
|
case "education": {
|
||||||
return <Education />;
|
return <Education />;
|
||||||
case "awards":
|
}
|
||||||
|
case "awards": {
|
||||||
return <Awards />;
|
return <Awards />;
|
||||||
case "certifications":
|
}
|
||||||
|
case "certifications": {
|
||||||
return <Certifications />;
|
return <Certifications />;
|
||||||
case "skills":
|
}
|
||||||
|
case "skills": {
|
||||||
return <Skills />;
|
return <Skills />;
|
||||||
case "interests":
|
}
|
||||||
|
case "interests": {
|
||||||
return <Interests />;
|
return <Interests />;
|
||||||
case "publications":
|
}
|
||||||
|
case "publications": {
|
||||||
return <Publications />;
|
return <Publications />;
|
||||||
case "volunteer":
|
}
|
||||||
|
case "volunteer": {
|
||||||
return <Volunteer />;
|
return <Volunteer />;
|
||||||
case "languages":
|
}
|
||||||
|
case "languages": {
|
||||||
return <Languages />;
|
return <Languages />;
|
||||||
case "projects":
|
}
|
||||||
|
case "projects": {
|
||||||
return <Projects />;
|
return <Projects />;
|
||||||
case "references":
|
}
|
||||||
|
case "references": {
|
||||||
return <References />;
|
return <References />;
|
||||||
default:
|
}
|
||||||
|
default: {
|
||||||
if (section.startsWith("custom.")) return <Custom id={section.split(".")[1]} />;
|
if (section.startsWith("custom.")) return <Custom id={section.split(".")[1]} />;
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Azurill = ({ columns, isFirstPage = false }: TemplateProps) => {
|
export const Azurill = ({ columns, isFirstPage = false }: TemplateProps) => {
|
||||||
|
|||||||
@ -84,9 +84,9 @@ const Summary = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
dangerouslySetInnerHTML={{ __html: section.content }}
|
||||||
className="wysiwyg col-span-4"
|
className="wysiwyg col-span-4"
|
||||||
style={{ columns: section.columns }}
|
style={{ columns: section.columns }}
|
||||||
dangerouslySetInnerHTML={{ __html: section.content }}
|
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
@ -124,7 +124,7 @@ const Link = ({ url, icon, label, className }: LinkProps) => {
|
|||||||
rel="noreferrer noopener nofollow"
|
rel="noreferrer noopener nofollow"
|
||||||
className={cn("inline-block", className)}
|
className={cn("inline-block", className)}
|
||||||
>
|
>
|
||||||
{label || url.label || url.href}
|
{label ?? (url.label || url.href)}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -149,7 +149,7 @@ const Section = <T,>({
|
|||||||
summaryKey,
|
summaryKey,
|
||||||
keywordsKey,
|
keywordsKey,
|
||||||
}: SectionProps<T>) => {
|
}: SectionProps<T>) => {
|
||||||
if (!section.visible || !section.items.length) return null;
|
if (!section.visible || section.items.length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id={section.id} className="grid grid-cols-5 border-t pt-2.5">
|
<section id={section.id} className="grid grid-cols-5 border-t pt-2.5">
|
||||||
@ -177,7 +177,7 @@ const Section = <T,>({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{summary !== undefined && !isEmptyString(summary) && (
|
{summary !== undefined && !isEmptyString(summary) && (
|
||||||
<div className="wysiwyg" dangerouslySetInnerHTML={{ __html: summary }} />
|
<div dangerouslySetInnerHTML={{ __html: summary }} className="wysiwyg" />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{level !== undefined && level > 0 && <Rating level={level} />}
|
{level !== undefined && level > 0 && <Rating level={level} />}
|
||||||
@ -460,37 +460,51 @@ const Custom = ({ id }: { id: string }) => {
|
|||||||
|
|
||||||
const mapSectionToComponent = (section: SectionKey) => {
|
const mapSectionToComponent = (section: SectionKey) => {
|
||||||
switch (section) {
|
switch (section) {
|
||||||
case "profiles":
|
case "profiles": {
|
||||||
return <Profiles />;
|
return <Profiles />;
|
||||||
case "summary":
|
}
|
||||||
|
case "summary": {
|
||||||
return <Summary />;
|
return <Summary />;
|
||||||
case "experience":
|
}
|
||||||
|
case "experience": {
|
||||||
return <Experience />;
|
return <Experience />;
|
||||||
case "education":
|
}
|
||||||
|
case "education": {
|
||||||
return <Education />;
|
return <Education />;
|
||||||
case "awards":
|
}
|
||||||
|
case "awards": {
|
||||||
return <Awards />;
|
return <Awards />;
|
||||||
case "certifications":
|
}
|
||||||
|
case "certifications": {
|
||||||
return <Certifications />;
|
return <Certifications />;
|
||||||
case "skills":
|
}
|
||||||
|
case "skills": {
|
||||||
return <Skills />;
|
return <Skills />;
|
||||||
case "interests":
|
}
|
||||||
|
case "interests": {
|
||||||
return <Interests />;
|
return <Interests />;
|
||||||
case "publications":
|
}
|
||||||
|
case "publications": {
|
||||||
return <Publications />;
|
return <Publications />;
|
||||||
case "volunteer":
|
}
|
||||||
|
case "volunteer": {
|
||||||
return <Volunteer />;
|
return <Volunteer />;
|
||||||
case "languages":
|
}
|
||||||
|
case "languages": {
|
||||||
return <Languages />;
|
return <Languages />;
|
||||||
case "projects":
|
}
|
||||||
|
case "projects": {
|
||||||
return <Projects />;
|
return <Projects />;
|
||||||
case "references":
|
}
|
||||||
|
case "references": {
|
||||||
return <References />;
|
return <References />;
|
||||||
default:
|
}
|
||||||
|
default: {
|
||||||
if (section.startsWith("custom.")) return <Custom id={section.split(".")[1]} />;
|
if (section.startsWith("custom.")) return <Custom id={section.split(".")[1]} />;
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Bronzor = ({ columns, isFirstPage = false }: TemplateProps) => {
|
export const Bronzor = ({ columns, isFirstPage = false }: TemplateProps) => {
|
||||||
|
|||||||
@ -84,9 +84,9 @@ const Summary = () => {
|
|||||||
<h4 className="mb-2 border-b pb-0.5 text-sm font-bold">{section.name}</h4>
|
<h4 className="mb-2 border-b pb-0.5 text-sm font-bold">{section.name}</h4>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
dangerouslySetInnerHTML={{ __html: section.content }}
|
||||||
className="wysiwyg"
|
className="wysiwyg"
|
||||||
style={{ columns: section.columns }}
|
style={{ columns: section.columns }}
|
||||||
dangerouslySetInnerHTML={{ __html: section.content }}
|
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
@ -127,7 +127,7 @@ const Link = ({ url, icon, label, className }: LinkProps) => {
|
|||||||
rel="noreferrer noopener nofollow"
|
rel="noreferrer noopener nofollow"
|
||||||
className={cn("inline-block", className)}
|
className={cn("inline-block", className)}
|
||||||
>
|
>
|
||||||
{label || url.label || url.href}
|
{label ?? (url.label || url.href)}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -152,7 +152,7 @@ const Section = <T,>({
|
|||||||
summaryKey,
|
summaryKey,
|
||||||
keywordsKey,
|
keywordsKey,
|
||||||
}: SectionProps<T>) => {
|
}: SectionProps<T>) => {
|
||||||
if (!section.visible || !section.items.length) return null;
|
if (!section.visible || section.items.length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id={section.id} className="grid">
|
<section id={section.id} className="grid">
|
||||||
@ -178,7 +178,7 @@ const Section = <T,>({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{summary !== undefined && !isEmptyString(summary) && (
|
{summary !== undefined && !isEmptyString(summary) && (
|
||||||
<div className="wysiwyg" dangerouslySetInnerHTML={{ __html: summary }} />
|
<div dangerouslySetInnerHTML={{ __html: summary }} className="wysiwyg" />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{level !== undefined && level > 0 && <Rating level={level} />}
|
{level !== undefined && level > 0 && <Rating level={level} />}
|
||||||
@ -461,37 +461,51 @@ const Custom = ({ id }: { id: string }) => {
|
|||||||
|
|
||||||
const mapSectionToComponent = (section: SectionKey) => {
|
const mapSectionToComponent = (section: SectionKey) => {
|
||||||
switch (section) {
|
switch (section) {
|
||||||
case "profiles":
|
case "profiles": {
|
||||||
return <Profiles />;
|
return <Profiles />;
|
||||||
case "summary":
|
}
|
||||||
|
case "summary": {
|
||||||
return <Summary />;
|
return <Summary />;
|
||||||
case "experience":
|
}
|
||||||
|
case "experience": {
|
||||||
return <Experience />;
|
return <Experience />;
|
||||||
case "education":
|
}
|
||||||
|
case "education": {
|
||||||
return <Education />;
|
return <Education />;
|
||||||
case "awards":
|
}
|
||||||
|
case "awards": {
|
||||||
return <Awards />;
|
return <Awards />;
|
||||||
case "certifications":
|
}
|
||||||
|
case "certifications": {
|
||||||
return <Certifications />;
|
return <Certifications />;
|
||||||
case "skills":
|
}
|
||||||
|
case "skills": {
|
||||||
return <Skills />;
|
return <Skills />;
|
||||||
case "interests":
|
}
|
||||||
|
case "interests": {
|
||||||
return <Interests />;
|
return <Interests />;
|
||||||
case "publications":
|
}
|
||||||
|
case "publications": {
|
||||||
return <Publications />;
|
return <Publications />;
|
||||||
case "volunteer":
|
}
|
||||||
|
case "volunteer": {
|
||||||
return <Volunteer />;
|
return <Volunteer />;
|
||||||
case "languages":
|
}
|
||||||
|
case "languages": {
|
||||||
return <Languages />;
|
return <Languages />;
|
||||||
case "projects":
|
}
|
||||||
|
case "projects": {
|
||||||
return <Projects />;
|
return <Projects />;
|
||||||
case "references":
|
}
|
||||||
|
case "references": {
|
||||||
return <References />;
|
return <References />;
|
||||||
default:
|
}
|
||||||
|
default: {
|
||||||
if (section.startsWith("custom.")) return <Custom id={section.split(".")[1]} />;
|
if (section.startsWith("custom.")) return <Custom id={section.split(".")[1]} />;
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Chikorita = ({ columns, isFirstPage = false }: TemplateProps) => {
|
export const Chikorita = ({ columns, isFirstPage = false }: TemplateProps) => {
|
||||||
|
|||||||
@ -104,9 +104,9 @@ const Summary = () => {
|
|||||||
<h4 className="mb-2 text-base font-bold">{section.name}</h4>
|
<h4 className="mb-2 text-base font-bold">{section.name}</h4>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
dangerouslySetInnerHTML={{ __html: section.content }}
|
||||||
className="wysiwyg"
|
className="wysiwyg"
|
||||||
style={{ columns: section.columns }}
|
style={{ columns: section.columns }}
|
||||||
dangerouslySetInnerHTML={{ __html: section.content }}
|
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
@ -144,7 +144,7 @@ const Link = ({ url, icon, label, className }: LinkProps) => {
|
|||||||
rel="noreferrer noopener nofollow"
|
rel="noreferrer noopener nofollow"
|
||||||
className={cn("inline-block", className)}
|
className={cn("inline-block", className)}
|
||||||
>
|
>
|
||||||
{label || url.label || url.href}
|
{label ?? (url.label || url.href)}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -169,7 +169,7 @@ const Section = <T,>({
|
|||||||
summaryKey,
|
summaryKey,
|
||||||
keywordsKey,
|
keywordsKey,
|
||||||
}: SectionProps<T>) => {
|
}: SectionProps<T>) => {
|
||||||
if (!section.visible || !section.items.length) return null;
|
if (!section.visible || section.items.length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id={section.id} className="grid">
|
<section id={section.id} className="grid">
|
||||||
@ -202,7 +202,7 @@ const Section = <T,>({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{summary !== undefined && !isEmptyString(summary) && (
|
{summary !== undefined && !isEmptyString(summary) && (
|
||||||
<div className="wysiwyg" dangerouslySetInnerHTML={{ __html: summary }} />
|
<div dangerouslySetInnerHTML={{ __html: summary }} className="wysiwyg" />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{level !== undefined && level > 0 && <Rating level={level} />}
|
{level !== undefined && level > 0 && <Rating level={level} />}
|
||||||
@ -487,37 +487,51 @@ const Custom = ({ id }: { id: string }) => {
|
|||||||
|
|
||||||
const mapSectionToComponent = (section: SectionKey) => {
|
const mapSectionToComponent = (section: SectionKey) => {
|
||||||
switch (section) {
|
switch (section) {
|
||||||
case "profiles":
|
case "profiles": {
|
||||||
return <Profiles />;
|
return <Profiles />;
|
||||||
case "summary":
|
}
|
||||||
|
case "summary": {
|
||||||
return <Summary />;
|
return <Summary />;
|
||||||
case "experience":
|
}
|
||||||
|
case "experience": {
|
||||||
return <Experience />;
|
return <Experience />;
|
||||||
case "education":
|
}
|
||||||
|
case "education": {
|
||||||
return <Education />;
|
return <Education />;
|
||||||
case "awards":
|
}
|
||||||
|
case "awards": {
|
||||||
return <Awards />;
|
return <Awards />;
|
||||||
case "certifications":
|
}
|
||||||
|
case "certifications": {
|
||||||
return <Certifications />;
|
return <Certifications />;
|
||||||
case "skills":
|
}
|
||||||
|
case "skills": {
|
||||||
return <Skills />;
|
return <Skills />;
|
||||||
case "interests":
|
}
|
||||||
|
case "interests": {
|
||||||
return <Interests />;
|
return <Interests />;
|
||||||
case "publications":
|
}
|
||||||
|
case "publications": {
|
||||||
return <Publications />;
|
return <Publications />;
|
||||||
case "volunteer":
|
}
|
||||||
|
case "volunteer": {
|
||||||
return <Volunteer />;
|
return <Volunteer />;
|
||||||
case "languages":
|
}
|
||||||
|
case "languages": {
|
||||||
return <Languages />;
|
return <Languages />;
|
||||||
case "projects":
|
}
|
||||||
|
case "projects": {
|
||||||
return <Projects />;
|
return <Projects />;
|
||||||
case "references":
|
}
|
||||||
|
case "references": {
|
||||||
return <References />;
|
return <References />;
|
||||||
default:
|
}
|
||||||
|
default: {
|
||||||
if (section.startsWith("custom.")) return <Custom id={section.split(".")[1]} />;
|
if (section.startsWith("custom.")) return <Custom id={section.split(".")[1]} />;
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Ditto = ({ columns, isFirstPage = false }: TemplateProps) => {
|
export const Ditto = ({ columns, isFirstPage = false }: TemplateProps) => {
|
||||||
|
|||||||
@ -82,9 +82,9 @@ const Summary = () => {
|
|||||||
return (
|
return (
|
||||||
<section id={section.id}>
|
<section id={section.id}>
|
||||||
<div
|
<div
|
||||||
|
dangerouslySetInnerHTML={{ __html: section.content }}
|
||||||
className="wysiwyg"
|
className="wysiwyg"
|
||||||
style={{ columns: section.columns }}
|
style={{ columns: section.columns }}
|
||||||
dangerouslySetInnerHTML={{ __html: section.content }}
|
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
@ -122,7 +122,7 @@ const Link = ({ url, icon, label, className }: LinkProps) => {
|
|||||||
rel="noreferrer noopener nofollow"
|
rel="noreferrer noopener nofollow"
|
||||||
className={cn("inline-block", className)}
|
className={cn("inline-block", className)}
|
||||||
>
|
>
|
||||||
{label || url.label || url.href}
|
{label ?? (url.label || url.href)}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -147,7 +147,7 @@ const Section = <T,>({
|
|||||||
summaryKey,
|
summaryKey,
|
||||||
keywordsKey,
|
keywordsKey,
|
||||||
}: SectionProps<T>) => {
|
}: SectionProps<T>) => {
|
||||||
if (!section.visible || !section.items.length) return null;
|
if (!section.visible || section.items.length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id={section.id} className="grid">
|
<section id={section.id} className="grid">
|
||||||
@ -173,7 +173,7 @@ const Section = <T,>({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{summary !== undefined && !isEmptyString(summary) && (
|
{summary !== undefined && !isEmptyString(summary) && (
|
||||||
<div className="wysiwyg" dangerouslySetInnerHTML={{ __html: summary }} />
|
<div dangerouslySetInnerHTML={{ __html: summary }} className="wysiwyg" />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{level !== undefined && level > 0 && <Rating level={level} />}
|
{level !== undefined && level > 0 && <Rating level={level} />}
|
||||||
@ -456,35 +456,48 @@ const Custom = ({ id }: { id: string }) => {
|
|||||||
|
|
||||||
const mapSectionToComponent = (section: SectionKey) => {
|
const mapSectionToComponent = (section: SectionKey) => {
|
||||||
switch (section) {
|
switch (section) {
|
||||||
case "profiles":
|
case "profiles": {
|
||||||
return <Profiles />;
|
return <Profiles />;
|
||||||
case "experience":
|
}
|
||||||
|
case "experience": {
|
||||||
return <Experience />;
|
return <Experience />;
|
||||||
case "education":
|
}
|
||||||
|
case "education": {
|
||||||
return <Education />;
|
return <Education />;
|
||||||
case "awards":
|
}
|
||||||
|
case "awards": {
|
||||||
return <Awards />;
|
return <Awards />;
|
||||||
case "certifications":
|
}
|
||||||
|
case "certifications": {
|
||||||
return <Certifications />;
|
return <Certifications />;
|
||||||
case "skills":
|
}
|
||||||
|
case "skills": {
|
||||||
return <Skills />;
|
return <Skills />;
|
||||||
case "interests":
|
}
|
||||||
|
case "interests": {
|
||||||
return <Interests />;
|
return <Interests />;
|
||||||
case "publications":
|
}
|
||||||
|
case "publications": {
|
||||||
return <Publications />;
|
return <Publications />;
|
||||||
case "volunteer":
|
}
|
||||||
|
case "volunteer": {
|
||||||
return <Volunteer />;
|
return <Volunteer />;
|
||||||
case "languages":
|
}
|
||||||
|
case "languages": {
|
||||||
return <Languages />;
|
return <Languages />;
|
||||||
case "projects":
|
}
|
||||||
|
case "projects": {
|
||||||
return <Projects />;
|
return <Projects />;
|
||||||
case "references":
|
}
|
||||||
|
case "references": {
|
||||||
return <References />;
|
return <References />;
|
||||||
default:
|
}
|
||||||
|
default: {
|
||||||
if (section.startsWith("custom.")) return <Custom id={section.split(".")[1]} />;
|
if (section.startsWith("custom.")) return <Custom id={section.split(".")[1]} />;
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Gengar = ({ columns, isFirstPage = false }: TemplateProps) => {
|
export const Gengar = ({ columns, isFirstPage = false }: TemplateProps) => {
|
||||||
|
|||||||
@ -84,9 +84,9 @@ const Summary = () => {
|
|||||||
<h4 className="mb-2 border-b pb-0.5 text-sm font-bold">{section.name}</h4>
|
<h4 className="mb-2 border-b pb-0.5 text-sm font-bold">{section.name}</h4>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
dangerouslySetInnerHTML={{ __html: section.content }}
|
||||||
className="wysiwyg"
|
className="wysiwyg"
|
||||||
style={{ columns: section.columns }}
|
style={{ columns: section.columns }}
|
||||||
dangerouslySetInnerHTML={{ __html: section.content }}
|
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
@ -130,7 +130,7 @@ const Link = ({ url, icon, label, className }: LinkProps) => {
|
|||||||
rel="noreferrer noopener nofollow"
|
rel="noreferrer noopener nofollow"
|
||||||
className={cn("inline-block", className)}
|
className={cn("inline-block", className)}
|
||||||
>
|
>
|
||||||
{label || url.label || url.href}
|
{label ?? (url.label || url.href)}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -155,7 +155,7 @@ const Section = <T,>({
|
|||||||
summaryKey,
|
summaryKey,
|
||||||
keywordsKey,
|
keywordsKey,
|
||||||
}: SectionProps<T>) => {
|
}: SectionProps<T>) => {
|
||||||
if (!section.visible || !section.items.length) return null;
|
if (!section.visible || section.items.length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id={section.id} className="grid">
|
<section id={section.id} className="grid">
|
||||||
@ -183,7 +183,7 @@ const Section = <T,>({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{summary !== undefined && !isEmptyString(summary) && (
|
{summary !== undefined && !isEmptyString(summary) && (
|
||||||
<div className="wysiwyg" dangerouslySetInnerHTML={{ __html: summary }} />
|
<div dangerouslySetInnerHTML={{ __html: summary }} className="wysiwyg" />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{level !== undefined && level > 0 && <Rating level={level} />}
|
{level !== undefined && level > 0 && <Rating level={level} />}
|
||||||
@ -466,37 +466,51 @@ const Custom = ({ id }: { id: string }) => {
|
|||||||
|
|
||||||
const mapSectionToComponent = (section: SectionKey) => {
|
const mapSectionToComponent = (section: SectionKey) => {
|
||||||
switch (section) {
|
switch (section) {
|
||||||
case "profiles":
|
case "profiles": {
|
||||||
return <Profiles />;
|
return <Profiles />;
|
||||||
case "summary":
|
}
|
||||||
|
case "summary": {
|
||||||
return <Summary />;
|
return <Summary />;
|
||||||
case "experience":
|
}
|
||||||
|
case "experience": {
|
||||||
return <Experience />;
|
return <Experience />;
|
||||||
case "education":
|
}
|
||||||
|
case "education": {
|
||||||
return <Education />;
|
return <Education />;
|
||||||
case "awards":
|
}
|
||||||
|
case "awards": {
|
||||||
return <Awards />;
|
return <Awards />;
|
||||||
case "certifications":
|
}
|
||||||
|
case "certifications": {
|
||||||
return <Certifications />;
|
return <Certifications />;
|
||||||
case "skills":
|
}
|
||||||
|
case "skills": {
|
||||||
return <Skills />;
|
return <Skills />;
|
||||||
case "interests":
|
}
|
||||||
|
case "interests": {
|
||||||
return <Interests />;
|
return <Interests />;
|
||||||
case "publications":
|
}
|
||||||
|
case "publications": {
|
||||||
return <Publications />;
|
return <Publications />;
|
||||||
case "volunteer":
|
}
|
||||||
|
case "volunteer": {
|
||||||
return <Volunteer />;
|
return <Volunteer />;
|
||||||
case "languages":
|
}
|
||||||
|
case "languages": {
|
||||||
return <Languages />;
|
return <Languages />;
|
||||||
case "projects":
|
}
|
||||||
|
case "projects": {
|
||||||
return <Projects />;
|
return <Projects />;
|
||||||
case "references":
|
}
|
||||||
|
case "references": {
|
||||||
return <References />;
|
return <References />;
|
||||||
default:
|
}
|
||||||
|
default: {
|
||||||
if (section.startsWith("custom.")) return <Custom id={section.split(".")[1]} />;
|
if (section.startsWith("custom.")) return <Custom id={section.split(".")[1]} />;
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Glalie = ({ columns, isFirstPage = false }: TemplateProps) => {
|
export const Glalie = ({ columns, isFirstPage = false }: TemplateProps) => {
|
||||||
|
|||||||
@ -15,31 +15,44 @@ import { Rhyhorn } from "./rhyhorn";
|
|||||||
|
|
||||||
export const getTemplate = (template: Template) => {
|
export const getTemplate = (template: Template) => {
|
||||||
switch (template) {
|
switch (template) {
|
||||||
case "azurill":
|
case "azurill": {
|
||||||
return Azurill;
|
return Azurill;
|
||||||
case "bronzor":
|
}
|
||||||
|
case "bronzor": {
|
||||||
return Bronzor;
|
return Bronzor;
|
||||||
case "chikorita":
|
}
|
||||||
|
case "chikorita": {
|
||||||
return Chikorita;
|
return Chikorita;
|
||||||
case "ditto":
|
}
|
||||||
|
case "ditto": {
|
||||||
return Ditto;
|
return Ditto;
|
||||||
case "gengar":
|
}
|
||||||
|
case "gengar": {
|
||||||
return Gengar;
|
return Gengar;
|
||||||
case "glalie":
|
}
|
||||||
|
case "glalie": {
|
||||||
return Glalie;
|
return Glalie;
|
||||||
case "kakuna":
|
}
|
||||||
|
case "kakuna": {
|
||||||
return Kakuna;
|
return Kakuna;
|
||||||
case "leafish":
|
}
|
||||||
|
case "leafish": {
|
||||||
return Leafish;
|
return Leafish;
|
||||||
case "nosepass":
|
}
|
||||||
|
case "nosepass": {
|
||||||
return Nosepass;
|
return Nosepass;
|
||||||
case "onyx":
|
}
|
||||||
return Onyx;
|
case "onyx": {
|
||||||
case "pikachu":
|
|
||||||
return Pikachu;
|
|
||||||
case "rhyhorn":
|
|
||||||
return Rhyhorn;
|
|
||||||
default:
|
|
||||||
return Onyx;
|
return Onyx;
|
||||||
}
|
}
|
||||||
|
case "pikachu": {
|
||||||
|
return Pikachu;
|
||||||
|
}
|
||||||
|
case "rhyhorn": {
|
||||||
|
return Rhyhorn;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return Onyx;
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -110,9 +110,9 @@ const Summary = () => {
|
|||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
dangerouslySetInnerHTML={{ __html: section.content }}
|
||||||
className="wysiwyg"
|
className="wysiwyg"
|
||||||
style={{ columns: section.columns }}
|
style={{ columns: section.columns }}
|
||||||
dangerouslySetInnerHTML={{ __html: section.content }}
|
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
@ -150,7 +150,7 @@ const Link = ({ url, icon, label, className }: LinkProps) => {
|
|||||||
rel="noreferrer noopener nofollow"
|
rel="noreferrer noopener nofollow"
|
||||||
className={cn("inline-block", className)}
|
className={cn("inline-block", className)}
|
||||||
>
|
>
|
||||||
{label || url.label || url.href}
|
{label ?? (url.label || url.href)}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -175,7 +175,7 @@ const Section = <T,>({
|
|||||||
summaryKey,
|
summaryKey,
|
||||||
keywordsKey,
|
keywordsKey,
|
||||||
}: SectionProps<T>) => {
|
}: SectionProps<T>) => {
|
||||||
if (!section.visible || !section.items.length) return null;
|
if (!section.visible || section.items.length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id={section.id} className="grid">
|
<section id={section.id} className="grid">
|
||||||
@ -200,7 +200,7 @@ const Section = <T,>({
|
|||||||
<div>{children?.(item as T)}</div>
|
<div>{children?.(item as T)}</div>
|
||||||
|
|
||||||
{summary !== undefined && !isEmptyString(summary) && (
|
{summary !== undefined && !isEmptyString(summary) && (
|
||||||
<div className="wysiwyg" dangerouslySetInnerHTML={{ __html: summary }} />
|
<div dangerouslySetInnerHTML={{ __html: summary }} className="wysiwyg" />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{level !== undefined && level > 0 && <Rating level={level} />}
|
{level !== undefined && level > 0 && <Rating level={level} />}
|
||||||
@ -417,35 +417,48 @@ const Custom = ({ id }: { id: string }) => {
|
|||||||
|
|
||||||
const mapSectionToComponent = (section: SectionKey) => {
|
const mapSectionToComponent = (section: SectionKey) => {
|
||||||
switch (section) {
|
switch (section) {
|
||||||
case "summary":
|
case "summary": {
|
||||||
return <Summary />;
|
return <Summary />;
|
||||||
case "experience":
|
}
|
||||||
|
case "experience": {
|
||||||
return <Experience />;
|
return <Experience />;
|
||||||
case "education":
|
}
|
||||||
|
case "education": {
|
||||||
return <Education />;
|
return <Education />;
|
||||||
case "awards":
|
}
|
||||||
|
case "awards": {
|
||||||
return <Awards />;
|
return <Awards />;
|
||||||
case "certifications":
|
}
|
||||||
|
case "certifications": {
|
||||||
return <Certifications />;
|
return <Certifications />;
|
||||||
case "skills":
|
}
|
||||||
|
case "skills": {
|
||||||
return <Skills />;
|
return <Skills />;
|
||||||
case "interests":
|
}
|
||||||
|
case "interests": {
|
||||||
return <Interests />;
|
return <Interests />;
|
||||||
case "publications":
|
}
|
||||||
|
case "publications": {
|
||||||
return <Publications />;
|
return <Publications />;
|
||||||
case "volunteer":
|
}
|
||||||
|
case "volunteer": {
|
||||||
return <Volunteer />;
|
return <Volunteer />;
|
||||||
case "languages":
|
}
|
||||||
|
case "languages": {
|
||||||
return <Languages />;
|
return <Languages />;
|
||||||
case "projects":
|
}
|
||||||
|
case "projects": {
|
||||||
return <Projects />;
|
return <Projects />;
|
||||||
case "references":
|
}
|
||||||
|
case "references": {
|
||||||
return <References />;
|
return <References />;
|
||||||
default:
|
}
|
||||||
|
default: {
|
||||||
if (section.startsWith("custom.")) return <Custom id={section.split(".")[1]} />;
|
if (section.startsWith("custom.")) return <Custom id={section.split(".")[1]} />;
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Kakuna = ({ columns, isFirstPage = false }: TemplateProps) => {
|
export const Kakuna = ({ columns, isFirstPage = false }: TemplateProps) => {
|
||||||
|
|||||||
@ -44,9 +44,9 @@ const Header = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
dangerouslySetInnerHTML={{ __html: section.content }}
|
||||||
className="wysiwyg"
|
className="wysiwyg"
|
||||||
style={{ columns: section.columns }}
|
style={{ columns: section.columns }}
|
||||||
dangerouslySetInnerHTML={{ __html: section.content }}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -147,7 +147,7 @@ const Link = ({ url, icon, label, className }: LinkProps) => {
|
|||||||
rel="noreferrer noopener nofollow"
|
rel="noreferrer noopener nofollow"
|
||||||
className={cn("inline-block", className)}
|
className={cn("inline-block", className)}
|
||||||
>
|
>
|
||||||
{label || url.label || url.href}
|
{label ?? (url.label || url.href)}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -172,7 +172,7 @@ const Section = <T,>({
|
|||||||
summaryKey,
|
summaryKey,
|
||||||
keywordsKey,
|
keywordsKey,
|
||||||
}: SectionProps<T>) => {
|
}: SectionProps<T>) => {
|
||||||
if (!section.visible || !section.items.length) return null;
|
if (!section.visible || section.items.length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id={section.id} className="grid">
|
<section id={section.id} className="grid">
|
||||||
@ -197,7 +197,7 @@ const Section = <T,>({
|
|||||||
<div>{children?.(item as T)}</div>
|
<div>{children?.(item as T)}</div>
|
||||||
|
|
||||||
{summary !== undefined && !isEmptyString(summary) && (
|
{summary !== undefined && !isEmptyString(summary) && (
|
||||||
<div className="wysiwyg" dangerouslySetInnerHTML={{ __html: summary }} />
|
<div dangerouslySetInnerHTML={{ __html: summary }} className="wysiwyg" />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{level !== undefined && level > 0 && <Rating level={level} />}
|
{level !== undefined && level > 0 && <Rating level={level} />}
|
||||||
@ -414,33 +414,45 @@ const Custom = ({ id }: { id: string }) => {
|
|||||||
|
|
||||||
const mapSectionToComponent = (section: SectionKey) => {
|
const mapSectionToComponent = (section: SectionKey) => {
|
||||||
switch (section) {
|
switch (section) {
|
||||||
case "experience":
|
case "experience": {
|
||||||
return <Experience />;
|
return <Experience />;
|
||||||
case "education":
|
}
|
||||||
|
case "education": {
|
||||||
return <Education />;
|
return <Education />;
|
||||||
case "awards":
|
}
|
||||||
|
case "awards": {
|
||||||
return <Awards />;
|
return <Awards />;
|
||||||
case "certifications":
|
}
|
||||||
|
case "certifications": {
|
||||||
return <Certifications />;
|
return <Certifications />;
|
||||||
case "skills":
|
}
|
||||||
|
case "skills": {
|
||||||
return <Skills />;
|
return <Skills />;
|
||||||
case "interests":
|
}
|
||||||
|
case "interests": {
|
||||||
return <Interests />;
|
return <Interests />;
|
||||||
case "publications":
|
}
|
||||||
|
case "publications": {
|
||||||
return <Publications />;
|
return <Publications />;
|
||||||
case "volunteer":
|
}
|
||||||
|
case "volunteer": {
|
||||||
return <Volunteer />;
|
return <Volunteer />;
|
||||||
case "languages":
|
}
|
||||||
|
case "languages": {
|
||||||
return <Languages />;
|
return <Languages />;
|
||||||
case "projects":
|
}
|
||||||
|
case "projects": {
|
||||||
return <Projects />;
|
return <Projects />;
|
||||||
case "references":
|
}
|
||||||
|
case "references": {
|
||||||
return <References />;
|
return <References />;
|
||||||
default:
|
}
|
||||||
|
default: {
|
||||||
if (section.startsWith("custom.")) return <Custom id={section.split(".")[1]} />;
|
if (section.startsWith("custom.")) return <Custom id={section.split(".")[1]} />;
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Leafish = ({ columns, isFirstPage = false }: TemplateProps) => {
|
export const Leafish = ({ columns, isFirstPage = false }: TemplateProps) => {
|
||||||
|
|||||||
@ -98,9 +98,9 @@ const Summary = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
dangerouslySetInnerHTML={{ __html: section.content }}
|
||||||
className="wysiwyg"
|
className="wysiwyg"
|
||||||
style={{ columns: section.columns }}
|
style={{ columns: section.columns }}
|
||||||
dangerouslySetInnerHTML={{ __html: section.content }}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@ -126,7 +126,7 @@ const Link = ({ url, icon, label, className }: LinkProps) => {
|
|||||||
rel="noreferrer noopener nofollow"
|
rel="noreferrer noopener nofollow"
|
||||||
className={cn("inline-block", className)}
|
className={cn("inline-block", className)}
|
||||||
>
|
>
|
||||||
{label || url.label || url.href}
|
{label ?? (url.label || url.href)}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -150,7 +150,7 @@ const Section = <T,>({
|
|||||||
summaryKey,
|
summaryKey,
|
||||||
keywordsKey,
|
keywordsKey,
|
||||||
}: SectionProps<T>) => {
|
}: SectionProps<T>) => {
|
||||||
if (!section.visible || !section.items.length) return null;
|
if (!section.visible || section.items.length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id={section.id} className={cn("grid", dateKey !== undefined && "gap-y-4")}>
|
<section id={section.id} className={cn("grid", dateKey !== undefined && "gap-y-4")}>
|
||||||
@ -187,7 +187,7 @@ const Section = <T,>({
|
|||||||
{url !== undefined && <Link url={url} />}
|
{url !== undefined && <Link url={url} />}
|
||||||
|
|
||||||
{summary !== undefined && !isEmptyString(summary) && (
|
{summary !== undefined && !isEmptyString(summary) && (
|
||||||
<div className="wysiwyg" dangerouslySetInnerHTML={{ __html: summary }} />
|
<div dangerouslySetInnerHTML={{ __html: summary }} className="wysiwyg" />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{keywords !== undefined && keywords.length > 0 && (
|
{keywords !== undefined && keywords.length > 0 && (
|
||||||
@ -222,7 +222,7 @@ const Section = <T,>({
|
|||||||
{url !== undefined && <Link url={url} />}
|
{url !== undefined && <Link url={url} />}
|
||||||
|
|
||||||
{summary !== undefined && !isEmptyString(summary) && (
|
{summary !== undefined && !isEmptyString(summary) && (
|
||||||
<div className="wysiwyg" dangerouslySetInnerHTML={{ __html: summary }} />
|
<div dangerouslySetInnerHTML={{ __html: summary }} className="wysiwyg" />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{keywords !== undefined && keywords.length > 0 && (
|
{keywords !== undefined && keywords.length > 0 && (
|
||||||
@ -464,37 +464,51 @@ const Custom = ({ id }: { id: string }) => {
|
|||||||
|
|
||||||
const mapSectionToComponent = (section: SectionKey) => {
|
const mapSectionToComponent = (section: SectionKey) => {
|
||||||
switch (section) {
|
switch (section) {
|
||||||
case "profiles":
|
case "profiles": {
|
||||||
return <Profiles />;
|
return <Profiles />;
|
||||||
case "summary":
|
}
|
||||||
|
case "summary": {
|
||||||
return <Summary />;
|
return <Summary />;
|
||||||
case "experience":
|
}
|
||||||
|
case "experience": {
|
||||||
return <Experience />;
|
return <Experience />;
|
||||||
case "education":
|
}
|
||||||
|
case "education": {
|
||||||
return <Education />;
|
return <Education />;
|
||||||
case "awards":
|
}
|
||||||
|
case "awards": {
|
||||||
return <Awards />;
|
return <Awards />;
|
||||||
case "certifications":
|
}
|
||||||
|
case "certifications": {
|
||||||
return <Certifications />;
|
return <Certifications />;
|
||||||
case "skills":
|
}
|
||||||
|
case "skills": {
|
||||||
return <Skills />;
|
return <Skills />;
|
||||||
case "interests":
|
}
|
||||||
|
case "interests": {
|
||||||
return <Interests />;
|
return <Interests />;
|
||||||
case "publications":
|
}
|
||||||
|
case "publications": {
|
||||||
return <Publications />;
|
return <Publications />;
|
||||||
case "volunteer":
|
}
|
||||||
|
case "volunteer": {
|
||||||
return <Volunteer />;
|
return <Volunteer />;
|
||||||
case "languages":
|
}
|
||||||
|
case "languages": {
|
||||||
return <Languages />;
|
return <Languages />;
|
||||||
case "projects":
|
}
|
||||||
|
case "projects": {
|
||||||
return <Projects />;
|
return <Projects />;
|
||||||
case "references":
|
}
|
||||||
|
case "references": {
|
||||||
return <References />;
|
return <References />;
|
||||||
default:
|
}
|
||||||
|
default: {
|
||||||
if (section.startsWith("custom.")) return <Custom id={section.split(".")[1]} />;
|
if (section.startsWith("custom.")) return <Custom id={section.split(".")[1]} />;
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Nosepass = ({ columns, isFirstPage = false }: TemplateProps) => {
|
export const Nosepass = ({ columns, isFirstPage = false }: TemplateProps) => {
|
||||||
|
|||||||
@ -113,9 +113,9 @@ const Summary = () => {
|
|||||||
<h4 className="font-bold text-primary">{section.name}</h4>
|
<h4 className="font-bold text-primary">{section.name}</h4>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
dangerouslySetInnerHTML={{ __html: section.content }}
|
||||||
className="wysiwyg"
|
className="wysiwyg"
|
||||||
style={{ columns: section.columns }}
|
style={{ columns: section.columns }}
|
||||||
dangerouslySetInnerHTML={{ __html: section.content }}
|
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
@ -153,7 +153,7 @@ const Link = ({ url, icon, label, className }: LinkProps) => {
|
|||||||
rel="noreferrer noopener nofollow"
|
rel="noreferrer noopener nofollow"
|
||||||
className={cn("inline-block", className)}
|
className={cn("inline-block", className)}
|
||||||
>
|
>
|
||||||
{label || url.label || url.href}
|
{label ?? (url.label || url.href)}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -178,7 +178,7 @@ const Section = <T,>({
|
|||||||
summaryKey,
|
summaryKey,
|
||||||
keywordsKey,
|
keywordsKey,
|
||||||
}: SectionProps<T>) => {
|
}: SectionProps<T>) => {
|
||||||
if (!section.visible || !section.items.length) return null;
|
if (!section.visible || section.items.length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id={section.id} className="grid">
|
<section id={section.id} className="grid">
|
||||||
@ -204,7 +204,7 @@ const Section = <T,>({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{summary !== undefined && !isEmptyString(summary) && (
|
{summary !== undefined && !isEmptyString(summary) && (
|
||||||
<div className="wysiwyg" dangerouslySetInnerHTML={{ __html: summary }} />
|
<div dangerouslySetInnerHTML={{ __html: summary }} className="wysiwyg" />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{level !== undefined && level > 0 && <Rating level={level} />}
|
{level !== undefined && level > 0 && <Rating level={level} />}
|
||||||
@ -455,35 +455,48 @@ const Custom = ({ id }: { id: string }) => {
|
|||||||
|
|
||||||
const mapSectionToComponent = (section: SectionKey) => {
|
const mapSectionToComponent = (section: SectionKey) => {
|
||||||
switch (section) {
|
switch (section) {
|
||||||
case "summary":
|
case "summary": {
|
||||||
return <Summary />;
|
return <Summary />;
|
||||||
case "experience":
|
}
|
||||||
|
case "experience": {
|
||||||
return <Experience />;
|
return <Experience />;
|
||||||
case "education":
|
}
|
||||||
|
case "education": {
|
||||||
return <Education />;
|
return <Education />;
|
||||||
case "awards":
|
}
|
||||||
|
case "awards": {
|
||||||
return <Awards />;
|
return <Awards />;
|
||||||
case "certifications":
|
}
|
||||||
|
case "certifications": {
|
||||||
return <Certifications />;
|
return <Certifications />;
|
||||||
case "skills":
|
}
|
||||||
|
case "skills": {
|
||||||
return <Skills />;
|
return <Skills />;
|
||||||
case "interests":
|
}
|
||||||
|
case "interests": {
|
||||||
return <Interests />;
|
return <Interests />;
|
||||||
case "publications":
|
}
|
||||||
|
case "publications": {
|
||||||
return <Publications />;
|
return <Publications />;
|
||||||
case "volunteer":
|
}
|
||||||
|
case "volunteer": {
|
||||||
return <Volunteer />;
|
return <Volunteer />;
|
||||||
case "languages":
|
}
|
||||||
|
case "languages": {
|
||||||
return <Languages />;
|
return <Languages />;
|
||||||
case "projects":
|
}
|
||||||
|
case "projects": {
|
||||||
return <Projects />;
|
return <Projects />;
|
||||||
case "references":
|
}
|
||||||
|
case "references": {
|
||||||
return <References />;
|
return <References />;
|
||||||
default:
|
}
|
||||||
|
default: {
|
||||||
if (section.startsWith("custom.")) return <Custom id={section.split(".")[1]} />;
|
if (section.startsWith("custom.")) return <Custom id={section.split(".")[1]} />;
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Onyx = ({ columns, isFirstPage = false }: TemplateProps) => {
|
export const Onyx = ({ columns, isFirstPage = false }: TemplateProps) => {
|
||||||
|
|||||||
@ -105,9 +105,9 @@ const Summary = () => {
|
|||||||
<h4 className="mb-2 border-b border-primary text-base font-bold">{section.name}</h4>
|
<h4 className="mb-2 border-b border-primary text-base font-bold">{section.name}</h4>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
dangerouslySetInnerHTML={{ __html: section.content }}
|
||||||
className="wysiwyg"
|
className="wysiwyg"
|
||||||
style={{ columns: section.columns }}
|
style={{ columns: section.columns }}
|
||||||
dangerouslySetInnerHTML={{ __html: section.content }}
|
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
@ -153,7 +153,7 @@ const Link = ({ url, icon, label, className }: LinkProps) => {
|
|||||||
rel="noreferrer noopener nofollow"
|
rel="noreferrer noopener nofollow"
|
||||||
className={cn("inline-block", className)}
|
className={cn("inline-block", className)}
|
||||||
>
|
>
|
||||||
{label || url.label || url.href}
|
{label ?? (url.label || url.href)}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -178,7 +178,7 @@ const Section = <T,>({
|
|||||||
summaryKey,
|
summaryKey,
|
||||||
keywordsKey,
|
keywordsKey,
|
||||||
}: SectionProps<T>) => {
|
}: SectionProps<T>) => {
|
||||||
if (!section.visible || !section.items.length) return null;
|
if (!section.visible || section.items.length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id={section.id} className="grid">
|
<section id={section.id} className="grid">
|
||||||
@ -204,7 +204,7 @@ const Section = <T,>({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{summary !== undefined && !isEmptyString(summary) && (
|
{summary !== undefined && !isEmptyString(summary) && (
|
||||||
<div className="wysiwyg" dangerouslySetInnerHTML={{ __html: summary }} />
|
<div dangerouslySetInnerHTML={{ __html: summary }} className="wysiwyg" />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{level !== undefined && level > 0 && <Rating level={level} />}
|
{level !== undefined && level > 0 && <Rating level={level} />}
|
||||||
@ -487,37 +487,51 @@ const Custom = ({ id }: { id: string }) => {
|
|||||||
|
|
||||||
const mapSectionToComponent = (section: SectionKey) => {
|
const mapSectionToComponent = (section: SectionKey) => {
|
||||||
switch (section) {
|
switch (section) {
|
||||||
case "profiles":
|
case "profiles": {
|
||||||
return <Profiles />;
|
return <Profiles />;
|
||||||
case "summary":
|
}
|
||||||
|
case "summary": {
|
||||||
return <Summary />;
|
return <Summary />;
|
||||||
case "experience":
|
}
|
||||||
|
case "experience": {
|
||||||
return <Experience />;
|
return <Experience />;
|
||||||
case "education":
|
}
|
||||||
|
case "education": {
|
||||||
return <Education />;
|
return <Education />;
|
||||||
case "awards":
|
}
|
||||||
|
case "awards": {
|
||||||
return <Awards />;
|
return <Awards />;
|
||||||
case "certifications":
|
}
|
||||||
|
case "certifications": {
|
||||||
return <Certifications />;
|
return <Certifications />;
|
||||||
case "skills":
|
}
|
||||||
|
case "skills": {
|
||||||
return <Skills />;
|
return <Skills />;
|
||||||
case "interests":
|
}
|
||||||
|
case "interests": {
|
||||||
return <Interests />;
|
return <Interests />;
|
||||||
case "publications":
|
}
|
||||||
|
case "publications": {
|
||||||
return <Publications />;
|
return <Publications />;
|
||||||
case "volunteer":
|
}
|
||||||
|
case "volunteer": {
|
||||||
return <Volunteer />;
|
return <Volunteer />;
|
||||||
case "languages":
|
}
|
||||||
|
case "languages": {
|
||||||
return <Languages />;
|
return <Languages />;
|
||||||
case "projects":
|
}
|
||||||
|
case "projects": {
|
||||||
return <Projects />;
|
return <Projects />;
|
||||||
case "references":
|
}
|
||||||
|
case "references": {
|
||||||
return <References />;
|
return <References />;
|
||||||
default:
|
}
|
||||||
|
default: {
|
||||||
if (section.startsWith("custom.")) return <Custom id={section.split(".")[1]} />;
|
if (section.startsWith("custom.")) return <Custom id={section.split(".")[1]} />;
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Pikachu = ({ columns, isFirstPage = false }: TemplateProps) => {
|
export const Pikachu = ({ columns, isFirstPage = false }: TemplateProps) => {
|
||||||
|
|||||||
@ -85,9 +85,9 @@ const Summary = () => {
|
|||||||
<h4 className="mb-2 border-b pb-0.5 text-sm font-bold">{section.name}</h4>
|
<h4 className="mb-2 border-b pb-0.5 text-sm font-bold">{section.name}</h4>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
dangerouslySetInnerHTML={{ __html: section.content }}
|
||||||
className="wysiwyg"
|
className="wysiwyg"
|
||||||
style={{ columns: section.columns }}
|
style={{ columns: section.columns }}
|
||||||
dangerouslySetInnerHTML={{ __html: section.content }}
|
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
@ -125,7 +125,7 @@ const Link = ({ url, icon, label, className }: LinkProps) => {
|
|||||||
rel="noreferrer noopener nofollow"
|
rel="noreferrer noopener nofollow"
|
||||||
className={cn("inline-block", className)}
|
className={cn("inline-block", className)}
|
||||||
>
|
>
|
||||||
{label || url.label || url.href}
|
{label ?? (url.label || url.href)}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -150,7 +150,7 @@ const Section = <T,>({
|
|||||||
summaryKey,
|
summaryKey,
|
||||||
keywordsKey,
|
keywordsKey,
|
||||||
}: SectionProps<T>) => {
|
}: SectionProps<T>) => {
|
||||||
if (!section.visible || !section.items.length) return null;
|
if (!section.visible || section.items.length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id={section.id} className="grid">
|
<section id={section.id} className="grid">
|
||||||
@ -176,7 +176,7 @@ const Section = <T,>({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{summary !== undefined && !isEmptyString(summary) && (
|
{summary !== undefined && !isEmptyString(summary) && (
|
||||||
<div className="wysiwyg" dangerouslySetInnerHTML={{ __html: summary }} />
|
<div dangerouslySetInnerHTML={{ __html: summary }} className="wysiwyg" />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{level !== undefined && level > 0 && <Rating level={level} />}
|
{level !== undefined && level > 0 && <Rating level={level} />}
|
||||||
@ -459,37 +459,51 @@ const Custom = ({ id }: { id: string }) => {
|
|||||||
|
|
||||||
const mapSectionToComponent = (section: SectionKey) => {
|
const mapSectionToComponent = (section: SectionKey) => {
|
||||||
switch (section) {
|
switch (section) {
|
||||||
case "profiles":
|
case "profiles": {
|
||||||
return <Profiles />;
|
return <Profiles />;
|
||||||
case "summary":
|
}
|
||||||
|
case "summary": {
|
||||||
return <Summary />;
|
return <Summary />;
|
||||||
case "experience":
|
}
|
||||||
|
case "experience": {
|
||||||
return <Experience />;
|
return <Experience />;
|
||||||
case "education":
|
}
|
||||||
|
case "education": {
|
||||||
return <Education />;
|
return <Education />;
|
||||||
case "awards":
|
}
|
||||||
|
case "awards": {
|
||||||
return <Awards />;
|
return <Awards />;
|
||||||
case "certifications":
|
}
|
||||||
|
case "certifications": {
|
||||||
return <Certifications />;
|
return <Certifications />;
|
||||||
case "skills":
|
}
|
||||||
|
case "skills": {
|
||||||
return <Skills />;
|
return <Skills />;
|
||||||
case "interests":
|
}
|
||||||
|
case "interests": {
|
||||||
return <Interests />;
|
return <Interests />;
|
||||||
case "publications":
|
}
|
||||||
|
case "publications": {
|
||||||
return <Publications />;
|
return <Publications />;
|
||||||
case "volunteer":
|
}
|
||||||
|
case "volunteer": {
|
||||||
return <Volunteer />;
|
return <Volunteer />;
|
||||||
case "languages":
|
}
|
||||||
|
case "languages": {
|
||||||
return <Languages />;
|
return <Languages />;
|
||||||
case "projects":
|
}
|
||||||
|
case "projects": {
|
||||||
return <Projects />;
|
return <Projects />;
|
||||||
case "references":
|
}
|
||||||
|
case "references": {
|
||||||
return <References />;
|
return <References />;
|
||||||
default:
|
}
|
||||||
|
default: {
|
||||||
if (section.startsWith("custom.")) return <Custom id={section.split(".")[1]} />;
|
if (section.startsWith("custom.")) return <Custom id={section.split(".")[1]} />;
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Rhyhorn = ({ columns, isFirstPage = false }: TemplateProps) => {
|
export const Rhyhorn = ({ columns, isFirstPage = false }: TemplateProps) => {
|
||||||
|
|||||||
@ -4,8 +4,3 @@ export type TemplateProps = {
|
|||||||
columns: SectionKey[][];
|
columns: SectionKey[][];
|
||||||
isFirstPage?: boolean;
|
isFirstPage?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type BaseProps = {
|
|
||||||
children?: React.ReactNode;
|
|
||||||
className?: string;
|
|
||||||
};
|
|
||||||
|
|||||||
@ -16,7 +16,7 @@ export default defineConfig({
|
|||||||
|
|
||||||
server: {
|
server: {
|
||||||
host: true,
|
host: true,
|
||||||
port: +(process.env.__DEV__ARTBOARD_PORT ?? 6173),
|
port: 6173,
|
||||||
fs: { allow: [searchForWorkspaceRoot(process.cwd())] },
|
fs: { allow: [searchForWorkspaceRoot(process.cwd())] },
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@ -16,6 +16,18 @@
|
|||||||
},
|
},
|
||||||
"plugins": ["lingui"],
|
"plugins": ["lingui"],
|
||||||
"rules": {
|
"rules": {
|
||||||
|
// react
|
||||||
|
"react/no-unescaped-entities": "off",
|
||||||
|
"react/jsx-sort-props": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"reservedFirst": true,
|
||||||
|
"callbacksLast": true,
|
||||||
|
"shorthandFirst": true,
|
||||||
|
"noSortAlphabetically": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
// react-hooks
|
// react-hooks
|
||||||
"react-hooks/exhaustive-deps": "off",
|
"react-hooks/exhaustive-deps": "off",
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
const { join } = require("path");
|
const path = require("node:path");
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: {
|
plugins: {
|
||||||
"postcss-import": {},
|
"postcss-import": {},
|
||||||
"tailwindcss/nesting": {},
|
"tailwindcss/nesting": {},
|
||||||
tailwindcss: { config: join(__dirname, "tailwind.config.js") },
|
tailwindcss: { config: path.join(__dirname, "tailwind.config.js") },
|
||||||
autoprefixer: {},
|
autoprefixer: {},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -54,7 +54,7 @@ export const AiActions = ({ value, onChange, className }: Props) => {
|
|||||||
toast({
|
toast({
|
||||||
variant: "error",
|
variant: "error",
|
||||||
title: t`Oops, the server returned an error.`,
|
title: t`Oops, the server returned an error.`,
|
||||||
description: (error as Error)?.message,
|
description: (error as Error).message,
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|||||||
@ -27,10 +27,7 @@ export const Copyright = ({ className }: Props) => (
|
|||||||
<span>{t`By the community, for the community.`}</span>
|
<span>{t`By the community, for the community.`}</span>
|
||||||
<span>
|
<span>
|
||||||
<Trans>
|
<Trans>
|
||||||
A passion project by{" "}
|
A passion project by <a href="https://www.amruthpillai.com/">Amruth Pillai</a>
|
||||||
<a target="_blank" rel="noopener noreferrer nofollow" href="https://www.amruthpillai.com/">
|
|
||||||
Amruth Pillai
|
|
||||||
</a>
|
|
||||||
</Trans>
|
</Trans>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
|||||||
@ -12,13 +12,15 @@ export const Icon = ({ size = 32, className }: Props) => {
|
|||||||
let src = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
|
let src = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
|
||||||
|
|
||||||
switch (isDarkMode) {
|
switch (isDarkMode) {
|
||||||
case false:
|
case false: {
|
||||||
src = "/icon/dark.svg";
|
src = "/icon/dark.svg";
|
||||||
break;
|
break;
|
||||||
case true:
|
}
|
||||||
|
case true: {
|
||||||
src = "/icon/light.svg";
|
src = "/icon/light.svg";
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<img
|
<img
|
||||||
|
|||||||
@ -38,8 +38,8 @@ export const LocaleCombobox = ({ value, onValueChange }: Props) => {
|
|||||||
<Command shouldFilter={false}>
|
<Command shouldFilter={false}>
|
||||||
<CommandInput
|
<CommandInput
|
||||||
value={search}
|
value={search}
|
||||||
onValueChange={setSearch}
|
|
||||||
placeholder={t`Search for a language`}
|
placeholder={t`Search for a language`}
|
||||||
|
onValueChange={setSearch}
|
||||||
/>
|
/>
|
||||||
<CommandList>
|
<CommandList>
|
||||||
<CommandEmpty>{t`No results found`}</CommandEmpty>
|
<CommandEmpty>{t`No results found`}</CommandEmpty>
|
||||||
@ -48,10 +48,10 @@ export const LocaleCombobox = ({ value, onValueChange }: Props) => {
|
|||||||
<div className="max-h-60">
|
<div className="max-h-60">
|
||||||
{options.map(({ original }) => (
|
{options.map(({ original }) => (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
disabled={false}
|
|
||||||
key={original.locale}
|
key={original.locale}
|
||||||
|
disabled={false}
|
||||||
value={original.locale.trim()}
|
value={original.locale.trim()}
|
||||||
onSelect={async (selectedValue) => {
|
onSelect={(selectedValue) => {
|
||||||
const result = options.find(
|
const result = options.find(
|
||||||
({ original }) => original.locale.trim() === selectedValue,
|
({ original }) => original.locale.trim() === selectedValue,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -12,13 +12,15 @@ export const Logo = ({ size = 32, className }: Props) => {
|
|||||||
let src = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
|
let src = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
|
||||||
|
|
||||||
switch (isDarkMode) {
|
switch (isDarkMode) {
|
||||||
case false:
|
case false: {
|
||||||
src = "/logo/light.svg";
|
src = "/logo/light.svg";
|
||||||
break;
|
break;
|
||||||
case true:
|
}
|
||||||
|
case true: {
|
||||||
src = "/logo/dark.svg";
|
src = "/logo/dark.svg";
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<img
|
<img
|
||||||
|
|||||||
@ -12,9 +12,18 @@ export const UserAvatar = ({ size = 36, className }: Props) => {
|
|||||||
|
|
||||||
if (!user) return null;
|
if (!user) return null;
|
||||||
|
|
||||||
let picture: React.ReactNode = null;
|
let picture: React.ReactNode;
|
||||||
|
|
||||||
if (!user.picture) {
|
if (user.picture) {
|
||||||
|
picture = (
|
||||||
|
<img
|
||||||
|
alt={user.name}
|
||||||
|
src={user.picture}
|
||||||
|
className="rounded-full"
|
||||||
|
style={{ width: size, height: size }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
const initials = getInitials(user.name);
|
const initials = getInitials(user.name);
|
||||||
|
|
||||||
picture = (
|
picture = (
|
||||||
@ -25,15 +34,6 @@ export const UserAvatar = ({ size = 36, className }: Props) => {
|
|||||||
{initials}
|
{initials}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
picture = (
|
|
||||||
<img
|
|
||||||
alt={user.name}
|
|
||||||
src={user.picture}
|
|
||||||
className="rounded-full"
|
|
||||||
style={{ width: size, height: size }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div className={className}>{picture}</div>;
|
return <div className={className}>{picture}</div>;
|
||||||
|
|||||||
@ -24,7 +24,11 @@ export const UserOptions = ({ children }: Props) => {
|
|||||||
<DropdownMenuTrigger asChild>{children}</DropdownMenuTrigger>
|
<DropdownMenuTrigger asChild>{children}</DropdownMenuTrigger>
|
||||||
|
|
||||||
<DropdownMenuContent side="top" align="start" className="w-48">
|
<DropdownMenuContent side="top" align="start" className="w-48">
|
||||||
<DropdownMenuItem onClick={() => navigate("/dashboard/settings")}>
|
<DropdownMenuItem
|
||||||
|
onClick={() => {
|
||||||
|
navigate("/dashboard/settings");
|
||||||
|
}}
|
||||||
|
>
|
||||||
{t`Settings`}
|
{t`Settings`}
|
||||||
{/* eslint-disable-next-line lingui/no-unlocalized-strings */}
|
{/* eslint-disable-next-line lingui/no-unlocalized-strings */}
|
||||||
<KeyboardShortcut>⇧S</KeyboardShortcut>
|
<KeyboardShortcut>⇧S</KeyboardShortcut>
|
||||||
|
|||||||
@ -40,9 +40,9 @@ type Action =
|
|||||||
toastId?: ToasterToast["id"];
|
toastId?: ToasterToast["id"];
|
||||||
};
|
};
|
||||||
|
|
||||||
interface State {
|
type State = {
|
||||||
toasts: ToasterToast[];
|
toasts: ToasterToast[];
|
||||||
}
|
};
|
||||||
|
|
||||||
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
|
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
|
||||||
|
|
||||||
@ -64,17 +64,19 @@ const addToRemoveQueue = (toastId: string) => {
|
|||||||
|
|
||||||
export const reducer = (state: State, action: Action): State => {
|
export const reducer = (state: State, action: Action): State => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case "ADD_TOAST":
|
case "ADD_TOAST": {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
|
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
case "UPDATE_TOAST":
|
case "UPDATE_TOAST": {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
toasts: state.toasts.map((t) => (t.id === action.toast.id ? { ...t, ...action.toast } : t)),
|
toasts: state.toasts.map((t) => (t.id === action.toast.id ? { ...t, ...action.toast } : t)),
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
case "DISMISS_TOAST": {
|
case "DISMISS_TOAST": {
|
||||||
const { toastId } = action;
|
const { toastId } = action;
|
||||||
@ -82,9 +84,9 @@ export const reducer = (state: State, action: Action): State => {
|
|||||||
if (toastId) {
|
if (toastId) {
|
||||||
addToRemoveQueue(toastId);
|
addToRemoveQueue(toastId);
|
||||||
} else {
|
} else {
|
||||||
state.toasts.forEach((toast) => {
|
for (const toast of state.toasts) {
|
||||||
addToRemoveQueue(toast.id);
|
addToRemoveQueue(toast.id);
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -99,7 +101,7 @@ export const reducer = (state: State, action: Action): State => {
|
|||||||
),
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case "REMOVE_TOAST":
|
case "REMOVE_TOAST": {
|
||||||
if (action.toastId === undefined) {
|
if (action.toastId === undefined) {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
@ -111,17 +113,18 @@ export const reducer = (state: State, action: Action): State => {
|
|||||||
toasts: state.toasts.filter((t) => t.id !== action.toastId),
|
toasts: state.toasts.filter((t) => t.id !== action.toastId),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const listeners: Array<(state: State) => void> = [];
|
const listeners: ((state: State) => void)[] = [];
|
||||||
|
|
||||||
let memoryState: State = { toasts: [] };
|
let memoryState: State = { toasts: [] };
|
||||||
|
|
||||||
function dispatch(action: Action) {
|
function dispatch(action: Action) {
|
||||||
memoryState = reducer(memoryState, action);
|
memoryState = reducer(memoryState, action);
|
||||||
listeners.forEach((listener) => {
|
for (const listener of listeners) {
|
||||||
listener(memoryState);
|
listener(memoryState);
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Toast = Omit<ToasterToast, "id">;
|
type Toast = Omit<ToasterToast, "id">;
|
||||||
@ -129,12 +132,15 @@ type Toast = Omit<ToasterToast, "id">;
|
|||||||
function toast({ ...props }: Toast) {
|
function toast({ ...props }: Toast) {
|
||||||
const id = createId();
|
const id = createId();
|
||||||
|
|
||||||
const update = (props: ToasterToast) =>
|
const update = (props: ToasterToast) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "UPDATE_TOAST",
|
type: "UPDATE_TOAST",
|
||||||
toast: { ...props, id },
|
toast: { ...props, id },
|
||||||
});
|
});
|
||||||
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
|
};
|
||||||
|
const dismiss = () => {
|
||||||
|
dispatch({ type: "DISMISS_TOAST", toastId: id });
|
||||||
|
};
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "ADD_TOAST",
|
type: "ADD_TOAST",
|
||||||
@ -170,7 +176,9 @@ function useToast() {
|
|||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
toast,
|
toast,
|
||||||
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
|
dismiss: (toastId?: string) => {
|
||||||
|
dispatch({ type: "DISMISS_TOAST", toastId });
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,18 +4,13 @@ import _axios from "axios";
|
|||||||
import createAuthRefreshInterceptor from "axios-auth-refresh";
|
import createAuthRefreshInterceptor from "axios-auth-refresh";
|
||||||
import { redirect } from "react-router-dom";
|
import { redirect } from "react-router-dom";
|
||||||
|
|
||||||
|
import { refresh } from "@/client/services/auth";
|
||||||
|
|
||||||
import { USER_KEY } from "../constants/query-keys";
|
import { USER_KEY } from "../constants/query-keys";
|
||||||
import { toast } from "../hooks/use-toast";
|
import { toast } from "../hooks/use-toast";
|
||||||
import { refresh } from "../services/auth/refresh";
|
|
||||||
import { translateError } from "../services/errors/translate-error";
|
import { translateError } from "../services/errors/translate-error";
|
||||||
import { queryClient } from "./query-client";
|
import { queryClient } from "./query-client";
|
||||||
|
|
||||||
export type ServerError = {
|
|
||||||
statusCode: number;
|
|
||||||
message: string;
|
|
||||||
error: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const axios = _axios.create({ baseURL: "/api", withCredentials: true });
|
export const axios = _axios.create({ baseURL: "/api", withCredentials: true });
|
||||||
|
|
||||||
// Intercept responses to transform ISO dates to JS date objects
|
// Intercept responses to transform ISO dates to JS date objects
|
||||||
@ -36,7 +31,7 @@ axios.interceptors.response.use(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.reject(error);
|
return Promise.reject(new Error(message));
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -46,25 +41,17 @@ const axiosForRefresh = _axios.create({ baseURL: "/api", withCredentials: true }
|
|||||||
|
|
||||||
// Interceptor to handle expired access token errors
|
// Interceptor to handle expired access token errors
|
||||||
const handleAuthError = async () => {
|
const handleAuthError = async () => {
|
||||||
try {
|
|
||||||
await refresh(axiosForRefresh);
|
await refresh(axiosForRefresh);
|
||||||
|
|
||||||
return Promise.resolve();
|
await Promise.resolve();
|
||||||
} catch (error) {
|
|
||||||
return Promise.reject(error);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Interceptor to handle expired refresh token errors
|
// Interceptor to handle expired refresh token errors
|
||||||
const handleRefreshError = async () => {
|
const handleRefreshError = async () => {
|
||||||
try {
|
void queryClient.invalidateQueries({ queryKey: USER_KEY });
|
||||||
queryClient.invalidateQueries({ queryKey: USER_KEY });
|
|
||||||
redirect("/auth/login");
|
redirect("/auth/login");
|
||||||
|
|
||||||
return Promise.resolve();
|
await Promise.resolve();
|
||||||
} catch (error) {
|
|
||||||
return Promise.reject(error);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Intercept responses to check for 401 and 403 errors, refresh token and retry the request
|
// Intercept responses to check for 401 and 403 errors, refresh token and retry the request
|
||||||
|
|||||||
@ -13,6 +13,7 @@ export async function dynamicActivate(locale: string) {
|
|||||||
i18n.loadAndActivate({ locale, messages });
|
i18n.loadAndActivate({ locale, messages });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||||
if (dayjsLocales[locale]) {
|
if (dayjsLocales[locale]) {
|
||||||
dayjs.locale(await dayjsLocales[locale]());
|
dayjs.locale(await dayjsLocales[locale]());
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,10 +1,37 @@
|
|||||||
import { StrictMode } from "react";
|
import * as Sentry from "@sentry/react";
|
||||||
|
import { StrictMode, useEffect } from "react";
|
||||||
import * as ReactDOM from "react-dom/client";
|
import * as ReactDOM from "react-dom/client";
|
||||||
import { RouterProvider } from "react-router-dom";
|
import {
|
||||||
|
createRoutesFromChildren,
|
||||||
|
matchRoutes,
|
||||||
|
RouterProvider,
|
||||||
|
useLocation,
|
||||||
|
useNavigationType,
|
||||||
|
} from "react-router-dom";
|
||||||
|
|
||||||
import { router } from "./router";
|
import { router } from "./router";
|
||||||
|
|
||||||
const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement);
|
if (import.meta.env.VITE_CLIENT_SENTRY_DSN) {
|
||||||
|
Sentry.init({
|
||||||
|
dsn: import.meta.env.VITE_CLIENT_SENTRY_DSN,
|
||||||
|
integrations: [
|
||||||
|
Sentry.reactRouterV6BrowserTracingIntegration({
|
||||||
|
useEffect,
|
||||||
|
matchRoutes,
|
||||||
|
useLocation,
|
||||||
|
useNavigationType,
|
||||||
|
createRoutesFromChildren,
|
||||||
|
}),
|
||||||
|
Sentry.replayIntegration(),
|
||||||
|
],
|
||||||
|
tracesSampleRate: 0.5,
|
||||||
|
replaysOnErrorSampleRate: 0.5,
|
||||||
|
replaysSessionSampleRate: 0.25,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
const root = ReactDOM.createRoot(document.querySelector("#root")!);
|
||||||
|
|
||||||
root.render(
|
root.render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
|
|||||||
@ -40,7 +40,7 @@ export const BackupOtpPage = () => {
|
|||||||
await backupOtp(data);
|
await backupOtp(data);
|
||||||
|
|
||||||
navigate("/dashboard");
|
navigate("/dashboard");
|
||||||
} catch (error) {
|
} catch {
|
||||||
form.reset();
|
form.reset();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -87,7 +87,13 @@ export const BackupOtpPage = () => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="mt-4 flex items-center gap-x-2">
|
<div className="mt-4 flex items-center gap-x-2">
|
||||||
<Button variant="link" className="px-5" onClick={() => navigate(-1)}>
|
<Button
|
||||||
|
variant="link"
|
||||||
|
className="px-5"
|
||||||
|
onClick={() => {
|
||||||
|
navigate(-1);
|
||||||
|
}}
|
||||||
|
>
|
||||||
<ArrowLeft size={14} className="mr-2" />
|
<ArrowLeft size={14} className="mr-2" />
|
||||||
<span>{t`Back`}</span>
|
<span>{t`Back`}</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -89,7 +89,13 @@ export const ForgotPasswordPage = () => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="mt-4 flex items-center gap-x-2">
|
<div className="mt-4 flex items-center gap-x-2">
|
||||||
<Button variant="link" className="px-5" onClick={() => navigate(-1)}>
|
<Button
|
||||||
|
variant="link"
|
||||||
|
className="px-5"
|
||||||
|
onClick={() => {
|
||||||
|
navigate(-1);
|
||||||
|
}}
|
||||||
|
>
|
||||||
<ArrowLeft size={14} className="mr-2" />
|
<ArrowLeft size={14} className="mr-2" />
|
||||||
<span>{t`Back`}</span>
|
<span>{t`Back`}</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -30,7 +30,7 @@ export const LoginPage = () => {
|
|||||||
const { login, loading } = useLogin();
|
const { login, loading } = useLogin();
|
||||||
|
|
||||||
const { providers } = useAuthProviders();
|
const { providers } = useAuthProviders();
|
||||||
const emailAuthDisabled = !providers || !providers.includes("email");
|
const emailAuthDisabled = !providers?.includes("email");
|
||||||
|
|
||||||
const formRef = useRef<HTMLFormElement>(null);
|
const formRef = useRef<HTMLFormElement>(null);
|
||||||
usePasswordToggle(formRef);
|
usePasswordToggle(formRef);
|
||||||
@ -43,7 +43,7 @@ export const LoginPage = () => {
|
|||||||
const onSubmit = async (data: FormValues) => {
|
const onSubmit = async (data: FormValues) => {
|
||||||
try {
|
try {
|
||||||
await login(data);
|
await login(data);
|
||||||
} catch (error) {
|
} catch {
|
||||||
form.reset();
|
form.reset();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -34,7 +34,7 @@ export const RegisterPage = () => {
|
|||||||
const disableSignups = import.meta.env.VITE_DISABLE_SIGNUPS === "true";
|
const disableSignups = import.meta.env.VITE_DISABLE_SIGNUPS === "true";
|
||||||
|
|
||||||
const { providers } = useAuthProviders();
|
const { providers } = useAuthProviders();
|
||||||
const emailAuthDisabled = !providers || !providers.includes("email");
|
const emailAuthDisabled = !providers?.includes("email");
|
||||||
|
|
||||||
const formRef = useRef<HTMLFormElement>(null);
|
const formRef = useRef<HTMLFormElement>(null);
|
||||||
usePasswordToggle(formRef);
|
usePasswordToggle(formRef);
|
||||||
@ -55,7 +55,7 @@ export const RegisterPage = () => {
|
|||||||
await register(data);
|
await register(data);
|
||||||
|
|
||||||
navigate("/auth/verify-email");
|
navigate("/auth/verify-email");
|
||||||
} catch (error) {
|
} catch {
|
||||||
form.reset();
|
form.reset();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -26,7 +26,7 @@ type FormValues = z.infer<typeof resetPasswordSchema>;
|
|||||||
export const ResetPasswordPage = () => {
|
export const ResetPasswordPage = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const token = searchParams.get("token") || "";
|
const token = searchParams.get("token") ?? "";
|
||||||
|
|
||||||
const { resetPassword, loading } = useResetPassword();
|
const { resetPassword, loading } = useResetPassword();
|
||||||
|
|
||||||
@ -43,7 +43,7 @@ export const ResetPasswordPage = () => {
|
|||||||
await resetPassword(data);
|
await resetPassword(data);
|
||||||
|
|
||||||
navigate("/auth/login");
|
navigate("/auth/login");
|
||||||
} catch (error) {
|
} catch {
|
||||||
form.reset();
|
form.reset();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -33,7 +33,7 @@ export const VerifyEmailPage = () => {
|
|||||||
|
|
||||||
if (!token) return;
|
if (!token) return;
|
||||||
|
|
||||||
handleVerifyEmail(token);
|
void handleVerifyEmail(token);
|
||||||
}, [token, navigate, verifyEmail]);
|
}, [token, navigate, verifyEmail]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user