mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2025-11-09 20:12:26 +10:00
release: v4.1.0
This commit is contained in:
@ -22,14 +22,8 @@ Dockerfile
|
||||
*.sublime-workspace
|
||||
|
||||
# IDE - VSCode
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
|
||||
# IDE - Visual Studio
|
||||
.vs/*
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
|
||||
21
.env.example
21
.env.example
@ -4,14 +4,6 @@ NODE_ENV=development
|
||||
# Ports
|
||||
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
|
||||
# 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
|
||||
@ -53,19 +45,18 @@ STORAGE_ACCESS_KEY=minioadmin
|
||||
STORAGE_SECRET_KEY=minioadmin
|
||||
STORAGE_USE_SSL=false
|
||||
|
||||
# Redis (for cache & server session management)
|
||||
REDIS_URL=redis://default:password@localhost:6379
|
||||
|
||||
# Sentry (for error reporting, Optional)
|
||||
# VITE_SENTRY_DSN=
|
||||
# SERVER_SENTRY_DSN=
|
||||
# VITE_CLIENT_SENTRY_DSN=
|
||||
|
||||
# Crowdin (Optional)
|
||||
CROWDIN_PROJECT_ID=
|
||||
CROWDIN_PERSONAL_TOKEN=
|
||||
# CROWDIN_PROJECT_ID=
|
||||
# CROWDIN_PERSONAL_TOKEN=
|
||||
|
||||
# Email (Optional)
|
||||
# Flags (Optional)
|
||||
# DISABLE_EMAIL_AUTH=true
|
||||
# VITE_DISABLE_SIGNUPS=false
|
||||
# SKIP_STORAGE_BUCKET_CHECK=false
|
||||
|
||||
# GitHub (OAuth, Optional)
|
||||
# GITHUB_CLIENT_ID=
|
||||
|
||||
@ -8,6 +8,9 @@
|
||||
"extends": ["plugin:prettier/recommended"],
|
||||
"plugins": ["simple-import-sort", "unused-imports"],
|
||||
"rules": {
|
||||
// eslint
|
||||
"no-return-await": "off",
|
||||
|
||||
// simple-import-sort
|
||||
"simple-import-sort/imports": "error",
|
||||
"simple-import-sort/exports": "error",
|
||||
@ -43,10 +46,36 @@
|
||||
},
|
||||
{
|
||||
"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": {
|
||||
// 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
|
||||
description: What variant of Reactive Resume are you using?
|
||||
options:
|
||||
- Cloud (http://rxresu.me)
|
||||
- Cloud (https://rxresu.me)
|
||||
- Self-Hosted
|
||||
validations:
|
||||
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
|
||||
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
|
||||
uses: docker/login-action@v3.1.0
|
||||
with:
|
||||
@ -54,6 +51,13 @@ jobs:
|
||||
username: ${{ github.repository_owner }}
|
||||
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
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5.5.1
|
||||
@ -111,9 +115,6 @@ jobs:
|
||||
pattern: digests-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.3.0
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3.1.0
|
||||
with:
|
||||
@ -127,6 +128,13 @@ jobs:
|
||||
username: ${{ github.repository_owner }}
|
||||
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
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5.5.1
|
||||
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
@ -16,14 +16,8 @@ node_modules
|
||||
*.sublime-workspace
|
||||
|
||||
# IDE - VSCode
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
|
||||
# IDE - Visual Studio
|
||||
.vs/*
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.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 **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).
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
## 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:
|
||||
|
||||
@ -57,15 +57,13 @@ You can also visit `http://localhost:3000/api/health`, the health check endpoint
|
||||
"info": {
|
||||
"database": { "status": "up" },
|
||||
"storage": { "status": "up" },
|
||||
"browser": { "status": "up", "version": "Chrome/119.0.6045.9" },
|
||||
"redis": { "status": "up" }
|
||||
"browser": { "status": "up", "version": "Chrome/119.0.6045.9" }
|
||||
},
|
||||
"error": {},
|
||||
"details": {
|
||||
"database": { "status": "up" },
|
||||
"storage": { "status": "up" },
|
||||
"browser": { "status": "up", "version": "Chrome/119.0.6045.9" },
|
||||
"redis": { "status": "up" }
|
||||
"browser": { "status": "up", "version": "Chrome/119.0.6045.9" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@ -40,11 +40,11 @@ Start creating your standout resume with Reactive Resume today!
|
||||
|
||||
- **Free, forever** and open-source
|
||||
- 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/))
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
@ -69,7 +69,6 @@ Start creating your standout resume with Reactive Resume today!
|
||||
- NestJS, for the backend
|
||||
- Postgres (primary database)
|
||||
- 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)
|
||||
- Browserless (for headless chrome, to print PDFs and generate previews)
|
||||
- SMTP Server (to send password recovery emails)
|
||||
|
||||
@ -11,4 +11,4 @@
|
||||
|
||||
## 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": {
|
||||
// react
|
||||
"react/no-unescaped-entities": "off",
|
||||
"react/jsx-sort-props": [
|
||||
"error",
|
||||
{
|
||||
"reservedFirst": true,
|
||||
"callbacksLast": true,
|
||||
"shorthandFirst": true,
|
||||
"noSortAlphabetically": true
|
||||
}
|
||||
],
|
||||
|
||||
// react-hooks
|
||||
"react-hooks/exhaustive-deps": "off",
|
||||
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
const { join } = require("path");
|
||||
const path = require("node:path");
|
||||
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {
|
||||
config: join(__dirname, "tailwind.config.js"),
|
||||
config: path.join(__dirname, "tailwind.config.js"),
|
||||
},
|
||||
autoprefixer: {},
|
||||
},
|
||||
|
||||
@ -4,7 +4,8 @@ import { RouterProvider } from "react-router-dom";
|
||||
|
||||
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(
|
||||
<StrictMode>
|
||||
|
||||
@ -39,20 +39,20 @@ export const ArtboardPage = () => {
|
||||
`${metadata.typography.lineHeight}`,
|
||||
);
|
||||
|
||||
document.documentElement.style.setProperty("--color-text", `${metadata.theme.text}`);
|
||||
document.documentElement.style.setProperty("--color-primary", `${metadata.theme.primary}`);
|
||||
document.documentElement.style.setProperty(
|
||||
"--color-background",
|
||||
`${metadata.theme.background}`,
|
||||
);
|
||||
document.documentElement.style.setProperty("--color-text", metadata.theme.text);
|
||||
document.documentElement.style.setProperty("--color-primary", metadata.theme.primary);
|
||||
document.documentElement.style.setProperty("--color-background", metadata.theme.background);
|
||||
}, [metadata]);
|
||||
|
||||
// Typography Options
|
||||
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("underline-links", metadata.typography.underlineLinks);
|
||||
});
|
||||
}
|
||||
}, [metadata]);
|
||||
|
||||
return <Outlet />;
|
||||
|
||||
@ -38,11 +38,11 @@ export const BuilderLayout = () => {
|
||||
|
||||
return (
|
||||
<TransformWrapper
|
||||
ref={transformRef}
|
||||
centerOnInit
|
||||
maxScale={2}
|
||||
minScale={0.4}
|
||||
initialScale={0.8}
|
||||
ref={transformRef}
|
||||
limitToBounds={false}
|
||||
>
|
||||
<TransformComponent
|
||||
@ -56,8 +56,8 @@ export const BuilderLayout = () => {
|
||||
<AnimatePresence>
|
||||
{layout.map((columns, pageIndex) => (
|
||||
<motion.div
|
||||
layout
|
||||
key={pageIndex}
|
||||
layout
|
||||
initial={{ opacity: 0, x: -200, y: 0 }}
|
||||
animate={{ opacity: 1, x: 0, transition: { delay: pageIndex * 0.3 } }}
|
||||
exit={{ opacity: 0, x: -200 }}
|
||||
|
||||
@ -20,7 +20,10 @@ export const Providers = () => {
|
||||
};
|
||||
|
||||
const resumeData = window.localStorage.getItem("resume");
|
||||
if (resumeData) return setResume(JSON.parse(resumeData));
|
||||
if (resumeData) {
|
||||
setResume(JSON.parse(resumeData));
|
||||
return;
|
||||
}
|
||||
|
||||
window.addEventListener("message", handleMessage);
|
||||
|
||||
@ -34,6 +37,7 @@ export const Providers = () => {
|
||||
// setResume(sampleResume);
|
||||
// }, [setResume]);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (!resume) return null;
|
||||
|
||||
return <Outlet />;
|
||||
|
||||
@ -8,5 +8,7 @@ export type ArtboardStore = {
|
||||
|
||||
export const useArtboardStore = create<ArtboardStore>()((set) => ({
|
||||
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
|
||||
dangerouslySetInnerHTML={{ __html: section.content }}
|
||||
className="wysiwyg"
|
||||
style={{ columns: section.columns }}
|
||||
dangerouslySetInnerHTML={{ __html: section.content }}
|
||||
/>
|
||||
</main>
|
||||
</section>
|
||||
@ -133,7 +133,7 @@ const Link = ({ url, icon, label, className }: LinkProps) => {
|
||||
rel="noreferrer noopener nofollow"
|
||||
className={cn("inline-block", className)}
|
||||
>
|
||||
{label || url.label || url.href}
|
||||
{label ?? (url.label || url.href)}
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
@ -158,7 +158,7 @@ const Section = <T,>({
|
||||
summaryKey,
|
||||
keywordsKey,
|
||||
}: SectionProps<T>) => {
|
||||
if (!section.visible || !section.items.length) return null;
|
||||
if (!section.visible || section.items.length === 0) return null;
|
||||
|
||||
return (
|
||||
<section id={section.id} className="grid">
|
||||
@ -196,7 +196,7 @@ const Section = <T,>({
|
||||
<div>{children?.(item as T)}</div>
|
||||
|
||||
{summary !== undefined && !isEmptyString(summary) && (
|
||||
<div className="wysiwyg" dangerouslySetInnerHTML={{ __html: summary }} />
|
||||
<div dangerouslySetInnerHTML={{ __html: summary }} className="wysiwyg" />
|
||||
)}
|
||||
|
||||
{level !== undefined && level > 0 && <Rating level={level} />}
|
||||
@ -449,37 +449,51 @@ const Custom = ({ id }: { id: string }) => {
|
||||
|
||||
const mapSectionToComponent = (section: SectionKey) => {
|
||||
switch (section) {
|
||||
case "profiles":
|
||||
case "profiles": {
|
||||
return <Profiles />;
|
||||
case "summary":
|
||||
}
|
||||
case "summary": {
|
||||
return <Summary />;
|
||||
case "experience":
|
||||
}
|
||||
case "experience": {
|
||||
return <Experience />;
|
||||
case "education":
|
||||
}
|
||||
case "education": {
|
||||
return <Education />;
|
||||
case "awards":
|
||||
}
|
||||
case "awards": {
|
||||
return <Awards />;
|
||||
case "certifications":
|
||||
}
|
||||
case "certifications": {
|
||||
return <Certifications />;
|
||||
case "skills":
|
||||
}
|
||||
case "skills": {
|
||||
return <Skills />;
|
||||
case "interests":
|
||||
}
|
||||
case "interests": {
|
||||
return <Interests />;
|
||||
case "publications":
|
||||
}
|
||||
case "publications": {
|
||||
return <Publications />;
|
||||
case "volunteer":
|
||||
}
|
||||
case "volunteer": {
|
||||
return <Volunteer />;
|
||||
case "languages":
|
||||
}
|
||||
case "languages": {
|
||||
return <Languages />;
|
||||
case "projects":
|
||||
}
|
||||
case "projects": {
|
||||
return <Projects />;
|
||||
case "references":
|
||||
}
|
||||
case "references": {
|
||||
return <References />;
|
||||
default:
|
||||
}
|
||||
default: {
|
||||
if (section.startsWith("custom.")) return <Custom id={section.split(".")[1]} />;
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const Azurill = ({ columns, isFirstPage = false }: TemplateProps) => {
|
||||
|
||||
@ -84,9 +84,9 @@ const Summary = () => {
|
||||
</div>
|
||||
|
||||
<div
|
||||
dangerouslySetInnerHTML={{ __html: section.content }}
|
||||
className="wysiwyg col-span-4"
|
||||
style={{ columns: section.columns }}
|
||||
dangerouslySetInnerHTML={{ __html: section.content }}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
@ -124,7 +124,7 @@ const Link = ({ url, icon, label, className }: LinkProps) => {
|
||||
rel="noreferrer noopener nofollow"
|
||||
className={cn("inline-block", className)}
|
||||
>
|
||||
{label || url.label || url.href}
|
||||
{label ?? (url.label || url.href)}
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
@ -149,7 +149,7 @@ const Section = <T,>({
|
||||
summaryKey,
|
||||
keywordsKey,
|
||||
}: SectionProps<T>) => {
|
||||
if (!section.visible || !section.items.length) return null;
|
||||
if (!section.visible || section.items.length === 0) return null;
|
||||
|
||||
return (
|
||||
<section id={section.id} className="grid grid-cols-5 border-t pt-2.5">
|
||||
@ -177,7 +177,7 @@ const Section = <T,>({
|
||||
</div>
|
||||
|
||||
{summary !== undefined && !isEmptyString(summary) && (
|
||||
<div className="wysiwyg" dangerouslySetInnerHTML={{ __html: summary }} />
|
||||
<div dangerouslySetInnerHTML={{ __html: summary }} className="wysiwyg" />
|
||||
)}
|
||||
|
||||
{level !== undefined && level > 0 && <Rating level={level} />}
|
||||
@ -460,37 +460,51 @@ const Custom = ({ id }: { id: string }) => {
|
||||
|
||||
const mapSectionToComponent = (section: SectionKey) => {
|
||||
switch (section) {
|
||||
case "profiles":
|
||||
case "profiles": {
|
||||
return <Profiles />;
|
||||
case "summary":
|
||||
}
|
||||
case "summary": {
|
||||
return <Summary />;
|
||||
case "experience":
|
||||
}
|
||||
case "experience": {
|
||||
return <Experience />;
|
||||
case "education":
|
||||
}
|
||||
case "education": {
|
||||
return <Education />;
|
||||
case "awards":
|
||||
}
|
||||
case "awards": {
|
||||
return <Awards />;
|
||||
case "certifications":
|
||||
}
|
||||
case "certifications": {
|
||||
return <Certifications />;
|
||||
case "skills":
|
||||
}
|
||||
case "skills": {
|
||||
return <Skills />;
|
||||
case "interests":
|
||||
}
|
||||
case "interests": {
|
||||
return <Interests />;
|
||||
case "publications":
|
||||
}
|
||||
case "publications": {
|
||||
return <Publications />;
|
||||
case "volunteer":
|
||||
}
|
||||
case "volunteer": {
|
||||
return <Volunteer />;
|
||||
case "languages":
|
||||
}
|
||||
case "languages": {
|
||||
return <Languages />;
|
||||
case "projects":
|
||||
}
|
||||
case "projects": {
|
||||
return <Projects />;
|
||||
case "references":
|
||||
}
|
||||
case "references": {
|
||||
return <References />;
|
||||
default:
|
||||
}
|
||||
default: {
|
||||
if (section.startsWith("custom.")) return <Custom id={section.split(".")[1]} />;
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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>
|
||||
|
||||
<div
|
||||
dangerouslySetInnerHTML={{ __html: section.content }}
|
||||
className="wysiwyg"
|
||||
style={{ columns: section.columns }}
|
||||
dangerouslySetInnerHTML={{ __html: section.content }}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
@ -127,7 +127,7 @@ const Link = ({ url, icon, label, className }: LinkProps) => {
|
||||
rel="noreferrer noopener nofollow"
|
||||
className={cn("inline-block", className)}
|
||||
>
|
||||
{label || url.label || url.href}
|
||||
{label ?? (url.label || url.href)}
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
@ -152,7 +152,7 @@ const Section = <T,>({
|
||||
summaryKey,
|
||||
keywordsKey,
|
||||
}: SectionProps<T>) => {
|
||||
if (!section.visible || !section.items.length) return null;
|
||||
if (!section.visible || section.items.length === 0) return null;
|
||||
|
||||
return (
|
||||
<section id={section.id} className="grid">
|
||||
@ -178,7 +178,7 @@ const Section = <T,>({
|
||||
</div>
|
||||
|
||||
{summary !== undefined && !isEmptyString(summary) && (
|
||||
<div className="wysiwyg" dangerouslySetInnerHTML={{ __html: summary }} />
|
||||
<div dangerouslySetInnerHTML={{ __html: summary }} className="wysiwyg" />
|
||||
)}
|
||||
|
||||
{level !== undefined && level > 0 && <Rating level={level} />}
|
||||
@ -461,37 +461,51 @@ const Custom = ({ id }: { id: string }) => {
|
||||
|
||||
const mapSectionToComponent = (section: SectionKey) => {
|
||||
switch (section) {
|
||||
case "profiles":
|
||||
case "profiles": {
|
||||
return <Profiles />;
|
||||
case "summary":
|
||||
}
|
||||
case "summary": {
|
||||
return <Summary />;
|
||||
case "experience":
|
||||
}
|
||||
case "experience": {
|
||||
return <Experience />;
|
||||
case "education":
|
||||
}
|
||||
case "education": {
|
||||
return <Education />;
|
||||
case "awards":
|
||||
}
|
||||
case "awards": {
|
||||
return <Awards />;
|
||||
case "certifications":
|
||||
}
|
||||
case "certifications": {
|
||||
return <Certifications />;
|
||||
case "skills":
|
||||
}
|
||||
case "skills": {
|
||||
return <Skills />;
|
||||
case "interests":
|
||||
}
|
||||
case "interests": {
|
||||
return <Interests />;
|
||||
case "publications":
|
||||
}
|
||||
case "publications": {
|
||||
return <Publications />;
|
||||
case "volunteer":
|
||||
}
|
||||
case "volunteer": {
|
||||
return <Volunteer />;
|
||||
case "languages":
|
||||
}
|
||||
case "languages": {
|
||||
return <Languages />;
|
||||
case "projects":
|
||||
}
|
||||
case "projects": {
|
||||
return <Projects />;
|
||||
case "references":
|
||||
}
|
||||
case "references": {
|
||||
return <References />;
|
||||
default:
|
||||
}
|
||||
default: {
|
||||
if (section.startsWith("custom.")) return <Custom id={section.split(".")[1]} />;
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const Chikorita = ({ columns, isFirstPage = false }: TemplateProps) => {
|
||||
|
||||
@ -104,9 +104,9 @@ const Summary = () => {
|
||||
<h4 className="mb-2 text-base font-bold">{section.name}</h4>
|
||||
|
||||
<div
|
||||
dangerouslySetInnerHTML={{ __html: section.content }}
|
||||
className="wysiwyg"
|
||||
style={{ columns: section.columns }}
|
||||
dangerouslySetInnerHTML={{ __html: section.content }}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
@ -144,7 +144,7 @@ const Link = ({ url, icon, label, className }: LinkProps) => {
|
||||
rel="noreferrer noopener nofollow"
|
||||
className={cn("inline-block", className)}
|
||||
>
|
||||
{label || url.label || url.href}
|
||||
{label ?? (url.label || url.href)}
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
@ -169,7 +169,7 @@ const Section = <T,>({
|
||||
summaryKey,
|
||||
keywordsKey,
|
||||
}: SectionProps<T>) => {
|
||||
if (!section.visible || !section.items.length) return null;
|
||||
if (!section.visible || section.items.length === 0) return null;
|
||||
|
||||
return (
|
||||
<section id={section.id} className="grid">
|
||||
@ -202,7 +202,7 @@ const Section = <T,>({
|
||||
</div>
|
||||
|
||||
{summary !== undefined && !isEmptyString(summary) && (
|
||||
<div className="wysiwyg" dangerouslySetInnerHTML={{ __html: summary }} />
|
||||
<div dangerouslySetInnerHTML={{ __html: summary }} className="wysiwyg" />
|
||||
)}
|
||||
|
||||
{level !== undefined && level > 0 && <Rating level={level} />}
|
||||
@ -487,37 +487,51 @@ const Custom = ({ id }: { id: string }) => {
|
||||
|
||||
const mapSectionToComponent = (section: SectionKey) => {
|
||||
switch (section) {
|
||||
case "profiles":
|
||||
case "profiles": {
|
||||
return <Profiles />;
|
||||
case "summary":
|
||||
}
|
||||
case "summary": {
|
||||
return <Summary />;
|
||||
case "experience":
|
||||
}
|
||||
case "experience": {
|
||||
return <Experience />;
|
||||
case "education":
|
||||
}
|
||||
case "education": {
|
||||
return <Education />;
|
||||
case "awards":
|
||||
}
|
||||
case "awards": {
|
||||
return <Awards />;
|
||||
case "certifications":
|
||||
}
|
||||
case "certifications": {
|
||||
return <Certifications />;
|
||||
case "skills":
|
||||
}
|
||||
case "skills": {
|
||||
return <Skills />;
|
||||
case "interests":
|
||||
}
|
||||
case "interests": {
|
||||
return <Interests />;
|
||||
case "publications":
|
||||
}
|
||||
case "publications": {
|
||||
return <Publications />;
|
||||
case "volunteer":
|
||||
}
|
||||
case "volunteer": {
|
||||
return <Volunteer />;
|
||||
case "languages":
|
||||
}
|
||||
case "languages": {
|
||||
return <Languages />;
|
||||
case "projects":
|
||||
}
|
||||
case "projects": {
|
||||
return <Projects />;
|
||||
case "references":
|
||||
}
|
||||
case "references": {
|
||||
return <References />;
|
||||
default:
|
||||
}
|
||||
default: {
|
||||
if (section.startsWith("custom.")) return <Custom id={section.split(".")[1]} />;
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const Ditto = ({ columns, isFirstPage = false }: TemplateProps) => {
|
||||
|
||||
@ -82,9 +82,9 @@ const Summary = () => {
|
||||
return (
|
||||
<section id={section.id}>
|
||||
<div
|
||||
dangerouslySetInnerHTML={{ __html: section.content }}
|
||||
className="wysiwyg"
|
||||
style={{ columns: section.columns }}
|
||||
dangerouslySetInnerHTML={{ __html: section.content }}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
@ -122,7 +122,7 @@ const Link = ({ url, icon, label, className }: LinkProps) => {
|
||||
rel="noreferrer noopener nofollow"
|
||||
className={cn("inline-block", className)}
|
||||
>
|
||||
{label || url.label || url.href}
|
||||
{label ?? (url.label || url.href)}
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
@ -147,7 +147,7 @@ const Section = <T,>({
|
||||
summaryKey,
|
||||
keywordsKey,
|
||||
}: SectionProps<T>) => {
|
||||
if (!section.visible || !section.items.length) return null;
|
||||
if (!section.visible || section.items.length === 0) return null;
|
||||
|
||||
return (
|
||||
<section id={section.id} className="grid">
|
||||
@ -173,7 +173,7 @@ const Section = <T,>({
|
||||
</div>
|
||||
|
||||
{summary !== undefined && !isEmptyString(summary) && (
|
||||
<div className="wysiwyg" dangerouslySetInnerHTML={{ __html: summary }} />
|
||||
<div dangerouslySetInnerHTML={{ __html: summary }} className="wysiwyg" />
|
||||
)}
|
||||
|
||||
{level !== undefined && level > 0 && <Rating level={level} />}
|
||||
@ -456,35 +456,48 @@ const Custom = ({ id }: { id: string }) => {
|
||||
|
||||
const mapSectionToComponent = (section: SectionKey) => {
|
||||
switch (section) {
|
||||
case "profiles":
|
||||
case "profiles": {
|
||||
return <Profiles />;
|
||||
case "experience":
|
||||
}
|
||||
case "experience": {
|
||||
return <Experience />;
|
||||
case "education":
|
||||
}
|
||||
case "education": {
|
||||
return <Education />;
|
||||
case "awards":
|
||||
}
|
||||
case "awards": {
|
||||
return <Awards />;
|
||||
case "certifications":
|
||||
}
|
||||
case "certifications": {
|
||||
return <Certifications />;
|
||||
case "skills":
|
||||
}
|
||||
case "skills": {
|
||||
return <Skills />;
|
||||
case "interests":
|
||||
}
|
||||
case "interests": {
|
||||
return <Interests />;
|
||||
case "publications":
|
||||
}
|
||||
case "publications": {
|
||||
return <Publications />;
|
||||
case "volunteer":
|
||||
}
|
||||
case "volunteer": {
|
||||
return <Volunteer />;
|
||||
case "languages":
|
||||
}
|
||||
case "languages": {
|
||||
return <Languages />;
|
||||
case "projects":
|
||||
}
|
||||
case "projects": {
|
||||
return <Projects />;
|
||||
case "references":
|
||||
}
|
||||
case "references": {
|
||||
return <References />;
|
||||
default:
|
||||
}
|
||||
default: {
|
||||
if (section.startsWith("custom.")) return <Custom id={section.split(".")[1]} />;
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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>
|
||||
|
||||
<div
|
||||
dangerouslySetInnerHTML={{ __html: section.content }}
|
||||
className="wysiwyg"
|
||||
style={{ columns: section.columns }}
|
||||
dangerouslySetInnerHTML={{ __html: section.content }}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
@ -130,7 +130,7 @@ const Link = ({ url, icon, label, className }: LinkProps) => {
|
||||
rel="noreferrer noopener nofollow"
|
||||
className={cn("inline-block", className)}
|
||||
>
|
||||
{label || url.label || url.href}
|
||||
{label ?? (url.label || url.href)}
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
@ -155,7 +155,7 @@ const Section = <T,>({
|
||||
summaryKey,
|
||||
keywordsKey,
|
||||
}: SectionProps<T>) => {
|
||||
if (!section.visible || !section.items.length) return null;
|
||||
if (!section.visible || section.items.length === 0) return null;
|
||||
|
||||
return (
|
||||
<section id={section.id} className="grid">
|
||||
@ -183,7 +183,7 @@ const Section = <T,>({
|
||||
</div>
|
||||
|
||||
{summary !== undefined && !isEmptyString(summary) && (
|
||||
<div className="wysiwyg" dangerouslySetInnerHTML={{ __html: summary }} />
|
||||
<div dangerouslySetInnerHTML={{ __html: summary }} className="wysiwyg" />
|
||||
)}
|
||||
|
||||
{level !== undefined && level > 0 && <Rating level={level} />}
|
||||
@ -466,37 +466,51 @@ const Custom = ({ id }: { id: string }) => {
|
||||
|
||||
const mapSectionToComponent = (section: SectionKey) => {
|
||||
switch (section) {
|
||||
case "profiles":
|
||||
case "profiles": {
|
||||
return <Profiles />;
|
||||
case "summary":
|
||||
}
|
||||
case "summary": {
|
||||
return <Summary />;
|
||||
case "experience":
|
||||
}
|
||||
case "experience": {
|
||||
return <Experience />;
|
||||
case "education":
|
||||
}
|
||||
case "education": {
|
||||
return <Education />;
|
||||
case "awards":
|
||||
}
|
||||
case "awards": {
|
||||
return <Awards />;
|
||||
case "certifications":
|
||||
}
|
||||
case "certifications": {
|
||||
return <Certifications />;
|
||||
case "skills":
|
||||
}
|
||||
case "skills": {
|
||||
return <Skills />;
|
||||
case "interests":
|
||||
}
|
||||
case "interests": {
|
||||
return <Interests />;
|
||||
case "publications":
|
||||
}
|
||||
case "publications": {
|
||||
return <Publications />;
|
||||
case "volunteer":
|
||||
}
|
||||
case "volunteer": {
|
||||
return <Volunteer />;
|
||||
case "languages":
|
||||
}
|
||||
case "languages": {
|
||||
return <Languages />;
|
||||
case "projects":
|
||||
}
|
||||
case "projects": {
|
||||
return <Projects />;
|
||||
case "references":
|
||||
}
|
||||
case "references": {
|
||||
return <References />;
|
||||
default:
|
||||
}
|
||||
default: {
|
||||
if (section.startsWith("custom.")) return <Custom id={section.split(".")[1]} />;
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const Glalie = ({ columns, isFirstPage = false }: TemplateProps) => {
|
||||
|
||||
@ -15,31 +15,44 @@ import { Rhyhorn } from "./rhyhorn";
|
||||
|
||||
export const getTemplate = (template: Template) => {
|
||||
switch (template) {
|
||||
case "azurill":
|
||||
case "azurill": {
|
||||
return Azurill;
|
||||
case "bronzor":
|
||||
}
|
||||
case "bronzor": {
|
||||
return Bronzor;
|
||||
case "chikorita":
|
||||
}
|
||||
case "chikorita": {
|
||||
return Chikorita;
|
||||
case "ditto":
|
||||
}
|
||||
case "ditto": {
|
||||
return Ditto;
|
||||
case "gengar":
|
||||
}
|
||||
case "gengar": {
|
||||
return Gengar;
|
||||
case "glalie":
|
||||
}
|
||||
case "glalie": {
|
||||
return Glalie;
|
||||
case "kakuna":
|
||||
}
|
||||
case "kakuna": {
|
||||
return Kakuna;
|
||||
case "leafish":
|
||||
}
|
||||
case "leafish": {
|
||||
return Leafish;
|
||||
case "nosepass":
|
||||
}
|
||||
case "nosepass": {
|
||||
return Nosepass;
|
||||
case "onyx":
|
||||
return Onyx;
|
||||
case "pikachu":
|
||||
return Pikachu;
|
||||
case "rhyhorn":
|
||||
return Rhyhorn;
|
||||
default:
|
||||
}
|
||||
case "onyx": {
|
||||
return Onyx;
|
||||
}
|
||||
case "pikachu": {
|
||||
return Pikachu;
|
||||
}
|
||||
case "rhyhorn": {
|
||||
return Rhyhorn;
|
||||
}
|
||||
default: {
|
||||
return Onyx;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -110,9 +110,9 @@ const Summary = () => {
|
||||
</h4>
|
||||
|
||||
<div
|
||||
dangerouslySetInnerHTML={{ __html: section.content }}
|
||||
className="wysiwyg"
|
||||
style={{ columns: section.columns }}
|
||||
dangerouslySetInnerHTML={{ __html: section.content }}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
@ -150,7 +150,7 @@ const Link = ({ url, icon, label, className }: LinkProps) => {
|
||||
rel="noreferrer noopener nofollow"
|
||||
className={cn("inline-block", className)}
|
||||
>
|
||||
{label || url.label || url.href}
|
||||
{label ?? (url.label || url.href)}
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
@ -175,7 +175,7 @@ const Section = <T,>({
|
||||
summaryKey,
|
||||
keywordsKey,
|
||||
}: SectionProps<T>) => {
|
||||
if (!section.visible || !section.items.length) return null;
|
||||
if (!section.visible || section.items.length === 0) return null;
|
||||
|
||||
return (
|
||||
<section id={section.id} className="grid">
|
||||
@ -200,7 +200,7 @@ const Section = <T,>({
|
||||
<div>{children?.(item as T)}</div>
|
||||
|
||||
{summary !== undefined && !isEmptyString(summary) && (
|
||||
<div className="wysiwyg" dangerouslySetInnerHTML={{ __html: summary }} />
|
||||
<div dangerouslySetInnerHTML={{ __html: summary }} className="wysiwyg" />
|
||||
)}
|
||||
|
||||
{level !== undefined && level > 0 && <Rating level={level} />}
|
||||
@ -417,35 +417,48 @@ const Custom = ({ id }: { id: string }) => {
|
||||
|
||||
const mapSectionToComponent = (section: SectionKey) => {
|
||||
switch (section) {
|
||||
case "summary":
|
||||
case "summary": {
|
||||
return <Summary />;
|
||||
case "experience":
|
||||
}
|
||||
case "experience": {
|
||||
return <Experience />;
|
||||
case "education":
|
||||
}
|
||||
case "education": {
|
||||
return <Education />;
|
||||
case "awards":
|
||||
}
|
||||
case "awards": {
|
||||
return <Awards />;
|
||||
case "certifications":
|
||||
}
|
||||
case "certifications": {
|
||||
return <Certifications />;
|
||||
case "skills":
|
||||
}
|
||||
case "skills": {
|
||||
return <Skills />;
|
||||
case "interests":
|
||||
}
|
||||
case "interests": {
|
||||
return <Interests />;
|
||||
case "publications":
|
||||
}
|
||||
case "publications": {
|
||||
return <Publications />;
|
||||
case "volunteer":
|
||||
}
|
||||
case "volunteer": {
|
||||
return <Volunteer />;
|
||||
case "languages":
|
||||
}
|
||||
case "languages": {
|
||||
return <Languages />;
|
||||
case "projects":
|
||||
}
|
||||
case "projects": {
|
||||
return <Projects />;
|
||||
case "references":
|
||||
}
|
||||
case "references": {
|
||||
return <References />;
|
||||
default:
|
||||
}
|
||||
default: {
|
||||
if (section.startsWith("custom.")) return <Custom id={section.split(".")[1]} />;
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const Kakuna = ({ columns, isFirstPage = false }: TemplateProps) => {
|
||||
|
||||
@ -44,9 +44,9 @@ const Header = () => {
|
||||
</div>
|
||||
|
||||
<div
|
||||
dangerouslySetInnerHTML={{ __html: section.content }}
|
||||
className="wysiwyg"
|
||||
style={{ columns: section.columns }}
|
||||
dangerouslySetInnerHTML={{ __html: section.content }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -147,7 +147,7 @@ const Link = ({ url, icon, label, className }: LinkProps) => {
|
||||
rel="noreferrer noopener nofollow"
|
||||
className={cn("inline-block", className)}
|
||||
>
|
||||
{label || url.label || url.href}
|
||||
{label ?? (url.label || url.href)}
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
@ -172,7 +172,7 @@ const Section = <T,>({
|
||||
summaryKey,
|
||||
keywordsKey,
|
||||
}: SectionProps<T>) => {
|
||||
if (!section.visible || !section.items.length) return null;
|
||||
if (!section.visible || section.items.length === 0) return null;
|
||||
|
||||
return (
|
||||
<section id={section.id} className="grid">
|
||||
@ -197,7 +197,7 @@ const Section = <T,>({
|
||||
<div>{children?.(item as T)}</div>
|
||||
|
||||
{summary !== undefined && !isEmptyString(summary) && (
|
||||
<div className="wysiwyg" dangerouslySetInnerHTML={{ __html: summary }} />
|
||||
<div dangerouslySetInnerHTML={{ __html: summary }} className="wysiwyg" />
|
||||
)}
|
||||
|
||||
{level !== undefined && level > 0 && <Rating level={level} />}
|
||||
@ -414,33 +414,45 @@ const Custom = ({ id }: { id: string }) => {
|
||||
|
||||
const mapSectionToComponent = (section: SectionKey) => {
|
||||
switch (section) {
|
||||
case "experience":
|
||||
case "experience": {
|
||||
return <Experience />;
|
||||
case "education":
|
||||
}
|
||||
case "education": {
|
||||
return <Education />;
|
||||
case "awards":
|
||||
}
|
||||
case "awards": {
|
||||
return <Awards />;
|
||||
case "certifications":
|
||||
}
|
||||
case "certifications": {
|
||||
return <Certifications />;
|
||||
case "skills":
|
||||
}
|
||||
case "skills": {
|
||||
return <Skills />;
|
||||
case "interests":
|
||||
}
|
||||
case "interests": {
|
||||
return <Interests />;
|
||||
case "publications":
|
||||
}
|
||||
case "publications": {
|
||||
return <Publications />;
|
||||
case "volunteer":
|
||||
}
|
||||
case "volunteer": {
|
||||
return <Volunteer />;
|
||||
case "languages":
|
||||
}
|
||||
case "languages": {
|
||||
return <Languages />;
|
||||
case "projects":
|
||||
}
|
||||
case "projects": {
|
||||
return <Projects />;
|
||||
case "references":
|
||||
}
|
||||
case "references": {
|
||||
return <References />;
|
||||
default:
|
||||
}
|
||||
default: {
|
||||
if (section.startsWith("custom.")) return <Custom id={section.split(".")[1]} />;
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const Leafish = ({ columns, isFirstPage = false }: TemplateProps) => {
|
||||
|
||||
@ -98,9 +98,9 @@ const Summary = () => {
|
||||
</div>
|
||||
|
||||
<div
|
||||
dangerouslySetInnerHTML={{ __html: section.content }}
|
||||
className="wysiwyg"
|
||||
style={{ columns: section.columns }}
|
||||
dangerouslySetInnerHTML={{ __html: section.content }}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
@ -126,7 +126,7 @@ const Link = ({ url, icon, label, className }: LinkProps) => {
|
||||
rel="noreferrer noopener nofollow"
|
||||
className={cn("inline-block", className)}
|
||||
>
|
||||
{label || url.label || url.href}
|
||||
{label ?? (url.label || url.href)}
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
@ -150,7 +150,7 @@ const Section = <T,>({
|
||||
summaryKey,
|
||||
keywordsKey,
|
||||
}: SectionProps<T>) => {
|
||||
if (!section.visible || !section.items.length) return null;
|
||||
if (!section.visible || section.items.length === 0) return null;
|
||||
|
||||
return (
|
||||
<section id={section.id} className={cn("grid", dateKey !== undefined && "gap-y-4")}>
|
||||
@ -187,7 +187,7 @@ const Section = <T,>({
|
||||
{url !== undefined && <Link url={url} />}
|
||||
|
||||
{summary !== undefined && !isEmptyString(summary) && (
|
||||
<div className="wysiwyg" dangerouslySetInnerHTML={{ __html: summary }} />
|
||||
<div dangerouslySetInnerHTML={{ __html: summary }} className="wysiwyg" />
|
||||
)}
|
||||
|
||||
{keywords !== undefined && keywords.length > 0 && (
|
||||
@ -222,7 +222,7 @@ const Section = <T,>({
|
||||
{url !== undefined && <Link url={url} />}
|
||||
|
||||
{summary !== undefined && !isEmptyString(summary) && (
|
||||
<div className="wysiwyg" dangerouslySetInnerHTML={{ __html: summary }} />
|
||||
<div dangerouslySetInnerHTML={{ __html: summary }} className="wysiwyg" />
|
||||
)}
|
||||
|
||||
{keywords !== undefined && keywords.length > 0 && (
|
||||
@ -464,37 +464,51 @@ const Custom = ({ id }: { id: string }) => {
|
||||
|
||||
const mapSectionToComponent = (section: SectionKey) => {
|
||||
switch (section) {
|
||||
case "profiles":
|
||||
case "profiles": {
|
||||
return <Profiles />;
|
||||
case "summary":
|
||||
}
|
||||
case "summary": {
|
||||
return <Summary />;
|
||||
case "experience":
|
||||
}
|
||||
case "experience": {
|
||||
return <Experience />;
|
||||
case "education":
|
||||
}
|
||||
case "education": {
|
||||
return <Education />;
|
||||
case "awards":
|
||||
}
|
||||
case "awards": {
|
||||
return <Awards />;
|
||||
case "certifications":
|
||||
}
|
||||
case "certifications": {
|
||||
return <Certifications />;
|
||||
case "skills":
|
||||
}
|
||||
case "skills": {
|
||||
return <Skills />;
|
||||
case "interests":
|
||||
}
|
||||
case "interests": {
|
||||
return <Interests />;
|
||||
case "publications":
|
||||
}
|
||||
case "publications": {
|
||||
return <Publications />;
|
||||
case "volunteer":
|
||||
}
|
||||
case "volunteer": {
|
||||
return <Volunteer />;
|
||||
case "languages":
|
||||
}
|
||||
case "languages": {
|
||||
return <Languages />;
|
||||
case "projects":
|
||||
}
|
||||
case "projects": {
|
||||
return <Projects />;
|
||||
case "references":
|
||||
}
|
||||
case "references": {
|
||||
return <References />;
|
||||
default:
|
||||
}
|
||||
default: {
|
||||
if (section.startsWith("custom.")) return <Custom id={section.split(".")[1]} />;
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const Nosepass = ({ columns, isFirstPage = false }: TemplateProps) => {
|
||||
|
||||
@ -113,9 +113,9 @@ const Summary = () => {
|
||||
<h4 className="font-bold text-primary">{section.name}</h4>
|
||||
|
||||
<div
|
||||
dangerouslySetInnerHTML={{ __html: section.content }}
|
||||
className="wysiwyg"
|
||||
style={{ columns: section.columns }}
|
||||
dangerouslySetInnerHTML={{ __html: section.content }}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
@ -153,7 +153,7 @@ const Link = ({ url, icon, label, className }: LinkProps) => {
|
||||
rel="noreferrer noopener nofollow"
|
||||
className={cn("inline-block", className)}
|
||||
>
|
||||
{label || url.label || url.href}
|
||||
{label ?? (url.label || url.href)}
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
@ -178,7 +178,7 @@ const Section = <T,>({
|
||||
summaryKey,
|
||||
keywordsKey,
|
||||
}: SectionProps<T>) => {
|
||||
if (!section.visible || !section.items.length) return null;
|
||||
if (!section.visible || section.items.length === 0) return null;
|
||||
|
||||
return (
|
||||
<section id={section.id} className="grid">
|
||||
@ -204,7 +204,7 @@ const Section = <T,>({
|
||||
</div>
|
||||
|
||||
{summary !== undefined && !isEmptyString(summary) && (
|
||||
<div className="wysiwyg" dangerouslySetInnerHTML={{ __html: summary }} />
|
||||
<div dangerouslySetInnerHTML={{ __html: summary }} className="wysiwyg" />
|
||||
)}
|
||||
|
||||
{level !== undefined && level > 0 && <Rating level={level} />}
|
||||
@ -455,35 +455,48 @@ const Custom = ({ id }: { id: string }) => {
|
||||
|
||||
const mapSectionToComponent = (section: SectionKey) => {
|
||||
switch (section) {
|
||||
case "summary":
|
||||
case "summary": {
|
||||
return <Summary />;
|
||||
case "experience":
|
||||
}
|
||||
case "experience": {
|
||||
return <Experience />;
|
||||
case "education":
|
||||
}
|
||||
case "education": {
|
||||
return <Education />;
|
||||
case "awards":
|
||||
}
|
||||
case "awards": {
|
||||
return <Awards />;
|
||||
case "certifications":
|
||||
}
|
||||
case "certifications": {
|
||||
return <Certifications />;
|
||||
case "skills":
|
||||
}
|
||||
case "skills": {
|
||||
return <Skills />;
|
||||
case "interests":
|
||||
}
|
||||
case "interests": {
|
||||
return <Interests />;
|
||||
case "publications":
|
||||
}
|
||||
case "publications": {
|
||||
return <Publications />;
|
||||
case "volunteer":
|
||||
}
|
||||
case "volunteer": {
|
||||
return <Volunteer />;
|
||||
case "languages":
|
||||
}
|
||||
case "languages": {
|
||||
return <Languages />;
|
||||
case "projects":
|
||||
}
|
||||
case "projects": {
|
||||
return <Projects />;
|
||||
case "references":
|
||||
}
|
||||
case "references": {
|
||||
return <References />;
|
||||
default:
|
||||
}
|
||||
default: {
|
||||
if (section.startsWith("custom.")) return <Custom id={section.split(".")[1]} />;
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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>
|
||||
|
||||
<div
|
||||
dangerouslySetInnerHTML={{ __html: section.content }}
|
||||
className="wysiwyg"
|
||||
style={{ columns: section.columns }}
|
||||
dangerouslySetInnerHTML={{ __html: section.content }}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
@ -153,7 +153,7 @@ const Link = ({ url, icon, label, className }: LinkProps) => {
|
||||
rel="noreferrer noopener nofollow"
|
||||
className={cn("inline-block", className)}
|
||||
>
|
||||
{label || url.label || url.href}
|
||||
{label ?? (url.label || url.href)}
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
@ -178,7 +178,7 @@ const Section = <T,>({
|
||||
summaryKey,
|
||||
keywordsKey,
|
||||
}: SectionProps<T>) => {
|
||||
if (!section.visible || !section.items.length) return null;
|
||||
if (!section.visible || section.items.length === 0) return null;
|
||||
|
||||
return (
|
||||
<section id={section.id} className="grid">
|
||||
@ -204,7 +204,7 @@ const Section = <T,>({
|
||||
</div>
|
||||
|
||||
{summary !== undefined && !isEmptyString(summary) && (
|
||||
<div className="wysiwyg" dangerouslySetInnerHTML={{ __html: summary }} />
|
||||
<div dangerouslySetInnerHTML={{ __html: summary }} className="wysiwyg" />
|
||||
)}
|
||||
|
||||
{level !== undefined && level > 0 && <Rating level={level} />}
|
||||
@ -487,37 +487,51 @@ const Custom = ({ id }: { id: string }) => {
|
||||
|
||||
const mapSectionToComponent = (section: SectionKey) => {
|
||||
switch (section) {
|
||||
case "profiles":
|
||||
case "profiles": {
|
||||
return <Profiles />;
|
||||
case "summary":
|
||||
}
|
||||
case "summary": {
|
||||
return <Summary />;
|
||||
case "experience":
|
||||
}
|
||||
case "experience": {
|
||||
return <Experience />;
|
||||
case "education":
|
||||
}
|
||||
case "education": {
|
||||
return <Education />;
|
||||
case "awards":
|
||||
}
|
||||
case "awards": {
|
||||
return <Awards />;
|
||||
case "certifications":
|
||||
}
|
||||
case "certifications": {
|
||||
return <Certifications />;
|
||||
case "skills":
|
||||
}
|
||||
case "skills": {
|
||||
return <Skills />;
|
||||
case "interests":
|
||||
}
|
||||
case "interests": {
|
||||
return <Interests />;
|
||||
case "publications":
|
||||
}
|
||||
case "publications": {
|
||||
return <Publications />;
|
||||
case "volunteer":
|
||||
}
|
||||
case "volunteer": {
|
||||
return <Volunteer />;
|
||||
case "languages":
|
||||
}
|
||||
case "languages": {
|
||||
return <Languages />;
|
||||
case "projects":
|
||||
}
|
||||
case "projects": {
|
||||
return <Projects />;
|
||||
case "references":
|
||||
}
|
||||
case "references": {
|
||||
return <References />;
|
||||
default:
|
||||
}
|
||||
default: {
|
||||
if (section.startsWith("custom.")) return <Custom id={section.split(".")[1]} />;
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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>
|
||||
|
||||
<div
|
||||
dangerouslySetInnerHTML={{ __html: section.content }}
|
||||
className="wysiwyg"
|
||||
style={{ columns: section.columns }}
|
||||
dangerouslySetInnerHTML={{ __html: section.content }}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
@ -125,7 +125,7 @@ const Link = ({ url, icon, label, className }: LinkProps) => {
|
||||
rel="noreferrer noopener nofollow"
|
||||
className={cn("inline-block", className)}
|
||||
>
|
||||
{label || url.label || url.href}
|
||||
{label ?? (url.label || url.href)}
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
@ -150,7 +150,7 @@ const Section = <T,>({
|
||||
summaryKey,
|
||||
keywordsKey,
|
||||
}: SectionProps<T>) => {
|
||||
if (!section.visible || !section.items.length) return null;
|
||||
if (!section.visible || section.items.length === 0) return null;
|
||||
|
||||
return (
|
||||
<section id={section.id} className="grid">
|
||||
@ -176,7 +176,7 @@ const Section = <T,>({
|
||||
</div>
|
||||
|
||||
{summary !== undefined && !isEmptyString(summary) && (
|
||||
<div className="wysiwyg" dangerouslySetInnerHTML={{ __html: summary }} />
|
||||
<div dangerouslySetInnerHTML={{ __html: summary }} className="wysiwyg" />
|
||||
)}
|
||||
|
||||
{level !== undefined && level > 0 && <Rating level={level} />}
|
||||
@ -459,37 +459,51 @@ const Custom = ({ id }: { id: string }) => {
|
||||
|
||||
const mapSectionToComponent = (section: SectionKey) => {
|
||||
switch (section) {
|
||||
case "profiles":
|
||||
case "profiles": {
|
||||
return <Profiles />;
|
||||
case "summary":
|
||||
}
|
||||
case "summary": {
|
||||
return <Summary />;
|
||||
case "experience":
|
||||
}
|
||||
case "experience": {
|
||||
return <Experience />;
|
||||
case "education":
|
||||
}
|
||||
case "education": {
|
||||
return <Education />;
|
||||
case "awards":
|
||||
}
|
||||
case "awards": {
|
||||
return <Awards />;
|
||||
case "certifications":
|
||||
}
|
||||
case "certifications": {
|
||||
return <Certifications />;
|
||||
case "skills":
|
||||
}
|
||||
case "skills": {
|
||||
return <Skills />;
|
||||
case "interests":
|
||||
}
|
||||
case "interests": {
|
||||
return <Interests />;
|
||||
case "publications":
|
||||
}
|
||||
case "publications": {
|
||||
return <Publications />;
|
||||
case "volunteer":
|
||||
}
|
||||
case "volunteer": {
|
||||
return <Volunteer />;
|
||||
case "languages":
|
||||
}
|
||||
case "languages": {
|
||||
return <Languages />;
|
||||
case "projects":
|
||||
}
|
||||
case "projects": {
|
||||
return <Projects />;
|
||||
case "references":
|
||||
}
|
||||
case "references": {
|
||||
return <References />;
|
||||
default:
|
||||
}
|
||||
default: {
|
||||
if (section.startsWith("custom.")) return <Custom id={section.split(".")[1]} />;
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const Rhyhorn = ({ columns, isFirstPage = false }: TemplateProps) => {
|
||||
|
||||
@ -4,8 +4,3 @@ export type TemplateProps = {
|
||||
columns: SectionKey[][];
|
||||
isFirstPage?: boolean;
|
||||
};
|
||||
|
||||
export type BaseProps = {
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
@ -16,7 +16,7 @@ export default defineConfig({
|
||||
|
||||
server: {
|
||||
host: true,
|
||||
port: +(process.env.__DEV__ARTBOARD_PORT ?? 6173),
|
||||
port: 6173,
|
||||
fs: { allow: [searchForWorkspaceRoot(process.cwd())] },
|
||||
},
|
||||
|
||||
|
||||
@ -16,6 +16,18 @@
|
||||
},
|
||||
"plugins": ["lingui"],
|
||||
"rules": {
|
||||
// react
|
||||
"react/no-unescaped-entities": "off",
|
||||
"react/jsx-sort-props": [
|
||||
"error",
|
||||
{
|
||||
"reservedFirst": true,
|
||||
"callbacksLast": true,
|
||||
"shorthandFirst": true,
|
||||
"noSortAlphabetically": true
|
||||
}
|
||||
],
|
||||
|
||||
// react-hooks
|
||||
"react-hooks/exhaustive-deps": "off",
|
||||
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
const { join } = require("path");
|
||||
const path = require("node:path");
|
||||
|
||||
module.exports = {
|
||||
plugins: {
|
||||
"postcss-import": {},
|
||||
"tailwindcss/nesting": {},
|
||||
tailwindcss: { config: join(__dirname, "tailwind.config.js") },
|
||||
tailwindcss: { config: path.join(__dirname, "tailwind.config.js") },
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
|
||||
@ -54,7 +54,7 @@ export const AiActions = ({ value, onChange, className }: Props) => {
|
||||
toast({
|
||||
variant: "error",
|
||||
title: t`Oops, the server returned an error.`,
|
||||
description: (error as Error)?.message,
|
||||
description: (error as Error).message,
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
|
||||
@ -27,10 +27,7 @@ export const Copyright = ({ className }: Props) => (
|
||||
<span>{t`By the community, for the community.`}</span>
|
||||
<span>
|
||||
<Trans>
|
||||
A passion project by{" "}
|
||||
<a target="_blank" rel="noopener noreferrer nofollow" href="https://www.amruthpillai.com/">
|
||||
Amruth Pillai
|
||||
</a>
|
||||
A passion project by <a href="https://www.amruthpillai.com/">Amruth Pillai</a>
|
||||
</Trans>
|
||||
</span>
|
||||
|
||||
|
||||
@ -12,13 +12,15 @@ export const Icon = ({ size = 32, className }: Props) => {
|
||||
let src = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
|
||||
|
||||
switch (isDarkMode) {
|
||||
case false:
|
||||
case false: {
|
||||
src = "/icon/dark.svg";
|
||||
break;
|
||||
case true:
|
||||
}
|
||||
case true: {
|
||||
src = "/icon/light.svg";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<img
|
||||
|
||||
@ -38,8 +38,8 @@ export const LocaleCombobox = ({ value, onValueChange }: Props) => {
|
||||
<Command shouldFilter={false}>
|
||||
<CommandInput
|
||||
value={search}
|
||||
onValueChange={setSearch}
|
||||
placeholder={t`Search for a language`}
|
||||
onValueChange={setSearch}
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty>{t`No results found`}</CommandEmpty>
|
||||
@ -48,10 +48,10 @@ export const LocaleCombobox = ({ value, onValueChange }: Props) => {
|
||||
<div className="max-h-60">
|
||||
{options.map(({ original }) => (
|
||||
<CommandItem
|
||||
disabled={false}
|
||||
key={original.locale}
|
||||
disabled={false}
|
||||
value={original.locale.trim()}
|
||||
onSelect={async (selectedValue) => {
|
||||
onSelect={(selectedValue) => {
|
||||
const result = options.find(
|
||||
({ original }) => original.locale.trim() === selectedValue,
|
||||
);
|
||||
|
||||
@ -12,13 +12,15 @@ export const Logo = ({ size = 32, className }: Props) => {
|
||||
let src = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
|
||||
|
||||
switch (isDarkMode) {
|
||||
case false:
|
||||
case false: {
|
||||
src = "/logo/light.svg";
|
||||
break;
|
||||
case true:
|
||||
}
|
||||
case true: {
|
||||
src = "/logo/dark.svg";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<img
|
||||
|
||||
@ -12,9 +12,18 @@ export const UserAvatar = ({ size = 36, className }: Props) => {
|
||||
|
||||
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);
|
||||
|
||||
picture = (
|
||||
@ -25,15 +34,6 @@ export const UserAvatar = ({ size = 36, className }: Props) => {
|
||||
{initials}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
picture = (
|
||||
<img
|
||||
alt={user.name}
|
||||
src={user.picture}
|
||||
className="rounded-full"
|
||||
style={{ width: size, height: size }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <div className={className}>{picture}</div>;
|
||||
|
||||
@ -24,7 +24,11 @@ export const UserOptions = ({ children }: Props) => {
|
||||
<DropdownMenuTrigger asChild>{children}</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent side="top" align="start" className="w-48">
|
||||
<DropdownMenuItem onClick={() => navigate("/dashboard/settings")}>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
navigate("/dashboard/settings");
|
||||
}}
|
||||
>
|
||||
{t`Settings`}
|
||||
{/* eslint-disable-next-line lingui/no-unlocalized-strings */}
|
||||
<KeyboardShortcut>⇧S</KeyboardShortcut>
|
||||
|
||||
@ -40,9 +40,9 @@ type Action =
|
||||
toastId?: ToasterToast["id"];
|
||||
};
|
||||
|
||||
interface State {
|
||||
type State = {
|
||||
toasts: ToasterToast[];
|
||||
}
|
||||
};
|
||||
|
||||
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
|
||||
|
||||
@ -64,17 +64,19 @@ const addToRemoveQueue = (toastId: string) => {
|
||||
|
||||
export const reducer = (state: State, action: Action): State => {
|
||||
switch (action.type) {
|
||||
case "ADD_TOAST":
|
||||
case "ADD_TOAST": {
|
||||
return {
|
||||
...state,
|
||||
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
|
||||
};
|
||||
}
|
||||
|
||||
case "UPDATE_TOAST":
|
||||
case "UPDATE_TOAST": {
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) => (t.id === action.toast.id ? { ...t, ...action.toast } : t)),
|
||||
};
|
||||
}
|
||||
|
||||
case "DISMISS_TOAST": {
|
||||
const { toastId } = action;
|
||||
@ -82,9 +84,9 @@ export const reducer = (state: State, action: Action): State => {
|
||||
if (toastId) {
|
||||
addToRemoveQueue(toastId);
|
||||
} else {
|
||||
state.toasts.forEach((toast) => {
|
||||
for (const toast of state.toasts) {
|
||||
addToRemoveQueue(toast.id);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
@ -99,7 +101,7 @@ export const reducer = (state: State, action: Action): State => {
|
||||
),
|
||||
};
|
||||
}
|
||||
case "REMOVE_TOAST":
|
||||
case "REMOVE_TOAST": {
|
||||
if (action.toastId === undefined) {
|
||||
return {
|
||||
...state,
|
||||
@ -111,17 +113,18 @@ export const reducer = (state: State, action: Action): State => {
|
||||
toasts: state.toasts.filter((t) => t.id !== action.toastId),
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const listeners: Array<(state: State) => void> = [];
|
||||
const listeners: ((state: State) => void)[] = [];
|
||||
|
||||
let memoryState: State = { toasts: [] };
|
||||
|
||||
function dispatch(action: Action) {
|
||||
memoryState = reducer(memoryState, action);
|
||||
listeners.forEach((listener) => {
|
||||
for (const listener of listeners) {
|
||||
listener(memoryState);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
type Toast = Omit<ToasterToast, "id">;
|
||||
@ -129,12 +132,15 @@ type Toast = Omit<ToasterToast, "id">;
|
||||
function toast({ ...props }: Toast) {
|
||||
const id = createId();
|
||||
|
||||
const update = (props: ToasterToast) =>
|
||||
const update = (props: ToasterToast) => {
|
||||
dispatch({
|
||||
type: "UPDATE_TOAST",
|
||||
toast: { ...props, id },
|
||||
});
|
||||
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
|
||||
};
|
||||
const dismiss = () => {
|
||||
dispatch({ type: "DISMISS_TOAST", toastId: id });
|
||||
};
|
||||
|
||||
dispatch({
|
||||
type: "ADD_TOAST",
|
||||
@ -170,7 +176,9 @@ function useToast() {
|
||||
return {
|
||||
...state,
|
||||
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 { redirect } from "react-router-dom";
|
||||
|
||||
import { refresh } from "@/client/services/auth";
|
||||
|
||||
import { USER_KEY } from "../constants/query-keys";
|
||||
import { toast } from "../hooks/use-toast";
|
||||
import { refresh } from "../services/auth/refresh";
|
||||
import { translateError } from "../services/errors/translate-error";
|
||||
import { queryClient } from "./query-client";
|
||||
|
||||
export type ServerError = {
|
||||
statusCode: number;
|
||||
message: string;
|
||||
error: string;
|
||||
};
|
||||
|
||||
export const axios = _axios.create({ baseURL: "/api", withCredentials: true });
|
||||
|
||||
// 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
|
||||
const handleAuthError = async () => {
|
||||
try {
|
||||
await refresh(axiosForRefresh);
|
||||
|
||||
return Promise.resolve();
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
await Promise.resolve();
|
||||
};
|
||||
|
||||
// Interceptor to handle expired refresh token errors
|
||||
const handleRefreshError = async () => {
|
||||
try {
|
||||
queryClient.invalidateQueries({ queryKey: USER_KEY });
|
||||
void queryClient.invalidateQueries({ queryKey: USER_KEY });
|
||||
redirect("/auth/login");
|
||||
|
||||
return Promise.resolve();
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
await Promise.resolve();
|
||||
};
|
||||
|
||||
// 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 });
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (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 { RouterProvider } from "react-router-dom";
|
||||
import {
|
||||
createRoutesFromChildren,
|
||||
matchRoutes,
|
||||
RouterProvider,
|
||||
useLocation,
|
||||
useNavigationType,
|
||||
} from "react-router-dom";
|
||||
|
||||
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(
|
||||
<StrictMode>
|
||||
|
||||
@ -40,7 +40,7 @@ export const BackupOtpPage = () => {
|
||||
await backupOtp(data);
|
||||
|
||||
navigate("/dashboard");
|
||||
} catch (error) {
|
||||
} catch {
|
||||
form.reset();
|
||||
}
|
||||
};
|
||||
@ -87,7 +87,13 @@ export const BackupOtpPage = () => {
|
||||
/>
|
||||
|
||||
<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" />
|
||||
<span>{t`Back`}</span>
|
||||
</Button>
|
||||
|
||||
@ -89,7 +89,13 @@ export const ForgotPasswordPage = () => {
|
||||
/>
|
||||
|
||||
<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" />
|
||||
<span>{t`Back`}</span>
|
||||
</Button>
|
||||
|
||||
@ -30,7 +30,7 @@ export const LoginPage = () => {
|
||||
const { login, loading } = useLogin();
|
||||
|
||||
const { providers } = useAuthProviders();
|
||||
const emailAuthDisabled = !providers || !providers.includes("email");
|
||||
const emailAuthDisabled = !providers?.includes("email");
|
||||
|
||||
const formRef = useRef<HTMLFormElement>(null);
|
||||
usePasswordToggle(formRef);
|
||||
@ -43,7 +43,7 @@ export const LoginPage = () => {
|
||||
const onSubmit = async (data: FormValues) => {
|
||||
try {
|
||||
await login(data);
|
||||
} catch (error) {
|
||||
} catch {
|
||||
form.reset();
|
||||
}
|
||||
};
|
||||
|
||||
@ -34,7 +34,7 @@ export const RegisterPage = () => {
|
||||
const disableSignups = import.meta.env.VITE_DISABLE_SIGNUPS === "true";
|
||||
|
||||
const { providers } = useAuthProviders();
|
||||
const emailAuthDisabled = !providers || !providers.includes("email");
|
||||
const emailAuthDisabled = !providers?.includes("email");
|
||||
|
||||
const formRef = useRef<HTMLFormElement>(null);
|
||||
usePasswordToggle(formRef);
|
||||
@ -55,7 +55,7 @@ export const RegisterPage = () => {
|
||||
await register(data);
|
||||
|
||||
navigate("/auth/verify-email");
|
||||
} catch (error) {
|
||||
} catch {
|
||||
form.reset();
|
||||
}
|
||||
};
|
||||
|
||||
@ -26,7 +26,7 @@ type FormValues = z.infer<typeof resetPasswordSchema>;
|
||||
export const ResetPasswordPage = () => {
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const token = searchParams.get("token") || "";
|
||||
const token = searchParams.get("token") ?? "";
|
||||
|
||||
const { resetPassword, loading } = useResetPassword();
|
||||
|
||||
@ -43,7 +43,7 @@ export const ResetPasswordPage = () => {
|
||||
await resetPassword(data);
|
||||
|
||||
navigate("/auth/login");
|
||||
} catch (error) {
|
||||
} catch {
|
||||
form.reset();
|
||||
}
|
||||
};
|
||||
|
||||
@ -33,7 +33,7 @@ export const VerifyEmailPage = () => {
|
||||
|
||||
if (!token) return;
|
||||
|
||||
handleVerifyEmail(token);
|
||||
void handleVerifyEmail(token);
|
||||
}, [token, navigate, verifyEmail]);
|
||||
|
||||
return (
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user