diff --git a/.dockerignore b/.dockerignore index 38ed7c1a2..538f2652b 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,62 +1,15 @@ -# 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 +docs +.env* +/data +.nitro +.cursor +.output +.vercel +.netlify +coverage .DS_Store -Thumbs.db -.editorconfig -.eslint* - -# Generated Files -.nx -.swc -fly.toml -stats.html -tools/compose/* -tools/scripts/* - -# Environment Variables -*.env* -!.env.example - -# Lingui Compiled Messages -apps/client/src/locales/_build/ -apps/client/src/locales/*/messages.mjs +.tanstack +node_modules +public/sw.js +public/workbox-*.js \ No newline at end of file diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index ff9b6fc9d..000000000 --- a/.editorconfig +++ /dev/null @@ -1,12 +0,0 @@ -root = true - -[*] -charset = utf-8 -indent_style = space -indent_size = 2 -insert_final_newline = true -trim_trailing_whitespace = true - -[*.md] -max_line_length = off -trim_trailing_whitespace = false diff --git a/.env.example b/.env.example index cd6a7d009..56de618f4 100644 --- a/.env.example +++ b/.env.example @@ -1,81 +1,77 @@ -# Environment -NODE_ENV=development +# --- Server --- +TZ="Etc/UTC" +APP_URL="http://localhost:3000" -# Ports -PORT=3000 +# Optional, uses APP_URL by default +# This can be set to a different URL (like http://host.docker.internal:3000 or http://{docker_service}:3000) +# to let the browser navigate to a non-public instance of Reactive Resume +PRINTER_APP_URL="http://host.docker.internal:3000" -# 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 -STORAGE_URL=http://localhost:9000/default # default is the bucket name specified in the STORAGE_BUCKET variable +# Note: set this to `http://host.docker.internal:3000` if your `gotenberg` service is running on docker, but Reactive Resume is running outside of Docker. -# Database (Prisma/PostgreSQL) -# This can be swapped out to use any other database, like MySQL -# Note: This is used only in the compose.yml file -POSTGRES_PORT=5432 -POSTGRES_DB=postgres -POSTGRES_USER=postgres -POSTGRES_PASSWORD=postgres +# --- Printer --- +GOTENBERG_ENDPOINT="http://localhost:4000" -# Database (Prisma/PostgreSQL) -DATABASE_URL=postgresql://postgres:postgres@localhost:5432/postgres?schema=public +# Gotenberg Authentication (Optional) +# TIP: It's safest to avoid exposing your Gotenberg instance to the public internet; connect via a private Docker network instead. +# However, if Gotenberg needs to be hosted remotely from Reactive Resume, use these credentials to enable basic authentication for security. +# For setup details and security best practices, see: https://gotenberg.dev/docs/configuration#api:~:text=API%5FENABLE%5FBASIC%5FAUTH +# GOTENBERG_USERNAME="" +# GOTENBERG_PASSWORD="" -# Authentication Secrets -# generated with `openssl rand -base64 64` -ACCESS_TOKEN_SECRET=access_token_secret -REFRESH_TOKEN_SECRET=refresh_token_secret +# --- Database (PostgreSQL) --- +DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres" -# Chrome Browser (for printing) -# generated with `openssl rand -hex 32` -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 +# --- Authentication --- +# Generated using `openssl rand -hex 32` +AUTH_SECRET="change-me-to-a-secure-secret-key-in-production" -# Mail Server (for e-mails) -# For testing, you can use https://ethereal.email/create -MAIL_FROM=noreply@localhost -# SMTP_URL=smtp://username:password@smtp.ethereal.email:587 +# Social Auth (Google, optional) +GOOGLE_CLIENT_ID="" +GOOGLE_CLIENT_SECRET="" -# Storage -STORAGE_ENDPOINT=localhost -STORAGE_PORT=9000 -STORAGE_REGION=us-east-1 -STORAGE_BUCKET=default -STORAGE_ACCESS_KEY=minioadmin -STORAGE_SECRET_KEY=minioadmin -STORAGE_USE_SSL=false -STORAGE_SKIP_BUCKET_CHECK=false +# Social Auth (GitHub, optional) +GITHUB_CLIENT_ID="" +GITHUB_CLIENT_SECRET="" -# Nx Cloud (Optional) -# NX_CLOUD_ACCESS_TOKEN= +# Custom OAuth Provider +OAUTH_PROVIDER_NAME="" +OAUTH_CLIENT_ID="" +OAUTH_CLIENT_SECRET="" +OAUTH_DISCOVERY_URL="" +OAUTH_AUTHORIZATION_URL="" -# Crowdin (Optional) -# CROWDIN_PROJECT_ID= -# CROWDIN_PERSONAL_TOKEN= +# --- Email (optional) --- +# If all keys are disabled, the app logs the email to be sent to the console instead. +SMTP_HOST="localhost" +SMTP_PORT="1025" +SMTP_USER="" +SMTP_PASS="" +SMTP_FROM="Reactive Resume " +SMTP_SECURE="false" -# Feature Flags (Optional) -# DISABLE_SIGNUPS=false -# DISABLE_EMAIL_AUTH=false +# --- Storage (optional) --- +# If all keys are disabled, the app uses local filesystem (/data) to store uploads instead. +# Make sure to mount this directory to a volume or the host filesystem to ensure data integrity. -# GitHub (OAuth, Optional) -# GITHUB_CLIENT_ID= -# GITHUB_CLIENT_SECRET= -# GITHUB_CALLBACK_URL=http://localhost:5173/api/auth/github/callback +# Seaweedfs +S3_ACCESS_KEY_ID="seaweedfs" +S3_SECRET_ACCESS_KEY="seaweedfs" +S3_REGION="us-east-1" +S3_ENDPOINT="http://localhost:8333" +S3_BUCKET="reactive-resume" +S3_FORCE_PATH_STYLE="true" -# Google (OAuth, Optional) -# GOOGLE_CLIENT_ID= -# GOOGLE_CLIENT_SECRET= -# GOOGLE_CALLBACK_URL=http://localhost:5173/api/auth/google/callback +# --- Feature Flags --- +# This flag bypasses the check that the endpoint to fetch data for the printing of PDFs `getByIdForPrinter`, is only accessible from the server. +# Useful for when you want to debug the /printer/{resumeId} route to quickly take a peek at the page that is sent to the printer. +FLAG_DEBUG_PRINTER="false" -# OpenID (Optional) -# VITE_OPENID_NAME= -# OPENID_AUTHORIZATION_URL= -# OPENID_CALLBACK_URL=http://localhost:5173/api/auth/openid/callback -# OPENID_CLIENT_ID= -# OPENID_CLIENT_SECRET= -# OPENID_ISSUER= -# OPENID_SCOPE=openid profile email -# OPENID_TOKEN_URL= -# OPENID_USER_INFO_URL= +# This flag disables new signups, both on the web app and the server. +FLAG_DISABLE_SIGNUP="false" + +# --- Others --- +# Google Cloud API Key (optional) +# This is not used within Reactive Resume, but in src/scripts/fonts/generate.ts to generate a list of fonts served by Google Fonts. +# Note: Make sure "Google Fonts Developer API" is unrestricted. +GOOGLE_CLOUD_API_KEY="" diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 3c3629e64..000000000 --- a/.eslintignore +++ /dev/null @@ -1 +0,0 @@ -node_modules diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index 3f6167f7a..000000000 --- a/.eslintrc.json +++ /dev/null @@ -1,94 +0,0 @@ -{ - "root": true, - "ignorePatterns": ["**/*"], - "plugins": ["@nx"], - "overrides": [ - { - "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], - "plugins": ["simple-import-sort", "unused-imports"], - "rules": { - // eslint - "no-console": "error", - "no-return-await": "off", - - // simple-import-sort - "simple-import-sort/imports": "error", - "simple-import-sort/exports": "error", - - // unused-imports - "unused-imports/no-unused-imports": "error", - "unused-imports/no-unused-vars": [ - "warn", - { - "vars": "all", - "varsIgnorePattern": "^_", - "args": "after-used", - "argsIgnorePattern": "^_" - } - ], - - // nx - "@nx/enforce-module-boundaries": [ - "error", - { - "allowCircularSelfDependency": true, - "enforceBuildableLibDependency": true, - "allow": [], - "depConstraints": [ - { - "sourceTag": "*", - "onlyDependOnLibsWithTags": ["*"] - } - ] - } - ] - } - }, - { - "files": ["*.ts", "*.tsx"], - "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-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/consistent-type-imports": "error", - "@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" - } - }, - { - "files": ["*.js", "*.jsx"], - "extends": ["plugin:@nx/javascript"], - "rules": { - // eslint - "no-console": "warn", - "no-unused-vars": "off" - } - } - ] -} diff --git a/.github/.well-known/funding-manifest-urls b/.github/.well-known/funding-manifest-urls deleted file mode 100644 index 906b8366f..000000000 --- a/.github/.well-known/funding-manifest-urls +++ /dev/null @@ -1 +0,0 @@ -https://rxresu.me/funding.json \ No newline at end of file diff --git a/.github/.well-known/funding.json b/.github/.well-known/funding.json new file mode 100644 index 000000000..8ac965bb9 --- /dev/null +++ b/.github/.well-known/funding.json @@ -0,0 +1,68 @@ +{ + "$schema": "https://fundingjson.org/schema/v1.1.0.json", + "version": "v1.1.0", + "entity": { + "type": "individual", + "role": "maintainer", + "name": "Amruth Pillai", + "email": "im.amruth@gmail.com", + "description": "Software Engineer", + "webpageUrl": { + "url": "https://rxresu.me/funding.json" + } + }, + "projects": [ + { + "guid": "reactive-resume", + "name": "Reactive Resume", + "description": "A free and open-source resume builder that simplifies the process of creating, updating, and sharing your resume.", + "webpageUrl": { + "url": "https://rxresu.me" + }, + "repositoryUrl": { + "url": "https://github.com/AmruthPillai/Reactive-Resume", + "wellKnown": "https://github.com/AmruthPillai/Reactive-Resume/blob/main/.github/.well-known/funding-manifest-urls" + }, + "licenses": ["spdx:MIT"], + "tags": ["data", "design", "productivity", "resume-builder"] + } + ], + "funding": { + "plans": [ + { + "guid": "sponsor", + "status": "active", + "name": "Sponsor", + "description": "Support the project on a recurring basis by becoming a sponsor.", + "amount": 10, + "currency": "EUR", + "frequency": "monthly", + "channels": ["github", "open-collective"] + }, + { + "guid": "donation", + "status": "active", + "name": "Donation", + "description": "Show your support for the project by making a one-time donation.", + "amount": 0, + "currency": "EUR", + "frequency": "one-time", + "channels": ["github", "open-collective"] + } + ], + "channels": [ + { + "guid": "github", + "type": "payment-provider", + "description": "GitHub Sponsors", + "address": "https://github.com/sponsors/AmruthPillai" + }, + { + "guid": "open-collective", + "type": "payment-provider", + "description": "Open Collective", + "address": "https://opencollective.com/reactive-resume" + } + ] + } +} diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 0348c6de0..189c84f84 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,2 +1,2 @@ github: AmruthPillai -open_collective: reactive-resume +open_collective: reactive-resume \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/1-bug-report.yml similarity index 63% rename from .github/ISSUE_TEMPLATE/bug-report.yml rename to .github/ISSUE_TEMPLATE/1-bug-report.yml index 3ec06560a..83319a1b9 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/1-bug-report.yml @@ -3,7 +3,7 @@ name: 🐞 Bug Report description: Create a bug report to help improve Reactive Resume title: "[Bug] " -labels: [bug, v4, needs triage] +labels: [bug, v5, needs triage] assignees: "AmruthPillai" body: @@ -28,43 +28,11 @@ body: - type: textarea attributes: - label: Current Behavior - description: A concise description of what you're experiencing. + label: Describe the bug you're experiencing + description: A detailed description of what you're experiencing. Please provide as much detail as possible as it will help me diagnose and fix the issue faster. validations: required: true - - type: textarea - attributes: - label: Expected Behavior - description: A concise description of what you expected to happen. - validations: - required: false - - - type: textarea - attributes: - label: Steps To Reproduce - description: Detailed steps to reproduce the behavior, so that it can be easily diagnosed. - placeholder: | - 1. Go to '...' - 2. Click on '....' - 3. Scroll down to '....' - 4. See error - validations: - required: false - - - type: dropdown - id: browsers - attributes: - label: What browsers are you seeing the problem on? - multiple: true - options: - - Firefox - - Chrome - - Safari - - Microsoft Edge - validations: - required: false - - type: dropdown id: template attributes: @@ -76,11 +44,12 @@ body: - Bronzor - Chikorita - Ditto + - Ditgar - Gengar - Glalie - Kakuna + - Lapras - Leafish - - Nosepass - Onyx - Pikachu - Rhyhorn diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/2-feature-request.yml similarity index 69% rename from .github/ISSUE_TEMPLATE/feature-request.yml rename to .github/ISSUE_TEMPLATE/2-feature-request.yml index 2caa86110..6f9aec820 100644 --- a/.github/ISSUE_TEMPLATE/feature-request.yml +++ b/.github/ISSUE_TEMPLATE/2-feature-request.yml @@ -3,7 +3,7 @@ name: ✨ Feature Request description: Suggest an feature or idea that you would like to see in Reactive Resume title: "[Feature] <title>" -labels: [enhancement, v4, needs triage] +labels: [feature, v5, needs triage] assignees: "AmruthPillai" body: @@ -18,6 +18,6 @@ body: - type: textarea attributes: label: Feature Description - description: A concise description of what feature you would like to see in Reactive Resume. + description: A detailed description of the feature you would like to see in Reactive Resume. Please provide as much detail as possible as it will help me implement the feature faster. validations: - required: true + required: true \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..ec4bb386b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: false \ No newline at end of file diff --git a/.github/workflows/publish-docker-image.yml b/.github/workflows/docker-build.yml similarity index 92% rename from .github/workflows/publish-docker-image.yml rename to .github/workflows/docker-build.yml index 9e03ef30d..96aca9e3a 100644 --- a/.github/workflows/publish-docker-image.yml +++ b/.github/workflows/docker-build.yml @@ -1,4 +1,4 @@ -name: Publish Docker Image +name: Build Docker Image on: workflow_dispatch: @@ -167,4 +167,14 @@ jobs: - name: Inspect image run: | docker buildx imagetools inspect ghcr.io/${{ env.IMAGE }}:latest - docker buildx imagetools inspect docker.io/${{ env.IMAGE }}:latest \ No newline at end of file + + - name: Redeploy Service + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + uses: appleboy/ssh-action@v1 + with: + key: ${{ secrets.SSH_KEY }} + host: ${{ secrets.SSH_HOST }} + username: ${{ secrets.SSH_USER }} + script: | + cd docker + ./manage_stack.sh up reactive_resume \ No newline at end of file diff --git a/.github/workflows/lint-test-build.yml b/.github/workflows/lint-test-build.yml deleted file mode 100644 index 146c0574d..000000000 --- a/.github/workflows/lint-test-build.yml +++ /dev/null @@ -1,45 +0,0 @@ -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.2.2 - with: - fetch-depth: 2 - - - name: Setup pnpm - uses: pnpm/action-setup@v4.0.0 - - - name: Setup Node.js - uses: actions/setup-node@v4.1.0 - with: - cache: "pnpm" - node-version: 22 - - - name: Install Dependencies - run: pnpm install --frozen-lockfile - - - name: Lint - run: pnpm run lint - - - name: Test - run: pnpm run test - - - name: Build - run: pnpm run build diff --git a/.github/workflows/sync-crowdin-translations.yml b/.github/workflows/sync-crowdin-translations.yml deleted file mode 100644 index 48042f217..000000000 --- a/.github/workflows/sync-crowdin-translations.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: Sync Crowdin Translations - -on: - workflow_dispatch: - schedule: - - cron: 0 0 * * * # everyday at midnight (UTC) - -jobs: - sync: - if: github.repository == 'AmruthPillai/Reactive-Resume' - runs-on: ubuntu-latest - - steps: - - name: Checkout Repository - uses: actions/checkout@v4.1.1 - - - name: Sync Translations - uses: crowdin/github-action@v1.15.2 - with: - upload_sources: true - upload_translations: true - download_translations: true - create_pull_request: true - localization_branch_name: "l10n" - pull_request_base_branch_name: "main" - pull_request_title: "New Translations from Crowdin" - pull_request_body: "You've got new translations to be merged into the app from contributors on Crowdin.\n\n_This pull request was automatically created by the [Crowdin Action](https://github.com/marketplace/actions/crowdin-action)._" - env: - GITHUB_TOKEN: ${{ github.token }} - CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} - CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} diff --git a/.gitignore b/.gitignore index 79fe9b0bb..4f5f0030b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,53 +1,15 @@ -# Compiled Output dist -tmp -/out-tsc - -# Project Dependencies -node_modules - -# 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 +.env* +/data +.nitro +.output +.vercel +.cursor +coverage +.netlify .DS_Store -Thumbs.db - -# Generated Files -.nx -.swc -.turbo -fly.toml -stats.html - -# Environment Variables -*.env* +.tanstack +node_modules !.env.example - -# Lingui Compiled Messages -apps/client/src/locales/_build/ -apps/client/src/locales/*/messages.*js +public/sw.js +public/workbox-*.js \ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 000000000..8d8913426 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,48 @@ +stages: + - build + +variables: + DOCKER_DRIVER: overlay2 + DOCKER_TLS_CERTDIR: "/certs" + +build-and-push: + stage: build + image: docker:24 + services: + - docker:24-dind + rules: + - if: $CI_COMMIT_BRANCH == "main" + - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/ + - if: $CI_PIPELINE_SOURCE == "merge_request_event" + - if: $CI_PIPELINE_SOURCE == "web" + before_script: + - apk add --no-cache jq + - echo "$CI_REGISTRY_PASSWORD" | docker login -u $CI_REGISTRY_USER --password-stdin $CI_REGISTRY + - | + export VERSION=$(jq -r .version package.json) + echo "VERSION=$VERSION" >> build.env + script: + - source build.env + - | + SHORT_SHA=$CI_COMMIT_SHORT_SHA + PRIMARY_TAG="$CI_REGISTRY_IMAGE:$SHORT_SHA" + + # Build the image + docker build -f ./Dockerfile -t $PRIMARY_TAG . + + # Tag with additional tags + docker tag $PRIMARY_TAG $CI_REGISTRY_IMAGE:latest + if [ -n "$VERSION" ] && [ "$VERSION" != "null" ]; then + docker tag $PRIMARY_TAG $CI_REGISTRY_IMAGE:v$VERSION + fi + + # Push all tags (only if not a merge request) + if [ "$CI_PIPELINE_SOURCE" != "merge_request_event" ]; then + docker push $PRIMARY_TAG + docker push $CI_REGISTRY_IMAGE:latest + if [ -n "$VERSION" ] && [ "$VERSION" != "null" ]; then + docker push $CI_REGISTRY_IMAGE:v$VERSION + fi + fi + after_script: + - docker logout $CI_REGISTRY || true diff --git a/.ncurc.cjs b/.ncurc.cjs new file mode 100644 index 000000000..c6547081c --- /dev/null +++ b/.ncurc.cjs @@ -0,0 +1,17 @@ +// @ts-check + +const nextPackages = ["@monaco-editor/react"]; + +const betaPackages = ["drizzle-orm", "drizzle-kit", "@better-auth/core", "@better-auth/passkey", "better-auth"]; + +/** @type {import('npm-check-updates').RunOptions} */ +module.exports = { + upgrade: true, + install: "always", + packageManager: "pnpm", + target: (packageName) => { + if (nextPackages.includes(packageName)) return "@next"; + if (betaPackages.includes(packageName)) return "@beta"; + return "latest"; + }, +}; diff --git a/.ncurc.json b/.ncurc.json deleted file mode 100644 index b44084530..000000000 --- a/.ncurc.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "$schema": "https://raw.githubusercontent.com/raineorshine/npm-check-updates/main/src/types/RunOptions.json", - "upgrade": true, - "target": "minor", - "install": "always", - "packageManager": "pnpm", - "reject": [ - "nx", - "eslint", - "@nx/*", - "@swc/*", - "@swc-node/*", - "@reactive-resume/*", - "eslint-plugin-unused-imports" - ] -} diff --git a/.npmrc b/.npmrc deleted file mode 100644 index 83252e62c..000000000 --- a/.npmrc +++ /dev/null @@ -1,3 +0,0 @@ -auto-install-peers=true -enable-pre-post-scripts=true -strict-peer-dependencies=false \ No newline at end of file diff --git a/.prettierignore b/.prettierignore deleted file mode 100644 index c418e9be7..000000000 --- a/.prettierignore +++ /dev/null @@ -1,7 +0,0 @@ -/dist -/coverage -/.nx/cache -stats.html -pnpm-lock.yaml -compose-dev.yml -compose.yml \ No newline at end of file diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index 36e1ed2fc..000000000 --- a/.prettierrc +++ /dev/null @@ -1,6 +0,0 @@ -{ - "printWidth": 100, - "endOfLine": "auto", - "plugins": ["prettier-plugin-tailwindcss"], - "tailwindFunctions": ["cn", "cva"] -} diff --git a/.vscode/extensions.json b/.vscode/extensions.json index d7df89c9c..ef3ea0582 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,3 +1,3 @@ { - "recommendations": ["esbenp.prettier-vscode", "dbaeumer.vscode-eslint"] + "recommendations": ["biomejs.biome", "bradlc.vscode-tailwindcss", "lokalise.i18n-ally"] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 46b3e644a..16aa729f6 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,9 +1,43 @@ { - "css.validate": false, - "i18n-ally.localesPaths": ["apps/client/src/locales"], - "tailwindCSS.experimental.classRegex": [ - ["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"], - ["cn\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"] - ], - "typescript.tsdk": "node_modules/typescript/lib" + "[mdx]": { + "editor.defaultFormatter": "unifiedjs.vscode-mdx" + }, + "biome.enabled": true, + "editor.codeActionsOnSave": { + "source.biome": "explicit", + "source.fixAll.biome": "explicit", + "source.organizeImports.biome": "explicit" + }, + "editor.defaultFormatter": "biomejs.biome", + "eslint.enable": false, + "files.associations": { + "*.css": "tailwindcss" + }, + "files.readonlyInclude": { + "locales/*.po": true, + "pnpm-lock.yaml": true, + "**/routeTree.gen.ts": true + }, + "files.watcherExclude": { + "locales/*.po": true, + "pnpm-lock.yaml": true, + "**/routeTree.gen.ts": true + }, + "i18n-ally.enabledParsers": ["po"], + "i18n-ally.localesPaths": ["locales"], + "i18n-ally.sourceLanguage": "en-US", + "prettier.enable": false, + "search.exclude": { + "locales/*.po": true, + "pnpm-lock.yaml": true, + "**/routeTree.gen.ts": true + }, + "tailwindCSS.classFunctions": ["cn", "cva"], + "tailwindCSS.experimental.classRegex": [ + ["class:\\s*?[\"'`]([^\"'`]*).*?,"], + ["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"], + ["cn\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"] + ], + "tailwindCSS.experimental.configFile": "src/styles/globals.css", + "typescript.tsdk": "node_modules/typescript/lib" } diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 7662073d1..000000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,19 +0,0 @@ -# Changelog - -### What’s changed from v3 to v4? - -**The entire app has been rebuilt and reimagined from the ground up.** - -The **user interface** has been greatly streamlined to prioritise your content and resume. The design of templates has also undergone a major overhaul. Previously, we utilised TailwindCSS for creating templates, but now you can rely on CSS (styled-components) to build any design you prefer. With this change, I hope to offer a **much wider variety of templates** compared to the previous version. - -When it comes to features, there are many to mention, but some highlights include the **ability to use your own OpenAI API key** (stored on your browser) and leverage GPTs to enhance your resume writing skills. With this, you can improve your writing, correct spelling and grammar, and even adjust the tone of the text to be more confident or casual. - -When you make your resume publicly available, you are provided with a link that you can share with potential recruiters and employers. This change allows you to **track the number of views or downloads your resume has received**, so you can stay informed about when someone has checked out your resume. - -When it comes to **security**, you now have the option to protect your account with **two-factor authentication**. This means that whenever you log in to Reactive Resume, you will also need to enter a one-time code generated on your phone. This additional step ensures that only you have access to your account. - -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, 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). diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md deleted file mode 100644 index a545f08a3..000000000 --- a/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,132 +0,0 @@ -# Contributor Covenant Code of Conduct - -## Our Pledge - -We as members, contributors, and leaders pledge to make participation in our -community a harassment-free experience for everyone, regardless of age, body -size, visible or invisible disability, ethnicity, sex characteristics, gender -identity and expression, level of experience, education, socio-economic status, -nationality, personal appearance, race, caste, color, religion, or sexual -identity and orientation. - -We pledge to act and interact in ways that contribute to an open, welcoming, -diverse, inclusive, and healthy community. - -## Our Standards - -Examples of behavior that contributes to a positive environment for our -community include: - -- Demonstrating empathy and kindness toward other people -- Being respectful of differing opinions, viewpoints, and experiences -- Giving and gracefully accepting constructive feedback -- Accepting responsibility and apologizing to those affected by our mistakes, - and learning from the experience -- Focusing on what is best not just for us as individuals, but for the overall - community - -Examples of unacceptable behavior include: - -- The use of sexualized language or imagery, and sexual attention or advances of - any kind -- Trolling, insulting or derogatory comments, and personal or political attacks -- Public or private harassment -- Publishing others' private information, such as a physical or email address, - without their explicit permission -- Other conduct which could reasonably be considered inappropriate in a - professional setting - -## Enforcement Responsibilities - -Community leaders are responsible for clarifying and enforcing our standards of -acceptable behavior and will take appropriate and fair corrective action in -response to any behavior that they deem inappropriate, threatening, offensive, -or harmful. - -Community leaders have the right and responsibility to remove, edit, or reject -comments, commits, code, wiki edits, issues, and other contributions that are -not aligned to this Code of Conduct, and will communicate reasons for moderation -decisions when appropriate. - -## Scope - -This Code of Conduct applies within all community spaces, and also applies when -an individual is officially representing the community in public spaces. -Examples of representing our community include using an official email address, -posting via an official social media account, or acting as an appointed -representative at an online or offline event. - -## Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported to the community leaders responsible for enforcement at -[INSERT CONTACT METHOD]. -All complaints will be reviewed and investigated promptly and fairly. - -All community leaders are obligated to respect the privacy and security of the -reporter of any incident. - -## Enforcement Guidelines - -Community leaders will follow these Community Impact Guidelines in determining -the consequences for any action they deem in violation of this Code of Conduct: - -### 1. Correction - -**Community Impact**: Use of inappropriate language or other behavior deemed -unprofessional or unwelcome in the community. - -**Consequence**: A private, written warning from community leaders, providing -clarity around the nature of the violation and an explanation of why the -behavior was inappropriate. A public apology may be requested. - -### 2. Warning - -**Community Impact**: A violation through a single incident or series of -actions. - -**Consequence**: A warning with consequences for continued behavior. No -interaction with the people involved, including unsolicited interaction with -those enforcing the Code of Conduct, for a specified period of time. This -includes avoiding interactions in community spaces as well as external channels -like social media. Violating these terms may lead to a temporary or permanent -ban. - -### 3. Temporary Ban - -**Community Impact**: A serious violation of community standards, including -sustained inappropriate behavior. - -**Consequence**: A temporary ban from any sort of interaction or public -communication with the community for a specified period of time. No public or -private interaction with the people involved, including unsolicited interaction -with those enforcing the Code of Conduct, is allowed during this period. -Violating these terms may lead to a permanent ban. - -### 4. Permanent Ban - -**Community Impact**: Demonstrating a pattern of violation of community -standards, including sustained inappropriate behavior, harassment of an -individual, or aggression toward or disparagement of classes of individuals. - -**Consequence**: A permanent ban from any sort of public interaction within the -community. - -## Attribution - -This Code of Conduct is adapted from the [Contributor Covenant][homepage], -version 2.1, available at -[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. - -Community Impact Guidelines were inspired by -[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. - -For answers to common questions about this code of conduct, see the FAQ at -[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at -[https://www.contributor-covenant.org/translations][translations]. - -[homepage]: https://www.contributor-covenant.org -[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html -[Mozilla CoC]: https://github.com/mozilla/diversity -[FAQ]: https://www.contributor-covenant.org/faq -[translations]: https://www.contributor-covenant.org/translations diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index d192d3d50..000000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,95 +0,0 @@ -# Contributing to Reactive Resume - -## 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 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: - -#### Requirements - -- Docker (with Docker Compose) -- Node.js 20 or higher (with pnpm) - -### 1. Fork and Clone the Repository - -```sh -git clone https://github.com/{your-github-username}/Reactive-Resume.git -cd Reactive-Resume -``` - -### 2. Install dependencies - -```sh -pnpm install -``` - -### 3. Copy .env.example to .env - -```sh -cp .env.example .env -``` - -Please have a brief look over the environment variables and change them if necessary, for example, change the ports if you have a conflicting service running on your machine already. - -### 4. Fire up all the required services through Docker Compose - -```sh -docker compose -f tools/compose/development.yml --env-file .env -p reactive-resume up -d -``` - -It should take just under half a minute for all the services to be booted up correctly. You can check the status of all services by running `docker compose -p reactive-resume ps` - -### 5. Run the development server - -```sh -pnpm prisma:migrate:dev -pnpm dev -``` - -If everything went well, the frontend should be running on `http://localhost:5173` and the backend api should be accessible through `http://localhost:3000`. There is a proxy present to also route all requests to `http://localhost:5173/api` directly to the API. If you need to change the `PORT` environment variable for the server, please make sure to update the `apps/client/proxy.conf.json` file as well with the new endpoint. - -You can also visit `http://localhost:3000/api/health`, the health check endpoint of the server to check if the server is running correctly, and it is able to connect to all it's dependent services. The output of the health check endpoint should look like this: - -```json -{ - "status": "ok", - "info": { - "database": { "status": "up" }, - "storage": { "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" } - } -} -``` - ---- - -## Pushing changes to the app - -Firstly, ensure that there is a GitHub Issue created for the feature or bugfix you are working on. If it does not exist, create an issue and assign it to yourself. - -Once you are happy with the changes you've made locally, commit it to your repository. Note that the project makes use of Conventional Commits, so commit messages would have to be in a specific format for it to be accepted. For example, a commit message to fix the translation on the homepage could look like: - -``` -git commit -m "fix(homepage): fix typo on homepage in the faq section" -``` - -It helps to be as descriptive as possible in commit messages so that users can be aware of the changes made by you. - -Then, you can run the basic checks, as to make sure the CI has the most chance of being successful: - -``` -pnpm run lint - -pnpm run test - -pnpm run build -``` - -Finally, create a pull request to merge the changes on your forked repository to the original repository hosted on AmruthPillai/Reactive-Resume. I can take a look at the changes you've made when I have the time and have it merged onto the app. diff --git a/Dockerfile b/Dockerfile index a9b9c812e..24d851ab3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,47 +1,48 @@ -ARG NX_CLOUD_ACCESS_TOKEN +# syntax=docker/dockerfile:1 -# --- Base Image --- -FROM node:lts-bullseye-slim AS base -ARG NX_CLOUD_ACCESS_TOKEN +# ---------- Dependencies Layer ---------- +FROM node:24-slim AS dependencies ENV PNPM_HOME="/pnpm" ENV PATH="$PNPM_HOME:$PATH" +RUN corepack enable +RUN mkdir -p /tmp/dev /tmp/prod + +COPY package.json pnpm-lock.yaml /tmp/dev/ +COPY package.json pnpm-lock.yaml /tmp/prod/ + +RUN --mount=type=cache,id=pnpm,target=/pnpm/store \ + cd /tmp/dev && pnpm install --frozen-lockfile + +RUN --mount=type=cache,id=pnpm,target=/pnpm/store \ + cd /tmp/prod && pnpm install --frozen-lockfile --prod + +# ---------- Builder Layer ---------- +FROM node:24-slim AS builder + +ENV PNPM_HOME="/pnpm" +ENV PATH="$PNPM_HOME:$PATH" RUN corepack enable WORKDIR /app -# --- Build Image --- -FROM base AS build -ARG NX_CLOUD_ACCESS_TOKEN - -COPY .npmrc package.json pnpm-lock.yaml ./ -COPY ./tools/prisma /app/tools/prisma -RUN pnpm install --frozen-lockfile - +COPY --from=dependencies /tmp/dev/node_modules ./node_modules COPY . . -ENV NX_CLOUD_ACCESS_TOKEN=$NX_CLOUD_ACCESS_TOKEN - RUN pnpm run build -# --- Release Image --- -FROM base AS release -ARG NX_CLOUD_ACCESS_TOKEN +# ---------- Runtime Layer ---------- +FROM node:24-slim AS runtime -RUN apt update && apt install -y dumb-init --no-install-recommends && rm -rf /var/lib/apt/lists/* +WORKDIR /app -COPY --chown=node:node --from=build /app/.npmrc /app/package.json /app/pnpm-lock.yaml ./ -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 +COPY --from=builder /app/.output ./.output +COPY --from=builder /app/migrations ./migrations +COPY --from=dependencies /tmp/prod/node_modules ./node_modules -CMD [ "dumb-init", "pnpm", "run", "start" ] +EXPOSE 3000/tcp + +ENTRYPOINT ["node", "-r", "reflect-metadata", ".output/server/index.mjs"] diff --git a/ISSUE_AI_CHAT_BUILDER.md b/ISSUE_AI_CHAT_BUILDER.md new file mode 100644 index 000000000..876810d08 --- /dev/null +++ b/ISSUE_AI_CHAT_BUILDER.md @@ -0,0 +1,194 @@ +# Feature Request: AI Chat Builder for Resume Editing + +I don't know much about this, so I'm looking for volunteers to help with the implementation of this feature I'd like to see on Reactive Resume, and I hope the community finds interest in this feature too. + +## Prerequisites + +For this feature to work, the following conditions must be met: + +- User is logged into Reactive Resume +- User has set up AI integration on their account + - This means that an AI provider, model and API key should be available for usage on the browser through `useAIStore` + +## User Story + +1. I have just created a new resume, or I am currently editing a resume I made earlier and I'm on the builder screen. +2. I know what I want, I need to make a few edits here and there, rewrite my summary so it reads better, but I'm lazy. +3. I see a button (outline) on the header, right next to the name on the resume and the menu icon that reads **"Build with AI"** and has a sparkles ✨ icon on its left. + - This button is only enabled if I have AI integration enabled. If I don't, this button shouldn't be visible at all so as to not hinder the user experience for non-AI enabled users. +4. Clicking on the button displays a chat overlay that can be toggled open/close which covers the right portion of the screen (about 380px width). +5. The chat window should animate open, sliding from the bottom. Below the window, there should be a floating action button which controls the visibility of this window. If the button hasn't been interacted with in the last 1 minute, hide the button until the user clicks on the "Build with AI" button again. +6. The AI chat should display an initial greeting message. Users can type in what they want to change specifically in the resume, some examples include: + - "Rewrite the professional summary" + - "Translate all of the section headings to German" + - "Make my work experience descriptions more impactful" + - "Add keywords related to software engineering" +7. The request should be sent using `await client.ai.chat({ input: { aiStoreData, resumeId } })`. Ideally, the oRPC chat route should explicitly accept the `resumeId` in its input and have it available in the context, ensuring that any AI-driven changes or updates are accurately mapped to the correct resume. +8. This should display the changes to be made to the resume and apply them. Maybe using tools? I'm not too sure if that's what tools are used for. + +## What Already Exists + +The codebase already has the foundational pieces in place: + +### AI Store (`src/integrations/ai/store.ts`) + +Manages the AI configuration stored in localStorage: + +```typescript +type AIStoreState = { + enabled: boolean; + provider: AIProvider; // "openai" | "gemini" | "anthropic" | "ollama" | ...; + model: string; + apiKey: string; + baseURL: string; + testStatus: TestStatus; +}; +``` + +You can check if AI is enabled via: + +```typescript +const enabled = useAIStore((state) => state.enabled); +``` + +### oRPC AI Router (`src/integrations/orpc/router/ai.ts`) + +Existing AI endpoints that demonstrate streaming patterns: + +```typescript +// Example: testConnection streams a response +testConnection: protectedProcedure + .input(z.object({ provider, model, apiKey, baseURL })) + .handler(async function* ({ input }) { + const stream = streamText({ + model: getModel(input), + messages: [{ role: "user", content: 'Respond with "1"' }], + }); + yield* stream.textStream; + }), +``` + +The router also uses `generateText` with structured output for parsing documents - this pattern could be useful for applying changes to resume data. + +### Resume Store (`src/components/resume/store/resume.ts`) + +Provides `updateResumeData()` for modifying the resume: + +```typescript +updateResumeData: (fn) => { + set((state) => { + if (!state.resume) return state; + if (state.resume.isLocked) { + // show error toast + return state; + } + fn(state.resume.data as WritableDraft<ResumeData>); + syncResume(current(state.resume)); + }); +}, +``` + +### Builder Header (`src/routes/builder/$resumeId/-components/header.tsx`) + +This is where the "Build with AI" button should be placed, next to the resume name and dropdown menu. + +## Technical Considerations + +Here are some implementation hints for anyone interested in picking this up: + +### 1. Button Placement + +Add the button in the `BuilderHeader` component, conditionally rendered based on `useAIStore().enabled`: + +```typescript +const enabled = useAIStore((state) => state.enabled); + +// In the JSX, only show if enabled +{enabled && ( + <Button variant="outline" onClick={openChatPanel}> + <SparkleIcon /> + Build with AI + </Button> +)} +``` + +### 2. Chat Panel Component + +- Create a new component (e.g., `AIChatPanel`) that renders as an overlay +- Width: ~380px, positioned on the right side of the screen +- Animate in from the bottom using Framer Motion (already in the project as `motion/react`) +- Include a floating action button (FAB) below the panel for quick toggle +- Auto-hide the FAB after 1 minute of inactivity + +### 3. Chat State Management + +Consider creating a new Zustand store for chat state: + +```typescript +type AIChatStore = { + isOpen: boolean; + messages: ChatMessage[]; + lastInteraction: number; + // actions... +}; +``` + +### 4. oRPC Route for Chat + +Create a new route in `src/integrations/orpc/router/ai.ts`: + +```typescript +chat: protectedProcedure + .input(z.object({ + ...aiCredentialsSchema.shape, + resumeId: z.string(), + message: z.string(), + })) + .handler(async function* ({ input, context }) { + // Get resume data for context + // Stream AI response + // Optionally use tools for structured changes + }), +``` + +### 5. Applying Changes + +The tricky part is how to apply AI-suggested changes to the resume. Some options: + +- **AI SDK Tools**: Use the Vercel AI SDK's tool calling feature to let the AI invoke specific functions like `updateSummary`, `translateHeadings`, etc. +- **Structured Output**: Have the AI return a JSON patch or specific update instructions that can be applied via `updateResumeData()` +- **Diff Display**: Show proposed changes before applying them, letting users accept/reject + +### 6. Resume Data Schema + +The resume data schema is defined in `src/schema/resume/data.ts` and includes sections like: +- `basics` (name, headline, summary, contact info) +- `experience`, `education`, `skills`, `projects`, etc. +- `metadata` (template, typography, colors) +- `customSections` + +## Design Principles + +The premise is simple and nothing that hasn't been done before, but I'd like to see it done **subtly** as this is not an AI-first app, it should just be AI-enabled. + +- The feature should feel like an optional enhancement, not a core requirement +- Non-AI users should not feel like they're missing out or see disabled features +- The UI should be clean and non-intrusive +- Performance should not be impacted when AI features are not in use + +## Getting Started + +If you're interested in contributing: + +1. Fork the repository +2. Set up the development environment (see README) +3. Configure AI settings in Dashboard > Settings > AI +4. Start exploring the code references mentioned above + +## Questions? + +Feel free to ask questions in the comments below. I'm happy to provide more context or clarify any requirements. + +--- + +**Labels**: `enhancement`, `help wanted`, `good first issue`, `ai` diff --git a/README.md b/README.md index 62f00e466..bf1f94dd6 100644 --- a/README.md +++ b/README.md @@ -1,116 +1,244 @@ +<!-- <div align="center" markdown="1"> <a href="https://go.warp.dev/Reactive-Resume"> <img alt="Warp Sponsorship" width="400" src="https://github.com/warpdotdev/brand-assets/blob/main/Github/Sponsor/Warp-Github-LG-03.png?raw=true" /> </a> -### [Warp, built for coding with multiple AI agents.](https://go.warp.dev/Reactive-Resume) + ### [Warp, built for coding with multiple AI agents.](https://go.warp.dev/Reactive-Resume) -[Available for MacOS, Linux, & Windows](https://go.warp.dev/Reactive-Resume)<br> + [Available for MacOS, Linux, & Windows](https://go.warp.dev/Reactive-Resume) +</div> + +--- +--> + +<div align="center"> + <a href="https://rxresu.me"> + <img src="public/opengraph/banner.jpg" alt="Reactive Resume" /> + </a> + + <h1>Reactive Resume</h1> + + <p>Reactive Resume is a free and open-source resume builder that simplifies the process of creating, updating, and sharing your resume.</p> + + <p> + <a href="https://rxresu.me"><strong>Get Started</strong></a> + · + <a href="https://docs.rxresu.me"><strong>Learn More</strong></a> + </p> + + <p> + <img src="https://img.shields.io/github/stars/AmruthPillai/Reactive-Resume?style=flat-square" alt="Stars" /> + <img src="https://img.shields.io/github/license/AmruthPillai/Reactive-Resume?style=flat-square" alt="License" /> + <img src="https://img.shields.io/docker/pulls/amruthpillai/reactive-resume?style=flat-square" alt="Docker Pulls" /> + <a href="https://discord.gg/hzwkZbyvUW"><img src="https://img.shields.io/discord/1173518977851473940?label=Discord&style=flat-square&logo=discord" alt="Discord" /></a> + <a href="https://crowdin.com/project/reactive-resume"><img src="https://badges.crowdin.net/reactive-resume/localized.svg?style=flat-square" alt="Crowdin" /></a> + <a href="https://opencollective.com/reactive-resume"><img src="https://img.shields.io/opencollective/all/reactive-resume?style=flat-square" alt="Open Collective" /></a> + </p> + + <a href="https://www.producthunt.com/products/reactive-resume?embed=true&utm_source=badge-featured&utm_medium=badge&utm_campaign=badge-reactive-resume-v5-2" target="_blank" rel="noopener noreferrer"><img alt="Reactive Resume v5 - A free and open-source resume builder. | Product Hunt" width="250" height="54" src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=1065182&theme=light&t=1768850745585"></a> +</div> --- -<img alt="Reactive Resume" width="800" src="https://i.imgur.com/FFc4nyZ.jpg" /> +Reactive Resume makes building resumes straightforward. Pick a template, fill in your details, and export to PDF—no account required for basic use. For those who want more control, the entire application can be self-hosted on your own infrastructure. -![App Version](https://img.shields.io/github/package-json/version/AmruthPillai/Reactive-Resume?label=version) -[![Docker Pulls](https://img.shields.io/docker/pulls/amruthpillai/reactive-resume)](https://hub.docker.com/repository/docker/amruthpillai/reactive-resume) -[![GitHub Sponsors](https://img.shields.io/github/sponsors/AmruthPillai)](https://github.com/sponsors/AmruthPillai) -[![Crowdin](https://badges.crowdin.net/reactive-resume/localized.svg)](https://crowdin.com/project/reactive-resume) -[![Discord](https://img.shields.io/discord/1173518977851473940?label=discord&link=https%3A%2F%2Fdiscord.gg%2FhzwkZbyvUW)](https://discord.gg/hzwkZbyvUW) - -# Reactive Resume - -A free and open-source resume builder that simplifies the process of creating, updating, and sharing your resume. - -### [Go to App](https://rxresu.me/) | [Docs](https://docs.rxresu.me/) - -</div> - -## Description - -Reactive Resume is a free and open-source resume builder that simplifies the process of creating, updating, and sharing your resume. With zero user tracking or advertising, your privacy is a top priority. The platform is extremely user-friendly and can be self-hosted in less than 30 seconds if you wish to own your data completely. - -It's available in multiple languages and comes packed with features such as real-time editing, dozens of templates, drag-and-drop customisation, and integration with OpenAI for enhancing your writing. - -You can share a personalised link of your resume to potential employers, track its views or downloads, and customise your page layout by dragging-and-dropping sections. The platform also supports various font options and provides dozens of templates to choose from. And yes, there's even a dark mode for a more comfortable viewing experience. - -Start creating your standout resume with Reactive Resume today! - -## Templates - -| Azurill | Bronzor | Chikorita | -| ----------------------------------------------------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------- | -| <img src="./apps/client/public/templates/jpg/azurill.jpg" width="200px" /> | <img src="./apps/client/public/templates/jpg/bronzor.jpg" width="200px" /> | <img src="./apps/client/public/templates/jpg/chikorita.jpg" width="200px" /> | - -| Ditto | Gengar | Glalie | -| -------------------------------------------------------------------- | ---------------------------------------------------------------------- | ---------------------------------------------------------------------- | -| <img src="./apps/client/public/templates/jpg/ditto.jpg" width="200px" /> | <img src="./apps/client/public/templates/jpg/gengar.jpg" width="200px" /> | <img src="./apps/client/public/templates/jpg/glalie.jpg" width="200px" /> | - -| Kakuna | Leafish | Nosepass | -| ---------------------------------------------------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------ | -| <img src="./apps/client/public/templates/jpg/kakuna.jpg" width="200px" /> | <img src="./apps/client/public/templates/jpg/leafish.jpg" width="200px" /> | <img src="./apps/client/public/templates/jpg/nosepass.jpg" width="200px" /> | - -| Onyx | Pikachu | Rhyhorn | -| ------------------------------------------------------------------- | ----------------------------------------------------------------------- | ----------------------------------------------------------------------- | -| <img src="./apps/client/public/templates/jpg/onyx.jpg" width="200px" /> | <img src="./apps/client/public/templates/jpg/pikachu.jpg" width="200px" /> | <img src="./apps/client/public/templates/jpg/rhyhorn.jpg" width="200px" /> | +Built with privacy as a core principle, Reactive Resume gives you complete ownership of your data. The codebase is fully open-source under the MIT license, with no tracking, no ads, and no hidden costs. ## Features -- **Free, forever** and open-source -- No telemetry, user tracking or advertising -- 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 -- **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 -- Customize the colours and layouts to add a personal touch to your resume -- Customise your page layout as you like just by dragging-and-dropping sections -- Create custom sections that are specific to your industry if the existing ones don't fit -- Jot down personal notes specific to your resume that's only visible to you -- Lock a resume to prevent making any further edits (useful for master templates) -- **Dozens of templates** to choose from, ranging from professional to modern -- Design your resume using the standardised EuroPass design template -- Supports printing resumes in A4 or Letter page formats -- Design your resume with any font that's available on [Google Fonts](https://fonts.google.com/) -- **Share a personalised link of your resume** to companies or recruiters for them to get the latest updates -- You can track the number of views or downloads your public resume has received -- Built with state-of-the-art (at the moment) and dependable technologies that's battle tested and peer reviewed by the open-source community on GitHub -- **MIT License**, so do what you like with the code as long as you credit the original author -- And yes, there’s a dark mode too 🌓 +**Resume Building** -## Built With +- Real-time preview as you type +- Multiple export formats (PDF, JSON) +- Drag-and-drop section ordering +- Custom sections for any content type +- Rich text editor with formatting support -- React (Vite), for the frontend -- 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 -- 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) -- GitHub/Google OAuth (for quickly authenticating users) -- LinguiJS and Crowdin (for translation management and localization) +**Templates** -## Star History +- Professionally designed templates +- A4 and Letter size support +- Customizable colors, fonts, and spacing +- Custom CSS for advanced styling -<a href="https://star-history.com/#AmruthPillai/Reactive-Resume&Date"> - <picture> - <source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=AmruthPillai/Reactive-Resume&type=Date&theme=dark" /> - <source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=AmruthPillai/Reactive-Resume&type=Date" /> - <img alt="Star History Chart" src="https://api.star-history.com/svg?repos=AmruthPillai/Reactive-Resume&type=Date" /> - </picture> -</a> +**Privacy & Control** + +- Self-host on your own infrastructure +- No tracking or analytics by default +- Full data export at any time +- Delete your data permanently with one click + +**Extras** + +- AI integration (OpenAI, Google Gemini, Anthropic Claude) +- Multi-language support +- Share resumes via unique links +- Import from JSON Resume format +- Dark mode support +- Passkey and two-factor authentication + +## Templates + +<table> + <tr> + <td align="center"> + <img src="public/templates/jpg/azurill.jpg" alt="Azurill" width="150" /> + <br /><sub><b>Azurill</b></sub> + </td> + <td align="center"> + <img src="public/templates/jpg/bronzor.jpg" alt="Bronzor" width="150" /> + <br /><sub><b>Bronzor</b></sub> + </td> + <td align="center"> + <img src="public/templates/jpg/chikorita.jpg" alt="Chikorita" width="150" /> + <br /><sub><b>Chikorita</b></sub> + </td> + <td align="center"> + <img src="public/templates/jpg/ditto.jpg" alt="Ditto" width="150" /> + <br /><sub><b>Ditto</b></sub> + </td> + </tr> + <tr> + <td align="center"> + <img src="public/templates/jpg/gengar.jpg" alt="Gengar" width="150" /> + <br /><sub><b>Gengar</b></sub> + </td> + <td align="center"> + <img src="public/templates/jpg/glalie.jpg" alt="Glalie" width="150" /> + <br /><sub><b>Glalie</b></sub> + </td> + <td align="center"> + <img src="public/templates/jpg/kakuna.jpg" alt="Kakuna" width="150" /> + <br /><sub><b>Kakuna</b></sub> + </td> + <td align="center"> + <img src="public/templates/jpg/lapras.jpg" alt="Lapras" width="150" /> + <br /><sub><b>Lapras</b></sub> + </td> + </tr> + <tr> + <td align="center"> + <img src="public/templates/jpg/leafish.jpg" alt="Leafish" width="150" /> + <br /><sub><b>Leafish</b></sub> + </td> + <td align="center"> + <img src="public/templates/jpg/onyx.jpg" alt="Onyx" width="150" /> + <br /><sub><b>Onyx</b></sub> + </td> + <td align="center"> + <img src="public/templates/jpg/pikachu.jpg" alt="Pikachu" width="150" /> + <br /><sub><b>Pikachu</b></sub> + </td> + <td align="center"> + <img src="public/templates/jpg/rhyhorn.jpg" alt="Rhyhorn" width="150" /> + <br /><sub><b>Rhyhorn</b></sub> + </td> + </tr> + <tr> + <td align="center"> + <img src="public/templates/jpg/ditgar.jpg" alt="Ditgar" width="150" /> + <br /><sub><b>Ditgar</b></sub> + </td> + </tr> +</table> + +## Quick Start + +The quickest way to run Reactive Resume locally: + +```bash +# Clone the repository +git clone https://github.com/AmruthPillai/Reactive-Resume.git +cd Reactive-Resume + +# Start all services +docker compose up -d + +# Access the app +open http://localhost:3000 +``` + +For detailed setup instructions, environment configuration, and self-hosting guides, see the [documentation](https://docs.rxresu.me). + +## Tech Stack + +| Category | Technology | +| ---------------- | ------------------------------------ | +| Framework | TanStack Start (React 19, Vite) | +| Runtime | Node.js | +| Language | TypeScript | +| Database | PostgreSQL with Drizzle ORM | +| API | ORPC (Type-safe RPC) | +| Auth | Better Auth | +| Styling | Tailwind CSS | +| UI Components | Radix UI | +| State Management | Zustand + TanStack Query | + +## Documentation + +Comprehensive guides are available at [docs.rxresu.me](https://docs.rxresu.me): + +| Guide | Description | +| --------------------------------------------------------------------------- | --------------------------------- | +| [Getting Started](https://docs.rxresu.me/getting-started) | First-time setup and basic usage | +| [Self-Hosting](https://docs.rxresu.me/guides/self-hosting-with-docker) | Deploy on your own server | +| [Development Setup](https://docs.rxresu.me/contributing/development) | Local development environment | +| [Project Architecture](https://docs.rxresu.me/contributing/architecture) | Codebase structure and patterns | +| [Exporting Your Resume](https://docs.rxresu.me/guides/exporting-your-resume)| PDF and JSON export options | + +## Self-Hosting + +Reactive Resume can be self-hosted using Docker. The stack includes: + +- **PostgreSQL** — Database for storing user data and resumes +- **Gotenberg** — Headless Chrome service for PDF generation +- **SeaweedFS** (optional) — S3-compatible storage for file uploads + +Pull the latest image from Docker Hub or GitHub Container Registry: + +```bash +# Docker Hub +docker pull amruthpillai/reactive-resume:latest + +# GitHub Container Registry +docker pull ghcr.io/amruthpillai/reactive-resume:latest +``` + +See the [self-hosting guide](https://docs.rxresu.me/guides/self-hosting-with-docker) for complete instructions. + +## Support + +Reactive Resume is and always will be free and open-source. If it has helped you land a job or saved you time, please consider supporting continued development: + +<p> + <a href="https://github.com/sponsors/AmruthPillai"> + <img src="https://img.shields.io/badge/GitHub%20Sponsors-Support-ea4aaa?style=flat-square&logo=github-sponsors" alt="GitHub Sponsors" /> + </a> + <a href="https://opencollective.com/reactive-resume"> + <img src="https://img.shields.io/badge/Open%20Collective-Contribute-7FADF2?style=flat-square&logo=open-collective" alt="Open Collective" /> + </a> +</p> + +Other ways to support: + +- Star this repository +- Report bugs and suggest features +- Improve documentation +- Help with translations + +## Contributing + +Contributions make open-source thrive. Whether fixing a typo or adding a feature, all contributions are welcome. + +1. Fork the repository +2. Create a feature branch (`git checkout -b feature/amazing-feature`) +3. Commit your changes (`git commit -m 'Add amazing feature'`) +4. Push to the branch (`git push origin feature/amazing-feature`) +5. Open a Pull Request + +See the [development setup guide](https://docs.rxresu.me/contributing/development) for detailed instructions on how to set up the project locally. ## License -Reactive Resume is packaged and distributed using the [MIT License](/LICENSE.md) which allows for commercial use, distribution, modification and private use provided that all copies of the software contain the same license and copyright. - -_By the community, for the community._ -A passion project by [Amruth Pillai](https://www.amruthpillai.com/) - -<p> - <a href="https://m.do.co/c/ceae1fff245e"> - <img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/PoweredByDO/DO_Powered_by_Badge_blue.svg" width="200px"> - </a> -</p> +[MIT](./LICENSE) — do whatever you want with it. diff --git a/SECURITY.md b/SECURITY.md deleted file mode 100644 index 2e11158de..000000000 --- a/SECURITY.md +++ /dev/null @@ -1,14 +0,0 @@ -# Security Policy - -## Supported Versions - -| Version | Supported | -| ------- | ------------------ | -| 4.x.x | :white_check_mark: | -| 3.x.x | :x: | -| 2.x.x | :x: | -| 1.x.x | :x: | - -## Reporting a Vulnerability - -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. diff --git a/apps/artboard/.eslintrc.json b/apps/artboard/.eslintrc.json deleted file mode 100644 index 257e8a3e2..000000000 --- a/apps/artboard/.eslintrc.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "extends": ["plugin:@nx/react", "../../.eslintrc.json"], - "ignorePatterns": ["!**/*"], - "overrides": [ - { - "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], - "rules": { - // eslint - "@typescript-eslint/no-require-imports": "off", - - // 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" - } - }, - { - "files": ["*.ts", "*.tsx"], - "rules": {} - }, - { - "files": ["*.js", "*.jsx"], - "rules": {} - } - ] -} diff --git a/apps/artboard/index.html b/apps/artboard/index.html deleted file mode 100644 index 1ba268476..000000000 --- a/apps/artboard/index.html +++ /dev/null @@ -1,44 +0,0 @@ -<!doctype html> -<html lang="en-US" translate="no"> - <head> - <base href="/" /> - - <!-- SEO --> - <title>Reactive Resume - A free and open-source resume builder - - - - - - - - - - - - - - - -
- - - - - - - - diff --git a/apps/artboard/postcss.config.js b/apps/artboard/postcss.config.js deleted file mode 100644 index b45c3e47a..000000000 --- a/apps/artboard/postcss.config.js +++ /dev/null @@ -1,10 +0,0 @@ -const path = require("node:path"); - -module.exports = { - plugins: { - tailwindcss: { - config: path.join(__dirname, "tailwind.config.js"), - }, - autoprefixer: {}, - }, -}; diff --git a/apps/artboard/project.json b/apps/artboard/project.json deleted file mode 100644 index 008ee754e..000000000 --- a/apps/artboard/project.json +++ /dev/null @@ -1,64 +0,0 @@ -{ - "name": "artboard", - "$schema": "../../node_modules/nx/schemas/project-schema.json", - "sourceRoot": "apps/artboard/src", - "projectType": "application", - "targets": { - "build": { - "executor": "@nx/vite:build", - "outputs": ["{options.outputPath}"], - "defaultConfiguration": "production", - "options": { - "outputPath": "dist/apps/artboard" - }, - "configurations": { - "development": { - "mode": "development" - }, - "production": { - "mode": "production" - } - } - }, - "serve": { - "executor": "@nx/vite:dev-server", - "defaultConfiguration": "development", - "options": { - "buildTarget": "artboard:build" - }, - "configurations": { - "development": { - "buildTarget": "artboard:build:development", - "hmr": true - }, - "production": { - "buildTarget": "artboard:build:production", - "hmr": false - } - } - }, - "preview": { - "executor": "@nx/vite:preview-server", - "defaultConfiguration": "development", - "options": { - "buildTarget": "artboard:build" - }, - "configurations": { - "development": { - "buildTarget": "artboard:build:development" - }, - "production": { - "buildTarget": "artboard:build:production" - } - } - }, - "lint": { - "executor": "@nx/eslint:lint", - "outputs": ["{options.outputFile}"], - "options": { - "lintFilePatterns": ["apps/artboard/**/*.{ts,tsx,js,jsx}"] - } - } - }, - "tags": ["frontend"] -} diff --git a/apps/artboard/public/favicon.ico b/apps/artboard/public/favicon.ico deleted file mode 100644 index 4d6cb74e8..000000000 Binary files a/apps/artboard/public/favicon.ico and /dev/null differ diff --git a/apps/artboard/public/favicon.png b/apps/artboard/public/favicon.png deleted file mode 100644 index 19fbcd08d..000000000 Binary files a/apps/artboard/public/favicon.png and /dev/null differ diff --git a/apps/artboard/public/favicon.svg b/apps/artboard/public/favicon.svg deleted file mode 100644 index caed06e9c..000000000 --- a/apps/artboard/public/favicon.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/apps/artboard/public/icon/dark.svg b/apps/artboard/public/icon/dark.svg deleted file mode 100644 index ed4657de6..000000000 --- a/apps/artboard/public/icon/dark.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/apps/artboard/public/icon/light.svg b/apps/artboard/public/icon/light.svg deleted file mode 100644 index fabf419a5..000000000 --- a/apps/artboard/public/icon/light.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/apps/artboard/src/assets/.gitkeep b/apps/artboard/src/assets/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/apps/artboard/src/components/brand-icon.tsx b/apps/artboard/src/components/brand-icon.tsx deleted file mode 100644 index 6f38fc92d..000000000 --- a/apps/artboard/src/components/brand-icon.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { forwardRef } from "react"; - -type BrandIconProps = { - slug: string; -}; - -export const BrandIcon = forwardRef(({ slug }, ref) => { - if (slug.toLowerCase() === "linkedin") { - return ( - LinkedIn - ); - } - - return ( - {slug} - ); -}); - -BrandIcon.displayName = "BrandIcon"; diff --git a/apps/artboard/src/components/page.tsx b/apps/artboard/src/components/page.tsx deleted file mode 100644 index aef5fa324..000000000 --- a/apps/artboard/src/components/page.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { useTheme } from "@reactive-resume/hooks"; -import { cn, pageSizeMap } from "@reactive-resume/utils"; - -import { useArtboardStore } from "../store/artboard"; - -type Props = { - mode?: "preview" | "builder"; - pageNumber: number; - children: React.ReactNode; -}; - -export const MM_TO_PX = 3.78; - -export const Page = ({ mode = "preview", pageNumber, children }: Props) => { - const { isDarkMode } = useTheme(); - - const page = useArtboardStore((state) => state.resume.metadata.page); - const fontFamily = useArtboardStore((state) => state.resume.metadata.typography.font.family); - - return ( -
- {mode === "builder" && page.options.pageNumbers && ( -
- Page {pageNumber} -
- )} - - {children} - - {mode === "builder" && page.options.breakLine && ( -
- )} -
- ); -}; diff --git a/apps/artboard/src/components/picture.tsx b/apps/artboard/src/components/picture.tsx deleted file mode 100644 index 1d0792a6d..000000000 --- a/apps/artboard/src/components/picture.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { cn, isUrl } from "@reactive-resume/utils"; - -import { useArtboardStore } from "../store/artboard"; - -type PictureProps = { - className?: string; -}; - -export const Picture = ({ className }: PictureProps) => { - const picture = useArtboardStore((state) => state.resume.basics.picture); - const fontSize = useArtboardStore((state) => state.resume.metadata.typography.font.size); - - if (!isUrl(picture.url) || picture.effects.hidden) return null; - - return ( - Profile - ); -}; diff --git a/apps/artboard/src/constants/helmet.ts b/apps/artboard/src/constants/helmet.ts deleted file mode 100644 index 3dbb7916c..000000000 --- a/apps/artboard/src/constants/helmet.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { HelmetData } from "react-helmet-async"; - -export const helmetData = new HelmetData({}); - -export const helmetContext = helmetData.context; diff --git a/apps/artboard/src/main.tsx b/apps/artboard/src/main.tsx deleted file mode 100644 index 26f9e5bad..000000000 --- a/apps/artboard/src/main.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { StrictMode } from "react"; -import * as ReactDOM from "react-dom/client"; -import { RouterProvider } from "react-router"; - -import { router } from "./router"; - -// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -const root = ReactDOM.createRoot(document.querySelector("#root")!); - -root.render( - - - , -); diff --git a/apps/artboard/src/pages/artboard.tsx b/apps/artboard/src/pages/artboard.tsx deleted file mode 100644 index 4b7e07b4f..000000000 --- a/apps/artboard/src/pages/artboard.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import { isLocalFont } from "@reactive-resume/utils"; -import { useEffect, useMemo } from "react"; -import { Helmet } from "react-helmet-async"; -import { Outlet } from "react-router"; -import webfontloader from "webfontloader"; - -import { useArtboardStore } from "../store/artboard"; - -export const ArtboardPage = () => { - const name = useArtboardStore((state) => state.resume.basics.name); - const metadata = useArtboardStore((state) => state.resume.metadata); - - const fontString = useMemo(() => { - const family = metadata.typography.font.family; - const variants = metadata.typography.font.variants.join(","); - const subset = metadata.typography.font.subset; - - return `${family}:${variants}:${subset}`; - }, [metadata.typography.font]); - - useEffect(() => { - const family = metadata.typography.font.family; - if (isLocalFont(family)) { - let frame = 0; - frame = requestAnimationFrame(() => { - const width = window.document.body.offsetWidth; - const height = window.document.body.offsetHeight; - const message = { type: "PAGE_LOADED", payload: { width, height } }; - window.postMessage(message, "*"); - }); - return () => { cancelAnimationFrame(frame); }; - } - - webfontloader.load({ - google: { families: [fontString] }, - active: () => { - const width = window.document.body.offsetWidth; - const height = window.document.body.offsetHeight; - const message = { type: "PAGE_LOADED", payload: { width, height } }; - window.postMessage(message, "*"); - }, - }); - }, [fontString]); - - // Font Size & Line Height - useEffect(() => { - document.documentElement.style.setProperty("font-size", `${metadata.typography.font.size}px`); - document.documentElement.style.setProperty("line-height", `${metadata.typography.lineHeight}`); - - document.documentElement.style.setProperty("--margin", `${metadata.page.margin}px`); - document.documentElement.style.setProperty("--font-size", `${metadata.typography.font.size}px`); - document.documentElement.style.setProperty( - "--line-height", - `${metadata.typography.lineHeight}`, - ); - - document.documentElement.style.setProperty("--color-foreground", 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(() => { - // 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 ( - <> - - {name} | Reactive Resume - {metadata.css.visible && ( - - )} - - - - - ); -}; diff --git a/apps/artboard/src/pages/builder.tsx b/apps/artboard/src/pages/builder.tsx deleted file mode 100644 index bdcd14831..000000000 --- a/apps/artboard/src/pages/builder.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import type { SectionKey } from "@reactive-resume/schema"; -import type { Template } from "@reactive-resume/utils"; -import { pageSizeMap } from "@reactive-resume/utils"; -import { AnimatePresence, motion } from "framer-motion"; -import { useEffect, useMemo, useRef, useState } from "react"; -import type { ReactZoomPanPinchRef } from "react-zoom-pan-pinch"; -import { TransformComponent, TransformWrapper } from "react-zoom-pan-pinch"; - -import { MM_TO_PX, Page } from "../components/page"; -import { useArtboardStore } from "../store/artboard"; -import { getTemplate } from "../templates"; - -export const BuilderLayout = () => { - const [wheelPanning, setWheelPanning] = useState(true); - - const transformRef = useRef(null); - - const layout = useArtboardStore((state) => state.resume.metadata.layout); - const format = useArtboardStore((state) => state.resume.metadata.page.format); - const template = useArtboardStore((state) => state.resume.metadata.template as Template); - - const Template = useMemo(() => getTemplate(template), [template]); - - useEffect(() => { - const handleMessage = (event: MessageEvent) => { - if (event.origin !== window.location.origin) return; - - if (event.data.type === "ZOOM_IN") transformRef.current?.zoomIn(0.2); - if (event.data.type === "ZOOM_OUT") transformRef.current?.zoomOut(0.2); - if (event.data.type === "CENTER_VIEW") transformRef.current?.centerView(); - if (event.data.type === "RESET_VIEW") { - transformRef.current?.resetTransform(0); - setTimeout(() => transformRef.current?.centerView(0.8, 0), 10); - } - if (event.data.type === "TOGGLE_PAN_MODE") { - setWheelPanning(event.data.panMode); - } - }; - - window.addEventListener("message", handleMessage); - - return () => { - window.removeEventListener("message", handleMessage); - }; - }, [transformRef]); - - return ( - - - - {layout.map((columns, pageIndex) => ( - - -