merge branch main

Signed-off-by: abizek <abishekilango@protonmail.com>
This commit is contained in:
abizek
2024-05-20 13:12:53 +05:30
301 changed files with 32052 additions and 26217 deletions

View File

@ -1,22 +1,62 @@
!README.md
.dockerignore
# Compiled Output
dist
tmp
/out-tsc
# Project Dependencies
.git
.gitignore
node_modules
# Docker
compose*.yml
Dockerfile
# IDEs and Editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vs/*
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# Miscellaneous
/.sass-cache
/connect.lock
/coverage
/libpeerconnection.log
npm-debug.log
yarn-error.log
testem.log
/typings
# System Files
.DS_Store
Thumbs.db
.editorconfig
.eslint*
.git
.github
.gitignore
.husky
# Generated Files
.nx
.prettier*
.vscode
*.env*
*.md
compose*.yml
dist
Dockerfile
node_modules
Thumbs.db
tmp
.swc
fly.toml
stats.html
tools/compose/*
tools/scripts/*
tools/scripts/*
# Environment Variables
*.env*
!.env.example
# Lingui Compiled Messages
apps/client/src/locales/_build/
apps/client/src/locales/*/messages.mjs

View File

@ -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
@ -38,6 +30,8 @@ REFRESH_TOKEN_SECRET=refresh_token_secret
CHROME_PORT=8080
CHROME_TOKEN=chrome_token
CHROME_URL=ws://localhost:8080
# Launch puppeteer with flag to ignore https errors
# CHROME_IGNORE_HTTPS_ERRORS=true
# Mail Server (for e-mails)
# For testing, you can use https://ethereal.email/create
@ -53,19 +47,22 @@ 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=
# SENTRY_AUTH_TOKEN=
# SERVER_SENTRY_DSN=
# VITE_CLIENT_SENTRY_DSN=
# Nx Cloud (Optional)
# NX_CLOUD_ACCESS_TOKEN=
# 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=

View File

@ -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,38 @@
},
{
"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": "off",
"@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/no-redundant-type-constituents": "off",
"@typescript-eslint/consistent-type-definitions": ["error", "type"],
// unicorn
"unicorn/no-null": "off",
"unicorn/prevent-abbreviations": "off",
"unicorn/prefer-string-replace-all": "off",
"unicorn/prefer-structured-clone": "off"
}
},
{

View File

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

View File

@ -1,44 +0,0 @@
name: Lint and Build
on:
push:
branches:
- main
pull_request:
branches:
- main
workflow_dispatch:
permissions:
actions: read
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
NX_BRANCH: ${{ github.event.number || github.ref_name }}
NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
jobs:
main:
name: Nx Cloud - Main Job
uses: nrwl/ci/.github/workflows/nx-cloud-main.yml@v0.14.0
with:
main-branch-name: main
number-of-agents: 3
init-commands: |
pnpm exec prisma generate
pnpm exec nx-cloud start-ci-run --stop-agents-after="build" --agent-count=3
parallel-commands: |
pnpm exec nx-cloud record -- pnpm exec nx format:check
parallel-commands-on-agents: |
pnpm exec nx affected --target=lint --parallel=3
pnpm exec nx affected --target=build --parallel=3
agents:
name: Nx Cloud - Agents
uses: nrwl/ci/.github/workflows/nx-cloud-agents.yml@v0.14.0
with:
number-of-agents: 3

54
.github/workflows/lint-test-build.yml vendored Normal file
View File

@ -0,0 +1,54 @@
name: Lint, Test & Build
concurrency:
group: "${{ github.workflow }}-${{ github.ref }}"
cancel-in-progress: true
on:
workflow_dispatch:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
main:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4.1.1
with:
fetch-depth: 2
- name: Setup pnpm
uses: pnpm/action-setup@v3.0.0
with:
version: 9.1.0
- name: Setup Node.js
uses: actions/setup-node@v4.0.2
with:
cache: "pnpm"
node-version: 20.12.2
- name: Install Dependencies
run: pnpm install --frozen-lockfile
- name: Lint
run: pnpm lint
- name: Format
run: pnpm format:check
- name: Test
run: pnpm test
- name: Build
run: pnpm build
env:
NODE_ENV: production
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}

View File

@ -7,7 +7,7 @@ on:
- "*"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
group: "${{ github.workflow }}-${{ github.ref }}"
cancel-in-progress: true
env:
@ -21,7 +21,6 @@ jobs:
version: ${{ steps.version.outputs.version }}
strategy:
fail-fast: false
matrix:
platform:
- linux/amd64
@ -29,7 +28,7 @@ jobs:
steps:
- name: Checkout Repository
uses: actions/checkout@v4.1.1
uses: actions/checkout@v4.1.4
- name: Extract version from package.json
id: version
@ -38,42 +37,47 @@ jobs:
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.0.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.0.0
- name: Login to Docker Hub
uses: docker/login-action@v3.0.0
uses: docker/login-action@v3.1.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}
- name: Login to GitHub Container Registery
uses: docker/login-action@v3.0.0
uses: docker/login-action@v3.1.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ github.token }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.3.0
- name: Extract Docker Metadata
id: meta
uses: docker/metadata-action@v5.0.0
uses: docker/metadata-action@v5.5.1
with:
tags: type=semver,pattern={{version}},prefix=v,value=${{ steps.version.outputs.version }}
images: |
${{ env.IMAGE }}
ghcr.io/${{ env.IMAGE }}
- name: Prepare a unique name for Artifacts
id: artifact_name
run: |
name=$(echo -n "${{ matrix.platform }}" | sed -e 's/[ \t:\/\\"<>|*?]/-/g' -e 's/--*/-/g')
echo "name=$name" >> "$GITHUB_OUTPUT"
- name: Build and Push by Digest
uses: docker/build-push-action@v5.0.0
uses: docker/build-push-action@v5.3.0
id: build
with:
context: .
cache-from: type=gha
cache-to: type=gha,mode=max
platforms: ${{ matrix.platform }}
labels: ${{ steps.meta.outputs.labels }}
outputs: type=image,name=${{ env.IMAGE }},push-by-digest=true,name-canonical=true,push=true
build-args: |
SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
NX_CLOUD_ACCESS_TOKEN=${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
- name: Export Digest
@ -83,9 +87,9 @@ jobs:
touch "/tmp/digests/${digest#sha256:}"
- name: Upload Digest
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4.3.3
with:
name: digests
name: digests-${{ steps.artifact_name.outputs.name }}
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1
@ -98,33 +102,34 @@ jobs:
steps:
- name: Checkout Repository
uses: actions/checkout@v4.1.1
uses: actions/checkout@v4.1.4
- name: Download Digest
uses: actions/download-artifact@v3.0.0
uses: actions/download-artifact@v4.1.7
with:
name: digests
path: /tmp/digests
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.0.0
pattern: digests-*
merge-multiple: true
- name: Login to Docker Hub
uses: docker/login-action@v3.0.0
uses: docker/login-action@v3.1.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}
- name: Login to GitHub Container Registery
uses: docker/login-action@v3.0.0
uses: docker/login-action@v3.1.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ github.token }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.3.0
- name: Extract Docker Metadata
id: meta
uses: docker/metadata-action@v5.0.0
uses: docker/metadata-action@v5.5.1
with:
tags: type=semver,pattern={{version}},prefix=v,value=${{ needs.build.outputs.version }}
images: |
@ -142,8 +147,18 @@ jobs:
docker buildx imagetools inspect ${{ env.IMAGE }}:${{ steps.meta.outputs.version }}
- name: Update Repository Description
uses: peter-evans/dockerhub-description@v3
uses: peter-evans/dockerhub-description@v4.0.0
with:
repository: ${{ github.repository }}
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}
- uses: sarisia/actions-status-discord@v1.14.3
if: always()
with:
username: ReleaseBot
webhook: ${{ secrets.DISCORD_WEBHOOK }}
status: ${{ job.status }}
title: "Release `${{ steps.meta.outputs.version }}`"
description: "A new version of Reactive Resume just dropped! 🚀"
url: "https://github.com/AmruthPillai/Reactive-Resume"

9
.gitignore vendored
View File

@ -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
@ -48,7 +42,6 @@ Thumbs.db
.swc
fly.toml
stats.html
libs/prisma
# Environment Variables
*.env*

View File

@ -2,5 +2,6 @@
"$schema": "https://raw.githubusercontent.com/raineorshine/npm-check-updates/main/src/types/RunOptions.json",
"upgrade": true,
"install": "always",
"reject": ["@nestjs-modules/mailer"]
"packageManager": "pnpm",
"reject": ["eslint", "vite-plugin-chunk-split", "@reactive-resume/*"]
}

View File

@ -10,5 +10,6 @@
"tools/compose/*"
]
},
"i18n-ally.localesPaths": ["apps/client/src/locales"]
"i18n-ally.localesPaths": ["apps/client/src/locales"],
"vitest.disableWorkspaceWarning": true
}

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

View File

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

View File

@ -1,39 +1,49 @@
ARG SENTRY_AUTH_TOKEN
ARG NX_CLOUD_ACCESS_TOKEN
# --- Base Image ---
FROM node:lts-bullseye-slim AS base
ARG SENTRY_AUTH_TOKEN
ARG NX_CLOUD_ACCESS_TOKEN
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
ARG NX_CLOUD_ACCESS_TOKEN
RUN corepack enable
RUN corepack enable pnpm && corepack prepare pnpm@9.0.6 --activate
WORKDIR /app
# --- Build Image ---
FROM base AS build
ENV NX_CLOUD_ACCESS_TOKEN=$NX_CLOUD_ACCESS_TOKEN
ARG SENTRY_AUTH_TOKEN
ARG NX_CLOUD_ACCESS_TOKEN
COPY .npmrc package.json pnpm-lock.yaml ./
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
COPY ./tools/prisma /app/tools/prisma
RUN pnpm install --frozen-lockfile
COPY . .
ENV SENTRY_AUTH_TOKEN=$SENTRY_AUTH_TOKEN
ENV NX_CLOUD_ACCESS_TOKEN=$NX_CLOUD_ACCESS_TOKEN
RUN pnpm run build
# --- Release Image ---
FROM base AS release
ARG NX_CLOUD_ACCESS_TOKEN
RUN apt update && apt install -y dumb-init --no-install-recommends
RUN apt update && apt install -y dumb-init --no-install-recommends && rm -rf /var/lib/apt/lists/*
COPY --chown=node:node --from=build /app/.npmrc /app/package.json /app/pnpm-lock.yaml ./
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-lockfile
RUN pnpm install --prod --frozen-lockfile
COPY --chown=node:node --from=build /app/dist ./dist
COPY --chown=node:node --from=build /app/tools/prisma ./tools/prisma
RUN pnpm run prisma:generate
ENV TZ=UTC
ENV PORT=3000
ENV NODE_ENV=production
EXPOSE 3000

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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
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,36 +449,50 @@ 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;
}
}
};

View File

@ -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,36 +460,50 @@ 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;
}
}
};

View File

@ -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,36 +461,50 @@ 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;
}
}
};

View File

@ -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,36 +487,50 @@ 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;
}
}
};

View File

@ -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,34 +456,47 @@ 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;
}
}
};

View File

@ -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,36 +466,50 @@ 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;
}
}
};

View File

@ -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":
}
case "onyx": {
return Onyx;
case "pikachu":
}
case "pikachu": {
return Pikachu;
case "rhyhorn":
}
case "rhyhorn": {
return Rhyhorn;
default:
}
default: {
return Onyx;
}
}
};

View File

@ -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,34 +417,47 @@ 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;
}
}
};

View File

@ -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,32 +414,44 @@ 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;
}
}
};

View File

@ -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,36 +464,50 @@ 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;
}
}
};

View File

@ -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,34 +455,47 @@ 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;
}
}
};

View File

@ -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,36 +487,50 @@ 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;
}
}
};

View File

@ -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,36 +459,50 @@ 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;
}
}
};

View File

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

View File

@ -2,7 +2,8 @@
import { nxViteTsPaths } from "@nx/vite/plugins/nx-tsconfig-paths.plugin";
import react from "@vitejs/plugin-react-swc";
import { defineConfig, searchForWorkspaceRoot, splitVendorChunkPlugin } from "vite";
import { defineConfig, searchForWorkspaceRoot } from "vite";
import { chunkSplitPlugin } from "vite-plugin-chunk-split";
export default defineConfig({
base: "/artboard/",
@ -11,15 +12,22 @@ export default defineConfig({
build: {
sourcemap: true,
emptyOutDir: true,
},
server: {
host: true,
port: +(process.env.__DEV__ARTBOARD_PORT ?? 6173),
port: 6173,
fs: { allow: [searchForWorkspaceRoot(process.cwd())] },
},
plugins: [react(), nxViteTsPaths(), splitVendorChunkPlugin()],
plugins: [
react(),
nxViteTsPaths(),
chunkSplitPlugin({
strategy: "unbundle",
}),
],
resolve: {
alias: {

View File

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

View File

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

View File

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

View File

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

View File

@ -12,12 +12,14 @@ 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 (

View File

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

View File

@ -12,12 +12,14 @@ 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 (

View File

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

View File

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

View File

@ -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,
@ -110,18 +112,19 @@ export const reducer = (state: State, action: Action): State => {
...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 });
},
};
}

View File

@ -4,18 +4,13 @@ import _axios from "axios";
import createAuthRefreshInterceptor from "axios-auth-refresh";
import { redirect } from "react-router-dom";
import { refreshToken } 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));
},
);
@ -45,26 +40,12 @@ axios.interceptors.response.use(
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);
}
};
const handleAuthError = () => refreshToken(axiosForRefresh);
// Interceptor to handle expired refresh token errors
const handleRefreshError = async () => {
try {
queryClient.invalidateQueries({ queryKey: USER_KEY });
redirect("/auth/login");
return Promise.resolve();
} catch (error) {
return Promise.reject(error);
}
await queryClient.invalidateQueries({ queryKey: USER_KEY });
redirect("/auth/login");
};
// 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 });
}
// 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

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 { 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: 1,
replaysOnErrorSampleRate: 1,
replaysSessionSampleRate: 1,
});
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const root = ReactDOM.createRoot(document.querySelector("#root")!);
root.render(
<StrictMode>

Some files were not shown because too many files have changed in this diff Show More