release: v4.1.0

This commit is contained in:
Amruth Pillai
2024-05-05 14:55:06 +02:00
parent 68252c35fc
commit e87b05a93a
282 changed files with 11461 additions and 10713 deletions

View File

@ -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

View File

@ -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=

View File

@ -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"
} }
}, },
{ {

View File

@ -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

View File

@ -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
View File

@ -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

View File

@ -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).

View File

@ -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" }
} }
} }
``` ```

View File

@ -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 its 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)

View File

@ -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.

View File

@ -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",

View File

@ -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: {},
}, },

View File

@ -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>

View File

@ -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 />;

View File

@ -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 }}

View File

@ -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 />;

View File

@ -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 });
},
})); }));

View File

@ -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) => {

View File

@ -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) => {

View File

@ -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) => {

View File

@ -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) => {

View File

@ -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) => {

View File

@ -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) => {

View File

@ -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;
}
}
}; };

View File

@ -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) => {

View File

@ -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) => {

View File

@ -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) => {

View File

@ -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) => {

View File

@ -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) => {

View File

@ -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) => {

View File

@ -4,8 +4,3 @@ export type TemplateProps = {
columns: SectionKey[][]; columns: SectionKey[][];
isFirstPage?: boolean; isFirstPage?: boolean;
}; };
export type BaseProps = {
children?: React.ReactNode;
className?: string;
};

View File

@ -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())] },
}, },

View File

@ -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",

View File

@ -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: {},
}, },
}; };

View File

@ -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);

View File

@ -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>

View File

@ -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

View File

@ -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,
); );

View File

@ -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

View File

@ -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>;

View File

@ -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>

View File

@ -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 });
},
}; };
} }

View File

@ -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

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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();
} }
}; };

View File

@ -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();
} }
}; };

View File

@ -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();
} }
}; };

View File

@ -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