Compare commits

..
632 Commits
Author SHA1 Message Date
Amruth Pillai 19b412d84d chore(release): v5.1.6 2026-05-26 10:12:56 +02:00
Amruth Pillai 7eea6675c0 chore: update dependencies 2026-05-26 10:09:58 +02:00
Amruth Pillai 273e17c0d3 fix: issue with color format handling, resolves #3104 2026-05-26 09:59:23 +02:00
Amruth Pillai 17cddbad65 fix: reduce default list item row gap 2026-05-26 00:13:38 +02:00
Amruth PillaiandCursor 7557ab13ab fix(api): delete agent threads with sequential cleanup
Remove attachments and soft-delete the thread before storage cleanup so
partial failures do not leave inconsistent DB state. Log storage errors
without failing the request after the thread is marked deleted.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-25 16:33:03 +02:00
Amruth PillaiandCursor c66560ee12 refactor(web): dedupe isRTL via utils locale module
Re-export isRTL from @reactive-resume/utils/locale in the web locale
helper and consolidate RTL detection tests in the utils package.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-25 16:32:58 +02:00
Amruth PillaiandCursor 24c882fa9f feat(pdf): roll out shared RTL layout to all templates
Introduce createRtlStyleHelpers and a single rtl flag on RenderProvider,
migrate every template page to mirrored layout styles, and rename
alignRight to alignEnd. Fix plain rich text rendering via PdfText
paragraph renderers and map legacy Times New Roman to Times-Roman.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-25 16:29:50 +02:00
Yu Sun 86fff7237f fix(auth): reconcile migrated social login accounts (#3095) 2026-05-25 15:46:57 +02:00
Eyal Meschmanandautofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> 266bc291eb Add RTL rendering for Rhyhorn template (#3099)
* Add RTL rendering for Rhyhorn template

* Add timeout to wait-healthy just command

* Revert prettier formatting

* Revert and ignore personal relevant files

* Revert prettier formatting from all modified files

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-05-25 15:46:51 +02:00
Amruth Pillai 6ec4da7914 chore: update dependencies 2026-05-25 15:44:40 +02:00
Umair Khurshid 75e9446134 docs(docker): use shallow clone in quick start (#3096) 2026-05-25 15:39:01 +02:00
Amruth Pillai 39e88dd365 chore: lint using react-doctor, update translations, dynamic imports 2026-05-21 09:56:26 +02:00
Amruth Pillai 3596102c63 chore: update dependencies 2026-05-20 23:12:39 +02:00
github-actions[bot]andCrowdin Bot c77684d317 [skip ci] chore(i18n): sync translations from crowdin (#3087)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-05-19 13:15:46 +02:00
Amruth Pillai 62f8270b3e Squashed commit of the following:
commit b2b0470a1d9267d042ec0ac66523c6635bf5b199
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Tue May 19 13:13:38 2026 +0200

    chore: update .gitignore to include .vite-hooks and modify pnpm-lock.yaml for dependencies

commit d28fadb5cd8706c874e616102878b4a394ec84c1
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Tue May 19 13:08:04 2026 +0200

    fix: remove timestamp conflict guard

commit c6998d9dbab19d09d3c8054feef1d2e4117555eb
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Tue May 19 12:11:51 2026 +0200

    chore(release): v5.1.5

commit f33d168711804880e1f12e88d24290aae16cc258
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Tue May 19 11:58:35 2026 +0200

    revert: compose.yml

commit d961e6535811a10c335525fb33a08d03e737278d
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Tue May 19 11:58:08 2026 +0200

    refactor(agent): replace 'revert' terminology with 'restore' for clarity, resolves #3086

commit 17f351171be218e33f01c469d95e4164d4c8dc57
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Tue May 19 11:10:41 2026 +0200

    refactor(pdf): simplify sidebar section filtering and update summary feature logic

commit d55179b9d76879e3204de185e8b53fadd0a107ed
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Tue May 19 09:53:37 2026 +0200

    chore: update pnpm-lock.yaml and turbo.json

commit 7cade6980e1a04352536bd44ef773f338c4ef599
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Tue May 19 09:38:30 2026 +0200

    fix(polyfill): add tested polyfill for Map Upsert methods

commit 26d175bb9c53d93225d1e907678445252c13d660
Merge: 1cf33dc6c 5b1297fa2
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Tue May 19 09:23:29 2026 +0200

    Merge remote-tracking branch 'origin/main' into feat/explore-hono-orpc-migration

    # Conflicts:
    #	packages/api/src/services/agent-url.ts
    #	packages/runtime-externals/package.json

commit 1cf33dc6c9d81735730ad656e16dab6501c6d6a1
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Tue May 19 09:22:12 2026 +0200

    chore: preserve branch changes before main sync

commit b380a4b00fdbcdd81ff4f8ef72b330fd027ccda5
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Mon May 18 07:50:28 2026 +0200

    chore: lot of fixes for monorepo migration

commit 8fcf0ec64e1c29572ebaff494338368bfcf75760
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Fri May 15 13:57:17 2026 +0200

    chore: update knip version and refine web app routing with new SEO endpoints

commit 234e68086ff15610a93877354c98e2c020364533
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Fri May 15 12:10:06 2026 +0200

    refactor(auth): update OAuth routes to include API prefix and remove unused schema endpoint

commit 91c84b9a8496b0ce21d71cae9f8b2a027638c9ac
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Fri May 15 11:54:29 2026 +0200

    chore: update dependencies and enhance PWA metadata in web app

commit 150117d4a5a9dd6cd92c64891aad8cae90f6a7af
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Fri May 15 11:12:35 2026 +0200

    docs: revise manifest-only pwa testing scope

commit 6b939a55661aec9dd8122b184e4b60a5c7325fb5
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Fri May 15 11:11:33 2026 +0200

    docs: add manifest-only pwa design

commit 1422e1fc96c400948b273210a1067251087d15d4
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Fri May 15 11:05:04 2026 +0200

    chore(dev): simplify server proxy config

commit bc2ff5a9f6fda41e6c40333c8f163aa23a6c5e48
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Fri May 15 11:04:50 2026 +0200

    docs: add unsafe oauth redirect plan

commit 445359ebe9b96c1515bf1c4c3f73ba8a8448ec12
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Fri May 15 11:04:34 2026 +0200

    feat(auth): add unsafe oauth redirect flag

commit 73fffdd24598e56b2793f7657919bc794835892e
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Fri May 15 10:55:02 2026 +0200

    docs: design unsafe oauth redirect flag

commit c0066aa19c15fc8a4c8e5179ed49889c117519f4
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Fri May 15 10:22:04 2026 +0200

    chore: update translation source paths

commit 9033da082418d252aafd6c2eed72f71f014be3d9
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Fri May 15 10:09:25 2026 +0200

    refactor(arch): react spa + hono migration

commit 6f27936c11bda895977dc63ee550c3346d4ce24b
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Fri May 15 01:10:47 2026 +0200

    docs: add docker nightly tagging design

commit ecc1fd9a88a0ee1dca2f1977dfc17f74527fe1da
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Thu May 14 20:05:44 2026 +0200

    feat: migrate to hono spa server
2026-05-19 13:14:21 +02:00
JamesGoslingsandAmruth Pillai 5b1297fa2b fix(pdf): register CJK fallback at primary font weights so bold rende… (#3080)
The CJK fallback (Noto Sans SC / Noto Serif SC) was only registered at
weight 400. When react-pdf rendered CJK characters with font-weight 700
(e.g. <strong> from a rich-text section, or templates' bold style), it
walked the font-family stack [primary, cjkFallback], failed on the
primary (no CJK glyphs), then fell back to the only registered fallback
variant (400) — and react-pdf does not synthesize bold. The bold style
was silently dropped for CJK runs in both the live preview and the
exported PDF, while still working for Latin runs.

Register the CJK fallback at the same weight range as the primary font
(lowest + highest, both styles). When body and heading share the same
fallback (the common case where both are sans or both are serif), merge
their weight ranges so each weight is registered exactly once.

webfontlist.json already ships all weights for the default CJK
fallbacks, so no font-list changes are required.

Closes #3079

Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
2026-05-19 09:09:28 +02:00
JamesGoslingsandAmruth Pillai dd7623f11e fix(pdf): align textkit line-box and font metrics to browser behaviour (#3070)
* fix(pdf): align textkit line-box and font metrics to browser behaviour

CJK characters in resumes with a tightened typography line-height
(< ~1.4) had their descenders clipped by the next line. Latin glyphs
in the same resume rendered fine. Fixes the visual regression vs the
v5.0.x Puppeteer-based renderer reported in issue #2986 and follow-ups.

The clipping is caused by two independent gaps in @react-pdf/textkit
relative to standard CSS line-box rules:

1. `height(run)` short-circuits to the user-supplied lineHeight and
   ignores the run's intrinsic ascent + descent. CSS line-boxes are
   spec'd as `max(line-height, content-area)` — when CJK glyphs are
   present the content-area is taller than a tightened lineHeight, so
   the box must grow. textkit didn't, so the baseline (computed from
   the real, larger CJK ascent) sat below the box and the descender
   bled into the next line.

2. `ascent / descent / lineGap` are read directly from fontkit's hhea
   defaults. For Source Han Sans/Serif (the CJK fallbacks registered
   in #3013) hhea is intentionally inflated for legacy Windows GDI
   compatibility (1.45 em vs 1.0 em), so even a fixed line-box would
   have been excessively tall. Browsers (and the v5.0.x Puppeteer
   renderer) read OS/2 sTypoAscender/Descender/LineGap instead, which
   are the values the type designers intend for modern shaping.

Both are upstream behaviours of `@react-pdf/textkit`, but waiting for
an upstream release would leave existing users with broken CJK output.
The fix is shipped as a pnpm patch (~30 LOC):

- `resolveTypoMetrics(font)`: prefer OS/2 typo metrics, fall back to
  hhea when an OS/2 table is absent (e.g. the StandardFont stand-ins
  for Helvetica/Courier/Times). Used by ascent/descent/lineGap so all
  height-related calculations stay consistent.
- `height(run)`: `Math.max(lineHeight || 0, intrinsic)` instead of
  the original short-circuit, matching CSS line-box rules.

The patch is self-contained: existing Latin-only resumes are
unaffected (IBM Plex Serif's typo metrics equal hhea; Roboto's typo
is slightly smaller, but only changes the rendered line-box for users
who set lineHeight below ~1.17, which already used to clip ascenders
under v5.1.x and now lays out as it would in a browser).

Tooling notes:
- `Dockerfile.dev` copies `patches/` before `pnpm install` so the
  dev image build no longer fails on `--frozen-lockfile`. The
  production `Dockerfile` already gets it for free via
  `turbo prune --docker` (the patch reference in package.json marks
  the directory as part of the pruned slice).
- The patch will become a no-op once an equivalent fix lands upstream
  in @react-pdf/textkit; the entry can then be removed from
  `pnpm.patchedDependencies` and the file deleted.

* fix(deps): regenerate lockfile and move patchedDependencies for pnpm 11

The previous commit's lockfile was authored by pnpm 8 (lockfileVersion 6.0)
and kept patchedDependencies under package.json#pnpm. The repository now
declares packageManager: pnpm@11.1.2, which:

- writes lockfileVersion 9.0 and rejects v6 with ERR_PNPM_LOCKFILE_BREAKING_CHANGE
  on --frozen-lockfile (CI failure observed in autofix.ci);
- reads pnpm settings from pnpm-workspace.yaml, silently ignoring the
  package.json#pnpm field — so the textkit patch was no longer applied.

Regenerate pnpm-lock.yaml with pnpm 11.1.2 and move patchedDependencies
to pnpm-workspace.yaml so the patch is applied and CI passes.

* chore: update dependencies

---------

Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
2026-05-18 08:08:47 +02:00
Adian Kozlica 63e8c3ca33 fix: polyfill Map.getOrInsertComputed for Waterfox (#3067) 2026-05-15 01:50:13 +02:00
Amruth Pillai e62090cce0 fix: monkey patch a nitro build error (resolves #3065) 2026-05-14 17:21:41 +02:00
Amruth Pillai 0510c7103b chore: update dependencies 2026-05-14 16:41:24 +02:00
github-actions[bot]andCrowdin Bot 1a5c5252d1 [skip ci] chore(i18n): sync translations from crowdin (#3064)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-05-14 16:00:23 +02:00
Amruth Pillai 9df2a5287d chore(release): v5.1.4 2026-05-14 15:57:40 +02:00
Amruth Pillai 6d8d8f6e55 feat: add AI agent workspace (#3062)
* chore(ai): remove local AI store now that providers live server-side

The Zustand-based useAIStore has been replaced by the server-side
aiProviders oRPC router (encrypted credentials persisted in DB).
Delete the dead store + tests, drop the ./store export, and remove
zustand/immer deps which are no longer referenced anywhere in
packages/ai/src/.

* feat(agent): archive/delete actions and read-only state for agent threads

- Backend: mark archived threads as read-only in threads.get and reject
  messages.send with CONFLICT when the thread is archived.
- Frontend: render archived threads in the sidebar with muted styling and
  an Archived badge; add a per-thread dropdown menu in the chat header
  with Archive (non-destructive) and Delete (with confirmation); show a
  read-only banner above the message list that disambiguates archived
  vs. missing-resource causes; suppress the Retry and Stop buttons in
  read-only mode.
- Tests: new packages/api/src/services/agent.test.ts covering the
  archived-thread isReadOnly flag and the archived-thread send refusal.

* fix(agent): abort run on archive and verify ownership before deleting thread

- threads.archive: before flipping status, abort any in-flight run controller
  and clear the active-run state on the thread; cleanup failures are logged
  but do not block the status update.
- threads.delete: assert thread ownership via getThread before destructive
  work so an authenticated user cannot wipe another user's attachment rows
  by passing a foreign threadId.

Adds focused tests for both behaviors.

* feat(agent): display patch diffs and surface revert conflicts

Render apply_resume_patch tool messages with a status-aware card (applied/
reverted/conflicted), expandable operation list, and a Revert button that
correctly handles RESUME_VERSION_CONFLICT responses. Adds unit tests for
the inverse-patch builder and the agentService.actions.revert flow.

* chore(agent): remove out-of-scope attachment tests accidentally added in Task 6

The Task 6 commit (73ef1acca) accidentally re-introduced three attachment-
related tests that belong to a separate task:

- `buildAttachmentModelParts > converts text, image, supported binary, and
  unsupported attachments into model parts`
- `agentService.messages.send > persists the user message with file UI parts
  and links selected attachments to it` (was failing — the `ToolLoopAgent`
  mock is not callable as a constructor)
- `agentService.messages.send > rejects attachments that are missing, foreign,
  or already linked before persisting a message`

These were likely re-added during a stash recovery and were not requested
for Task 6, whose scope was limited to the `agentService.actions.revert`
flow. Remove them along with the helpers/fixtures (`buildAttachment`,
`buildActiveThread`, `selectWhereResult`, `selectOrderByResult`) that they
were the only consumers of. `selectLimitResult` is preserved because it is
used by the revert tests.

* chore(agent): configure runtime dependencies

* feat(db): add agent workspace schema

* feat(api): add agent backend services

* feat(web): add agent workspace UI

* chore(agent): remove legacy builder assistant

* test(agent): make agent stream mocks constructible

* chore(web): remove unused resume replacement hook

* feat(api): add unsafe AI base URL flag

* chore(dev): expose local services in compose

* fix(web): normalize resume preview gaps

* feat(api): improve agent tool handling

* feat(web): polish agent workspace UI

* chore: update dependencies

* fix(api,web): address PR review feedback for agent workspace

Security/correctness:
- Restrict AI provider URLs to http/https even in unsafe mode
- Stop exposing Redis on host network by default
- Make .env.local optional and drop app profile in compose.dev.yml
- Store agent attachments with private ACL on S3
- Reset provider test status when provider/model/baseURL changes
- Decouple non-agent AI endpoints from REDIS_URL requirement
- Fix JSON Patch add inverse for existing object members
- Wrap resume patch + agent action insert in db transaction
- Validate partialMessage at runtime and rate-limit attachment uploads
- Add unique index on agent_messages (thread_id, sequence)

UX/bugs:
- Mark agent thread route as ssr: false and guard SSE chunk parsing
- Show config-specific banner only on known configuration error
- Gate AI provider checks behind loading state in resume import
- Fix relative-time formatter blank gap between 45-59 seconds
- Clarify thread delete confirmation message

Polish:
- Raise ENCRYPTION_SECRET minimum to 32 characters
- Bucket AI rate limits by resumeId/threadId/messageId
- Trim form values before submitting AI provider config
- Use single key identifier and nullish-coalesce baseURL display

* fix: address ai agent review feedback

* fix: preserve mobile agent chat state

* docs: add ai agent workspace guides

* feat: introduce design system for Reactive Resume
2026-05-14 15:00:04 +02:00
JamesGoslings 22c60c64b6 fix(fonts): restore legacy local font names via metric-compatible ali… (#3057)
* fix(fonts): restore legacy local font names via metric-compatible aliases

Closes #2989.

In v5.0.x the Puppeteer renderer resolved fonts like 'Times New Roman'
or 'Arial' through the browser's font stack. The v5.1 migration to
@react-pdf/renderer requires every font to be Font.register()-ed; the
legacy local-font names were not carried over, so resumes upgraded
from v5.0.x had their typography silently replaced with IBM Plex Serif,
changing line breaks, page counts and overall layout.

This adds a render-time alias layer mapping the old names to
metric-compatible web fonts already shipped in the webfont list:

  Times New Roman → Tinos
  Cambria         → Tinos
  Arial           → Arimo
  Garamond        → EB Garamond
  Calibri         → Source Sans 3

- packages/fonts:
  - new `legacyFontAliases` map and `resolveLegacyFontAlias` helper.
  - `getFont` falls back to the alias map when the direct lookup misses,
    so any caller that asked 'is this a known family?' now answers
    truthfully for the legacy names.
  - `getFontDisplayName` is intentionally unchanged: the typography
    sidebar keeps showing the user's original choice ('Times New Roman'),
    while the renderer transparently swaps in the alias target.

- packages/pdf/use-register-fonts:
  - `resolvePdfFontFamily` returns the alias target when one applies,
    so `Font.register` runs against the right web font and templates
    receive a family name they can actually render.

Backwards compatible: families that were never aliased (Roboto, IBM
Plex Serif, the standard PDF fonts, ...) take exactly the same code
path as before. The CJK glyph fallback added in #2986 / PR #3013
continues to apply on top of the resolved primary family.

* fix(fonts): use Carlito (not Source Sans 3) as Calibri alias

Per maintainer review feedback: Carlito is metric-compatible with
Calibri, while Source Sans 3 only matches visually. Switching gives
upgraded resumes the same line widths, line breaks and page counts
they had under v5.0.x.

- packages/fonts/webfontlist.json: add Carlito (Google Fonts, weights
  400/700 + italics) so it's a registerable target.
- packages/scripts/fonts/generate.ts: add a getMetricCompatibleFonts
  helper and merge it into the output, mirroring how Computer Modern
  fonts are appended. This way regenerating the list (`pnpm generate`)
  re-emits Carlito automatically and dedupes if it ever enters the
  Google Fonts popularity slice.
- packages/fonts/src/index.ts: alias `Calibri → Carlito`.
- packages/fonts/src/index.test.ts: update alias test cases.
2026-05-14 11:36:15 +02:00
Amruth Pillai affa1d6646 docs: enhance documentation and guides with new features and updates 2026-05-14 03:38:39 +02:00
SirSKillzandAmruth Pillai c71f3b0b92 Feat: Add configurable AI provider base URL flag and update documentation (#3059)
* feat: add FLAG_ALLOW_UNSAFE_AI_BASE_URL for configurable AI provider base URLs

* feat: add FLAG_ALLOW_UNSAFE_AI_BASE_URL documentation

* fix: remove AI_ALLOWED_BASE_URLS from documentation and environment variable reference

---------

Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
2026-05-14 03:01:37 +02:00
Amruth Pillai 6c4a4b2aa5 Render public resumes with PDF.js (#3061)
* fix(web): use native pdf viewer for public resumes

* fix(web): render public resumes with pdf.js

* chore: revert vite hook paths

* chore(web): address pdf viewer review
2026-05-14 02:49:43 +02:00
Amruth Pillai 1294d3354a feat(docker): enhance development setup with reactive_resume service and health checks 2026-05-13 15:35:08 +02:00
Claudeamruthpillaianthropic-code-agent[bot] <242468646+Claude@users.noreply.github.com>
42fc78dca1 [WIP] Fix dead link to Using Custom CSS in docs (#3056)
* Initial plan

* docs: remove reference to removed Custom CSS guide

Agent-Logs-Url: https://github.com/amruthpillai/reactive-resume/sessions/82961e42-251b-41da-80ee-7697968566f7

Co-authored-by: amruthpillai <1134738+amruthpillai@users.noreply.github.com>

---------

Co-authored-by: anthropic-code-agent[bot] <242468646+Claude@users.noreply.github.com>
Co-authored-by: amruthpillai <1134738+amruthpillai@users.noreply.github.com>
2026-05-13 11:22:25 +02:00
Amruth Pillai aa7af040fb chore(release): v5.1.3 2026-05-13 09:29:59 +02:00
Amruth Pillai 5f63dc876b feat: new template (scizor) 2026-05-13 09:26:38 +02:00
Kaloian Kozlev 014ceee31f Add DOCX to export formats in README (#3054) 2026-05-13 09:10:30 +02:00
github-actions[bot]andCrowdin Bot 70dff5bf49 [skip ci] chore(i18n): sync translations from crowdin (#3053)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-05-13 09:10:00 +02:00
Amruth Pillai c5787fe155 chore: translations for a new template 2026-05-13 08:06:31 +02:00
Amruth Pillai 286e165a60 [codex] Hide empty sections from page settings (#3052)
* feat(web): hide empty layout sections

* test: add "scizor" to templates metadata test cases
2026-05-13 01:11:19 +02:00
Amruth Pillai 00dafd0c68 feat: add new resume template "Scizor" 2026-05-13 01:07:35 +02:00
brone1323 d251d602fb fix(pdf): increase headerNameLineHeight to 1.3 to prevent descender clipping (#3050)
All Heading elements apply overflow:hidden via safeTextStyle in primitives.tsx.
At 1.5× heading font size with lineHeight 1.2, the line box is too tight to
fully render descenders (g, p, y, etc.) in the resume header name field,
causing them to appear visually cut off.

Raising headerNameLineHeight from 1.2 to 1.3 adds enough vertical room for
descenders across all 13 templates that share this constant.

Fixes #3042
2026-05-13 01:02:16 +02:00
Amruth Pillai e35ff83911 chore: update dependencies 2026-05-13 00:44:51 +02:00
Amruth Pillai 62b0a1d533 fix(cjk): resolve hyphenation callback with cjk content in resume 2026-05-11 22:04:45 +02:00
Amruth PillaiandCursor Agent 0a8fe05653 Fix split row right content promotion (#3039)
* Fix split row right content promotion

Co-authored-by: Amruth Pillai <im.amruth@gmail.com>

* refactor: remove MetaLine component and update sections to use Text instead

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-05-11 15:59:26 +02:00
Amruth Pillai de7baa5faf test: add ~500 tests across web, utils, api, import, ai, db, email, auth (#3038)
* test(web): add tests for zustand stores and pure helpers

Cover stores and pure helpers across the builder/dashboard/command-palette
surfaces that previously had 0% coverage:

- command-palette store (open/close, page stack, search clearing, goBack)
- builder assistant-store
- builder sidebar store + parseBuilderLayoutCookie / mapPanelLayoutToBuilderLayout
- builder section store (collapse, toggle, toggleAll)
- builder preview page-layout toggle
- dashboard resume-thumbnail render-size math + cache key
- MCP tool name + annotations invariants

* test(web): cover MCP helpers and template metadata

Add tests for previously 0%-coverage MCP and dialog helpers:

- buildMcpServerCard: server-info, tool catalog vs MCP_TOOL_NAME, prompts,
  resource templates, configuration schema, auth schemes
- registerPrompts (build/improve/review): registration, args schema, resource
  context with interpolated resume id, read-only / no-fabrication directives
- registerResources (resume://{id}, resume://_meta/schema): handler reads via
  oRPC client, error on missing id, schema returns valid JSON
- templates metadata: ids match display names, valid sidebar positions,
  unique image URLs, every entry has tags + description

* test(web): cover sidebar section helpers and layout screens

Add tests for previously near-0%-coverage modules:

- libs/resume/section: getSectionTitle / getSectionIcon return distinct,
  exhaustive results for every sidebar section + cover-letter; icon props
  forwarding; left/right sidebar collections do not overlap.
- layout/loading-screen: spinner + text render.
- layout/error-screen: error message surfaces, Refresh button triggers reset.
- layout/breakpoint-indicator: default + each corner positioning, all
  breakpoint labels rendered, print-hidden class applied.

* test(web): cover preview canvas math and font-weight defaults

Add tests for pure helpers that previously had no direct coverage:

- typography/getNextWeights: prefers 400 + 600 when both are available,
  returns null for unknown families, never produces duplicates, bounded
  to two weights from the 100..900 set.
- preview.shared/normalizeResumePreviewProps: documented defaults +
  pass-through.
- preview.shared/getScaledPreviewPageSize: scaling identity at 1, and
  fractional scaling.
- preview.shared/getPreviewCanvasScale: respects 4x desired scale for
  small pages, honors high devicePixelRatio, clamps to the 16M-pixel
  canvas budget for large pages.

* test(utils): cover DOCX section renderers and html-to-paragraphs

@reactive-resume/utils/resume/docx was previously at ~2.84% statement
coverage despite being load-bearing for the resume DOCX export.

- section-renderers: empty-string / hidden-section / hidden-item branches
  for renderSummary, renderBuiltInSection, and renderCustomSection;
  cover-letter and summary custom-section dispatch; unknown-type fallback;
  setRenderConfig idempotency.
- html-to-docx: whitespace-only short-circuit, multiple top-level blocks,
  h1..h6 paragraph mapping, inline style and link rendering, custom
  font/size/color/linkColor config, ignored script/comment nodes.

* test: cover DOCX builder smoke paths and reactive-resume JSON importer

- utils/resume/docx/builder: buildDocument runs end-to-end against the
  default and sample resume data, both page formats, full-width and
  sidebar layouts, and gracefully degrades with unparseable color or
  empty font family inputs.
- import/reactive-resume-json: ReactiveResumeJSONImporter validates
  malformed JSON, recovers missing built-in sections by appending them
  to page 1 without reordering, and preserves layouts that already
  contain every built-in section.

* test(import): cover JSONResumeImporter parse/convert

JSONResumeImporter (450 lines) was previously at 0% coverage. Add tests
for the public surface:

- Invalid JSON / invalid-shape errors are surfaced.
- basics, summary, picture, education, projects, skills, profiles all
  map to the corresponding ResumeData sections.
- Empty work/education entries (missing key field) are filtered out.
- Highlights become HTML list items in the description field.
- Skill level parsing flows through utils/level.parseLevel.
- formatLocation joins city, region, countryCode with commas.

* test(web): cover query client serializer and home-page animations

- libs/query/client: getQueryClient returns a fresh QueryClient,
  queryKeyHashFn produces stable JSON envelopes for matching keys
  (and distinct strings for different keys), dehydrate/hydrate
  round-trip Date values via the oRPC serializer.
- components/animation/spotlight: overlay container is
  pointer-events-none, both beam groups render, custom
  width/height/translateY/gradient props flow into inline styles.
- components/animation/comet-card: children mount inside the
  perspective wrapper, custom className is merged with the 3D
  baseline classes, glare overlay renders, mouse move/leave
  handlers do not throw.

* test(web): cover Copyright footer

Verify the footer's MIT license link, Amruth Pillai attribution,
external-tab targets, embedded app version (via __APP_VERSION__ stub),
and custom className merging — previously at 0% coverage.

* test(api): cover flags, auth providers, and resume-access cookies

Unlock @reactive-resume/api by mocking @reactive-resume/env/server and
@tanstack/react-start/server. Previously the only services tested were
the standalone AI test and resume-access-policy.

- services/flags: flagsService.getFlags reads disableSignups/disableEmailAuth
  from env (no stale cache).
- services/auth: providers.list always exposes credential + passkey, and
  only adds Google/GitHub/LinkedIn/custom when both id and secret are set;
  custom provider uses OAUTH_PROVIDER_NAME with a 'Custom OAuth' fallback.
- helpers/resume-access: hasResumeAccess validates against signed cookies
  with constant-time comparison; grantResumeAccess writes a 10-minute
  httpOnly cookie with the secure flag matching APP_URL's https-ness.

* test: cover statistics service and email transport via env mocks

- api/services/statistics: github star count succeeds, retries on
  non-OK, falls back to last-known on fetch error / non-positive /
  non-numeric responses; user and resume counts roll up DB count.
- email/src/transport: returns silently with no text/html, logs when
  SMTP is not configured, dispatches via nodemailer with the env
  config when fully wired, renders react elements to html + text
  bodies, swallows transport errors instead of crashing.

* test(api): cover resume-events publish + subscribe

- publishResumeUpdated issues pg_notify with channel and serialized
  event payload.
- subscribeResumeUpdated yields events whose resumeId+userId match
  the subscription, filters out other resumes/users, ignores
  malformed JSON and notifications on other channels, calls
  LISTEN/UNLISTEN and releases the client, and terminates
  immediately if the abort signal fires before iteration starts.

* test(api): cover oRPC auth resolution

resolveUserFromRequestHeaders is the single point where every oRPC
procedure picks up the authenticated user. Test the priority chain:

- x-api-key wins when present and valid
- on invalid api key, falls back to session via auth.api.getSession
- Bearer JWT in Authorization header is verified via verifyOAuthToken
- invalid Bearer falls back to session
- Authorization scheme other than Bearer is ignored entirely
- thrown errors from token verification are logged and swallowed
  (caller still tries session)
- returns null when no auth method succeeds

* test(api): cover storage helpers

inferContentType, isImageFile, processImageForUpload were 0%
coverage despite being on the picture upload path.

- inferContentType maps known image and pdf extensions, is
  case-insensitive, ignores path depth, and falls back to
  application/octet-stream for unknown.
- isImageFile allows only the upload allowlist (gif/png/jpeg/webp)
  and rejects image/svg+xml, application/pdf, and empty strings.
- processImageForUpload short-circuits to the original bytes when
  FLAG_DISABLE_IMAGE_PROCESSING is true, otherwise pipes through
  sharp and returns image/jpeg.

* test(import): broaden v4 importer section-mapping coverage

The existing v4 importer test focused on a single bug (description-only
custom items) and the skill/language level scaling. This new test
exercises the bulk of the v4 → v5 transformation path:

- basics, picture (with border), summary, customFields
- every section's filter-by-required-field invariant (awards needs
  title, certifications needs name, education needs institution,
  experience needs company, volunteer needs organization, etc.)
- experience / education / awards / certifications / references field
  renames between schemas
- language and skill level scaling (v4 0..10 → v5 0..5)

Brings reactive-resume-v4-json from ~66% statement coverage to a
materially higher figure (the bulk of the 410-line transformer body).

* test: cover buildDocx entry and AI configuration store

- utils/resume/docx/index: buildDocx returns a non-empty Blob for both
  default and populated resumes (previously 0% coverage despite being
  the public DOCX entry point).
- ai/store: useAIStore preserves verification status across no-op
  updates, but resets testStatus + enabled whenever provider, model,
  apiKey, or baseURL changes; canEnable is gated to testStatus=success;
  setEnabled(true) is refused unless verified; reset clears every
  field. Brings @reactive-resume/ai from ~72% to materially higher
  coverage.

* test(db): cover resume schema definitions

packages/db was previously at 0% coverage. Smoke-test the public
resume / resume_statistics / resume_analysis tables:

- getTableName matches the SQL identifier used by migrations
- expected columns are present on each table
- defaultResumeData wiring on the data column resolves to a valid
  shape

These are structural assertions that catch accidental renames /
removals without needing a live database connection.

* test(db): cover auth schema tables and relations export

- src/schema/auth: table-driven test for each of the 12 auth tables
  asserting SQL name and presence of the key columns (user/session/
  account/verification/two_factor/passkey/apikey/jwks/oauth_*).
- src/relations: smoke test confirming the relations export is defined.

Brings @reactive-resume/db from 0% to materially higher coverage.

* test(auth): cover getSession isomorphic helper

@reactive-resume/auth was previously at 0% coverage. functions.ts
is the server entry point that other packages call. Mock the auth
config + tanstack/react-start to verify:

- getSession forwards getRequestHeaders() to auth.api.getSession
- returns null when better-auth returns null

* test(web): cover BuilderSidebarEdge

Small presentational component on the builder layout — assert children
mount, left/right positioning class branches, and the sm:flex
mobile-hide behavior.

* test(web): cover section-title-locale resolver cache and hook

The section-title-locale module wraps createSectionTitleResolver with
a per-locale async cache and a React hook for consumers in the
builder. Cover:

- createSectionTitleResolverForLocale returns a usable resolver
- repeated calls for the same locale share a cached promise
- unknown locales fall back through resolveLocale
- useSectionTitleResolver returns null while loading and when no
  locale is passed
- the hook resolves to a function once the async loader settles

* test(web): cover BaseCommandGroup page-stack gating

BaseCommandGroup conditionally renders based on the top of the
command-palette page stack. Tests cover:

- root group renders when no sub-page is active
- root group hides when a sub-page is on top
- sub-page group renders only when its page matches
- mismatched sub-page leaves the group hidden

* test(web): cover ThemeProvider context

- useTheme outside ThemeProvider throws the documented error
- useTheme inside ThemeProvider returns the theme + setTheme +
  toggleTheme helpers

* test(web): cover ConfirmDialogProvider + useConfirm hook

- useConfirm outside provider throws the documented error
- confirm returns a pending promise
- promise resolves false when the Cancel button is clicked
- promise resolves true when the Confirm button is clicked
- works with custom confirmText label

apps/web has its own copy of this hook distinct from
packages/ui (mirrors the existing UI-package tests).

* test(web): cover PromptDialogProvider + usePrompt hook

- usePrompt outside provider throws the documented error
- returns a function when wrapped
- Cancel click resolves the promise to null
- Confirm click resolves to the current input value
- defaultValue option seeds the initial input value

* test(web): cover DashboardHeader

Small presentational header used across dashboard routes — title h1,
icon rendering, className merge, mobile sidebar trigger present and
hidden on md+.

* test(web): cover Create/Import resume cards

Both cards on the resumes dashboard wire a click handler to open
the appropriate dialog via the dialog store:

- CreateResumeCard opens resume.create
- ImportResumeCard opens resume.import

Also asserts the i18n copy strings (icons aside, the cards are
otherwise structural).

* test(web): cover command-palette language sub-page

LanguageCommandPage is a BaseCommandGroup gated on page='language'.
Tests assert:

- it is hidden when 'language' is not the top of the page stack
- when active, it renders a CommandItem per localeMap entry
- documented locale codes (en-US, de-DE, ja-JP) appear

* test(web): cover command-palette theme + preferences sub-pages

- ThemeCommandPage: hidden when 'theme' is not on top, renders Light
  and Dark options when active
- PreferencesCommandGroup: root group renders both Change theme to...
  and Change language to... items; clicking each pushes the
  corresponding page onto the command-palette stack

* test(ai): cover executePatchResume tool

- patchResumeInputSchema rejects empty operations and unknown op
  values; accepts valid replace/add/remove
- executePatchResume returns the applied operations on success
- executePatchResume throws when an operation targets an invalid path
  (passes through the underlying applyResumePatches validation)
- multi-op patches against top-level fields succeed end-to-end

* test(ai): cover sanitize edge branches

Hit the previously-uncovered branches in sanitize.ts:

- numeric 1 coerces to true
- '1' / '0' string shorthand coerces to true/false
- missing item.hidden gets salvaged to false
- empty input causes a non-Zod throw (caught + rethrown with generic message)

* test(ai): cover patch-proposal preview + normalize edge cases

- remove operations surface before-value with after=undefined
- buildResumePatchProposalPreview labels metadata/page paths sanely
- normalizeResumePatchProposals stamps every proposal with baseUpdatedAt
- normalizeResumePatchProposals preserves input order

* test(web): cover getLocaleOptions helper

Locale combobox surface — verify the option list mirrors localeMap
shape, uses locale codes as values, populates label + keywords with
the translated display name, and produces unique values.

* test(web): cover LevelTypeCombobox option mapping

LevelTypeCombobox maps levelDesignSchema.shape.type.options through
the internal getLevelTypeName labeler. Assert all 7 level types are
exposed and that each produces a non-empty label.

* test(web): cover ThemeToggleButton fallback paths

- aria-label flips between 'Switch to light theme' and 'Switch to
  dark theme' based on current theme
- clicking when document.startViewTransition is unavailable
  short-circuits to toggleTheme directly
- prefers-reduced-motion forces the direct toggle path even when
  the view-transition API is available

* test(web): cover NotFoundScreen

Mock the TanStack Router Link so the screen renders standalone, then
assert: documented error heading, routeId is surfaced verbatim, and
the Go Back link points to '..' (parent route).

* test(web): cover InformationSectionBuilder

Stub SectionBase so the donation/info section renders standalone.
Assert: donation prompt copy, OpenCollective CTA link, all 5
external resource links present, and external links target _blank
with rel=noopener.

* test(web): cover NotesSectionBuilder

Mock SectionBase, RichInput, and the resume-draft hooks so the
notes section renders in isolation. Assert: privacy hint copy
renders, RichInput is seeded with metadata.notes, and onChange
proxies through updateResumeData with a draft recipe that mutates
metadata.notes.

* test(web): cover TemplateSectionBuilder

Stub SectionBase and useCurrentResume so the right-sidebar template
section renders standalone. Asserts: current template name in the
heading, template tags rendered as badges, preview image points to
the catalog asset, and clicking the preview opens the
resume.template.gallery dialog.

* test(web): cover ColorPicker preset selection and trigger override

Mock the heavy @uiw/react-color-colorful dependency. Test:

- the trigger swatch reflects the controlled value
- clicking a preset color invokes onChange with an rgba() string
- a custom trigger replaces the default swatch when provided

* test(web): cover ExportSectionBuilder

Mock the heavy export pipelines (buildDocx, createResumePdfBlob,
downloadWithAnchor) and the resume-draft hook to test:

- JSON button packages resume.data as application/json and triggers
  download with the {name}.json filename
- DOCX button awaits buildDocx and downloads .docx
- PDF button awaits createResumePdfBlob and downloads .pdf

* test(web): cover ProfilesSectionBuilder

Stub the resume-draft hooks, SectionBase, SectionItem, and
SectionAddItemButton so the profiles section renders standalone:

- one SectionItem per profile with network as title and username as
  subtitle
- 'Add a new profile' affordance present
- when items.length > 0, the wrapper uses a solid border (not dashed)

* test(web): cover SkillsSectionBuilder

Mirror the profiles test for the skills section — verifies one
SectionItem per skill (name → title, proficiency → subtitle) and
the Add a new skill affordance.

* test(web): bulk-cover 7 left-sidebar section builders

Single test file covers awards, certifications, interests, languages,
publications, references, and volunteer builders. For each:

- one SectionItem rendered with the documented field → title/subtitle
  mapping
  - awards: title → awarder
  - certifications: title → 'issuer • date'
  - interests: name → (no subtitle)
  - languages: language → fluency
  - publications: title → publisher
  - references: name → (no subtitle)
  - volunteer: organization → location
- the 'Add a new {kind}' affordance with the matching copy

Mocks SectionBase, SectionItem, SectionAddItemButton, and the
resume-draft hooks so each builder renders standalone.

* test(web): cover ProjectsSectionBuilder buildSubtitle

The projects section is the only left-sidebar builder with a
composite subtitle. Tests three branches of its inline
buildSubtitle helper:

- period + website.label → joined with ' • '
- period only → just the period
- empty period + whitespace-only website.label → returns undefined

* test(web): cover Education + Experience section builders

- Education: school → title, degree → subtitle, add-new affordance
- Experience: position → subtitle when set; falls back to '1 role' /
  'N roles' (lingui plural) when position empty and roles[] present;
  add-new affordance

* test(web): cover CountUp animated number renderer

- default aria attributes (aria-live=polite, aria-atomic=true)
- initial textContent seeds to 'from' (up) or 'to' (down) value
- separator option formats with grouping
- decimal places are preserved when from/to are fractional
- aria-hidden=true strips aria-live + aria-atomic
- custom className is applied to the rendered span

* test(web): cover TextMaskEffect SVG renderer

- supplied text renders in every visible <text> layer
- aria-hidden + aria-label forwarded onto the root svg
- mouse enter/move/leave handlers don't throw
- custom className merges into the svg's class attribute

* test(web): cover URLInput prefix handling

- displayed input strips the https:// prefix so users only edit the
  meaningful portion
- editing re-adds the prefix on the way back through onChange
- pre-prefixed input is preserved
- cleared input emits an empty url (no prefix forced)
- hideLabelButton=true removes the popover trigger; default keeps it

* test(web): cover GithubStarsButton

Mocks useQuery + the CountUp animation so the button renders
standalone. Asserts:

- anchor points at the project repo with rel=noopener + target=_blank
- aria-label is the no-count copy when star count is undefined
- CountUp renders only once the count loads
- aria-label includes the localized count once data arrives

* test(web): cover ui/Combobox trigger label rendering

The shared Combobox wraps base-ui's combobox primitives. Smoke-test
the trigger label resolution:

- placeholder shows when nothing is selected
- selected option's label renders in the trigger
- multi-select default values render all labels
- empty options array renders the placeholder without crashing

* test(web): cover IconPicker trigger rendering

Stub react-window's Grid so happy-dom can render the picker without
layout-measurement deps. Verify:

- trigger renders an <i class='ph-{value}'> for the current value
- changing value updates the trigger icon class
- the picker emits a trigger button

* test(web): cover ChipInput add/dedupe/description behaviors

- existing chips render as Badges
- Enter adds the typed value to the chip list
- comma also commits the typed value
- duplicate input is dropped (onChange not called with a longer list)
- empty / whitespace-only input is dropped
- hideDescription removes the keyboard hint <kbd>; default keeps it

* test(web): cover StatisticsSectionBuilder

Mock useQuery + useParams + section-base so the right-sidebar
statistics section renders standalone:

- returns null content while the query is loading
- shows the private-resume hint when isPublic=false
- shows views/downloads counters and labels when isPublic=true
- includes 'Last viewed' timestamp copy when lastViewedAt is set

* test(web): cover TemplateGalleryDialog selection flow

- title + intro copy render
- one tile per template (>= 14)
- currently-selected template tile carries the ring-highlight
- clicking a different tile triggers updateResumeData with a recipe
  that sets metadata.template to the chosen template id

* test(web): cover Prefooter home-page section

- community tagline heading renders
- community-thanks paragraph renders
- the decorative TextMaskEffect renders an svg

* test(web): cover home-page Footer

- Resources and Community headings render
- documented resource links (Documentation, Sponsorships, Source
  Code, Changelog) all appear in the rendered output
- documented community links (Report an issue, Translations,
  Subreddit, Discord) all appear
- social anchors point at GitHub, LinkedIn, and X (Twitter)
- Copyright sub-component surfaces the app version via __APP_VERSION__

* test(web): cover home-page Header navigation

Mock TanStack Router Link + child components so the header renders
standalone. Verify:

- homepage anchor (/ link) carries the documented aria-label
- dashboard anchor points to /dashboard
- ThemeToggleButton and GithubStarsButton both mount
- the <nav> landmark is labeled 'Main navigation'

* chore: fix linter warnings
2026-05-11 14:25:10 +02:00
Amruth Pillai 48555f58e5 Merge branch 'main' of github.com:amruthpillai/reactive-resume 2026-05-11 13:56:01 +02:00
Amruth Pillai 0daf868cd4 fix(knip): remove duplicate export 2026-05-11 13:55:56 +02:00
github-actions[bot]andCrowdin Bot e574d4005f [skip ci] chore(i18n): sync translations from crowdin (#3037)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-05-11 13:54:49 +02:00
Amruth Pillai fda4e500b3 feat(toast): add non-invasive, dismissible donation banner 2026-05-11 13:50:32 +02:00
Amruth Pillai adfc9b527b docs: map existing codebase
Adds .planning/codebase/ with parallel-mapper outputs covering stack,
integrations, architecture, structure, conventions, testing, and concerns.
2026-05-11 11:16:33 +02:00
Amruth Pillai 71aadbd73d docs: update AGENTS.md with detailed codebase structure, environment setup, and common commands 2026-05-11 09:42:58 +02:00
Amruth Pillai 0713cf20d4 fix: filter invalid/empty section items and experience roles 2026-05-11 09:27:14 +02:00
Amruth Pillai 334ea48bc7 fix: enhance rich text handling in PDF generation
- Bump @tanstack/react-form version to 1.32.0 in package.json.
- Refactor rich-input component to simplify highlight configuration.
- Improve rich text HTML normalization to handle <mark> elements and apply styles correctly in PDF output.
- Update global CSS for WYSIWYG to adjust paragraph and list margins.
2026-05-11 09:04:42 +02:00
Amruth PillaiandCursor Agent 69c23211a0 Fix award title and date layout (#3029)
* Fix award title date layout

Co-authored-by: Amruth Pillai <im.amruth@gmail.com>

* Keep award date inline across templates

Co-authored-by: Amruth Pillai <im.amruth@gmail.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-05-11 05:54:51 +02:00
Amruth Pillai 143aaa741b fix(lapras): adjust lapras border color to fixed gray 2026-05-11 01:11:17 +02:00
Amruth Pillai e4cc6a8e57 docs: add react-pdf-html prose spacing design 2026-05-11 01:00:46 +02:00
Amruth Pillai 92a0e3ddb8 feat: implement resume preview loading enhancements and add tests for preview components 2026-05-11 00:59:16 +02:00
Amruth Pillai 4ebe9e5a67 fix: enhance resume schema validation and improve slugify function for CJK input 2026-05-11 00:27:47 +02:00
Amruth Pillai 0abee1048c fix(docs): update healthcheck test script on docs, resolves #3027 2026-05-11 00:09:07 +02:00
Amruth Pillai 83a407bc10 fix: add special hyphenation callback for Chinese script in PDF font registration 2026-05-10 23:54:37 +02:00
Amruth Pillai 02973a1eb1 fix: correct path for app source files in globals.css 2026-05-10 23:35:46 +02:00
Amruth Pillai 2e04e71f4a fix: update build process to ensure clean output for web app 2026-05-10 23:27:06 +02:00
Amruth Pillai 978cbaf1f3 fix: run knip --fix to add unlisted dependencies 2026-05-10 22:12:35 +02:00
Amruth Pillai 3cd228bd84 chore: update postcss to version 8.5.14 and sort font weights in typography components 2026-05-10 22:09:37 +02:00
Amruth Pillai 64ac3ff328 fix: revert to default hyphenation method 2026-05-10 21:50:27 +02:00
Amruth Pillai 846b7856a7 Merge branch 'main' of github.com:amruthpillai/reactive-resume 2026-05-10 20:46:31 +02:00
Amruth Pillai 7a60a42a04 chore: migrate from jsdom to happy-dom for testing environment 2026-05-10 20:46:28 +02:00
github-actions[bot]andCrowdin Bot b0de64ad13 [skip ci] chore(i18n): sync translations from crowdin (#3026)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-05-10 20:20:08 +02:00
Amruth Pillai b321e01658 Merge branch 'main' into feat/increase-test-coverage
# Conflicts:
#	packages/ui/src/components/input-group.test.tsx
2026-05-10 20:05:00 +02:00
Amruth Pillai 6a01207b6b test: add unit and component tests across the monorepo
Adds ~1000 tests to bring the previously-untested packages and apps
under coverage:

- packages/utils — string, color, date, file, level, locale, sanitize,
  field, html, network-icons, rate-limit, url, url-security, monorepo,
  resume/patch, resume/docx/link-utils, style helpers (~97% on the
  testable utility files)
- packages/ui — 28 component test files plus the use-controlled-state,
  use-mobile, use-confirm, use-prompt hooks (95% statements)
- packages/pdf — shared template helpers (filtering, columns, picture,
  metrics, section-links, page-size, rich-text-html, section-title)
- packages/schema — resumeDataSchema, page, templates, default
- packages/fonts — expanded coverage on font helpers
- packages/ai — resume sanitize and extraction template
- packages/api — resume-access-policy
- apps/web — error-message, locale, theme, pwa, dialogs/store, and
  the resume/move-item / section-actions / make-section-item helpers

Adds jsdom polyfills (ResizeObserver, IntersectionObserver,
scrollIntoView, matchMedia) and an explicit React Testing Library
cleanup hook to vitest.setup.ts so portal- and overlay-based components
work without per-test setup.
2026-05-10 20:00:07 +02:00
Amruth Pillai 2f6a8904e4 fix: enforce hasPassword to be a boolean 2026-05-10 19:56:50 +02:00
Amruth Pillai 56c9eb2ff4 fix(contact-list): resolves an issue where website label was uneditable 2026-05-10 19:30:09 +02:00
Amruth Pillai 33103536ae fix: fallback for cjk fonts when italic font style not available 2026-05-10 17:35:32 +02:00
Amruth PillaiandCursor Agent a93e7bd190 docs: add AGENTS.md with Cursor Cloud development instructions (#3024)
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-05-10 13:34:19 +02:00
github-actions[bot]andCrowdin Bot 4cd4b8c193 [skip ci] chore(i18n): sync translations from crowdin (#3023)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-05-10 13:27:59 +02:00
Amruth Pillai be9285aa33 chore(release): v5.1.2 2026-05-10 13:27:06 +02:00
Amruth Pillai 6787175a8a feat(ai): implement an AI chat window for agentic resume building (#3022) 2026-05-10 13:23:32 +02:00
Amruth Pillai 42e83cc676 fix: improper rendering of text blocks in PDFs 2026-05-10 13:22:21 +02:00
JamesGoslings 62f4532157 fix(pdf): register CJK fallback font so Chinese/Japanese/Korean text renders correctly (#3016)
Closes #2986.

Since v5.1.0 the renderer was migrated from Puppeteer to
@react-pdf/renderer. The new pipeline only registers the user-selected
typography family (e.g. Roboto, IBM Plex Serif), which contains no CJK
glyphs, so any Chinese / Japanese / Korean characters in the resume
fall back to .notdef and render as garbled boxes in both the in-app
preview and the exported PDF.

@react-pdf/renderer's textkit layer already supports per-codepoint
font substitution when a Text node is styled with `fontFamily` as a
string array — but only if every family in the stack has been
registered via Font.register. This change wires that up:

- packages/fonts: new `getPdfCjkFallbackFontFamily(family)` returns
  Noto Sans SC / Noto Serif SC depending on whether the primary font
  is sans-serif or serif, and `null` when no fallback is needed
  (standard PDF font, or primary already is the fallback). Source Han
  Sans/Serif SC covers all CJK-Unified ideographs, so a single font
  transparently handles Simplified/Traditional Chinese, Japanese
  kanji and Korean hanja.

- packages/pdf/hooks/use-register-fonts: after registering the
  primary body/heading fonts as before, additionally register the
  resolved CJK fallback (regular weight only — substitution is
  per-codepoint, not per-weight, so one face is enough). The
  function's return type is widened to a new `PdfTypography` whose
  `body.fontFamily` and `heading.fontFamily` become
  `[primary, cjkFallback]` two-element stacks.

- packages/pdf/document: cast the widened typography back through the
  schema-typed `ResumeData` so the wider runtime value reaches
  templates without changing the public `Typography` schema. All 15
  templates already consume `metadata.typography.body.fontFamily`
  directly, and `StyleSheet.fontFamily` accepts both string and
  string[], so no template edits are required.

Latin-only resumes are unaffected:
- `getPdfCjkFallbackFontFamily` returns `null` for standard PDF fonts
  and existing CJK selections, so the extra Font.register call is
  skipped.
- When no fallback applies, `registerFonts` returns the original
  typography reference unchanged (zero allocation).
- Even when the fallback is registered, textkit only consults it for
  codepoints the primary font cannot render, so Latin glyphs still
  come from the user-selected font with identical metrics.
2026-05-09 18:49:59 +02:00
Donovan WattsandAmruth Pillai fabe22089d fix(api): allow empty name in public resume output schema (#3012)
The redactResumeForViewer helper intentionally blanks the dashboard
title for non-owner viewers (the title can leak owner-only context like
"Senior Eng @ Foo - final draft"), but the getBySlug output schema
inherits name.min(1) from resumeSchema. Zod rejects the redacted
payload, oRPC throws Output validation failed, and every public resume
URL returns HTTP 500.

Relax the constraint to z.string() on the getBySlug output only -
ownership-gated procedures (getById, update, patch, list, ...) keep
min(1).

Fixes #3011

Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
2026-05-09 18:49:43 +02:00
Nicolas Oddo fa38f3e84a fix: correct v4 to v5 import for custom sections, skill levels, and hidden items (#3013) 2026-05-09 18:48:21 +02:00
github-actions[bot]andCrowdin Bot 0606e0072b [skip ci] chore(i18n): sync translations from crowdin (#3005)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-05-08 16:19:27 +02:00
Amruth PillaiandClaude bdfb854602 fix: resolve storage healthcheck path via LOCAL_STORAGE_PATH env var (#3004)
* fix: resolve local data directory to /app/data in production Docker

In the official Docker image, cwd is /app/apps/web (set via WORKDIR), but
the data volume is mounted at /app/data. Without pnpm-workspace.yaml present
in the runtime image, findWorkspaceRoot() returns null, so getLocalDataDirectory()
fell back to <cwd>/data = /app/apps/web/data, which the node user has no
permission to create. This caused the storage healthcheck to fail with
EACCES.

Add a production fallback: when cwd ends in apps/web, resolve the data
directory to two levels up (matching /app/data in the official image).

Re-resolves #2990.

https://claude.ai/code/session_015pSTtukxf7mFTty2Y6PHZf

* fix: replace apps/web heuristic with LOCAL_STORAGE_PATH env var

The previous fix special-cased a cwd ending in apps/web to land on /app/data,
but the heuristic could false-positive on any path with that suffix and was
fragile to Dockerfile changes. pnpm-workspace.yaml is never copied into the
runtime image, so the workspace-root walk was also dead code in production.

Replace the heuristic with an explicit LOCAL_STORAGE_PATH env var:
- Set LOCAL_STORAGE_PATH=/app/data in the Dockerfile (single source of truth).
- Add LOCAL_STORAGE_PATH to the env schema; storage and statistics services
  pass it through to getLocalDataDirectory.
- getLocalDataDirectory now uses the override when set, else workspace root
  (dev), else cwd/data.
- New Nitro plugin validates the resolved local data directory at startup
  and refuses to boot with a clear error if it isn't writable, surfacing
  permission issues immediately instead of at first upload/healthcheck.
- Document the new variable in .env.example and the Docker self-hosting docs.

https://claude.ai/code/session_015pSTtukxf7mFTty2Y6PHZf

* fix: address review feedback on storage path handling

- apps/web/plugins/2.storage.ts: use the default-import style for
  node:fs/promises (matches the rest of the repo, sidesteps any
  named-export concerns for fs.constants).
- packages/env/src/server.ts: reject relative LOCAL_STORAGE_PATH values
  via a zod refinement. Relative paths would be resolved against cwd,
  which differs between dev and Docker — exactly the same surprise the
  original bug had. Failing fast at config validation time gives a
  clear error before the server boots.

https://claude.ai/code/session_015pSTtukxf7mFTty2Y6PHZf

* fix: update data volume configuration in Docker Compose and enhance Nitro plugin

* fix: remove "Can I customize the templates?" FAQ entry from multiple language files

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-08 16:05:47 +02:00
github-actions[bot]andCrowdin Bot 05f094bd13 [skip ci] chore(i18n): sync translations from crowdin (#3001)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-05-08 13:07:45 +02:00
github-actions[bot]andCrowdin Bot d1c301de83 [skip ci] chore(i18n): sync translations from crowdin (#2999)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-05-08 13:06:37 +02:00
Amruth PillaiandClaude e42af3cd04 Remove "Can I customize the templates?" FAQ from homepage (#3000)
https://claude.ai/code/session_013T4jd8fPiWR5jMw2yxwgez

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-08 13:06:27 +02:00
Amruth Pillai f3375adecb chore(release): v5.1.1 2026-05-08 13:05:55 +02:00
Amruth Pillai d9e3289f69 chore: remove unused dependencies and update locale references in resume components 2026-05-08 13:01:18 +02:00
github-actions[bot]andCrowdin Bot 7c08007a9d [skip ci] chore(i18n): sync translations from crowdin (#2997)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-05-08 12:39:10 +02:00
Amruth Pillai 296f7951ec fix: make nested lists work in PDF renderer, resolves #2993 2026-05-08 12:29:16 +02:00
github-actions[bot]andCrowdin Bot bf1a540fd1 [skip ci] chore(i18n): sync translations from crowdin (#2995)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-05-08 11:58:47 +02:00
Amruth Pillai e4a574ccd0 chore: remove tests 2026-05-08 11:58:25 +02:00
Amruth Pillai 26ca5c29c3 fix: auto-load .env, resolves #2987 2026-05-08 11:57:59 +02:00
Amruth Pillai 2cd774dab7 feat: implement free-form resume page formats, resolves #2991 2026-05-08 11:28:18 +02:00
Amruth Pillai 9cbb30d3ba fix: resolves #2990, revert the local storage path to /app/data 2026-05-08 11:10:24 +02:00
Amruth Pillai ed42f181ca fix: update dialog component styles for improved responsiveness and overflow handling 2026-05-08 10:39:34 +02:00
Amruth Pillai 21cadd76fe fix: update sample resume picture URL to point to a valid image 2026-05-08 02:37:01 +02:00
Amruth Pillai 4c771307e0 fix: fallback to "IBM Plex Serif" when unknown font encountered 2026-05-08 00:51:07 +02:00
Amruth Pillai 6d54ffa88b fix: font registration to support italic styles in PDF generation 2026-05-08 00:29:10 +02:00
Amruth Pillai 5042ad9d1f feat: add PDF download endpoint and export createLocalizedResumeDocument function 2026-05-08 00:05:55 +02:00
Amruth Pillai 3b82aa90f3 fix: resolves #2793, accept looseObject from resumeDataSchema 2026-05-07 22:50:45 +02:00
Amruth Pillai 524a7a6f4e fix: resolves #2949, remove route preloading 2026-05-07 22:46:32 +02:00
Amruth Pillai 36a16f6483 fix(#2983): fixes #2983, experience role progression causes re-renders 2026-05-07 22:30:47 +02:00
Amruth Pillai e320f3a920 chore: remove @tanstack/zod-adapter 2026-05-07 22:22:58 +02:00
Amruth Pillai d5891ff035 fix(#2978): implement inlineLink functionality 2026-05-07 21:47:00 +02:00
Amruth Pillai 891ce60270 fix(#2984): add prose styles to rich-input editor 2026-05-07 21:43:07 +02:00
Amruth Pillai 1b0bb067b6 fix(#2976): update header name line height across multiple templates 2026-05-07 21:34:01 +02:00
autofix-ci[bot] 2b3af1f7b7 [autofix.ci] apply automated fixes 2026-05-07 19:29:59 +00:00
Amruth Pillai ced765e229 fix(#2978): show underline on all links 2026-05-07 21:29:10 +02:00
Amruth Pillai 023cb4a594 fix(#2981): runtime error when enabling 2fa 2026-05-07 21:23:53 +02:00
github-actions[bot]andCrowdin Bot a43538c919 [skip ci] chore(i18n): sync translations from crowdin (#2975)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-05-07 16:52:18 +02:00
Amruth Pillai faba604813 ci: do not run lingui:extract on autofix 2026-05-07 16:51:59 +02:00
github-actions[bot]andCrowdin Bot f8b3437e33 [skip ci] chore(i18n): sync translations from crowdin (#2974)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-05-07 16:50:29 +02:00
Amruth Pillai 4ac16df3d6 Merge branch 'main' of github.com:amruthpillai/reactive-resume 2026-05-07 16:49:48 +02:00
Amruth Pillai 4ae3e1a230 fix: update resume ID type from uuid to text and use generateId for ID generation 2026-05-07 16:49:44 +02:00
autofix-ci[bot] dcccbcfa9e [autofix.ci] apply automated fixes 2026-05-07 14:49:24 +00:00
Amruth Pillai 92a3916e76 fix: convert postgres uuid to text columns, to capture old cuid resume IDs 2026-05-07 16:48:25 +02:00
github-actions[bot]andCrowdin Bot 6321e99b9e [skip ci] chore(i18n): sync translations from crowdin (#2973)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-05-07 16:38:34 +02:00
github-actions[bot]andCrowdin Bot 94c953005e [skip ci] chore(i18n): sync translations from crowdin (#2972)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-05-07 16:31:49 +02:00
Amruth Pillai cf957692fc chore: update phosphor-icons-react-pdf to version 0.1.3 in package.json and pnpm-lock.yaml 2026-05-07 16:31:26 +02:00
Amruth Pillai 64492ce2f1 Merge branch 'main' of github.com:amruthpillai/reactive-resume 2026-05-07 15:19:19 +02:00
Amruth Pillai 4165c10a71 fix: readme banner image url 2026-05-07 15:19:13 +02:00
autofix-ci[bot] ef6961c9eb [autofix.ci] apply automated fixes 2026-05-07 13:18:55 +00:00
Amruth Pillai 13787db34a Merge branch 'main' of github.com:amruthpillai/reactive-resume 2026-05-07 15:16:34 +02:00
github-actions[bot]andCrowdin Bot f64daeb89e [skip ci] chore(i18n): sync translations from crowdin (#2971)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-05-07 15:15:23 +02:00
Amruth Pillai 6e447d1bcc Merge branch 'main' of github.com:amruthpillai/reactive-resume 2026-05-07 15:14:21 +02:00
Amruth Pillai 148d564fe8 fix: readme banner image url 2026-05-07 15:14:20 +02:00
Amruth Pillai 50ba37a27f v5.1.0 (#2970)
* chore(release): v5.1.0

* feat: implement resume thumbnails

* fix: remove unused mcp tools

* docs: fix formatting of docs
2026-05-07 15:12:33 +02:00
Joshua Fife 51c366310e Fixed docker compose for linux self hosted deployments (#2952) 2026-05-02 23:57:27 +02:00
Amruth Pillai 4e58ea6c8a chore: remove .devcontainer 2026-04-30 02:07:58 +02:00
Amruth Pillai fd892b0e14 fix: make the dash plugin optional
- fixes #2947
2026-04-29 23:30:07 +02:00
Amruth Pillai dc4830d41b chore: remove getSubsequentPageTopMarginStyle 2026-04-29 21:42:20 +02:00
Amruth Pillai 480e9a2612 fix: switch back to csp-report-only 2026-04-29 21:40:57 +02:00
Amruth Pillai 4cf5cdd181 fix: remove timeout to check data-wf-loaded 2026-04-29 21:38:40 +02:00
Amruth Pillai b9894c687c fix: handle /assets/ requests before it reaches orpc 2026-04-29 20:40:28 +02:00
autofix-ci[bot] 9048702cb8 [autofix.ci] apply automated fixes 2026-04-29 18:17:46 +00:00
Kuchizu dd94d070af fix: make AI resume analysis schema provider-compatible (#2939) 2026-04-29 20:16:29 +02:00
Amruth Pillai e035088269 fix: add-to-cache-list-conflicting-entries 2026-04-29 20:15:15 +02:00
Amruth Pillai 1fbe89bc01 update ipAddressHeaders 2026-04-29 19:55:31 +02:00
Amruth Pillai 4b071f2db7 update ipAddressHeaders 2026-04-29 19:54:21 +02:00
Amruth Pillai 80ca3a8875 update Dockerfile 2026-04-29 19:34:41 +02:00
Amruth Pillai e95be536fa remove CSP reporting 2026-04-29 19:19:36 +02:00
Amruth Pillai d6287dbd65 better rate limiting, verbose logging for username/slug path 2026-04-29 18:44:07 +02:00
Amruth Pillai 47abce351b sync translations with crowdin 2026-04-27 15:22:13 +02:00
Amruth Pillai 8f6c65b7fd chore(release): 🚀 v5.0.20 2026-04-27 15:21:07 +02:00
github-actions[bot]andCrowdin Bot 5a88ac12bf Sync Translations from Crowdin (#2935)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-04-27 15:13:05 +02:00
RuzenieAmruth Pillaiautofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
118004b3d3 feat:icon colors (#2928)
Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-04-27 15:12:55 +02:00
github-actions[bot]andCrowdin Bot 6a8dd480cb Sync Translations from Crowdin (#2934)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-04-27 13:39:40 +02:00
Amruth Pillai 5d8126d4b0 feat: combine settings pages into a single integrations page 2026-04-27 12:56:15 +02:00
JamesGoslings 8566a9fd0f fix(i18n): translate Skills section title to Chinese (zh-CN) (#2931) 2026-04-27 10:47:46 +02:00
github-actions[bot]andCrowdin Bot 0d0bc61582 Sync Translations from Crowdin (#2932)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-04-27 10:47:22 +02:00
JamesGoslings e3af637100 docs: list Meowth alongside existing templates (#2929) 2026-04-27 10:47:14 +02:00
github-actions[bot]andCrowdin Bot f752145c57 Sync Translations from Crowdin (#2926)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-04-27 10:45:59 +02:00
Amruth Pillai b87f200767 feat: Add better email templates for password reset and email verification. 2026-04-27 10:45:44 +02:00
Amruth Pillai 73ec8b2ffb chore: release version 5.0.20 with new Meowth resume template and updated translations 2026-04-27 08:52:18 +02:00
github-actions[bot]andCrowdin Bot 107537999f Sync Translations from Crowdin (#2925)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-04-27 08:50:43 +02:00
Amruth Pillai e1bccbcc93 chore: update dependencies, cleanup exports (knip) 2026-04-27 08:48:35 +02:00
JamesGoslingsAmruth Pillaiautofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
54e256be09 feat: add Meowth template with inline three-column entry header (Asian-style compact ATS) (#2923)
Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-04-27 08:45:08 +02:00
github-actions[bot]andCrowdin Bot b26d510294 Sync Translations from Crowdin (#2922)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-04-26 11:46:56 +02:00
Amruth Pillai 623ca5c675 allow loopback to localhost on mcp, only for local MCP clients 2026-04-26 10:59:10 +02:00
Amruth PillaiandClaude 76e00ae019 Revert "fix(resume): prevent orphaned section headings at page break (#2851)" (#2920)
This reverts commit 775f625f6e.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-26 01:23:22 +02:00
Amruth Pillai ff0ef12ecc Merge branch 'main' of github.com:amruthpillai/reactive-resume 2026-04-26 01:08:00 +02:00
Amruth Pillai 1bcb2e2af6 update changelog 2026-04-26 01:07:58 +02:00
github-actions[bot]andCrowdin Bot 796dc2ca41 Sync Translations from Crowdin (#2919)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-04-26 01:01:34 +02:00
github-actions[bot]andCrowdin Bot 80bf41254b Sync Translations from Crowdin (#2918)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-04-26 00:58:11 +02:00
Amruth Pillai bbc38d2f09 implement logic from #2853, thanks to @trigger-xyz 2026-04-26 00:57:50 +02:00
Christian PojoniClaude Opus 4.6autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
775f625f6e fix(resume): prevent orphaned section headings at page break (#2851)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-04-26 00:49:03 +02:00
2203933fac Sync Translations from Crowdin (#2917)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
2026-04-26 00:48:24 +02:00
Platinum1154andAmruth Pillai 907e32a731 feat: add text color support to the rich text editor (#2903)
* feat: add text color support to the rich text editor

* improve design of text color picker

* Update translations for color picker features in multiple languages

---------

Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
2026-04-26 00:48:03 +02:00
autofix-ci[bot] d6919e340b [autofix.ci] apply automated fixes 2026-04-25 22:34:53 +00:00
Amruth Pillai e05ae42fbb Remove "Reactive Resume" translations from multiple language files to streamline localization efforts. 2026-04-26 00:33:45 +02:00
github-actions[bot]andCrowdin Bot e99f39d139 Sync Translations from Crowdin (#2916)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-04-26 00:33:12 +02:00
Platinum1154andAmruth Pillai a4e7d6680d feat: add Chinese font options (#2905)
Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
2026-04-26 00:32:42 +02:00
iago macedoandAmruth Pillai 77ad14b359 feat: add OpenRouter as AI provider (#2906)
Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
2026-04-26 00:00:14 +02:00
Amruth Pillai 9b3916d43e chore: ⬆️ update dependencies 2026-04-25 23:34:19 +02:00
github-actions[bot]andCrowdin Bot 29f1aaf0da [skip ci] chore(i18n): sync translations from crowdin (#2914)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-04-25 17:32:04 +02:00
github-actions[bot]andCrowdin Bot 73cef02a4f Sync Translations from Crowdin (#2913)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-04-25 15:47:33 +02:00
github-actions[bot]andCrowdin Bot 2cb1897922 Sync Translations from Crowdin (#2912)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-04-25 15:32:52 +02:00
Amruth Pillai d0af9f4b4f test(security): cover url validation and form edge cases
Add and update tests for new security utilities and tightened UI behavior to prevent regressions in validation and error handling paths.

Made-with: Cursor
2026-04-25 15:31:19 +02:00
Amruth Pillai 08e9c80037 refactor(ui): improve error handling and input safety in app flows
Normalize frontend error rendering and tighten input/path handling across auth, builder, dashboard, and shared components for more resilient UX behavior.

Made-with: Cursor
2026-04-25 15:31:13 +02:00
Amruth Pillai a42dbcd452 feat(security): harden auth, oauth, and printer endpoints
Add stricter URL and redirect validation, endpoint rate limiting, safer defaults for printer and compose config, and CSP protections across server and API surfaces.

Made-with: Cursor
2026-04-25 15:31:06 +02:00
Amruth Pillai d3102565e4 chore(i18n): sync translation catalogs
Update locale catalogs to include the latest extracted strings used by the recent auth and security UX updates.

Made-with: Cursor
2026-04-25 15:30:51 +02:00
Amruth Pillai c0387298c5 docs(self-hosting): refresh docker and setup guidance
Clarify self-hosting, migration, and quickstart instructions to reflect the new secure defaults and deployment flow.

Made-with: Cursor
2026-04-25 15:30:47 +02:00
Amruth Pillai 9e1e593dc4 add SECURITY.md 2026-04-25 11:29:27 +02:00
Amruth Pillai 849aad6497 allow for running dev environment inside docker 2026-04-25 11:26:02 +02:00
Amruth Pillai 847d69b621 pin vite-plus versions to 0.1.19 2026-04-25 10:53:19 +02:00
Yeung Lihan 6ff754d125 fix: reduce preview wheel zoom sensitivity (#2911) 2026-04-25 09:43:58 +02:00
JamesGoslings ff3d4b1337 fix: align role period text to end in experience item (#2876) (#2908)
When an experience entry has multiple roles (Role Progression),
the period/date for each role was left-aligned in the grid layout,
causing it to appear off-center instead of right-aligned under the
location field.

Added `text-end` class to the role period element to match the
alignment behavior of the single-role header period. Uses `text-end`
instead of `text-right` for proper RTL language support.
2026-04-25 09:43:31 +02:00
github-actions[bot]andCrowdin Bot 2d2d5cd505 Sync Translations from Crowdin (#2902)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-04-23 14:17:30 +02:00
Amruth Pillai c6e8df0a00 - Pinned some packages to specific versions to avoid potential security vulnerabilities through transitive dependencies. (through running pnpm audit)
- Updated dependencies and lockfile.
- Synced translation catalogs from Crowdin.
2026-04-22 15:15:10 +02:00
Amruth Pillai 7df9b1e4b5 autocomplete passkey, if exists 2026-04-14 15:04:19 +02:00
github-actions[bot]andCrowdin Bot 0b4bac62a7 [skip ci] chore(i18n): sync translations from crowdin (#2892)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-04-14 14:42:06 +02:00
Amruth Pillai 82cb6e7590 update translations for passkeys 2026-04-14 14:40:07 +02:00
github-actions[bot]andCrowdin Bot 46dac6d3b1 [skip ci] chore(i18n): sync translations from crowdin (#2891)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-04-14 12:16:24 +02:00
Amruth Pillai b0b0d27c1f try again 2026-04-14 12:15:01 +02:00
Amruth Pillai b12d1184bc switch to crowdin github action 2026-04-14 11:53:20 +02:00
Amruth Pillai d6e0d9ac61 switch to crowdin github action 2026-04-14 11:50:07 +02:00
Amruth Pillai c19b9746c8 📦 v5.0.18 - https://docs.rxresu.me/changelog (Passkeys Support) 2026-04-14 10:51:04 +02:00
Amruth Pillai 4ed6177aee New translations en-us.po (Dutch) (#2886)
[ci skip]
2026-04-09 20:46:17 +02:00
Amruth Pillai 5900de101c update peer dependencies 2026-04-09 16:05:14 +02:00
Vedant Shankar BhavsarandAmruth Pillai 61b3324941 fix: improve email handling and user lookup in OAuth configuration (#2874)
* refactor: improve email handling and user lookup in OAuth configuration

* refactor: enhance OAuth user mapping and improve email handling

---------

Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
2026-04-09 16:02:30 +02:00
Amruth Pillai 0e858c5967 update webfontlist.json 2026-04-09 15:15:08 +02:00
russandClaude 8c968e92f4 fix: correct Computer Modern Sans italic font file mappings (#2881)
The 400italic and 700italic variants for Computer Modern Sans pointed
to incorrect filenames. `cmunsl.woff` does not exist in the upstream
bitmaks/cm-web-fonts repository (returns 404 from jsDelivr CDN).

This causes PDF export to fail with "Waiting failed: 5000ms exceeded"
because Puppeteer's waitForFonts stalls on the 404, preventing the
page from signalling data-wf-loaded="true" within the timeout.

Correct mapping verified against the upstream @font-face declarations
in bitmaks/cm-web-fonts font/Sans/cmun-sans.css:

  - 400 normal:  cmunss.woff (unchanged)
  - 400 italic:  cmunsi.woff (was cmunsl.woff, which does not exist)
  - 700 normal:  cmunsx.woff (unchanged)
  - 700 italic:  cmunso.woff (was cmunsi.woff, which is the 400 italic)

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-09 15:12:30 +02:00
Amruth Pillai bea8ff1beb Fix MCP tool names for Claude Desktop incompatibility (#2885)
* fixes #2884, rename tool names for claude to work

* update dependencies
2026-04-09 15:03:18 +02:00
Amruth Pillai 85e0b0a96d further improvements to the mcp server 2026-04-09 10:06:50 +02:00
Amruth Pillai 1b266ba7ac Implement Resume Analysis (#2882)
* Implement Resume Analysis

* 📦 v5.0.17 - https://docs.rxresu.me/changelog
2026-04-09 09:04:27 +02:00
Amruth Pillai 1810dc8b07 better mcp server 2026-04-09 00:28:31 +02:00
Amruth Pillai 06b9da39ed Sync Translations from Crowdin (#2864)
* New translations en-us.po (Romanian)
[ci skip]

* New translations en-us.po (French)
[ci skip]

* New translations en-us.po (Spanish)
[ci skip]

* New translations en-us.po (Afrikaans)
[ci skip]

* New translations en-us.po (Arabic)
[ci skip]

* New translations en-us.po (Bulgarian)
[ci skip]

* New translations en-us.po (Catalan)
[ci skip]

* New translations en-us.po (Czech)
[ci skip]

* New translations en-us.po (Danish)
[ci skip]

* New translations en-us.po (German)
[ci skip]

* New translations en-us.po (Greek)
[ci skip]

* New translations en-us.po (Finnish)
[ci skip]

* New translations en-us.po (Hebrew)
[ci skip]

* New translations en-us.po (Hungarian)
[ci skip]

* New translations en-us.po (Italian)
[ci skip]

* New translations en-us.po (Japanese)
[ci skip]

* New translations en-us.po (Korean)
[ci skip]

* New translations en-us.po (Lithuanian)
[ci skip]

* New translations en-us.po (Dutch)
[ci skip]

* New translations en-us.po (Norwegian)
[ci skip]

* New translations en-us.po (Polish)
[ci skip]

* New translations en-us.po (Portuguese)
[ci skip]

* New translations en-us.po (Russian)
[ci skip]

* New translations en-us.po (Slovak)
[ci skip]

* New translations en-us.po (Slovenian)
[ci skip]

* New translations en-us.po (Albanian)
[ci skip]

* New translations en-us.po (Serbian (Cyrillic))
[ci skip]

* New translations en-us.po (Swedish)
[ci skip]

* New translations en-us.po (Turkish)
[ci skip]

* New translations en-us.po (Ukrainian)
[ci skip]

* New translations en-us.po (Chinese Simplified)
[ci skip]

* New translations en-us.po (Chinese Traditional)
[ci skip]

* New translations en-us.po (Vietnamese)
[ci skip]

* New translations en-us.po (Portuguese, Brazilian)
[ci skip]

* New translations en-us.po (Indonesian)
[ci skip]

* New translations en-us.po (Persian)
[ci skip]

* New translations en-us.po (Khmer)
[ci skip]

* New translations en-us.po (Tamil)
[ci skip]

* New translations en-us.po (Bengali)
[ci skip]

* New translations en-us.po (Marathi)
[ci skip]

* New translations en-us.po (Thai)
[ci skip]

* New translations en-us.po (Latvian)
[ci skip]

* New translations en-us.po (Azerbaijani)
[ci skip]

* New translations en-us.po (Hindi)
[ci skip]

* New translations en-us.po (Malay)
[ci skip]

* New translations en-us.po (Telugu)
[ci skip]

* New translations en-us.po (English, United Kingdom)
[ci skip]

* New translations en-us.po (Malayalam)
[ci skip]

* New translations en-us.po (Uzbek)
[ci skip]

* New translations en-us.po (Kannada)
[ci skip]

* New translations en-us.po (Odia)
[ci skip]

* New translations en-us.po (Amharic)
[ci skip]

* New translations en-us.po (Nepali)
[ci skip]

* New translations en-us.po (Swedish)
[ci skip]
2026-04-05 09:50:40 +02:00
Amruth Pillai 11373763fd fixes #2868 2026-04-05 09:03:49 +02:00
Amruth Pillai b3224ce59b Merge branch 'main' of github.com:amruthpillai/reactive-resume 2026-04-04 20:59:38 +02:00
Amruth Pillai 145b17de0f fix #2865, spacing between section items messed up 2026-04-04 20:59:30 +02:00
Amruth Pillai 623c5f6f81 Downgrade Cosign installer version to v3 2026-04-04 12:53:26 +02:00
Amruth Pillai bcbe70d231 📦 v5.0.16 - https://docs.rxresu.me/changelog 2026-04-04 12:31:24 +02:00
Amruth Pillai 8b52b9d8fc update translations 2026-04-04 12:29:10 +02:00
Amruth Pillai 4ecc66d081 Update title/metadata of generated PDF (#2863) 2026-04-04 12:28:32 +02:00
Amruth Pillai 10d58175f1 refactor: update layout of resume items for better alignment and spacing 2026-04-04 12:21:05 +02:00
Amruth Pillai 923f5f6173 fixes #2733: Bug where date range is displayed on separate line (#2862) 2026-04-04 12:03:09 +02:00
Amruth Pillai 9332e1e3ff chore: update GitHub Actions workflows to use Node 24 and upgrade action versions 2026-04-02 00:28:36 +02:00
AaronJoel Dev 77f31fa127 fix: prevent desktop horizontal overflow in home hero (#2854) 2026-04-02 00:16:16 +02:00
Amruth Pillai 4fd43657dc 📦 v5.0.15 - https://docs.rxresu.me/changelog 2026-04-02 00:14:54 +02:00
Aman Gupta d9a24448e8 fix: refactor useCSSVariables to ensure valid highest font weights (#2852) 2026-03-31 22:41:24 +02:00
Amruth Pillai 69ff78b656 Sync Translations from Crowdin (#2848)
* New translations en-us.po (Romanian)
[ci skip]

* New translations en-us.po (French)
[ci skip]

* New translations en-us.po (Spanish)
[ci skip]

* New translations en-us.po (Afrikaans)
[ci skip]

* New translations en-us.po (Arabic)
[ci skip]

* New translations en-us.po (Bulgarian)
[ci skip]

* New translations en-us.po (Catalan)
[ci skip]

* New translations en-us.po (Czech)
[ci skip]

* New translations en-us.po (Danish)
[ci skip]

* New translations en-us.po (German)
[ci skip]

* New translations en-us.po (Greek)
[ci skip]

* New translations en-us.po (Finnish)
[ci skip]

* New translations en-us.po (Hebrew)
[ci skip]

* New translations en-us.po (Hungarian)
[ci skip]

* New translations en-us.po (Italian)
[ci skip]

* New translations en-us.po (Japanese)
[ci skip]

* New translations en-us.po (Korean)
[ci skip]

* New translations en-us.po (Lithuanian)
[ci skip]

* New translations en-us.po (Dutch)
[ci skip]

* New translations en-us.po (Norwegian)
[ci skip]

* New translations en-us.po (Polish)
[ci skip]

* New translations en-us.po (Portuguese)
[ci skip]

* New translations en-us.po (Russian)
[ci skip]

* New translations en-us.po (Slovak)
[ci skip]

* New translations en-us.po (Slovenian)
[ci skip]

* New translations en-us.po (Albanian)
[ci skip]

* New translations en-us.po (Serbian (Cyrillic))
[ci skip]

* New translations en-us.po (Swedish)
[ci skip]

* New translations en-us.po (Turkish)
[ci skip]

* New translations en-us.po (Ukrainian)
[ci skip]

* New translations en-us.po (Chinese Simplified)
[ci skip]

* New translations en-us.po (Chinese Traditional)
[ci skip]

* New translations en-us.po (Vietnamese)
[ci skip]

* New translations en-us.po (Portuguese, Brazilian)
[ci skip]

* New translations en-us.po (Indonesian)
[ci skip]

* New translations en-us.po (Persian)
[ci skip]

* New translations en-us.po (Khmer)
[ci skip]

* New translations en-us.po (Tamil)
[ci skip]

* New translations en-us.po (Bengali)
[ci skip]

* New translations en-us.po (Marathi)
[ci skip]

* New translations en-us.po (Thai)
[ci skip]

* New translations en-us.po (Latvian)
[ci skip]

* New translations en-us.po (Azerbaijani)
[ci skip]

* New translations en-us.po (Hindi)
[ci skip]

* New translations en-us.po (Malay)
[ci skip]

* New translations en-us.po (Telugu)
[ci skip]

* New translations en-us.po (English, United Kingdom)
[ci skip]

* New translations en-us.po (Malayalam)
[ci skip]

* New translations en-us.po (Uzbek)
[ci skip]

* New translations en-us.po (Kannada)
[ci skip]

* New translations en-us.po (Odia)
[ci skip]

* New translations en-us.po (Amharic)
[ci skip]

* New translations en-us.po (Nepali)
[ci skip]
2026-03-29 23:34:21 +02:00
Amruth Pillai 0794b5c955 update dependencies 2026-03-29 23:09:25 +02:00
Amruth Pillai 5c986cc68d Sync Translations from Crowdin (#2838)
* New translations en-us.po (Romanian)
[ci skip]

* New translations en-us.po (French)
[ci skip]

* New translations en-us.po (Spanish)
[ci skip]

* New translations en-us.po (Afrikaans)
[ci skip]

* New translations en-us.po (Arabic)
[ci skip]

* New translations en-us.po (Bulgarian)
[ci skip]

* New translations en-us.po (Catalan)
[ci skip]

* New translations en-us.po (Czech)
[ci skip]

* New translations en-us.po (Danish)
[ci skip]

* New translations en-us.po (German)
[ci skip]

* New translations en-us.po (Greek)
[ci skip]

* New translations en-us.po (Finnish)
[ci skip]

* New translations en-us.po (Hebrew)
[ci skip]

* New translations en-us.po (Hungarian)
[ci skip]

* New translations en-us.po (Italian)
[ci skip]

* New translations en-us.po (Japanese)
[ci skip]

* New translations en-us.po (Korean)
[ci skip]

* New translations en-us.po (Lithuanian)
[ci skip]

* New translations en-us.po (Dutch)
[ci skip]

* New translations en-us.po (Norwegian)
[ci skip]

* New translations en-us.po (Polish)
[ci skip]

* New translations en-us.po (Portuguese)
[ci skip]

* New translations en-us.po (Russian)
[ci skip]

* New translations en-us.po (Slovak)
[ci skip]

* New translations en-us.po (Slovenian)
[ci skip]

* New translations en-us.po (Albanian)
[ci skip]

* New translations en-us.po (Serbian (Cyrillic))
[ci skip]

* New translations en-us.po (Swedish)
[ci skip]

* New translations en-us.po (Turkish)
[ci skip]

* New translations en-us.po (Ukrainian)
[ci skip]

* New translations en-us.po (Chinese Simplified)
[ci skip]

* New translations en-us.po (Chinese Traditional)
[ci skip]

* New translations en-us.po (Vietnamese)
[ci skip]

* New translations en-us.po (Portuguese, Brazilian)
[ci skip]

* New translations en-us.po (Indonesian)
[ci skip]

* New translations en-us.po (Persian)
[ci skip]

* New translations en-us.po (Khmer)
[ci skip]

* New translations en-us.po (Tamil)
[ci skip]

* New translations en-us.po (Bengali)
[ci skip]

* New translations en-us.po (Marathi)
[ci skip]

* New translations en-us.po (Thai)
[ci skip]

* New translations en-us.po (Latvian)
[ci skip]

* New translations en-us.po (Azerbaijani)
[ci skip]

* New translations en-us.po (Hindi)
[ci skip]

* New translations en-us.po (Malay)
[ci skip]

* New translations en-us.po (Telugu)
[ci skip]

* New translations en-us.po (English, United Kingdom)
[ci skip]

* New translations en-us.po (Malayalam)
[ci skip]

* New translations en-us.po (Uzbek)
[ci skip]

* New translations en-us.po (Kannada)
[ci skip]

* New translations en-us.po (Odia)
[ci skip]

* New translations en-us.po (Amharic)
[ci skip]

* New translations en-us.po (Nepali)
[ci skip]

* New translations en-us.po (Russian)
[ci skip]
2026-03-29 23:07:48 +02:00
autofix-ci[bot] 688c002822 [autofix.ci] apply automated fixes 2026-03-24 10:58:09 +00:00
Amruth Pillai ced49d5314 Update Crowdin configuration file 2026-03-24 11:57:03 +01:00
Amruth Pillai 3653baad9b 📦 v5.0.14 - https://docs.rxresu.me/changelog 2026-03-24 11:22:45 +01:00
b4aaf9712f feat(mcp): add OAuth 2.1 for claude.ai MCP connector (#2829)
* feat(mcp): add OAuth 2.1 authentication for claude.ai MCP connector

Enable OAuth 2.1 (RFC 8414 + RFC 7591) for the MCP endpoint using
better-auth's MCP plugin. This allows claude.ai and other MCP clients
to authenticate via Dynamic Client Registration and Authorization Code
flow with PKCE, using the existing login page.

- Add `mcp()` plugin to better-auth config with login page redirect
- Add `.well-known/oauth-authorization-server` discovery endpoint
- Add `.well-known/oauth-protected-resource` metadata endpoint
- Update MCP handler to accept Bearer tokens via `getMcpSession`
- Retain `x-api-key` fallback for backward compatibility
- Return proper HTTP 401 + WWW-Authenticate header for unauthed requests
- Add `oauthApplication`, `oauthAccessToken`, `oauthConsent` tables

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(mcp): use typed AuthError and suppress noisy verifyApiKey throws

- Replace string-matching error detection with instanceof AuthError
- Wrap verifyApiKey in try-catch to avoid logging malformed key errors
- Move console.error below auth check so 401s don't pollute logs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(mcp): add database migration for OAuth tables

Creates oauth_application, oauth_access_token, and oauth_consent tables
required for MCP OAuth 2.1 Dynamic Client Registration flow.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(mcp): resolve OAuth Bearer token auth for oRPC tool calls

The oRPC context only checked session cookies and API keys, causing
MCP tool calls from OAuth clients (claude.ai) to fail with Unauthorized
even though the MCP endpoint itself authenticated successfully.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(mcp): look up user by userId from OAuth access token

getMcpSession returns OAuthAccessToken (with userId), not a session
object with a user property. Must query the user table by userId.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor(mcp): migrate from deprecated mcp() plugin to @better-auth/oauth-provider

The better-auth MCP plugin is marked for deprecation in favor of the
OAuth Provider plugin. This refactors the entire OAuth 2.1 flow to use
@better-auth/oauth-provider with JWT-based token verification, replacing
the opaque token lookup via getMcpSession().

Key changes:
- Replace mcp() with jwt() + oauthProvider() in auth config
- Replace getMcpSession() with verifyAccessToken() (JWT/JWKS)
- Replace oauthApplication table with oauthClient (RFC 7591 compliant)
- Add oauthRefreshToken table and jwks table for JWT signing keys
- Extract shared authBaseUrl and verifyOAuthToken helper
- Hoist McpServer to module scope (avoid per-request reconstruction)
- Update .well-known discovery endpoints for OAuth Provider

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(mcp): resolve OAuth 2.1 flow for claude.ai MCP connector

Multiple fixes required to make the full MCP OAuth flow work with
claude.ai's implementation:

- Add RFC 8414 discovery route at /.well-known/oauth-authorization-server/api/auth
  (claude.ai appends the issuer path per spec)
- Add /auth/oauth server route to handle login/consent flow
  (generates auth codes directly, bypassing h3 cookie issues)
- Default token_endpoint_auth_method to "none" via onRequest plugin hook
  (claude.ai omits this field, causing confidential client rejection)
- Strip prompt=consent from authorize requests via onRequest hook
  (better-auth checks prompt before skipConsent, causing redirect loops)
- Add validAudiences for MCP resource URL
  (JWT aud claim contains the MCP URL, not the base URL)
- Disable CSRF check for cross-origin OAuth flows
- Log token endpoint errors for debugging
- Set skipConsent on OAuth clients via /auth/oauth route

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(mcp): harden OAuth security and enforce lock on delete

- Scope CSRF bypass to OAuth2 paths only instead of disabling globally
- Validate redirect_uri against registered client URIs (prevents code interception)
- Use pathname matching instead of fragile url.includes() for route guards
- Replace biased modulo code generation with crypto.randomBytes
- Enforce resume lock check on delete (previously silently ignored)
- Remove debug console.error logging of OAuth token response bodies
- Use Response.json() consistently for MCP 401 response

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Update dependencies, refine ignore patterns, and enhance documentation

- Updated various dependencies in package.json and pnpm-lock.yaml for improved stability and features.
- Adjusted ignore patterns in knip.json to include specific component directories.
- Enhanced documentation for the MCP server, clarifying authentication methods and configuration options.
- Made minor adjustments to VSCode settings for better code organization.

* fix(mcp): resolve OAuth client registration and stale token handling

Claude.ai sends token_endpoint_auth_method: "client_secret_post" without
a client_secret during Dynamic Client Registration, causing Better Auth to
reject it as an unauthenticated confidential client. Force to "none" for
unauthenticated registrations.

Also catch JWKS verification errors (e.g. key rotation after redeployment)
so stale Bearer tokens return 401 instead of 200 with an error body,
allowing clients to re-initiate the OAuth flow.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* reiterate on tests

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
2026-03-24 11:03:56 +01:00
Amruth Pillai e3274d7c95 New translations en-us.po (French) (#2834)
[ci skip]
2026-03-22 17:49:55 +01:00
Copilotamruthpillaicopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
a7a3d53dbd [WIP] Fix issue 2830 related to sidebar width slider (#2832)
* Initial plan

* fix: sidebar width slider not updating resume layout

Co-authored-by: amruthpillai <1134738+amruthpillai@users.noreply.github.com>
Agent-Logs-Url: https://github.com/amruthpillai/reactive-resume/sessions/55cb1943-2066-4871-8a12-0a0a4cb35fa6

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: amruthpillai <1134738+amruthpillai@users.noreply.github.com>
2026-03-21 08:24:42 +01:00
Amruth Pillai f8d776106f fixes #2826 2026-03-19 19:16:28 +01:00
Amruth Pillai e34f648455 remove input-otp component, upstream issues 2026-03-19 18:43:05 +01:00
Amruth Pillai f4e2d05f25 Merge branch 'main' of github.com:amruthpillai/reactive-resume 2026-03-19 12:55:46 +01:00
Amruth Pillai bbc17b8995 cleaner logging in dev and prod 2026-03-19 12:55:37 +01:00
Amruth Pillai a66bc13f4f Sync Translations from Crowdin (#2825)
* New translations en-us.po (Romanian)
[ci skip]

* New translations en-us.po (French)
[ci skip]

* New translations en-us.po (Spanish)
[ci skip]

* New translations en-us.po (Afrikaans)
[ci skip]

* New translations en-us.po (Arabic)
[ci skip]

* New translations en-us.po (Bulgarian)
[ci skip]

* New translations en-us.po (Catalan)
[ci skip]

* New translations en-us.po (Czech)
[ci skip]

* New translations en-us.po (Danish)
[ci skip]

* New translations en-us.po (German)
[ci skip]

* New translations en-us.po (Greek)
[ci skip]

* New translations en-us.po (Finnish)
[ci skip]

* New translations en-us.po (Hebrew)
[ci skip]

* New translations en-us.po (Hungarian)
[ci skip]

* New translations en-us.po (Italian)
[ci skip]

* New translations en-us.po (Japanese)
[ci skip]

* New translations en-us.po (Korean)
[ci skip]

* New translations en-us.po (Lithuanian)
[ci skip]

* New translations en-us.po (Dutch)
[ci skip]

* New translations en-us.po (Norwegian)
[ci skip]

* New translations en-us.po (Polish)
[ci skip]

* New translations en-us.po (Portuguese)
[ci skip]

* New translations en-us.po (Russian)
[ci skip]

* New translations en-us.po (Slovak)
[ci skip]

* New translations en-us.po (Slovenian)
[ci skip]

* New translations en-us.po (Albanian)
[ci skip]

* New translations en-us.po (Serbian (Cyrillic))
[ci skip]

* New translations en-us.po (Swedish)
[ci skip]

* New translations en-us.po (Turkish)
[ci skip]

* New translations en-us.po (Ukrainian)
[ci skip]

* New translations en-us.po (Chinese Simplified)
[ci skip]

* New translations en-us.po (Chinese Traditional)
[ci skip]

* New translations en-us.po (Vietnamese)
[ci skip]

* New translations en-us.po (Portuguese, Brazilian)
[ci skip]

* New translations en-us.po (Indonesian)
[ci skip]

* New translations en-us.po (Persian)
[ci skip]

* New translations en-us.po (Khmer)
[ci skip]

* New translations en-us.po (Tamil)
[ci skip]

* New translations en-us.po (Bengali)
[ci skip]

* New translations en-us.po (Marathi)
[ci skip]

* New translations en-us.po (Thai)
[ci skip]

* New translations en-us.po (Latvian)
[ci skip]

* New translations en-us.po (Azerbaijani)
[ci skip]

* New translations en-us.po (Hindi)
[ci skip]

* New translations en-us.po (Malay)
[ci skip]

* New translations en-us.po (Telugu)
[ci skip]

* New translations en-us.po (English, United Kingdom)
[ci skip]

* New translations en-us.po (Malayalam)
[ci skip]

* New translations en-us.po (Uzbek)
[ci skip]

* New translations en-us.po (Kannada)
[ci skip]

* New translations en-us.po (Odia)
[ci skip]

* New translations en-us.po (Amharic)
[ci skip]

* New translations en-us.po (Nepali)
[ci skip]
2026-03-19 10:23:34 +01:00
Amruth Pillai 7858efbd2b remove prepare script 2026-03-19 10:22:37 +01:00
Amruth Pillai 7da5e2c0c2 update schema.json and openapi/spec.json 2026-03-19 10:07:19 +01:00
Amruth Pillai 177c550a0c fix changelog formatting 2026-03-19 10:03:15 +01:00
Amruth Pillai 882725cabf 📦 v5.0.13 - https://docs.rxresu.me/changelog 2026-03-19 09:57:34 +01:00
Luka FagundesandAmruth Pillai 3e16586d7a feat(jobs): add job listings with AI-powered resume tailoring (#2788)
* feat: add job listings feature with JSearch API integration, resume tailoring, and per-user rate limiting

* feat(jobs): add search filters UI, filter helper functions with tests, and job_search_quota DB migration

* feat(jobs): add pagination with 30 results per page and prev/next navigation

* refactor(job-detail): Adjust sheet width and scroll area height

* feat(ai): Add resume tailoring feature and prompt

* refactor(ai): Revise tailoring prompts and schema for full skill rewrite

* feat(ai): Add reference tailoring and output sanitization

* feat(testing): Add Vitest testing framework

* fix: address PR review - atomic rate limiting, calendar-month quota, skill sync warning, gitignore routeTree.gen.ts

* feat(jobs): Add location filter to job listings

* feat(job-listings): Add DOCX document generation

* feat(job-listings): Enable search by location and on Enter key

* feat(job-listings): Split location filter into city, state, and country

* feat(jobs): Implement job search adapter and JSearch

* Update 'locale/' directory

* feat(resume): Simplify filename generation and add tests

* fix(JSearch): reduce JSearch API usage to 1 request per search to prevent quota exhaustion

* fix(JSearch): Displayed quota amounts on Job Search functionality and settings fixed to pull from RapidAPI/JSearch response

* fix(internal rate limit): Removed internal rate limit and .env.example addition, cloud based implementation handles.

* style(job-filters): Adjust layout of switch filters

* fix(typecheck): Fixed typecheck issues introduced to sync with origin

* feat(jobs): Enhance tailor dialog with apply link and tags

* feat(locale files): updated locale files with the latest build

* feat(jobs): Add job search provider and integrate testing functionality

- Introduced `createJobSearchProvider` function to instantiate a JSearchProvider.
- Enhanced job search provider with methods for searching jobs, retrieving job details, and testing connection.
- Updated `vite.config.ts` to include new testing configurations and plugins.
- Added new dependencies in `package.json` for testing and document generation.
- Removed obsolete `vitest.config.ts` file.
- Improved job search provider tests for better coverage and reliability.

* refactor: Update job search routes and remove obsolete test configurations

- Removed the test configuration from `vite.config.ts`.
- Updated localization files to reflect changes in job search routes, renaming references from `jobs` to `job-search` across multiple languages.
- Adjusted autofix workflow to run formatting without the `--fix` flag for better control over code style adjustments.

* chore: Update dependencies and improve animation performance

- Added `jsdom` as a new dependency in `package.json`.
- Updated `vite-plus` and `vitest` to the latest versions for better compatibility.
- Enhanced animation components with `willChange` styles to optimize rendering performance.
- Adjusted various UI components to improve responsiveness and visual effects.
- Removed obsolete job details functionality from the job search provider and related tests.

* chore(locales): Update localization files for job search improvements

- Modified job search related strings to remove references to "this month" for a more concise format.
- Updated file references in localization entries to reflect changes in the job search component structure.
- Added new strings for API usage, quota remaining, and job fetching error messages across multiple languages.
- Removed obsolete "Monthly Usage" string from localization files.

* chore(dependencies): Update @typescript/native-preview to version 7.0.0-dev.20260319.1

---------

Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
2026-03-19 09:48:02 +01:00
Amruth Pillai 56a1838315 update routeTree.gen.ts, ignore files 2026-03-18 22:11:37 +01:00
Amruth Pillai 2d76cc5cfb update autofix 2026-03-18 22:08:12 +01:00
Amruth Pillai 192880e416 use vite+ 2026-03-18 22:03:24 +01:00
Amruth Pillai d1dac8aeca Sync Translations from Crowdin (#2824)
* New translations en-us.po (French)
[ci skip]

* New translations en-us.po (English, United Kingdom)
[ci skip]

* New translations en-us.po (French)
[ci skip]
2026-03-18 21:37:19 +01:00
Héphaïsto f13093249d Update French translation for 'Volunteer' to 'Bénévolat' (#2823)
Bénévole means The Volunteer (The person doing the action)
Bénévolat means actions made by volunteers so it is more adequat
2026-03-18 18:46:42 +01:00
Amruth PillaiCopilot Autofix powered by AIautofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
99c602e3c7 Migrate from Biome to Oxlint/Oxfmt (#2822)
* Migrate from Biome to Oxlint/Oxfmt

* pin version of autofix

* set version of autofix

* pin version of autofix

* [autofix.ci] apply automated fixes

* better comments, test formatter

* [autofix.ci] apply automated fixes

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-18 14:59:05 +01:00
Amruth PillaiandCopilot Autofix powered by AI 040755bec9 Sync Translations from Crowdin (#2820)
* New translations en-us.po (Romanian)
[ci skip]

* New translations en-us.po (French)
[ci skip]

* New translations en-us.po (Spanish)
[ci skip]

* New translations en-us.po (Afrikaans)
[ci skip]

* New translations en-us.po (Arabic)
[ci skip]

* New translations en-us.po (Bulgarian)
[ci skip]

* New translations en-us.po (Catalan)
[ci skip]

* New translations en-us.po (Czech)
[ci skip]

* New translations en-us.po (Danish)
[ci skip]

* New translations en-us.po (German)
[ci skip]

* New translations en-us.po (Greek)
[ci skip]

* New translations en-us.po (Finnish)
[ci skip]

* New translations en-us.po (Hebrew)
[ci skip]

* New translations en-us.po (Hungarian)
[ci skip]

* New translations en-us.po (Italian)
[ci skip]

* New translations en-us.po (Japanese)
[ci skip]

* New translations en-us.po (Korean)
[ci skip]

* New translations en-us.po (Lithuanian)
[ci skip]

* New translations en-us.po (Dutch)
[ci skip]

* New translations en-us.po (Norwegian)
[ci skip]

* New translations en-us.po (Polish)
[ci skip]

* New translations en-us.po (Portuguese)
[ci skip]

* New translations en-us.po (Russian)
[ci skip]

* New translations en-us.po (Slovak)
[ci skip]

* New translations en-us.po (Slovenian)
[ci skip]

* New translations en-us.po (Albanian)
[ci skip]

* New translations en-us.po (Serbian (Cyrillic))
[ci skip]

* New translations en-us.po (Swedish)
[ci skip]

* New translations en-us.po (Turkish)
[ci skip]

* New translations en-us.po (Ukrainian)
[ci skip]

* New translations en-us.po (Chinese Simplified)
[ci skip]

* New translations en-us.po (Chinese Traditional)
[ci skip]

* New translations en-us.po (Vietnamese)
[ci skip]

* New translations en-us.po (Portuguese, Brazilian)
[ci skip]

* New translations en-us.po (Indonesian)
[ci skip]

* New translations en-us.po (Persian)
[ci skip]

* New translations en-us.po (Khmer)
[ci skip]

* New translations en-us.po (Tamil)
[ci skip]

* New translations en-us.po (Bengali)
[ci skip]

* New translations en-us.po (Marathi)
[ci skip]

* New translations en-us.po (Thai)
[ci skip]

* New translations en-us.po (Latvian)
[ci skip]

* New translations en-us.po (Azerbaijani)
[ci skip]

* New translations en-us.po (Hindi)
[ci skip]

* New translations en-us.po (Malay)
[ci skip]

* New translations en-us.po (Telugu)
[ci skip]

* New translations en-us.po (English, United Kingdom)
[ci skip]

* New translations en-us.po (Malayalam)
[ci skip]

* New translations en-us.po (Uzbek)
[ci skip]

* New translations en-us.po (Kannada)
[ci skip]

* New translations en-us.po (Odia)
[ci skip]

* New translations en-us.po (Amharic)
[ci skip]

* New translations en-us.po (Nepali)
[ci skip]

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-18 11:56:42 +01:00
Amruth Pillai 1063680774 update translations 2026-03-18 11:04:00 +01:00
Amruth Pillai 1f8644bb54 add slovenian locale 2026-03-18 10:45:04 +01:00
Amruth Pillai 1d7c4b2615 update dependencies, fix type issues in auth, add migrations for missing indexes 2026-03-18 10:42:08 +01:00
Amruth Pillai d8ffd00fa6 update translations 2026-03-18 09:54:10 +01:00
Amruth Pillai f431c0fd89 language updates 2026-03-17 23:52:09 +01:00
Amruth Pillai 571a1c1efe use rtl friendly classes 2026-03-17 23:50:52 +01:00
Amruth Pillai 547afaa18f remove dead code 2026-03-17 23:50:04 +01:00
Amruth Pillai 7789c39fe3 better changelog 2026-03-17 23:48:14 +01:00
Amruth Pillai bfb2e8bb7e update changelog 2026-03-17 23:45:33 +01:00
Amruth Pillai 189290e093 consistent imports 2026-03-17 23:42:35 +01:00
Martin Joneš 7b541f3567 Resolve chromedp hostname to IP (#2811) 2026-03-17 23:42:04 +01:00
Amruth Pillai 5cd16a62d9 v5.0.12 (#2814)
* refactor to @base-ui/react

* fix all

* fixes to accordion

* more updates

* switch to chat/completions api from openai

* update version to v5.0.12
2026-03-17 23:38:06 +01:00
Amruth PillaiandCopilot Autofix powered by AI 89beb43ea2 Sync Translations from Crowdin (#2810)
* New translations en-us.po (English, United Kingdom)
[ci skip]

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-17 16:03:32 +01:00
Amruth Pillai 55757857bd Sync Translations from Crowdin (#2809)
* New translations en-us.po (English, United Kingdom)
[ci skip]

* New translations en-us.po (English, United Kingdom)
[ci skip]
2026-03-17 15:49:57 +01:00
Amruth Pillai 584444c1fc New translations en-us.po (English, United Kingdom) (#2808)
[ci skip]
2026-03-17 12:38:50 +01:00
fb61bb4a63 fix: resolve multi-page PDF crashes and Gemini API ingestion errors (#2781)
* fix: resolve multi-page PDF crashes and Gemini API ingestion errors

* fix type errors

* refactor: address PR review feedback and prevent call stack recursion in regex payload scanner

---------

Co-authored-by: Ofir <ofir@example.com>
Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
2026-03-16 23:08:40 +01:00
Yeung Lihan ea8fd838d9 feat(import): normalize missing layout sections for reactive resume json (#2799)
* feat(import): normalize missing layout sections for reactive resume json

* chore: add comments
2026-03-16 23:08:21 +01:00
Franco CarraraandFranco Carrara a3c58b0068 add or statement so logged users can see public resumes (#2801)
Co-authored-by: Franco Carrara <franco.carrara@crombie.dev>
2026-03-13 04:20:35 +01:00
Amruth Pillai e0e8879caf New translations en-us.po (German) (#2800)
[ci skip]
2026-03-12 20:12:47 +01:00
Yao Changyi feab19913b feat: Allow sidebar button list to scroll internally on overflow (#2791) 2026-03-10 22:58:56 +01:00
Yao Changyi afbe56af0d fix: Fix unnecessary scrollbar appearing on the right sidebar (#2792) 2026-03-10 22:58:21 +01:00
Amruth PillaiandCopilot d22ab8202c Sync Translations from Crowdin (#2797)
* New translations en-us.po (French)
[ci skip]

* Update locales/fr-FR.po

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update locales/fr-FR.po

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update locales/fr-FR.po

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-10 22:57:16 +01:00
Amruth Pillai f4b6db222c update dependencies 2026-03-06 12:11:22 +01:00
Amruth Pillai 44177c0d1b Sync Translations from Crowdin (#2779)
* New translations en-us.po (German)
[ci skip]

* New translations en-us.po (Romanian)
[ci skip]

* New translations en-us.po (French)
[ci skip]

* New translations en-us.po (Spanish)
[ci skip]

* New translations en-us.po (Afrikaans)
[ci skip]

* New translations en-us.po (Arabic)
[ci skip]

* New translations en-us.po (Bulgarian)
[ci skip]

* New translations en-us.po (Catalan)
[ci skip]

* New translations en-us.po (Czech)
[ci skip]

* New translations en-us.po (Danish)
[ci skip]

* New translations en-us.po (Greek)
[ci skip]

* New translations en-us.po (Finnish)
[ci skip]

* New translations en-us.po (Hebrew)
[ci skip]

* New translations en-us.po (Hungarian)
[ci skip]

* New translations en-us.po (Italian)
[ci skip]

* New translations en-us.po (Japanese)
[ci skip]

* New translations en-us.po (Korean)
[ci skip]

* New translations en-us.po (Lithuanian)
[ci skip]

* New translations en-us.po (Dutch)
[ci skip]

* New translations en-us.po (Norwegian)
[ci skip]

* New translations en-us.po (Polish)
[ci skip]

* New translations en-us.po (Portuguese)
[ci skip]

* New translations en-us.po (Russian)
[ci skip]

* New translations en-us.po (Slovak)
[ci skip]

* New translations en-us.po (Slovenian)
[ci skip]

* New translations en-us.po (Albanian)
[ci skip]

* New translations en-us.po (Serbian (Cyrillic))
[ci skip]

* New translations en-us.po (Swedish)
[ci skip]

* New translations en-us.po (Turkish)
[ci skip]

* New translations en-us.po (Ukrainian)
[ci skip]

* New translations en-us.po (Chinese Simplified)
[ci skip]

* New translations en-us.po (Chinese Traditional)
[ci skip]

* New translations en-us.po (Vietnamese)
[ci skip]

* New translations en-us.po (Portuguese, Brazilian)
[ci skip]

* New translations en-us.po (Indonesian)
[ci skip]

* New translations en-us.po (Persian)
[ci skip]

* New translations en-us.po (Khmer)
[ci skip]

* New translations en-us.po (Tamil)
[ci skip]

* New translations en-us.po (Bengali)
[ci skip]

* New translations en-us.po (Marathi)
[ci skip]

* New translations en-us.po (Thai)
[ci skip]

* New translations en-us.po (Latvian)
[ci skip]

* New translations en-us.po (Azerbaijani)
[ci skip]

* New translations en-us.po (Hindi)
[ci skip]

* New translations en-us.po (Malay)
[ci skip]

* New translations en-us.po (Telugu)
[ci skip]

* New translations en-us.po (Malayalam)
[ci skip]

* New translations en-us.po (Uzbek)
[ci skip]

* New translations en-us.po (Kannada)
[ci skip]

* New translations en-us.po (Odia)
[ci skip]

* New translations en-us.po (Amharic)
[ci skip]

* New translations en-us.po (Nepali)
[ci skip]
2026-03-06 10:30:11 +01:00
Amruth Pillai 1d59ea3bd0 fix migration 2026-03-05 11:59:14 +01:00
TheDuke427androot 5e9ae64b2b fix(experience): remove bold from role titles and tighten role progression spacing (#2777)
Co-authored-by: root <root@reactive-resume-dev.one.one.one.one>
2026-03-05 10:13:59 +01:00
Amruth Pillai 46392d9dfc fixes #2774 2026-03-05 10:10:55 +01:00
Amruth Pillai 6734da4bee add translations for role progression, fixes #2775 2026-03-05 09:51:07 +01:00
Amruth Pillai 27edc963ce 📦 v5.0.11 - https://docs.rxresu.me/changelog 2026-03-04 23:06:23 +01:00
4b46bda4e6 feat(experience): add role progression to show career advancement within a company (#2761)
* feat(experience): add role progression support for career advancement

* fix(experience): remove indent and border from role progression items

* refactor(experience): enhance role progression UI and functionality

- Updated the ExperienceItem component to improve role display and styling.
- Refactored the CreateExperienceDialog and UpdateExperienceDialog to support role reordering using drag-and-drop.
- Added role handling in JSON and ReactiveResume importers.
- Enhanced experience schema to include roles, ensuring better data structure for career progression.
- Improved overall user experience with clearer role management in the experience section.

---------

Co-authored-by: root <root@reactive-resume-dev.one.one.one.one>
Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
2026-03-04 21:34:37 +01:00
François Martin df81d0342f fix(printer): make PDF margins match resume background color (#2770)
* fix(printer): match PDF margins to resume background color

* fix(printer): preserve vertical margins when content wraps pages
2026-03-04 20:38:02 +01:00
Amruth Pillai 4825eed22c New translations en-us.po (German) (#2763)
[ci skip]
2026-03-04 20:33:21 +01:00
Amruth Pillai b0711351f7 update dependencies 2026-03-04 20:32:56 +01:00
Amruth PillaiandCursor Agent b7e4c86f4e Project quality audit (#2758)
* Harden security, health checks, and dependency hygiene

Co-authored-by: Amruth Pillai <im.amruth@gmail.com>

* Finalize health and storage hardening adjustments

Co-authored-by: Amruth Pillai <im.amruth@gmail.com>

* remove use of [REDACTED]

* update dependencies

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-02-28 01:14:02 +01:00
Amruth PillaiandCopilot c25c068d20 Sync Translations from Crowdin (#2756)
* New translations en-us.po (Indonesian)
[ci skip]

* Update locales/id-ID.po

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-28 01:06:15 +01:00
Amruth PillaiandCursor Agent d217d5dc4f Add AGENTS.md with Cursor Cloud development setup instructions (#2757)
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-02-27 15:18:14 +01:00
Amruth Pillai 1ed80140b5 fix typescript error 2026-02-26 22:52:38 +01:00
Amruth Pillai 28ae7714c8 - fixes #2693, fixes #2716 2026-02-26 22:50:34 +01:00
Amruth Pillai 9d70bc4fd3 - fixes #2707 2026-02-26 22:44:46 +01:00
Amruth Pillai e5a51013c1 Sync Translations from Crowdin (#2746)
* New translations en-us.po (Dutch)
[ci skip]

* New translations en-us.po (Dutch)
[ci skip]
2026-02-26 22:39:26 +01:00
Amruth Pillai eeaac9a86f fixes #2727 2026-02-26 22:39:05 +01:00
Amruth Pillai 269dbc600f 📦 v5.0.10 · Changelog: https://docs.rxresu.me/changelog 2026-02-24 10:55:58 +01:00
Amruth Pillai 9b29a44429 fix(get-section-component): display titles for summary-type custom sections (#2744) 2026-02-24 10:48:44 +01:00
YeungandAmruth Pillai e9fa161148 fix(ai): prevent Edge/password-manager autofill and save prompts in AI settings (#2719)
* fix(ai): prevent password autofill in AI settings

* fix(ai): avoid password manager prompts for API key fields

* fix(ai): disable autocomplete for AI settings inputs

* update code style

---------

Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
2026-02-24 10:43:49 +01:00
Amruth Pillai 299d6d4878 fix(tailwindcss): replace start-[x]/end-[x] with inset-s-[x]/inset-e-[x] 2026-02-24 10:36:53 +01:00
Nyndow ea63ae6e72 fixing the pdf downloader in offline mode (#2743) 2026-02-24 10:14:06 +01:00
Kyle Tse 4d50cd9b71 fix: bold formatting not visible in rich text editor (#2731)
The editor_content CSS class used --page-body-font-weight-bold without
a fallback value. This variable is only injected in the resume preview
context (via use-css-variables.tsx), so in the editor UI the variable
was undefined, causing <strong> tags to render with no visible weight
change.

Adding a 'bold' fallback ensures bold text is correctly displayed in
the editor regardless of whether the CSS variable is set.

Fixes #2730
2026-02-24 10:11:35 +01:00
YeungandAmruth Pillai 8b3f3bcc35 fix(auth): avoid dropping session_token Set-Cookie on sign-in (#2718)
* fix credentials sign-in session cookie handling

* refactor(auth): rename originWith to withHostname

* refactor(auth): preserve localhost/127.0.0.1 sibling trust with LOCAL_ORIGINS

* update dependencies, update code style

---------

Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
2026-02-24 10:08:32 +01:00
Jacob f73a5117a1 fix: remove redundant resume- prefix from download filename, page spacing (#2709)
* fix: remove redundant resume- prefix from download filename

* fix: add spacing between pages in shared link view
2026-02-24 09:57:10 +01:00
YeungandAmruth Pillai 2d1e13b9b5 fix(auth): normalize autocomplete tokens for login and register (#2714)
Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
2026-02-23 21:07:50 +01:00
Amruth Pillai b6c274eeb6 - fixes #2711
- fix improper re-ordering of chips
- update dependencies, translations
2026-02-23 21:02:35 +01:00
Amruth Pillai 4b78a7c9dd Sync Translations from Crowdin (#2712)
* New translations en-us.po (French)
[ci skip]

* New translations en-us.po (Spanish)
[ci skip]
2026-02-23 19:39:05 +01:00
Amruth Pillai 87e2f2f391 add new feature flag FLAG_DISABLE_IMAGE_PROCESSING 2026-02-10 18:33:07 +01:00
Amruth Pillai f237c42093 replace discord invite link 2026-02-10 03:30:03 +01:00
Amruth Pillai a80f86e99e fix clipping of heading issue on lapras template 2026-02-10 03:23:41 +01:00
Amruth Pillai eb62ed99ce remove error-causing plugins in orpc 2026-02-10 01:43:11 +01:00
Amruth Pillai 45fc32323f Merge branch 'main' of github.com:amruthpillai/reactive-resume 2026-02-10 01:30:34 +01:00
Amruth Pillai f1d69da019 remove duplicate indexes on postgres, add index on created_at for user, resume 2026-02-10 01:30:32 +01:00
Amruth PillaiandClaude 9d131fdb99 Update CLAUDE.md with comprehensive codebase documentation (#2702)
Add details on ORPC procedure types, MCP server, all 13 resume templates,
Biome/TypeScript configuration, state management (Zundo), global providers,
Shadcn UI setup, build/deployment info, and expanded environment variables.

https://claude.ai/code/session_01NZ48uTqAAbNn3wAsSMsMGV

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-10 00:46:21 +01:00
Amruth Pillai 29ef6307e0 Sync Translations from Crowdin (#2701)
* New translations en-us.po (Romanian)
[ci skip]

* New translations en-us.po (French)
[ci skip]

* New translations en-us.po (Spanish)
[ci skip]

* New translations en-us.po (Afrikaans)
[ci skip]

* New translations en-us.po (Arabic)
[ci skip]

* New translations en-us.po (Bulgarian)
[ci skip]

* New translations en-us.po (Catalan)
[ci skip]

* New translations en-us.po (Czech)
[ci skip]

* New translations en-us.po (Danish)
[ci skip]

* New translations en-us.po (German)
[ci skip]

* New translations en-us.po (Greek)
[ci skip]

* New translations en-us.po (Finnish)
[ci skip]

* New translations en-us.po (Hebrew)
[ci skip]

* New translations en-us.po (Hungarian)
[ci skip]

* New translations en-us.po (Italian)
[ci skip]

* New translations en-us.po (Japanese)
[ci skip]

* New translations en-us.po (Korean)
[ci skip]

* New translations en-us.po (Lithuanian)
[ci skip]

* New translations en-us.po (Dutch)
[ci skip]

* New translations en-us.po (Norwegian)
[ci skip]

* New translations en-us.po (Polish)
[ci skip]

* New translations en-us.po (Portuguese)
[ci skip]

* New translations en-us.po (Russian)
[ci skip]

* New translations en-us.po (Slovak)
[ci skip]

* New translations en-us.po (Slovenian)
[ci skip]

* New translations en-us.po (Albanian)
[ci skip]

* New translations en-us.po (Serbian (Cyrillic))
[ci skip]

* New translations en-us.po (Swedish)
[ci skip]

* New translations en-us.po (Turkish)
[ci skip]

* New translations en-us.po (Ukrainian)
[ci skip]

* New translations en-us.po (Chinese Simplified)
[ci skip]

* New translations en-us.po (Chinese Traditional)
[ci skip]

* New translations en-us.po (Vietnamese)
[ci skip]

* New translations en-us.po (Portuguese, Brazilian)
[ci skip]

* New translations en-us.po (Indonesian)
[ci skip]

* New translations en-us.po (Persian)
[ci skip]

* New translations en-us.po (Khmer)
[ci skip]

* New translations en-us.po (Tamil)
[ci skip]

* New translations en-us.po (Bengali)
[ci skip]

* New translations en-us.po (Marathi)
[ci skip]

* New translations en-us.po (Thai)
[ci skip]

* New translations en-us.po (Latvian)
[ci skip]

* New translations en-us.po (Azerbaijani)
[ci skip]

* New translations en-us.po (Hindi)
[ci skip]

* New translations en-us.po (Malay)
[ci skip]

* New translations en-us.po (Telugu)
[ci skip]

* New translations en-us.po (Malayalam)
[ci skip]

* New translations en-us.po (Uzbek)
[ci skip]

* New translations en-us.po (Kannada)
[ci skip]

* New translations en-us.po (Odia)
[ci skip]

* New translations en-us.po (Amharic)
[ci skip]

* New translations en-us.po (Nepali)
[ci skip]
2026-02-10 00:18:14 +01:00
Amruth Pillai c3c771002f add computer modern fonts to the list of possible typography options 2026-02-10 00:12:04 +01:00
Amruth Pillai 554903b818 update spec.json 2026-02-09 23:20:27 +01:00
Amruth Pillai 6b71ecd7c6 Update dependencies and localization files
- Removed `@modelcontextprotocol/sdk` from ignoreDependencies in knip.json.
- Updated versions for several dependencies in package.json and pnpm-lock.yaml:
  - `@ai-sdk/google` to 3.0.23
  - `@ai-sdk/react` to 3.0.80
  - `@aws-sdk/client-s3` to 3.986.0
  - `ai` to 6.0.78
- Added new Afrikaans localization file and updated multiple existing localization files to remove passkey-related entries.
2026-02-09 23:10:45 +01:00
Amruth Pillai 833b8343ac building a better mcp server 2026-02-09 23:09:14 +01:00
Amruth Pillai 8e060890d3 remove ownership verification 2026-02-09 18:58:18 +01:00
Amruth Pillai abf0fc74e5 Add ownership verification 2026-02-09 18:57:58 +01:00
Amruth Pillai 6242c8c182 fix: temporarily disable passkeys functionality due to upstream issues (#2700)
* fix: temporarily disable passkeys functionality due to upstream issues

* remove sourcemaps from git
2026-02-09 14:52:39 +01:00
Amruth Pillai 15327d74d8 Sync Translations from Crowdin (#2697)
* New translations en-us.po (Romanian)
[ci skip]

* New translations en-us.po (French)
[ci skip]

* New translations en-us.po (Spanish)
[ci skip]

* New translations en-us.po (Afrikaans)
[ci skip]

* New translations en-us.po (Arabic)
[ci skip]

* New translations en-us.po (Bulgarian)
[ci skip]

* New translations en-us.po (Catalan)
[ci skip]

* New translations en-us.po (Czech)
[ci skip]

* New translations en-us.po (Danish)
[ci skip]

* New translations en-us.po (German)
[ci skip]

* New translations en-us.po (Greek)
[ci skip]

* New translations en-us.po (Finnish)
[ci skip]

* New translations en-us.po (Hebrew)
[ci skip]

* New translations en-us.po (Hungarian)
[ci skip]

* New translations en-us.po (Italian)
[ci skip]

* New translations en-us.po (Japanese)
[ci skip]

* New translations en-us.po (Korean)
[ci skip]

* New translations en-us.po (Lithuanian)
[ci skip]

* New translations en-us.po (Dutch)
[ci skip]

* New translations en-us.po (Norwegian)
[ci skip]

* New translations en-us.po (Polish)
[ci skip]

* New translations en-us.po (Portuguese)
[ci skip]

* New translations en-us.po (Russian)
[ci skip]

* New translations en-us.po (Slovak)
[ci skip]

* New translations en-us.po (Slovenian)
[ci skip]

* New translations en-us.po (Albanian)
[ci skip]

* New translations en-us.po (Serbian (Cyrillic))
[ci skip]

* New translations en-us.po (Swedish)
[ci skip]

* New translations en-us.po (Turkish)
[ci skip]

* New translations en-us.po (Ukrainian)
[ci skip]

* New translations en-us.po (Chinese Simplified)
[ci skip]

* New translations en-us.po (Chinese Traditional)
[ci skip]

* New translations en-us.po (Vietnamese)
[ci skip]

* New translations en-us.po (Portuguese, Brazilian)
[ci skip]

* New translations en-us.po (Indonesian)
[ci skip]

* New translations en-us.po (Persian)
[ci skip]

* New translations en-us.po (Khmer)
[ci skip]

* New translations en-us.po (Tamil)
[ci skip]

* New translations en-us.po (Bengali)
[ci skip]

* New translations en-us.po (Marathi)
[ci skip]

* New translations en-us.po (Thai)
[ci skip]

* New translations en-us.po (Latvian)
[ci skip]

* New translations en-us.po (Azerbaijani)
[ci skip]

* New translations en-us.po (Hindi)
[ci skip]

* New translations en-us.po (Malay)
[ci skip]

* New translations en-us.po (Telugu)
[ci skip]

* New translations en-us.po (Malayalam)
[ci skip]

* New translations en-us.po (Uzbek)
[ci skip]

* New translations en-us.po (Kannada)
[ci skip]

* New translations en-us.po (Odia)
[ci skip]

* New translations en-us.po (Amharic)
[ci skip]

* New translations en-us.po (Nepali)
[ci skip]
2026-02-09 14:48:54 +01:00
Amruth Pillai 90c34ca572 📦 v5.0.7 - Changelog: https://docs.rxresu.me/changelog (#2696) 2026-02-09 01:50:31 +01:00
Amruth Pillai 2b8fa9c7e8 fix: auto-select 2 font weights when picking a new font-family 2026-02-08 18:14:16 +01:00
Amruth Pillai 2f7aabcfe9 Update self-hosting guide link in README 2026-02-08 11:45:05 +01:00
Amruth Pillai ef6d054598 fixes #2612 2026-02-08 01:43:08 +01:00
Amruth Pillai 201d707aea fixes #2655 2026-02-08 01:39:58 +01:00
Amruth Pillai d8b0902ac9 fixes #2691 2026-02-08 01:39:24 +01:00
Amruth Pillai 01c75bd796 📦 v5.0.6 · Changelog: https://docs.rxresu.me/changelog 2026-02-08 01:18:41 +01:00
Amruth Pillai 0a609306a6 add error logging on server only 2026-02-08 01:16:38 +01:00
Amruth Pillai 5bdc6de862 update imports/exports 2026-02-08 00:28:28 +01:00
Amruth Pillai cc01fb9418 Feature: Implement Atomic Resume Patching API (#2692) 2026-02-08 00:16:11 +01:00
Arjun EarthpersonandAmruth Pillai 5e8e1349bc Return updated resume object from PUT /resume/{id} instead of void (#2688)
* implement PUT response

* Refactor resume router and service to use DTOs for input and output schemas

* define explicitly, the fields to be returned

---------

Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
2026-02-07 00:49:33 +01:00
Amruth Pillai ddbb71fb78 update dependencies 2026-02-06 23:07:37 +01:00
Amruth Pillai 3289453448 add .devcontainer 2026-02-06 19:15:56 +01:00
Amruth Pillai 611af8ee72 Add build badge and documentation link to README
Added a build badge for Ona and linked to documentation.
2026-02-06 19:05:40 +01:00
Amruth PillaiandCopilot 69b96a592d Sync Translations from Crowdin (#2686)
* New translations en-us.po (French)
[ci skip]

* Update locales/fr-FR.po

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-05 14:37:59 +01:00
Amruth Pillai 3151f6a9cc update dependencies 2026-02-03 22:55:22 +01:00
Amruth Pillai 726968288c fix lint errors 2026-02-02 17:37:15 +01:00
Arjun Earthperson ad086f337a update sample with cover letter and other existing fields (#2678) 2026-02-02 17:36:24 +01:00
Amruth Pillai 66d09820c3 rename service from "app" to "reactive_resume" 2026-02-02 13:27:14 +01:00
Amruth Pillai 5ec73e9234 fix no name returned in github 2026-02-02 13:03:49 +01:00
Amruth Pillai 17569d0658 add failure update to ai test status, when testing connection 2026-02-02 01:03:01 +01:00
Amruth Pillai 14ea464c0a add changelog, update README.md 2026-02-02 01:01:02 +01:00
Amruth Pillai da9a3c0b12 fixes #2663: validate AI output for test connection endpoint (#2674) 2026-02-02 00:43:31 +01:00
Abdul Alim f5a9ffb776 fix(layout): handle text overflow in sidebar/footer elements (#2672) 2026-02-01 22:17:30 +01:00
Amruth PillaiandCopilot 19181b79c8 Sync Translations from Crowdin (#2667)
* New translations en-us.po (Spanish)
[ci skip]

* New translations en-us.po (Italian)
[ci skip]

* Update locales/it-IT.po

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-31 17:10:26 +01:00
Amruth Pillai 1509678578 fix auth error: "name is required" (#2668) 2026-01-31 17:09:14 +01:00
Amruth Pillai c4d5dd4a3c New translations en-us.po (Italian) (#2665)
[ci skip]
2026-01-31 15:42:43 +01:00
Amruth Pillai ca734ca547 Sync Translations from Crowdin (#2664)
* New translations en-us.po (Romanian)
[ci skip]

* New translations en-us.po (French)
[ci skip]

* New translations en-us.po (Spanish)
[ci skip]

* New translations en-us.po (Afrikaans)
[ci skip]

* New translations en-us.po (Arabic)
[ci skip]

* New translations en-us.po (Bulgarian)
[ci skip]

* New translations en-us.po (Catalan)
[ci skip]

* New translations en-us.po (Czech)
[ci skip]

* New translations en-us.po (Danish)
[ci skip]

* New translations en-us.po (German)
[ci skip]

* New translations en-us.po (Greek)
[ci skip]

* New translations en-us.po (Finnish)
[ci skip]

* New translations en-us.po (Hebrew)
[ci skip]

* New translations en-us.po (Hungarian)
[ci skip]

* New translations en-us.po (Italian)
[ci skip]

* New translations en-us.po (Japanese)
[ci skip]

* New translations en-us.po (Korean)
[ci skip]

* New translations en-us.po (Lithuanian)
[ci skip]

* New translations en-us.po (Dutch)
[ci skip]

* New translations en-us.po (Norwegian)
[ci skip]

* New translations en-us.po (Polish)
[ci skip]

* New translations en-us.po (Portuguese)
[ci skip]

* New translations en-us.po (Russian)
[ci skip]

* New translations en-us.po (Slovak)
[ci skip]

* New translations en-us.po (Albanian)
[ci skip]

* New translations en-us.po (Serbian (Cyrillic))
[ci skip]

* New translations en-us.po (Swedish)
[ci skip]

* New translations en-us.po (Turkish)
[ci skip]

* New translations en-us.po (Ukrainian)
[ci skip]

* New translations en-us.po (Chinese Simplified)
[ci skip]

* New translations en-us.po (Chinese Traditional)
[ci skip]

* New translations en-us.po (Vietnamese)
[ci skip]

* New translations en-us.po (Portuguese, Brazilian)
[ci skip]

* New translations en-us.po (Indonesian)
[ci skip]

* New translations en-us.po (Persian)
[ci skip]

* New translations en-us.po (Khmer)
[ci skip]

* New translations en-us.po (Tamil)
[ci skip]

* New translations en-us.po (Bengali)
[ci skip]

* New translations en-us.po (Marathi)
[ci skip]

* New translations en-us.po (Thai)
[ci skip]

* New translations en-us.po (Latvian)
[ci skip]

* New translations en-us.po (Azerbaijani)
[ci skip]

* New translations en-us.po (Hindi)
[ci skip]

* New translations en-us.po (Malay)
[ci skip]

* New translations en-us.po (Telugu)
[ci skip]

* New translations en-us.po (Malayalam)
[ci skip]

* New translations en-us.po (Uzbek)
[ci skip]

* New translations en-us.po (Kannada)
[ci skip]

* New translations en-us.po (Odia)
[ci skip]

* New translations en-us.po (Amharic)
[ci skip]

* New translations en-us.po (Nepali)
[ci skip]

* New translations en-us.po (Slovenian)
[ci skip]
2026-01-31 14:14:16 +01:00
Amruth PillaiandCursor 8d347f5162 Feature: Implement Embedding Links in Titles of all Section Items (#2662)
* feat: add options.showLinkInTitle to baseItemSchema

Add itemOptionsSchema with showLinkInTitle boolean property to control
whether the website URL is rendered as a hyperlink on the title instead
of a separate link at the bottom. The field is optional for backwards
compatibility with existing resumes.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat: add hideLabelButton prop to URLInput

When hideLabelButton is true, the tag/label button is hidden from the
URL input. This is used when showLinkInTitle is enabled since the label
is not needed when the link is shown in the title.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat: create LinkedTitle component for title-as-link rendering

Add a reusable component that conditionally renders the title as a
hyperlink when showLinkInTitle is true and a website URL is provided.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat: add showLinkInTitle option to experience section

- Add Switch toggle in experience dialog for showLinkInTitle option
- Update URLInput to hide label button when showLinkInTitle is enabled
- Use LinkedTitle component in experience-item for conditional link rendering
- Hide bottom website link when showLinkInTitle is enabled

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat: add showLinkInTitle option to all section items

- Update education, projects, awards, certifications, publications,
  volunteer, references, and profiles dialogs with Switch toggle
- Add LinkedTitle component usage in all corresponding item components
- Conditionally hide bottom website link when showLinkInTitle is enabled
- Add hideLabelButton prop to URLInput when showLinkInTitle is enabled

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore: extract i18n strings for showLinkInTitle feature

Add "Show link in title" translation string to all locale catalogs.

Co-authored-by: Cursor <cursoragent@cursor.com>

* update dependencies, fix an issue with glalie template and non-clickable links, fix better-auth type error

* remove unused export

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-01-31 12:16:23 +01:00
Amruth Pillai 4d368ab6f6 Sync Translations from Crowdin (#2660)
* New translations en-us.po (Romanian)
[ci skip]

* New translations en-us.po (French)
[ci skip]

* New translations en-us.po (Spanish)
[ci skip]

* New translations en-us.po (Afrikaans)
[ci skip]

* New translations en-us.po (Arabic)
[ci skip]

* New translations en-us.po (Bulgarian)
[ci skip]

* New translations en-us.po (Catalan)
[ci skip]

* New translations en-us.po (Czech)
[ci skip]

* New translations en-us.po (Danish)
[ci skip]

* New translations en-us.po (German)
[ci skip]

* New translations en-us.po (Greek)
[ci skip]

* New translations en-us.po (Finnish)
[ci skip]

* New translations en-us.po (Hebrew)
[ci skip]

* New translations en-us.po (Hungarian)
[ci skip]

* New translations en-us.po (Italian)
[ci skip]

* New translations en-us.po (Japanese)
[ci skip]

* New translations en-us.po (Korean)
[ci skip]

* New translations en-us.po (Lithuanian)
[ci skip]

* New translations en-us.po (Dutch)
[ci skip]

* New translations en-us.po (Norwegian)
[ci skip]

* New translations en-us.po (Polish)
[ci skip]

* New translations en-us.po (Portuguese)
[ci skip]

* New translations en-us.po (Russian)
[ci skip]

* New translations en-us.po (Slovak)
[ci skip]

* New translations en-us.po (Albanian)
[ci skip]

* New translations en-us.po (Serbian (Cyrillic))
[ci skip]

* New translations en-us.po (Swedish)
[ci skip]

* New translations en-us.po (Turkish)
[ci skip]

* New translations en-us.po (Ukrainian)
[ci skip]

* New translations en-us.po (Chinese Simplified)
[ci skip]

* New translations en-us.po (Chinese Traditional)
[ci skip]

* New translations en-us.po (Vietnamese)
[ci skip]

* New translations en-us.po (Portuguese, Brazilian)
[ci skip]

* New translations en-us.po (Indonesian)
[ci skip]

* New translations en-us.po (Persian)
[ci skip]

* New translations en-us.po (Khmer)
[ci skip]

* New translations en-us.po (Tamil)
[ci skip]

* New translations en-us.po (Bengali)
[ci skip]

* New translations en-us.po (Marathi)
[ci skip]

* New translations en-us.po (Thai)
[ci skip]

* New translations en-us.po (Latvian)
[ci skip]

* New translations en-us.po (Azerbaijani)
[ci skip]

* New translations en-us.po (Hindi)
[ci skip]

* New translations en-us.po (Malay)
[ci skip]

* New translations en-us.po (Telugu)
[ci skip]

* New translations en-us.po (Malayalam)
[ci skip]

* New translations en-us.po (Uzbek)
[ci skip]

* New translations en-us.po (Kannada)
[ci skip]

* New translations en-us.po (Odia)
[ci skip]

* New translations en-us.po (Amharic)
[ci skip]

* New translations en-us.po (Nepali)
[ci skip]

* New translations en-us.po (Slovenian)
[ci skip]
2026-01-31 11:09:44 +01:00
Amruth Pillai cabacdc44b Bump version from 5.0.4 to 5.0.5 2026-01-31 03:18:10 +01:00
Amruth Pillai 71dac2021d Feature: Implement Cover Letters as a custom section type (#2659) 2026-01-31 03:17:20 +01:00
Amruth Pillai a8d8d0e340 Feature: Implement full-screen mode for rich text editor (#2658) 2026-01-31 02:29:34 +01:00
Amruth Pillai 97274d1c80 Sync Translations from Crowdin (#2656)
* New translations en-us.po (Romanian)
[ci skip]

* New translations en-us.po (French)
[ci skip]

* New translations en-us.po (Spanish)
[ci skip]

* New translations en-us.po (Afrikaans)
[ci skip]

* New translations en-us.po (Arabic)
[ci skip]

* New translations en-us.po (Bulgarian)
[ci skip]

* New translations en-us.po (Catalan)
[ci skip]

* New translations en-us.po (Czech)
[ci skip]

* New translations en-us.po (Danish)
[ci skip]

* New translations en-us.po (German)
[ci skip]

* New translations en-us.po (Greek)
[ci skip]

* New translations en-us.po (Finnish)
[ci skip]

* New translations en-us.po (Hebrew)
[ci skip]

* New translations en-us.po (Hungarian)
[ci skip]

* New translations en-us.po (Italian)
[ci skip]

* New translations en-us.po (Japanese)
[ci skip]

* New translations en-us.po (Korean)
[ci skip]

* New translations en-us.po (Lithuanian)
[ci skip]

* New translations en-us.po (Dutch)
[ci skip]

* New translations en-us.po (Norwegian)
[ci skip]

* New translations en-us.po (Polish)
[ci skip]

* New translations en-us.po (Portuguese)
[ci skip]

* New translations en-us.po (Russian)
[ci skip]

* New translations en-us.po (Slovak)
[ci skip]

* New translations en-us.po (Albanian)
[ci skip]

* New translations en-us.po (Serbian (Cyrillic))
[ci skip]

* New translations en-us.po (Swedish)
[ci skip]

* New translations en-us.po (Turkish)
[ci skip]

* New translations en-us.po (Ukrainian)
[ci skip]

* New translations en-us.po (Chinese Simplified)
[ci skip]

* New translations en-us.po (Chinese Traditional)
[ci skip]

* New translations en-us.po (Vietnamese)
[ci skip]

* New translations en-us.po (Portuguese, Brazilian)
[ci skip]

* New translations en-us.po (Indonesian)
[ci skip]

* New translations en-us.po (Persian)
[ci skip]

* New translations en-us.po (Khmer)
[ci skip]

* New translations en-us.po (Tamil)
[ci skip]

* New translations en-us.po (Bengali)
[ci skip]

* New translations en-us.po (Marathi)
[ci skip]

* New translations en-us.po (Thai)
[ci skip]

* New translations en-us.po (Latvian)
[ci skip]

* New translations en-us.po (Azerbaijani)
[ci skip]

* New translations en-us.po (Hindi)
[ci skip]

* New translations en-us.po (Malay)
[ci skip]

* New translations en-us.po (Telugu)
[ci skip]

* New translations en-us.po (Malayalam)
[ci skip]

* New translations en-us.po (Uzbek)
[ci skip]

* New translations en-us.po (Kannada)
[ci skip]

* New translations en-us.po (Odia)
[ci skip]

* New translations en-us.po (Amharic)
[ci skip]

* New translations en-us.po (Slovenian)
[ci skip]

* New translations en-us.po (Nepali)
[ci skip]

* New translations en-us.po (Romanian)
[ci skip]

* New translations en-us.po (French)
[ci skip]

* New translations en-us.po (Spanish)
[ci skip]

* New translations en-us.po (Afrikaans)
[ci skip]

* New translations en-us.po (Arabic)
[ci skip]

* New translations en-us.po (Bulgarian)
[ci skip]

* New translations en-us.po (Catalan)
[ci skip]

* New translations en-us.po (Czech)
[ci skip]

* New translations en-us.po (Danish)
[ci skip]

* New translations en-us.po (German)
[ci skip]

* New translations en-us.po (Greek)
[ci skip]

* New translations en-us.po (Finnish)
[ci skip]

* New translations en-us.po (Hebrew)
[ci skip]

* New translations en-us.po (Hungarian)
[ci skip]

* New translations en-us.po (Italian)
[ci skip]

* New translations en-us.po (Japanese)
[ci skip]

* New translations en-us.po (Korean)
[ci skip]

* New translations en-us.po (Lithuanian)
[ci skip]

* New translations en-us.po (Dutch)
[ci skip]

* New translations en-us.po (Norwegian)
[ci skip]

* New translations en-us.po (Polish)
[ci skip]

* New translations en-us.po (Portuguese)
[ci skip]

* New translations en-us.po (Russian)
[ci skip]

* New translations en-us.po (Slovak)
[ci skip]

* New translations en-us.po (Albanian)
[ci skip]

* New translations en-us.po (Serbian (Cyrillic))
[ci skip]

* New translations en-us.po (Swedish)
[ci skip]

* New translations en-us.po (Turkish)
[ci skip]

* New translations en-us.po (Ukrainian)
[ci skip]

* New translations en-us.po (Chinese Simplified)
[ci skip]

* New translations en-us.po (Chinese Traditional)
[ci skip]

* New translations en-us.po (Vietnamese)
[ci skip]

* New translations en-us.po (Portuguese, Brazilian)
[ci skip]

* New translations en-us.po (Indonesian)
[ci skip]

* New translations en-us.po (Persian)
[ci skip]

* New translations en-us.po (Khmer)
[ci skip]

* New translations en-us.po (Tamil)
[ci skip]

* New translations en-us.po (Bengali)
[ci skip]

* New translations en-us.po (Marathi)
[ci skip]

* New translations en-us.po (Thai)
[ci skip]

* New translations en-us.po (Latvian)
[ci skip]

* New translations en-us.po (Azerbaijani)
[ci skip]

* New translations en-us.po (Hindi)
[ci skip]

* New translations en-us.po (Malay)
[ci skip]

* New translations en-us.po (Telugu)
[ci skip]

* New translations en-us.po (Malayalam)
[ci skip]

* New translations en-us.po (Uzbek)
[ci skip]

* New translations en-us.po (Kannada)
[ci skip]

* New translations en-us.po (Odia)
[ci skip]

* New translations en-us.po (Amharic)
[ci skip]

* New translations en-us.po (Nepali)
[ci skip]

* New translations en-us.po (Slovenian)
[ci skip]
2026-01-31 02:27:24 +01:00
Amruth Pillai aa12fcbd36 Feature: Implement a new custom section type: summary (#2657)
* feat: add summaryItemSchema for custom summary section type

* feat: add CreateSummaryItemDialog and UpdateSummaryItemDialog

* feat: register summary item dialog types in store

* feat: route summary item dialogs in manager

* feat: add SummaryItem render component

* feat: handle summary type in renderItemByType and hide title for summary sections

* feat: handle summary type in sidebar helpers

* feat: add summary to custom section type options

* fix: update type definitions to support CustomSectionType for summary sections

* chore: extract new i18n strings for summary section

* style: apply biome formatting fixes

* chore: remove TODO.md file containing outdated feature specifications
2026-01-31 01:53:27 +01:00
Amruth Pillai 3d1c2d1fb6 Feature: Create a new useFormBlocker hook to block the user from closing a dialog or navigating away from a page if the form is dirty (#2654)
* feat: add useFormBlocker hook for dialog dirty state protection

* feat: add useFormBlocker hook for dialog dirty state protection

- Create useFormBlocker hook that blocks dialog closing when forms have unsaved changes
- Use onPointerDownOutside and onEscapeKeyDown to intercept close attempts
- Show confirmation dialog with Leave/Stay options using useConfirm
- Integrate with CreateResumeDialog, UpdateResumeDialog, and DuplicateResumeDialog
- All strings are translatable via Lingui

* Feature: Create a new `useFormBlocker` hook to block the user from closing a dialog or navigating away from a page if the form is dirty.
2026-01-31 01:13:38 +01:00
Amruth Pillai f6fcbdcad0 Sync Translations from Crowdin (#2640)
* New translations en-us.po (Ukrainian)
[ci skip]

* New translations en-us.po (Romanian)
[ci skip]

* New translations en-us.po (French)
[ci skip]

* New translations en-us.po (Spanish)
[ci skip]

* New translations en-us.po (Afrikaans)
[ci skip]

* New translations en-us.po (Arabic)
[ci skip]

* New translations en-us.po (Bulgarian)
[ci skip]

* New translations en-us.po (Catalan)
[ci skip]

* New translations en-us.po (Czech)
[ci skip]

* New translations en-us.po (Danish)
[ci skip]

* New translations en-us.po (German)
[ci skip]

* New translations en-us.po (Greek)
[ci skip]

* New translations en-us.po (Finnish)
[ci skip]

* New translations en-us.po (Hebrew)
[ci skip]

* New translations en-us.po (Hungarian)
[ci skip]

* New translations en-us.po (Italian)
[ci skip]

* New translations en-us.po (Japanese)
[ci skip]

* New translations en-us.po (Korean)
[ci skip]

* New translations en-us.po (Lithuanian)
[ci skip]

* New translations en-us.po (Dutch)
[ci skip]

* New translations en-us.po (Norwegian)
[ci skip]

* New translations en-us.po (Polish)
[ci skip]

* New translations en-us.po (Portuguese)
[ci skip]

* New translations en-us.po (Russian)
[ci skip]

* New translations en-us.po (Slovak)
[ci skip]

* New translations en-us.po (Albanian)
[ci skip]

* New translations en-us.po (Serbian (Cyrillic))
[ci skip]

* New translations en-us.po (Swedish)
[ci skip]

* New translations en-us.po (Turkish)
[ci skip]

* New translations en-us.po (Chinese Simplified)
[ci skip]

* New translations en-us.po (Chinese Traditional)
[ci skip]

* New translations en-us.po (Vietnamese)
[ci skip]

* New translations en-us.po (Portuguese, Brazilian)
[ci skip]

* New translations en-us.po (Indonesian)
[ci skip]

* New translations en-us.po (Persian)
[ci skip]

* New translations en-us.po (Khmer)
[ci skip]

* New translations en-us.po (Tamil)
[ci skip]

* New translations en-us.po (Bengali)
[ci skip]

* New translations en-us.po (Marathi)
[ci skip]

* New translations en-us.po (Thai)
[ci skip]

* New translations en-us.po (Latvian)
[ci skip]

* New translations en-us.po (Azerbaijani)
[ci skip]

* New translations en-us.po (Hindi)
[ci skip]

* New translations en-us.po (Malay)
[ci skip]

* New translations en-us.po (Telugu)
[ci skip]

* New translations en-us.po (Malayalam)
[ci skip]

* New translations en-us.po (Uzbek)
[ci skip]

* New translations en-us.po (Kannada)
[ci skip]

* New translations en-us.po (Odia)
[ci skip]

* New translations en-us.po (Amharic)
[ci skip]

* New translations en-us.po (Nepali)
[ci skip]

* New translations en-us.po (Slovenian)
[ci skip]
2026-01-29 14:20:16 +01:00
Amruth PillaiandCopilot 30e6ba0809 Sync Translations from Crowdin (#2636)
* New translations en-us.po (Ukrainian)
[ci skip]

* New translations en-us.po (Slovenian)
[ci skip]

* Update locales/uk-UA.po

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-28 22:11:26 +01:00
Tom 3cb0fc9f30 fix: remove empty keywords spacing in interests items (#2631)
Only render keywords fields when they contain values
2026-01-28 21:06:12 +01:00
Amruth PillaiandCopilot 5f578c3327 Sync Translations from Crowdin (#2634)
* New translations en-us.po (Ukrainian)
[ci skip]

* Update locales/uk-UA.po

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-28 21:05:43 +01:00
Amruth Pillai e56402058a Sync Translations from Crowdin (#2630)
* New translations en-us.po (Romanian)
[ci skip]

* New translations en-us.po (French)
[ci skip]

* New translations en-us.po (Spanish)
[ci skip]

* New translations en-us.po (Afrikaans)
[ci skip]

* New translations en-us.po (Arabic)
[ci skip]

* New translations en-us.po (Bulgarian)
[ci skip]

* New translations en-us.po (Catalan)
[ci skip]

* New translations en-us.po (Czech)
[ci skip]

* New translations en-us.po (Danish)
[ci skip]

* New translations en-us.po (German)
[ci skip]

* New translations en-us.po (Greek)
[ci skip]

* New translations en-us.po (Finnish)
[ci skip]

* New translations en-us.po (Hebrew)
[ci skip]

* New translations en-us.po (Hungarian)
[ci skip]

* New translations en-us.po (Italian)
[ci skip]

* New translations en-us.po (Japanese)
[ci skip]

* New translations en-us.po (Korean)
[ci skip]

* New translations en-us.po (Lithuanian)
[ci skip]

* New translations en-us.po (Dutch)
[ci skip]

* New translations en-us.po (Norwegian)
[ci skip]

* New translations en-us.po (Polish)
[ci skip]

* New translations en-us.po (Portuguese)
[ci skip]

* New translations en-us.po (Russian)
[ci skip]

* New translations en-us.po (Slovak)
[ci skip]

* New translations en-us.po (Albanian)
[ci skip]

* New translations en-us.po (Serbian (Cyrillic))
[ci skip]

* New translations en-us.po (Swedish)
[ci skip]

* New translations en-us.po (Turkish)
[ci skip]

* New translations en-us.po (Ukrainian)
[ci skip]

* New translations en-us.po (Chinese Simplified)
[ci skip]

* New translations en-us.po (Chinese Traditional)
[ci skip]

* New translations en-us.po (Vietnamese)
[ci skip]

* New translations en-us.po (Portuguese, Brazilian)
[ci skip]

* New translations en-us.po (Indonesian)
[ci skip]

* New translations en-us.po (Persian)
[ci skip]

* New translations en-us.po (Khmer)
[ci skip]

* New translations en-us.po (Tamil)
[ci skip]

* New translations en-us.po (Bengali)
[ci skip]

* New translations en-us.po (Marathi)
[ci skip]

* New translations en-us.po (Thai)
[ci skip]

* New translations en-us.po (Latvian)
[ci skip]

* New translations en-us.po (Azerbaijani)
[ci skip]

* New translations en-us.po (Hindi)
[ci skip]

* New translations en-us.po (Malay)
[ci skip]

* New translations en-us.po (Telugu)
[ci skip]

* New translations en-us.po (Malayalam)
[ci skip]

* New translations en-us.po (Uzbek)
[ci skip]

* New translations en-us.po (Kannada)
[ci skip]

* New translations en-us.po (Odia)
[ci skip]

* New translations en-us.po (Amharic)
[ci skip]

* New translations en-us.po (Nepali)
[ci skip]
2026-01-28 12:06:16 +01:00
Amruth Pillai 19ae21e797 📦 v5.0.4 - Changelog: https://docs.rxresu.me/changelog 2026-01-28 11:45:58 +01:00
Amruth Pillai b89a31d4ae update translation sources 2026-01-28 11:42:38 +01:00
Fynn FischbachandAmruth Pillai 07e06a6ed4 feat(builder): arrange sidebar builder depending on section type in template #2564 (#2603)
* feat: introduce template sidebar type

* feat: align sidebar builder depending on template alignment

* revert: keep full width toggle for now until we merge SortableLayoutItem on template change.

* fix: typo

* use string literals instead of enum, modify logic for layout display slightly

---------

Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
2026-01-28 11:37:44 +01:00
Amruth Pillai 89102d6612 Bring back Undo/Redo functionality in the resume builder (#2629) 2026-01-28 11:20:11 +01:00
Amruth PillaiandCopilot feda173b3b Sync Translations from Crowdin (#2628)
* New translations en-us.po (Swedish)
[ci skip]

* Update locales/sv-SE.po

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update locales/sv-SE.po

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update locales/sv-SE.po

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-28 10:44:44 +01:00
Amruth PillaiandCopilot 02f812ce96 Sync Translations from Crowdin (#2625)
* New translations en-us.po (French)
[ci skip]

* Update locales/fr-FR.po

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-27 20:59:56 +01:00
Fynn Fischbach 7250bed701 Fix/#2607 remove empty proficiency spacing (#2626)
* fix: remove empty proficiency and keywords spacing in skills items

Only render proficiency and keywords fields when they contain values,
preventing empty inline-block elements from reserving unwanted space
in templates like Pikachu.

Fixes #2607

* style: biome check --write
2026-01-27 20:59:40 +01:00
Amruth PillaiandCopilot 2b11b1a234 Sync Translations from Crowdin (#2623)
* New translations en-us.po (French)
[ci skip]

* Update locales/fr-FR.po

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-27 17:10:57 +01:00
Amruth PillaiandCursor Agent 18e8aadf18 Fix GitHub OAuth login for migrated users (#2620)
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-01-27 13:03:09 +01:00
G 4840a292df Fixed trailing Git Merge text (#2619)
updated French locale to remove failed gitmerge chars
2026-01-27 12:39:53 +01:00
Amruth Pillai 76ca1995ca New translations en-us.po (Portuguese, Brazilian) (#2616)
[ci skip]
2026-01-27 01:45:52 +01:00
Amruth Pillai 0f333b1f24 another attempt to improve printer service communication 2026-01-26 20:37:27 +01:00
Amruth Pillai 1f8eaa423f Merge branch 'main' of github.com:amruthpillai/reactive-resume 2026-01-26 19:36:52 +01:00
Amruth Pillai 4e4d3670a2 desperate attempts to reduce chrome usage 2026-01-26 19:36:50 +01:00
Amruth Pillai cd9a6a80e3 New translations en-us.po (Persian) (#2602)
[ci skip]
2026-01-26 14:57:51 +01:00
Amruth Pillai 429618d5f8 fixes #2542, fixes #2573 2026-01-26 12:38:37 +01:00
Amruth Pillai 1e814a77cc Refactor Pikachu template layout to conditionally render header and page picture based on isFirstPage property 2026-01-26 11:19:08 +01:00
Amruth Pillai 41b0c57725 fix pikachu template to respect the fullWidth property 2026-01-26 11:16:05 +01:00
Amruth Pillai 470822f4a9 Update package dependencies and fix README link for self-hosting guide 2026-01-26 11:11:45 +01:00
Amruth Pillai 946bf9ec38 - fixes #2598 2026-01-26 06:38:59 +01:00
Amruth Pillai 49585940f7 Sync Translations from Crowdin (#2597)
* New translations en-us.po (Romanian)
[ci skip]

* New translations en-us.po (French)
[ci skip]

* New translations en-us.po (Spanish)
[ci skip]

* New translations en-us.po (Afrikaans)
[ci skip]

* New translations en-us.po (Arabic)
[ci skip]

* New translations en-us.po (Bulgarian)
[ci skip]

* New translations en-us.po (Catalan)
[ci skip]

* New translations en-us.po (Czech)
[ci skip]

* New translations en-us.po (Danish)
[ci skip]

* New translations en-us.po (German)
[ci skip]

* New translations en-us.po (Greek)
[ci skip]

* New translations en-us.po (Finnish)
[ci skip]

* New translations en-us.po (Hebrew)
[ci skip]

* New translations en-us.po (Hungarian)
[ci skip]

* New translations en-us.po (Italian)
[ci skip]

* New translations en-us.po (Japanese)
[ci skip]

* New translations en-us.po (Korean)
[ci skip]

* New translations en-us.po (Lithuanian)
[ci skip]

* New translations en-us.po (Dutch)
[ci skip]

* New translations en-us.po (Norwegian)
[ci skip]

* New translations en-us.po (Polish)
[ci skip]

* New translations en-us.po (Portuguese)
[ci skip]

* New translations en-us.po (Russian)
[ci skip]

* New translations en-us.po (Slovak)
[ci skip]

* New translations en-us.po (Albanian)
[ci skip]

* New translations en-us.po (Serbian (Cyrillic))
[ci skip]

* New translations en-us.po (Swedish)
[ci skip]

* New translations en-us.po (Turkish)
[ci skip]

* New translations en-us.po (Ukrainian)
[ci skip]

* New translations en-us.po (Chinese Simplified)
[ci skip]

* New translations en-us.po (Chinese Traditional)
[ci skip]

* New translations en-us.po (Vietnamese)
[ci skip]

* New translations en-us.po (Portuguese, Brazilian)
[ci skip]

* New translations en-us.po (Indonesian)
[ci skip]

* New translations en-us.po (Persian)
[ci skip]

* New translations en-us.po (Khmer)
[ci skip]

* New translations en-us.po (Tamil)
[ci skip]

* New translations en-us.po (Bengali)
[ci skip]

* New translations en-us.po (Marathi)
[ci skip]

* New translations en-us.po (Thai)
[ci skip]

* New translations en-us.po (Latvian)
[ci skip]

* New translations en-us.po (Azerbaijani)
[ci skip]

* New translations en-us.po (Hindi)
[ci skip]

* New translations en-us.po (Malay)
[ci skip]

* New translations en-us.po (Telugu)
[ci skip]

* New translations en-us.po (Malayalam)
[ci skip]

* New translations en-us.po (Uzbek)
[ci skip]

* New translations en-us.po (Kannada)
[ci skip]

* New translations en-us.po (Odia)
[ci skip]

* New translations en-us.po (Amharic)
[ci skip]

* New translations en-us.po (Nepali)
[ci skip]
2026-01-26 00:11:24 +01:00
Amruth Pillai 9257d62216 update links to pdf examples 2026-01-25 23:44:45 +01:00
Amruth Pillai 3a9d2e7652 New Feature: Free-Form Page Formats for PDFs (#2595) 2026-01-25 23:39:16 +01:00
Amruth Pillai 4b2e3c0ded Sync Translations from Crowdin (#2594)
* New translations en-us.po (Afrikaans)
[ci skip]

* New translations en-us.po (Tamil)
[ci skip]
2026-01-25 22:36:03 +01:00
Amruth Pillai 6978df4c3c fix a lot of the templates 2026-01-25 22:28:06 +01:00
Amruth Pillai 1f13189ec3 Merge branch 'main' of github.com:amruthpillai/reactive-resume 2026-01-25 22:06:15 +01:00
Amruth Pillai c08e8081fe remove dead code, update screenshots, add PWA support 2026-01-25 22:06:12 +01:00
Amruth Pillai d8c9b5a936 New translations en-us.po (Afrikaans) (#2592)
[ci skip]
2026-01-25 20:37:15 +01:00
Amruth Pillai fa823e1296 Merge branch 'main' of github.com:amruthpillai/reactive-resume 2026-01-25 19:41:43 +01:00
Amruth Pillai de89ab957b 📦 v5.0.3 - Changelog: https://docs.rxresu.me/changelog 2026-01-25 19:41:32 +01:00
Amruth Pillai 96920495d0 Sync Translations from Crowdin (#2590)
* New translations en-us.po (Romanian)
[ci skip]

* New translations en-us.po (French)
[ci skip]

* New translations en-us.po (Spanish)
[ci skip]

* New translations en-us.po (Arabic)
[ci skip]

* New translations en-us.po (Bulgarian)
[ci skip]

* New translations en-us.po (Catalan)
[ci skip]

* New translations en-us.po (Czech)
[ci skip]

* New translations en-us.po (Danish)
[ci skip]

* New translations en-us.po (German)
[ci skip]

* New translations en-us.po (Greek)
[ci skip]

* New translations en-us.po (Finnish)
[ci skip]

* New translations en-us.po (Hebrew)
[ci skip]

* New translations en-us.po (Hungarian)
[ci skip]

* New translations en-us.po (Italian)
[ci skip]

* New translations en-us.po (Japanese)
[ci skip]

* New translations en-us.po (Korean)
[ci skip]

* New translations en-us.po (Lithuanian)
[ci skip]

* New translations en-us.po (Dutch)
[ci skip]

* New translations en-us.po (Norwegian)
[ci skip]

* New translations en-us.po (Polish)
[ci skip]

* New translations en-us.po (Portuguese)
[ci skip]

* New translations en-us.po (Russian)
[ci skip]

* New translations en-us.po (Slovak)
[ci skip]

* New translations en-us.po (Albanian)
[ci skip]

* New translations en-us.po (Serbian (Cyrillic))
[ci skip]

* New translations en-us.po (Swedish)
[ci skip]

* New translations en-us.po (Turkish)
[ci skip]

* New translations en-us.po (Ukrainian)
[ci skip]

* New translations en-us.po (Chinese Simplified)
[ci skip]

* New translations en-us.po (Chinese Traditional)
[ci skip]

* New translations en-us.po (Vietnamese)
[ci skip]

* New translations en-us.po (Portuguese, Brazilian)
[ci skip]

* New translations en-us.po (Indonesian)
[ci skip]

* New translations en-us.po (Persian)
[ci skip]

* New translations en-us.po (Khmer)
[ci skip]

* New translations en-us.po (Tamil)
[ci skip]

* New translations en-us.po (Bengali)
[ci skip]

* New translations en-us.po (Marathi)
[ci skip]

* New translations en-us.po (Thai)
[ci skip]

* New translations en-us.po (Latvian)
[ci skip]

* New translations en-us.po (Azerbaijani)
[ci skip]

* New translations en-us.po (Hindi)
[ci skip]

* New translations en-us.po (Malay)
[ci skip]

* New translations en-us.po (Telugu)
[ci skip]

* New translations en-us.po (Malayalam)
[ci skip]

* New translations en-us.po (Uzbek)
[ci skip]

* New translations en-us.po (Kannada)
[ci skip]

* New translations en-us.po (Odia)
[ci skip]

* New translations en-us.po (Amharic)
[ci skip]

* New translations en-us.po (Nepali)
[ci skip]
2026-01-25 19:35:38 +01:00
Amruth Pillai 92044de6b8 more chip input fixes 2026-01-25 19:35:20 +01:00
Amruth Pillai 8e8ef300ec fix #2585 - allow for re-ordering keywords, editing keywords in-place and deleting keywords (#2591) 2026-01-25 19:23:15 +01:00
Amruth Pillai 982220430e fix #2587 - allow for customizable endpoints for all AI providers (#2589) 2026-01-25 18:29:36 +01:00
IbrahimandAmruth Pillai 3f55c24e36 Improved the Right to Left Direction implementation for relevant languages. (#2583)
* Added support for right to left direction of bullet points in the page render and text editor

* Added right to left direction support by adding a toggle button in the layout and enhancing the css element of the page render to support it without affecting the default left to right direction

* Moved the Right to Life Direction Toggle feature to be integrated with the Languages section using locale

* change all occurrances of pl, pr, ml, mr, left, right to start/end equivalents

---------

Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
2026-01-25 18:18:20 +01:00
Amruth Pillai 0c65612368 - an assortment of bugfixes and improvements
- remove line numbers from generated locale files
- add .gitattributes to not display diffs of .po files
2026-01-25 17:49:23 +01:00
Amruth Pillai 3071b7dd37 Sync Translations from Crowdin (#2582)
* New translations en-us.po (Romanian)
[ci skip]

* New translations en-us.po (French)
[ci skip]

* New translations en-us.po (Spanish)
[ci skip]

* New translations en-us.po (Afrikaans)
[ci skip]

* New translations en-us.po (Arabic)
[ci skip]

* New translations en-us.po (Bulgarian)
[ci skip]

* New translations en-us.po (Catalan)
[ci skip]

* New translations en-us.po (Czech)
[ci skip]

* New translations en-us.po (Danish)
[ci skip]

* New translations en-us.po (German)
[ci skip]

* New translations en-us.po (Greek)
[ci skip]

* New translations en-us.po (Finnish)
[ci skip]

* New translations en-us.po (Hebrew)
[ci skip]

* New translations en-us.po (Hungarian)
[ci skip]

* New translations en-us.po (Italian)
[ci skip]

* New translations en-us.po (Japanese)
[ci skip]

* New translations en-us.po (Korean)
[ci skip]

* New translations en-us.po (Lithuanian)
[ci skip]

* New translations en-us.po (Dutch)
[ci skip]

* New translations en-us.po (Norwegian)
[ci skip]

* New translations en-us.po (Polish)
[ci skip]

* New translations en-us.po (Portuguese)
[ci skip]

* New translations en-us.po (Russian)
[ci skip]

* New translations en-us.po (Slovak)
[ci skip]

* New translations en-us.po (Albanian)
[ci skip]

* New translations en-us.po (Serbian (Cyrillic))
[ci skip]

* New translations en-us.po (Swedish)
[ci skip]

* New translations en-us.po (Turkish)
[ci skip]

* New translations en-us.po (Ukrainian)
[ci skip]

* New translations en-us.po (Chinese Simplified)
[ci skip]

* New translations en-us.po (Chinese Traditional)
[ci skip]

* New translations en-us.po (Vietnamese)
[ci skip]

* New translations en-us.po (Portuguese, Brazilian)
[ci skip]

* New translations en-us.po (Indonesian)
[ci skip]

* New translations en-us.po (Persian)
[ci skip]

* New translations en-us.po (Khmer)
[ci skip]

* New translations en-us.po (Tamil)
[ci skip]

* New translations en-us.po (Bengali)
[ci skip]

* New translations en-us.po (Marathi)
[ci skip]

* New translations en-us.po (Thai)
[ci skip]

* New translations en-us.po (Latvian)
[ci skip]

* New translations en-us.po (Azerbaijani)
[ci skip]

* New translations en-us.po (Hindi)
[ci skip]

* New translations en-us.po (Malay)
[ci skip]

* New translations en-us.po (Telugu)
[ci skip]

* New translations en-us.po (Malayalam)
[ci skip]

* New translations en-us.po (Uzbek)
[ci skip]

* New translations en-us.po (Kannada)
[ci skip]

* New translations en-us.po (Odia)
[ci skip]

* New translations en-us.po (Amharic)
[ci skip]

* New translations en-us.po (Nepali)
[ci skip]
2026-01-25 08:43:44 +01:00
Amruth Pillai 5daac24e73 Update package dependencies and synchronize translations
- Updated @tanstack/react-router and related packages to version 1.157.5 in package.json.
- Adjusted pnpm-lock.yaml to reflect the updated package versions.
- Modified localization files to update the "Last Updated" message reference and the resume deletion success message across multiple languages.
- Removed obsolete components related to resume cards and creation from the dashboard.
2026-01-25 03:15:19 +01:00
Amruth Pillai bcd2828305 update intro timelapse video landing page 2026-01-25 01:05:05 +01:00
Amruth Pillai b5f4e9af46 increase screenshot ttl to outdated resumes 2026-01-25 00:27:58 +01:00
Amruth Pillai 92f6a6a16b Sync Translations from Crowdin (#2576)
* New translations en-us.po (Romanian)
[ci skip]

* New translations en-us.po (French)
[ci skip]

* New translations en-us.po (Spanish)
[ci skip]

* New translations en-us.po (Afrikaans)
[ci skip]

* New translations en-us.po (Arabic)
[ci skip]

* New translations en-us.po (Bulgarian)
[ci skip]

* New translations en-us.po (Catalan)
[ci skip]

* New translations en-us.po (Czech)
[ci skip]

* New translations en-us.po (Danish)
[ci skip]

* New translations en-us.po (German)
[ci skip]

* New translations en-us.po (Greek)
[ci skip]

* New translations en-us.po (Finnish)
[ci skip]

* New translations en-us.po (Hebrew)
[ci skip]

* New translations en-us.po (Hungarian)
[ci skip]

* New translations en-us.po (Italian)
[ci skip]

* New translations en-us.po (Japanese)
[ci skip]

* New translations en-us.po (Korean)
[ci skip]

* New translations en-us.po (Lithuanian)
[ci skip]

* New translations en-us.po (Dutch)
[ci skip]

* New translations en-us.po (Norwegian)
[ci skip]

* New translations en-us.po (Polish)
[ci skip]

* New translations en-us.po (Portuguese)
[ci skip]

* New translations en-us.po (Russian)
[ci skip]

* New translations en-us.po (Slovak)
[ci skip]

* New translations en-us.po (Albanian)
[ci skip]

* New translations en-us.po (Serbian (Cyrillic))
[ci skip]

* New translations en-us.po (Swedish)
[ci skip]

* New translations en-us.po (Turkish)
[ci skip]

* New translations en-us.po (Ukrainian)
[ci skip]

* New translations en-us.po (Chinese Simplified)
[ci skip]

* New translations en-us.po (Chinese Traditional)
[ci skip]

* New translations en-us.po (Vietnamese)
[ci skip]

* New translations en-us.po (Portuguese, Brazilian)
[ci skip]

* New translations en-us.po (Indonesian)
[ci skip]

* New translations en-us.po (Persian)
[ci skip]

* New translations en-us.po (Khmer)
[ci skip]

* New translations en-us.po (Tamil)
[ci skip]

* New translations en-us.po (Bengali)
[ci skip]

* New translations en-us.po (Marathi)
[ci skip]

* New translations en-us.po (Thai)
[ci skip]

* New translations en-us.po (Latvian)
[ci skip]

* New translations en-us.po (Azerbaijani)
[ci skip]

* New translations en-us.po (Hindi)
[ci skip]

* New translations en-us.po (Malay)
[ci skip]

* New translations en-us.po (Telugu)
[ci skip]

* New translations en-us.po (Malayalam)
[ci skip]

* New translations en-us.po (Uzbek)
[ci skip]

* New translations en-us.po (Kannada)
[ci skip]

* New translations en-us.po (Odia)
[ci skip]

* New translations en-us.po (Amharic)
[ci skip]

* New translations en-us.po (Nepali)
[ci skip]

* New translations en-us.po (Afrikaans)
[ci skip]

* New translations en-us.po (Arabic)
[ci skip]

* New translations en-us.po (German)
[ci skip]

* New translations en-us.po (Albanian)
[ci skip]

* New translations en-us.po (Amharic)
[ci skip]

* update translations

* New translations en-us.po (Romanian)
[ci skip]

* New translations en-us.po (French)
[ci skip]

* New translations en-us.po (Spanish)
[ci skip]

* New translations en-us.po (Afrikaans)
[ci skip]

* New translations en-us.po (Arabic)
[ci skip]

* New translations en-us.po (Bulgarian)
[ci skip]

* New translations en-us.po (Catalan)
[ci skip]

* New translations en-us.po (Czech)
[ci skip]

* New translations en-us.po (Danish)
[ci skip]

* New translations en-us.po (German)
[ci skip]

* New translations en-us.po (Greek)
[ci skip]

* New translations en-us.po (Finnish)
[ci skip]

* New translations en-us.po (Hebrew)
[ci skip]

* New translations en-us.po (Hungarian)
[ci skip]

* New translations en-us.po (Italian)
[ci skip]

* New translations en-us.po (Japanese)
[ci skip]

* New translations en-us.po (Korean)
[ci skip]

* New translations en-us.po (Lithuanian)
[ci skip]

* New translations en-us.po (Dutch)
[ci skip]

* New translations en-us.po (Norwegian)
[ci skip]

* New translations en-us.po (Polish)
[ci skip]

* New translations en-us.po (Portuguese)
[ci skip]

* New translations en-us.po (Russian)
[ci skip]

* New translations en-us.po (Slovak)
[ci skip]

* New translations en-us.po (Albanian)
[ci skip]

* New translations en-us.po (Serbian (Cyrillic))
[ci skip]

* New translations en-us.po (Swedish)
[ci skip]

* New translations en-us.po (Turkish)
[ci skip]

* New translations en-us.po (Ukrainian)
[ci skip]

* New translations en-us.po (Chinese Simplified)
[ci skip]

* New translations en-us.po (Chinese Traditional)
[ci skip]

* New translations en-us.po (Vietnamese)
[ci skip]

* New translations en-us.po (Portuguese, Brazilian)
[ci skip]

* New translations en-us.po (Indonesian)
[ci skip]

* New translations en-us.po (Persian)
[ci skip]

* New translations en-us.po (Khmer)
[ci skip]

* New translations en-us.po (Tamil)
[ci skip]

* New translations en-us.po (Bengali)
[ci skip]

* New translations en-us.po (Marathi)
[ci skip]

* New translations en-us.po (Thai)
[ci skip]

* New translations en-us.po (Latvian)
[ci skip]

* New translations en-us.po (Azerbaijani)
[ci skip]

* New translations en-us.po (Hindi)
[ci skip]

* New translations en-us.po (Malay)
[ci skip]

* New translations en-us.po (Telugu)
[ci skip]

* New translations en-us.po (Malayalam)
[ci skip]

* New translations en-us.po (Uzbek)
[ci skip]

* New translations en-us.po (Kannada)
[ci skip]

* New translations en-us.po (Odia)
[ci skip]

* New translations en-us.po (Amharic)
[ci skip]

* New translations en-us.po (Nepali)
[ci skip]
2026-01-24 23:43:08 +01:00
Amruth Pillai 019a20d027 rename agent skill name 2026-01-24 23:21:55 +01:00
Amruth Pillai 1378feaf6a add additional launch args to browser 2026-01-24 23:19:58 +01:00
Amruth Pillai 22221e6fae add additional launch args to browser 2026-01-24 23:17:46 +01:00
Amruth Pillai d773ff5391 fix: add print:hidden to page decorators 2026-01-24 23:05:12 +01:00
Amruth Pillai be84f0cca6 📦 v5.0.2 · Changelog: https://docs.rxresu.me/changelog 2026-01-24 23:00:54 +01:00
Amruth Pillai 1419c16d04 add CLAUDE.md 2026-01-24 20:05:33 +01:00
Amruth Pillai 89f209014e fix issue with irregular heights of PDFs on resumes with sidebar backgrounds 2026-01-24 17:30:38 +01:00
Amruth Pillai 21aec46763 updates to printer, added changelog entry, restored deploy script in CI 2026-01-24 16:58:14 +01:00
Amruth Pillai 333b5e50c4 remove "-r require-metadata" from dockerfile 2026-01-24 14:18:37 +01:00
Amruth Pillai e94550b7b2 update product hunt launch date 2026-01-24 13:29:06 +01:00
Amruth Pillai dcdf8398da update version 2026-01-24 11:07:25 +01:00
Amruth Pillai 503f610ff6 Merge branch 'main' of github.com:amruthpillai/reactive-resume 2026-01-24 10:43:17 +01:00
Amruth Pillai c875cc858d Update .env.example with clearer instructions for PRINTER_APP_URL, remove pdf-lib dependency from package.json, and adjust translations for alignment and zoom features across multiple locales. Improve template layout consistency in various components. 2026-01-24 10:43:12 +01:00
Amruth Pillai 403384944c Sync Translations from Crowdin (#2570)
* New translations en-us.po (Romanian)
[ci skip]

* New translations en-us.po (French)
[ci skip]

* New translations en-us.po (Spanish)
[ci skip]

* New translations en-us.po (Afrikaans)
[ci skip]

* New translations en-us.po (Arabic)
[ci skip]

* New translations en-us.po (Bulgarian)
[ci skip]

* New translations en-us.po (Catalan)
[ci skip]

* New translations en-us.po (Czech)
[ci skip]

* New translations en-us.po (Danish)
[ci skip]

* New translations en-us.po (German)
[ci skip]

* New translations en-us.po (Greek)
[ci skip]

* New translations en-us.po (Finnish)
[ci skip]

* New translations en-us.po (Hebrew)
[ci skip]

* New translations en-us.po (Hungarian)
[ci skip]

* New translations en-us.po (Italian)
[ci skip]

* New translations en-us.po (Japanese)
[ci skip]

* New translations en-us.po (Korean)
[ci skip]

* New translations en-us.po (Lithuanian)
[ci skip]

* New translations en-us.po (Dutch)
[ci skip]

* New translations en-us.po (Norwegian)
[ci skip]

* New translations en-us.po (Polish)
[ci skip]

* New translations en-us.po (Portuguese)
[ci skip]

* New translations en-us.po (Russian)
[ci skip]

* New translations en-us.po (Slovak)
[ci skip]

* New translations en-us.po (Albanian)
[ci skip]

* New translations en-us.po (Serbian (Cyrillic))
[ci skip]

* New translations en-us.po (Swedish)
[ci skip]

* New translations en-us.po (Turkish)
[ci skip]

* New translations en-us.po (Ukrainian)
[ci skip]

* New translations en-us.po (Chinese Simplified)
[ci skip]

* New translations en-us.po (Chinese Traditional)
[ci skip]

* New translations en-us.po (Vietnamese)
[ci skip]

* New translations en-us.po (Portuguese, Brazilian)
[ci skip]

* New translations en-us.po (Indonesian)
[ci skip]

* New translations en-us.po (Persian)
[ci skip]

* New translations en-us.po (Khmer)
[ci skip]

* New translations en-us.po (Tamil)
[ci skip]

* New translations en-us.po (Bengali)
[ci skip]

* New translations en-us.po (Marathi)
[ci skip]

* New translations en-us.po (Thai)
[ci skip]

* New translations en-us.po (Latvian)
[ci skip]

* New translations en-us.po (Azerbaijani)
[ci skip]

* New translations en-us.po (Hindi)
[ci skip]

* New translations en-us.po (Malay)
[ci skip]

* New translations en-us.po (Telugu)
[ci skip]

* New translations en-us.po (Malayalam)
[ci skip]

* New translations en-us.po (Uzbek)
[ci skip]

* New translations en-us.po (Kannada)
[ci skip]

* New translations en-us.po (Odia)
[ci skip]

* New translations en-us.po (Amharic)
[ci skip]

* New translations en-us.po (Nepali)
[ci skip]
2026-01-24 09:05:20 +01:00
Amruth Pillai 4e73a81d4b - fixes #2562, add better error messages for duplicate resume slugs
- improvements made to ditgar template
- general improvements to all templates with backgrounds
- update dependencies and translations
- improved print function that handles single page and multi page resumes
2026-01-23 23:31:24 +01:00
Amruth Pillai ed74fb67f2 - fixes #2565
- adds pages for a variety of guides
- add images to the many of the guides and docs pages
2026-01-23 14:18:48 +01:00
Amruth Pillai 895af548a0 update email template 2026-01-23 10:24:45 +01:00
Amruth Pillai a32450ab22 add community spotlight page to the docs 2026-01-23 10:18:04 +01:00
Amruth Pillai a4fd7f8452 update key order 2026-01-23 09:30:17 +01:00
Amruth Pillai 8214f2a910 normalize section items with consistent css classes 2026-01-23 09:28:05 +01:00
Divyanshu Patil 06d54a9060 fix: username not allowing hyphens (#2558) 2026-01-23 08:31:43 +01:00
Amruth Pillai d801effbce Sync Translations from Crowdin (#2557)
* New translations en-us.po (French)
[ci skip]

* New translations en-us.po (Romanian)
[ci skip]

* New translations en-us.po (French)
[ci skip]

* New translations en-us.po (Spanish)
[ci skip]

* New translations en-us.po (Afrikaans)
[ci skip]

* New translations en-us.po (Arabic)
[ci skip]

* New translations en-us.po (Bulgarian)
[ci skip]

* New translations en-us.po (Catalan)
[ci skip]

* New translations en-us.po (Czech)
[ci skip]

* New translations en-us.po (Danish)
[ci skip]

* New translations en-us.po (German)
[ci skip]

* New translations en-us.po (Greek)
[ci skip]

* New translations en-us.po (Finnish)
[ci skip]

* New translations en-us.po (Hebrew)
[ci skip]

* New translations en-us.po (Hungarian)
[ci skip]

* New translations en-us.po (Italian)
[ci skip]

* New translations en-us.po (Japanese)
[ci skip]

* New translations en-us.po (Korean)
[ci skip]

* New translations en-us.po (Lithuanian)
[ci skip]

* New translations en-us.po (Dutch)
[ci skip]

* New translations en-us.po (Norwegian)
[ci skip]

* New translations en-us.po (Polish)
[ci skip]

* New translations en-us.po (Portuguese)
[ci skip]

* New translations en-us.po (Russian)
[ci skip]

* New translations en-us.po (Slovak)
[ci skip]

* New translations en-us.po (Albanian)
[ci skip]

* New translations en-us.po (Serbian (Cyrillic))
[ci skip]

* New translations en-us.po (Swedish)
[ci skip]

* New translations en-us.po (Turkish)
[ci skip]

* New translations en-us.po (Ukrainian)
[ci skip]

* New translations en-us.po (Chinese Simplified)
[ci skip]

* New translations en-us.po (Chinese Traditional)
[ci skip]

* New translations en-us.po (Vietnamese)
[ci skip]

* New translations en-us.po (Portuguese, Brazilian)
[ci skip]

* New translations en-us.po (Indonesian)
[ci skip]

* New translations en-us.po (Persian)
[ci skip]

* New translations en-us.po (Khmer)
[ci skip]

* New translations en-us.po (Tamil)
[ci skip]

* New translations en-us.po (Bengali)
[ci skip]

* New translations en-us.po (Marathi)
[ci skip]

* New translations en-us.po (Thai)
[ci skip]

* New translations en-us.po (Latvian)
[ci skip]

* New translations en-us.po (Azerbaijani)
[ci skip]

* New translations en-us.po (Hindi)
[ci skip]

* New translations en-us.po (Malay)
[ci skip]

* New translations en-us.po (Telugu)
[ci skip]

* New translations en-us.po (Malayalam)
[ci skip]

* New translations en-us.po (Uzbek)
[ci skip]

* New translations en-us.po (Kannada)
[ci skip]

* New translations en-us.po (Odia)
[ci skip]

* New translations en-us.po (Amharic)
[ci skip]

* New translations en-us.po (Nepali)
[ci skip]

* New translations en-us.po (Romanian)
[ci skip]

* New translations en-us.po (French)
[ci skip]

* New translations en-us.po (Spanish)
[ci skip]

* New translations en-us.po (Afrikaans)
[ci skip]

* New translations en-us.po (Arabic)
[ci skip]

* New translations en-us.po (Bulgarian)
[ci skip]

* New translations en-us.po (Catalan)
[ci skip]

* New translations en-us.po (Czech)
[ci skip]

* New translations en-us.po (Danish)
[ci skip]

* New translations en-us.po (German)
[ci skip]

* New translations en-us.po (Greek)
[ci skip]

* New translations en-us.po (Finnish)
[ci skip]

* New translations en-us.po (Hebrew)
[ci skip]

* New translations en-us.po (Hungarian)
[ci skip]

* New translations en-us.po (Italian)
[ci skip]

* New translations en-us.po (Japanese)
[ci skip]

* New translations en-us.po (Korean)
[ci skip]

* New translations en-us.po (Lithuanian)
[ci skip]

* New translations en-us.po (Dutch)
[ci skip]

* New translations en-us.po (Norwegian)
[ci skip]

* New translations en-us.po (Polish)
[ci skip]

* New translations en-us.po (Portuguese)
[ci skip]

* New translations en-us.po (Russian)
[ci skip]

* New translations en-us.po (Slovak)
[ci skip]

* New translations en-us.po (Albanian)
[ci skip]

* New translations en-us.po (Serbian (Cyrillic))
[ci skip]

* New translations en-us.po (Swedish)
[ci skip]

* New translations en-us.po (Turkish)
[ci skip]

* New translations en-us.po (Ukrainian)
[ci skip]

* New translations en-us.po (Chinese Simplified)
[ci skip]

* New translations en-us.po (Chinese Traditional)
[ci skip]

* New translations en-us.po (Vietnamese)
[ci skip]

* New translations en-us.po (Portuguese, Brazilian)
[ci skip]

* New translations en-us.po (Indonesian)
[ci skip]

* New translations en-us.po (Persian)
[ci skip]

* New translations en-us.po (Khmer)
[ci skip]

* New translations en-us.po (Tamil)
[ci skip]

* New translations en-us.po (Bengali)
[ci skip]

* New translations en-us.po (Marathi)
[ci skip]

* New translations en-us.po (Thai)
[ci skip]

* New translations en-us.po (Latvian)
[ci skip]

* New translations en-us.po (Azerbaijani)
[ci skip]

* New translations en-us.po (Hindi)
[ci skip]

* New translations en-us.po (Malay)
[ci skip]

* New translations en-us.po (Telugu)
[ci skip]

* New translations en-us.po (Malayalam)
[ci skip]

* New translations en-us.po (Uzbek)
[ci skip]

* New translations en-us.po (Kannada)
[ci skip]

* New translations en-us.po (Odia)
[ci skip]

* New translations en-us.po (Amharic)
[ci skip]

* New translations en-us.po (Nepali)
[ci skip]
2026-01-23 02:53:50 +01:00
Amruth Pillai 6658412f8b fixes #2539, show all possible font weight options for local fonts or unknown fontFamily 2026-01-23 02:04:11 +01:00
Amruth Pillai 11cbeb27f8 fixes #2552, implement FLAG_DISABLE_SIGNUPS and FLAG_DISABLE_EMAIL_AUTH 2026-01-23 01:45:42 +01:00
Amruth Pillai 6db9fee823 Merge pull request #2556 from VinhLe1410/main
chore: clean up Goternberg references
2026-01-23 01:08:59 +01:00
Amruth Pillai 35e2daa807 lot of bugfixes, better migration script 2026-01-23 01:07:52 +01:00
Phuc Vinh Le 0b61ddda2d chore: remove Goternberg references in Vite env file 2026-01-23 07:18:49 +11:00
Phuc Vinh Le 0045bbd785 fix: remove gotenberg reference in compose.yml file 2026-01-23 07:12:46 +11:00
Amruth Pillai a10954ed14 Merge branch 'main' of github.com:amruthpillai/reactive-resume 2026-01-22 15:46:11 +01:00
Amruth Pillai 0bc53b9c2a - simplify imports
- update translations
- convert image to base64 before sending to printer
- update development docs
2026-01-22 15:46:09 +01:00
Amruth Pillai 2aeed6c58f Add MIT License to the project 2026-01-21 23:44:48 +01:00
Amruth Pillai 36a9e27c93 Merge pull request #2547 from amruthpillai/l10n
Sync Translations from Crowdin
2026-01-21 23:43:23 +01:00
Amruth Pillai 36a0094631 Merge branch 'main' into l10n 2026-01-21 23:43:13 +01:00
Amruth Pillai 5d73998f82 add an alternative to browserless, for lightweight servers 2026-01-21 23:24:37 +01:00
Amruth Pillai 3cb6f763ca New translations en-us.po (Nepali)
[ci skip]
2026-01-21 23:04:53 +01:00
Amruth Pillai a438130784 New translations en-us.po (Amharic)
[ci skip]
2026-01-21 23:04:51 +01:00
Amruth Pillai c72d6205e0 New translations en-us.po (Odia)
[ci skip]
2026-01-21 23:04:50 +01:00
Amruth Pillai ce16f81dbf New translations en-us.po (Kannada)
[ci skip]
2026-01-21 23:04:49 +01:00
Amruth Pillai c786f58ab8 New translations en-us.po (Uzbek)
[ci skip]
2026-01-21 23:04:48 +01:00
Amruth Pillai ec19386317 New translations en-us.po (Malayalam)
[ci skip]
2026-01-21 23:04:47 +01:00
Amruth Pillai 5576184f1b New translations en-us.po (Telugu)
[ci skip]
2026-01-21 23:04:46 +01:00
Amruth Pillai ebb40664f2 New translations en-us.po (Malay)
[ci skip]
2026-01-21 23:04:44 +01:00
Amruth Pillai e5ad94e620 New translations en-us.po (Hindi)
[ci skip]
2026-01-21 23:04:43 +01:00
Amruth Pillai e5934bf94b New translations en-us.po (Azerbaijani)
[ci skip]
2026-01-21 23:04:42 +01:00
Amruth Pillai 5cfd7e00bb New translations en-us.po (Latvian)
[ci skip]
2026-01-21 23:04:41 +01:00
Amruth Pillai 649bfefd06 New translations en-us.po (Thai)
[ci skip]
2026-01-21 23:04:40 +01:00
Amruth Pillai 0e96b9bfae New translations en-us.po (Marathi)
[ci skip]
2026-01-21 23:04:39 +01:00
Amruth Pillai 4b87ab6fe9 New translations en-us.po (Bengali)
[ci skip]
2026-01-21 23:04:37 +01:00
Amruth Pillai ff2986b3f8 New translations en-us.po (Tamil)
[ci skip]
2026-01-21 23:04:36 +01:00
Amruth Pillai 8d75537153 New translations en-us.po (Khmer)
[ci skip]
2026-01-21 23:04:35 +01:00
Amruth Pillai 8ffce2a264 New translations en-us.po (Persian)
[ci skip]
2026-01-21 23:04:34 +01:00
Amruth Pillai 083466202e New translations en-us.po (Indonesian)
[ci skip]
2026-01-21 23:04:33 +01:00
Amruth Pillai 7dff245b1a New translations en-us.po (Portuguese, Brazilian)
[ci skip]
2026-01-21 23:04:32 +01:00
Amruth Pillai 272f0795d1 New translations en-us.po (Vietnamese)
[ci skip]
2026-01-21 23:04:30 +01:00
Amruth Pillai 22ebb7b8eb New translations en-us.po (Chinese Traditional)
[ci skip]
2026-01-21 23:04:29 +01:00
Amruth Pillai f6ab720d73 New translations en-us.po (Chinese Simplified)
[ci skip]
2026-01-21 23:04:28 +01:00
Amruth Pillai a735580540 New translations en-us.po (Ukrainian)
[ci skip]
2026-01-21 23:04:27 +01:00
Amruth Pillai 3bcd480345 New translations en-us.po (Turkish)
[ci skip]
2026-01-21 23:04:26 +01:00
Amruth Pillai 63684ce9a0 New translations en-us.po (Swedish)
[ci skip]
2026-01-21 23:04:24 +01:00
Amruth Pillai fc4678ae98 New translations en-us.po (Serbian (Cyrillic))
[ci skip]
2026-01-21 23:04:23 +01:00
Amruth Pillai f87575958a New translations en-us.po (Albanian)
[ci skip]
2026-01-21 23:04:22 +01:00
Amruth Pillai 6b051dbf60 New translations en-us.po (Slovak)
[ci skip]
2026-01-21 23:04:20 +01:00
Amruth Pillai fb8917cf40 New translations en-us.po (Russian)
[ci skip]
2026-01-21 23:04:19 +01:00
Amruth Pillai 8c674fe510 New translations en-us.po (Portuguese)
[ci skip]
2026-01-21 23:04:18 +01:00
Amruth Pillai f5c4487bb9 New translations en-us.po (Polish)
[ci skip]
2026-01-21 23:04:17 +01:00
Amruth Pillai 588cee17fb New translations en-us.po (Norwegian)
[ci skip]
2026-01-21 23:04:16 +01:00
Amruth Pillai ea1ac92a56 New translations en-us.po (Dutch)
[ci skip]
2026-01-21 23:04:15 +01:00
Amruth Pillai 12c415e505 New translations en-us.po (Lithuanian)
[ci skip]
2026-01-21 23:04:14 +01:00
Amruth Pillai 2c292e3bad New translations en-us.po (Korean)
[ci skip]
2026-01-21 23:04:12 +01:00
Amruth Pillai dc3d3c2971 New translations en-us.po (Japanese)
[ci skip]
2026-01-21 23:04:11 +01:00
Amruth Pillai c86b049c92 New translations en-us.po (Italian)
[ci skip]
2026-01-21 23:04:10 +01:00
Amruth Pillai b0ca3ddfab New translations en-us.po (Hungarian)
[ci skip]
2026-01-21 23:04:09 +01:00
Amruth Pillai 59fc84d975 New translations en-us.po (Hebrew)
[ci skip]
2026-01-21 23:04:08 +01:00
Amruth Pillai 13f817d057 New translations en-us.po (Finnish)
[ci skip]
2026-01-21 23:04:07 +01:00
Amruth Pillai b50e11e4b4 New translations en-us.po (Greek)
[ci skip]
2026-01-21 23:04:05 +01:00
Amruth Pillai d56d834245 New translations en-us.po (German)
[ci skip]
2026-01-21 23:04:04 +01:00
Amruth Pillai 06d23ac98e New translations en-us.po (Danish)
[ci skip]
2026-01-21 23:04:03 +01:00
Amruth Pillai 6ec5a409d0 New translations en-us.po (Czech)
[ci skip]
2026-01-21 23:04:01 +01:00
Amruth Pillai 7fd0a66eac New translations en-us.po (Catalan)
[ci skip]
2026-01-21 23:04:00 +01:00
Amruth Pillai 52184112b1 New translations en-us.po (Bulgarian)
[ci skip]
2026-01-21 23:03:59 +01:00
Amruth Pillai 89473ac4e7 New translations en-us.po (Arabic)
[ci skip]
2026-01-21 23:03:58 +01:00
Amruth Pillai 2f66afdc87 New translations en-us.po (Afrikaans)
[ci skip]
2026-01-21 23:03:57 +01:00
Amruth Pillai 16cbad2028 New translations en-us.po (Spanish)
[ci skip]
2026-01-21 23:03:55 +01:00
Amruth Pillai 2f49075d89 New translations en-us.po (French)
[ci skip]
2026-01-21 23:03:54 +01:00
Amruth Pillai 8771f2716a New translations en-us.po (Romanian)
[ci skip]
2026-01-21 23:03:53 +01:00
Amruth Pillai 4d72a953dd fix for Reflect.getMetadata 2026-01-21 22:01:55 +01:00
Amruth Pillai b5ea68af5b New translations en-us.po (French)
[ci skip]
2026-01-21 21:57:12 +01:00
Amruth Pillai 499346e7ba one more for annotations 2026-01-21 21:55:10 +01:00
Amruth Pillai 81f4a3e148 try this 2026-01-21 21:45:42 +01:00
Amruth Pillai 7fd54c4259 try again 2026-01-21 21:42:41 +01:00
Amruth Pillai fda2d384e6 update docker-build for beta 2026-01-21 21:14:44 +01:00
Amruth Pillai 270ccf746c beta 2026-01-21 21:08:21 +01:00
Amruth Pillai 6b51d1bbaa New translations en-us.po (French)
[ci skip]
2026-01-21 20:47:24 +01:00
Amruth Pillai a0e17f0c27 New translations en-us.po (French)
[ci skip]
2026-01-21 19:12:04 +01:00
Amruth Pillai 70064be7de - Use browserless over gotenberg
- Implement functionality to move items between sections or pages
- Enhance custom sections to have a `type` property
- Update the v4 importer to account for custom sections
- Update healthcheck to be a simple curl command
- Update dependencies to latest
and a lot more changes
2026-01-21 18:49:54 +01:00
Amruth PillaiandCopilot 332286f9ce Update locales/fr-FR.po
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-21 17:57:20 +01:00
Amruth Pillai 7584412fcb New translations en-us.po (French)
[ci skip]
2026-01-21 17:17:03 +01:00
Amruth Pillai b3c342b029 change launch date 2026-01-21 05:51:13 +01:00
Amruth Pillai cf6db1a0ee Merge pull request #2536 from amruthpillai/l10n
Sync Translations from Crowdin
2026-01-21 05:04:13 +01:00
Amruth Pillai 2ac5c9137c fixes #2540 2026-01-21 05:03:30 +01:00
Amruth Pillai 8bafefe1b8 New translations en-us.po (Nepali)
[ci skip]
2026-01-20 20:50:23 +01:00
Amruth Pillai 5a42b776ff New translations en-us.po (Amharic)
[ci skip]
2026-01-20 20:50:22 +01:00
Amruth Pillai dfe53629bc New translations en-us.po (Odia)
[ci skip]
2026-01-20 20:50:21 +01:00
Amruth Pillai fc6690d047 New translations en-us.po (Kannada)
[ci skip]
2026-01-20 20:50:20 +01:00
Amruth Pillai b3904750b8 New translations en-us.po (Uzbek)
[ci skip]
2026-01-20 20:50:19 +01:00
Amruth Pillai 63ea01b219 New translations en-us.po (Malayalam)
[ci skip]
2026-01-20 20:50:18 +01:00
Amruth Pillai 4345f48dcc New translations en-us.po (Telugu)
[ci skip]
2026-01-20 20:50:17 +01:00
Amruth Pillai 7f8869649f New translations en-us.po (Malay)
[ci skip]
2026-01-20 20:50:16 +01:00
Amruth Pillai 06777e8285 New translations en-us.po (Hindi)
[ci skip]
2026-01-20 20:50:14 +01:00
Amruth Pillai 0907bddf75 New translations en-us.po (Azerbaijani)
[ci skip]
2026-01-20 20:50:13 +01:00
Amruth Pillai 8cf225a3e7 New translations en-us.po (Latvian)
[ci skip]
2026-01-20 20:50:12 +01:00
Amruth Pillai 71a931fdee New translations en-us.po (Thai)
[ci skip]
2026-01-20 20:50:11 +01:00
Amruth Pillai aa2109f4bd New translations en-us.po (Marathi)
[ci skip]
2026-01-20 20:50:10 +01:00
Amruth Pillai c2edf9651a New translations en-us.po (Bengali)
[ci skip]
2026-01-20 20:50:09 +01:00
Amruth Pillai 88e5c3e87e New translations en-us.po (Tamil)
[ci skip]
2026-01-20 20:50:08 +01:00
Amruth Pillai e2d593327a New translations en-us.po (Khmer)
[ci skip]
2026-01-20 20:50:07 +01:00
Amruth Pillai 104047f672 New translations en-us.po (Persian)
[ci skip]
2026-01-20 20:50:05 +01:00
Amruth Pillai 9c3faf1edf New translations en-us.po (Indonesian)
[ci skip]
2026-01-20 20:50:04 +01:00
Amruth Pillai 1dc5bf21e0 New translations en-us.po (Portuguese, Brazilian)
[ci skip]
2026-01-20 20:50:02 +01:00
Amruth Pillai 8bc4f3de84 New translations en-us.po (Vietnamese)
[ci skip]
2026-01-20 20:50:01 +01:00
Amruth Pillai 6af3b32302 New translations en-us.po (Chinese Traditional)
[ci skip]
2026-01-20 20:50:00 +01:00
Amruth Pillai dd517627c1 New translations en-us.po (Chinese Simplified)
[ci skip]
2026-01-20 20:49:59 +01:00
Amruth Pillai 05d5824131 New translations en-us.po (Ukrainian)
[ci skip]
2026-01-20 20:49:58 +01:00
Amruth Pillai 655138c0ea New translations en-us.po (Turkish)
[ci skip]
2026-01-20 20:49:57 +01:00
Amruth Pillai 7209eecaba New translations en-us.po (Swedish)
[ci skip]
2026-01-20 20:49:55 +01:00
Amruth Pillai 3d36849508 New translations en-us.po (Serbian (Cyrillic))
[ci skip]
2026-01-20 20:49:54 +01:00
Amruth Pillai 2e42146102 New translations en-us.po (Albanian)
[ci skip]
2026-01-20 20:49:53 +01:00
Amruth Pillai ab05f2533f New translations en-us.po (Slovak)
[ci skip]
2026-01-20 20:49:51 +01:00
Amruth Pillai 53f796aef7 New translations en-us.po (Russian)
[ci skip]
2026-01-20 20:49:50 +01:00
Amruth Pillai 09ea72cb2f New translations en-us.po (Portuguese)
[ci skip]
2026-01-20 20:49:49 +01:00
Amruth Pillai 84cc897fb4 New translations en-us.po (Polish)
[ci skip]
2026-01-20 20:49:47 +01:00
Amruth Pillai d3cc2b8011 New translations en-us.po (Norwegian)
[ci skip]
2026-01-20 20:49:46 +01:00
Amruth Pillai f4d56195c4 New translations en-us.po (Dutch)
[ci skip]
2026-01-20 20:49:45 +01:00
Amruth Pillai ce33a71a43 New translations en-us.po (Lithuanian)
[ci skip]
2026-01-20 20:49:44 +01:00
Amruth Pillai 90ae8511b5 New translations en-us.po (Korean)
[ci skip]
2026-01-20 20:49:43 +01:00
Amruth Pillai f41d53cf36 New translations en-us.po (Japanese)
[ci skip]
2026-01-20 20:49:42 +01:00
Amruth Pillai e16437aeed New translations en-us.po (Italian)
[ci skip]
2026-01-20 20:49:41 +01:00
Amruth Pillai c2d942d3f2 New translations en-us.po (Hungarian)
[ci skip]
2026-01-20 20:49:39 +01:00
Amruth Pillai cc5a90b3ca New translations en-us.po (Hebrew)
[ci skip]
2026-01-20 20:49:38 +01:00
Amruth Pillai 2942c5a329 New translations en-us.po (Finnish)
[ci skip]
2026-01-20 20:49:37 +01:00
Amruth Pillai d214ea8319 New translations en-us.po (Greek)
[ci skip]
2026-01-20 20:49:36 +01:00
Amruth Pillai 7a80b13db8 New translations en-us.po (German)
[ci skip]
2026-01-20 20:49:35 +01:00
Amruth Pillai d55e179890 New translations en-us.po (Danish)
[ci skip]
2026-01-20 20:49:34 +01:00
Amruth Pillai 33563a4cf8 New translations en-us.po (Czech)
[ci skip]
2026-01-20 20:49:32 +01:00
Amruth Pillai fe21523934 New translations en-us.po (Catalan)
[ci skip]
2026-01-20 20:49:31 +01:00
Amruth Pillai 9840613e0a New translations en-us.po (Bulgarian)
[ci skip]
2026-01-20 20:49:30 +01:00
Amruth Pillai 80c3e80dba New translations en-us.po (Arabic)
[ci skip]
2026-01-20 20:49:29 +01:00
Amruth Pillai c6ab0700b7 New translations en-us.po (Afrikaans)
[ci skip]
2026-01-20 20:49:28 +01:00
Amruth Pillai 6d23773569 New translations en-us.po (Spanish)
[ci skip]
2026-01-20 20:49:27 +01:00
Amruth Pillai f5bc16944f New translations en-us.po (French)
[ci skip]
2026-01-20 20:49:26 +01:00
Amruth Pillai 004b9bd8c4 New translations en-us.po (Romanian)
[ci skip]
2026-01-20 20:49:25 +01:00
Amruth Pillai 4b7fa5a303 run docker build only on tag release 2026-01-20 20:39:12 +01:00
Amruth Pillai 9efc9653a7 bring back warp 2026-01-20 20:37:51 +01:00
Amruth Pillai 0ff51a82ee fix flex width issue in pikachu 2026-01-20 20:34:21 +01:00
Amruth Pillai d874ea4f83 increase api rate limiter to 500 requests per day 2026-01-20 20:24:50 +01:00
Amruth Pillai 5688c97ee0 fixes #2530 2026-01-20 20:18:28 +01:00
Amruth Pillai 3cbf953ad2 fixes #2525 2026-01-20 19:59:54 +01:00
Amruth Pillai 4f61e921cd fixes #2526 2026-01-20 19:57:26 +01:00
Amruth Pillai f903e6099b fixes #2529 2026-01-20 19:50:33 +01:00
Amruth Pillai f3a6c0db6b fixes #2531 2026-01-20 19:40:25 +01:00
Amruth Pillai 5880078bfc fixes #2532 2026-01-20 19:38:13 +01:00
Amruth Pillai 6689232f7a New translations en-us.po (Spanish)
[ci skip]
2026-01-20 19:24:39 +01:00
Amruth Pillai 29cc5be9b3 New translations en-us.po (Chinese Simplified)
[ci skip]
2026-01-20 16:28:13 +01:00
Amruth Pillai 9dd0611ccc add sso documentation, fixes #2518 2026-01-20 14:20:13 +01:00
Amruth Pillai b0c8017839 update sitemaps 2026-01-20 13:30:09 +01:00
Amruth Pillai 891828af8e Merge pull request #2516 from amruthpillai/2514-bug-removing-the-whole-page-after-deleting-custom-section
fix: deleting a custom section deletes all items from the layout
2026-01-20 12:57:59 +01:00
Amruth Pillai 32466c758c fixes #2514 - deleting a custom section deletes all items from the layout 2026-01-20 12:55:57 +01:00
Amruth Pillai 3b11316982 New translations en-us.po (Chinese Simplified)
[ci skip]
2026-01-20 12:51:49 +01:00
Amruth Pillai b87e5dd023 add more guides on migration and self-hosting docker compose examples, change launch date for banner, update dependencies 2026-01-20 10:35:37 +01:00
Amruth Pillai 7e5597271b update naming convention of repository 2026-01-20 01:22:00 +01:00
Amruth Pillai 3d1334f7d9 add docker-hub-description.yml 2026-01-20 01:10:01 +01:00
Amruth Pillai 5f5c2e1a52 optimize images for faster loading, fix meta description tag 2026-01-20 01:03:02 +01:00
Amruth Pillai ce8d273c79 - skip running workflow on tag push
- remove ISSUE_AI_CHAT_BUILDER.md
2026-01-20 00:33:47 +01:00
Amruth Pillai 64e849a677 Merge pull request #2507 from amruthpillai/feat/v5
Reactive Resume v5
2026-01-19 23:34:27 +01:00
Amruth Pillai cad390fa13 initial commit of v5 2026-01-19 23:31:54 +01:00
Amruth Pillai 55bdfd0067 test new publish docker image flow 2026-01-19 22:49:28 +01:00
Amruth Pillai 8a164c416f test new publish docker image flow 2026-01-19 22:40:54 +01:00
Amruth Pillai 54ad6b65c2 change env 2026-01-19 20:45:30 +01:00
Amruth Pillai b0d5dd28e0 v4.5.6 - the last release of v4 2026-01-19 20:37:13 +01:00
Amruth Pillai ec86e40a8d Merge pull request #2504 from joyalgeorgekj/main
[FIX]: Incorrect/Invalid URL to see the new features in v4
2026-01-19 11:25:07 +01:00
Joyal George K J 4abcee00e1 Update link to 4.0.0 version release on GitHub releases 2026-01-19 15:33:30 +05:30
Amruth Pillai 91c8624d80 Merge pull request #2485 from MohammedAlhalwachi/fix-gengar
fix(artboard): fix padding regression in Gengar template (#2463)
2026-01-08 09:26:47 +01:00
Mohammed Alhalwachi f6dc3bf8df fix(artboard): fix padding regression in Gengar template (#2463) 2025-12-23 22:17:50 +03:00
Amruth Pillai a9b8d1d06f Merge pull request #2483 from y8a5y/main
[Feature] Permalink to share/download resume json data
2025-12-23 03:12:39 +01:00
y8a5y 76b61d0135 fix(Contrib file): add steps to ensure successful CI 2025-12-22 10:40:40 +01:00
y8a5y d7409832ab fix(Resume Controller): address CI lint issue 2025-12-22 10:26:36 +01:00
y8a5y 1749f95086 fix(Sharing section): address CodeRabbit nitpick nb4 2025-12-21 22:43:08 +01:00
y8a5y 2e11e64881 fix(Resume Controller): address CodeRabbit nitpick nb1 2025-12-21 22:42:48 +01:00
y8a5y db86f3275b feature(Sharing section): add Raw Json URL card to Sharing section 2025-12-21 14:37:37 +01:00
y8a5y 62c09ea142 feature(Resume Controller): add route to get Resume without incrementing view count 2025-12-21 14:35:24 +01:00
y8a5y 589bd2c162 refacto(Resume Controller): reflecting previous change in controller 2025-12-21 14:34:57 +01:00
y8a5y 4c7ed8ea0d refacto(Resume Service): move view count increment outside of findOne 2025-12-21 14:30:02 +01:00
Amruth Pillai 25021d1b20 Set up CI with Azure Pipelines
[skip ci]
2025-11-07 12:11:28 +01:00
Amruth Pillai 53fdfdf8db Merge pull request #2437 from Sahilbhatane/main
fix(artboard): bypass Google Fonts for local system fonts and post PAGE_LOADED immediately; add utils docstrings and unit tests for isLocalFont issue #2435
2025-11-06 17:19:33 +01:00
sahil 8a45f2de4d fixed issue [Bug] <Error MIME al cargar fuente “Arial” desde Google Fonts en exportaciones públicas>
Fixes #2435
2025-11-05 19:08:38 +05:30
sahil 4ccc7bae40 Fixed issue [Bug] <Error MIME al cargar fuente “Arial” desde Google Fonts en exportaciones públicas>
Fixes #2435
2025-11-05 18:59:20 +05:30
sahil 2585c47de8 Fixed bug [Bug] <Error MIME al cargar fuente “Arial” desde Google Fonts en exportaciones públicas>
Fixes #2435
2025-11-05 16:33:56 +05:30
sahil b49798950a FIxed bug [Bug] <Error MIME al cargar fuente “Arial” desde Google Fonts en exportaciones públicas>
Fixes #2435
2025-11-05 16:12:57 +05:30
Amruth Pillai 03f15f91b3 chore(package): bump version to 4.5.5 2025-11-04 18:36:52 +01:00
Amruth Pillai 8e13d9c3ac Merge pull request #2434 from AmruthPillai/2275-bug-certifications-texts-dont-fit-inside-the-page
Bugfix: links/urls don't fit inside the page
2025-11-04 18:34:42 +01:00
Amruth Pillai 80932eb80c Fix bug: links/urls don't fit inside the page 2025-11-04 18:31:47 +01:00
Amruth Pillai b2ae2c05d8 chore(package): bump version to 4.5.4 2025-11-04 16:07:10 +01:00
Amruth Pillai e349fc9bd1 Merge pull request #2421 from LinuxKunaL/main
fix(rich-input.tsx): replace default scrollbar with themed scrollbar in Summary Editor, fixed issue #2420
2025-11-04 15:35:36 +01:00
Amruth Pillai 53213dfb26 Refactor RichInput component: adjust ScrollArea padding and EditorContent min-height 2025-11-04 15:27:40 +01:00
Amruth Pillai 40f27a53b6 Merge branch 'main' into LinuxKunaL/main 2025-11-04 15:23:22 +01:00
Amruth Pillai 2d900600bf update translations 2025-11-04 15:22:53 +01:00
Amruth Pillai 047e9c248f Merge pull request #2426 from abhas20/feature
Added collapsible feature for each section
2025-11-04 15:21:36 +01:00
Amruth Pillai 1cc1c39903 implement expand/collapse all sections 2025-11-04 15:21:17 +01:00
Amruth Pillai f6f2a29a7a Merge remote-tracking branch 'origin/main' into feature 2025-11-04 14:39:50 +01:00
Amruth Pillai 4368f6a887 Merge pull request #2432 from Ofsen/main
chore(puppeteer): using puppeteer-core instead of puppeteer
2025-11-04 14:39:06 +01:00
Amruth Pillai f6db9fb387 Merge branch 'main' into Ofsen/main 2025-11-04 14:37:41 +01:00
Amruth Pillai afa3804bea add wellKnown url 2025-11-04 14:36:22 +01:00
Amruth Pillai e2f43b4931 add funding-manifest-urls 2025-11-04 14:34:57 +01:00
Amruth Pillai 067fdd0921 remove format script 2025-11-04 12:35:11 +01:00
Amruth Pillai 1296e6bd45 Merge branch 'main' of github.com:AmruthPillai/Reactive-Resume 2025-11-03 23:21:53 +01:00
Amruth Pillai d883edb51f Add funding.json file and update package.json dependencies
- Introduced a new funding.json file to support project funding details.
- Updated package.json to version 4.5.3 and modified various dependencies, including @babel/core, @babel/preset-react, and others to their latest versions.
- Adjusted pnpm-lock.yaml to reflect the changes in dependencies and their versions.
2025-11-03 23:21:29 +01:00
Ofsen 51fcf13d37 chore(puppeteer): using puppeteer-core instead of puppeteer 2025-11-03 20:24:57 +01:00
abhas20 91ef87b4e0 Fix(builder-page) typo: renamed toogleSection 2025-10-25 08:10:02 +05:30
abhas20 4c0cc947a2 updated(builder-page): added collapsible feature for each section 2025-10-24 18:38:52 +05:30
Amruth Pillai 6fcb7a4845 Merge pull request #2419 from AmruthPillai/l10n
New Translations from Crowdin
2025-10-18 22:32:56 +02:00
Amruth Pillai 213f96b189 Merge pull request #2415 from GouravNG/feat/rich-input-highlight
[Feature] Highlight the selected options under Rich inputs.
2025-10-18 22:32:23 +02:00
Amruth Pillai e8d6d4ad3a Merge pull request #2412 from alay-dev/main
fix(template): fix gengar template summary section not rendering in sidebar
2025-10-18 22:31:10 +02:00
Amruth Pillai 87370cfdf0 Apply suggestion from @AmruthPillai 2025-10-18 22:30:51 +02:00
Amruth Pillai 112a644927 Merge pull request #2410 from JuanJesusAlejoSillero/main
docs: add Gengar, Glalie, and Leafish templates while also updating image paths to relative in-repo URLs in README.md
2025-10-18 22:29:49 +02:00
Crowdin Bot 2977e3528a New Crowdin translations by GitHub Action 2025-10-18 00:10:39 +00:00
LinuxKunaL fd7b5e0dd4 fix(rich-input.tsx): replace default scrollbar with themed scrollbar in Summary Editor, fixed #2420 2025-10-17 01:22:42 +05:30
Gourav 21fd079f94 feat(rich-input): updating datastate on pressing rich input options
- when selecting the rich input option earlier only aria-pressed="true" was changing and not datastate now updated the code to manually update the data-state
2025-10-13 22:52:25 +05:30
Gourav 9bdc61b50d feat(rich-input): Highlight the selected options of rich inputs 2025-10-13 22:07:01 +05:30
Alay Naru 2de24d5b55 fix(template): fix gengar template summary section not rendering in sidebar 2025-10-12 18:49:44 +05:30
Gourav Gunaga 1cba9d0fb9 Merge branch 'AmruthPillai:main' into main 2025-10-11 11:02:20 +05:30
Juan Jesús Alejo Sillero 8f532bf4a6 docs: add Gengar, Glalie, and Leafish templates while also updating image paths to relative in-repo URLs in README.md 2025-10-11 01:24:51 +02:00
Amruth Pillai 4efdabd475 Merge branch 'main' of github.com:AmruthPillai/Reactive-Resume 2025-10-09 00:10:28 +02:00
Amruth Pillai 8803101dcd fix for long waiting times 2025-10-09 00:10:25 +02:00
Amruth Pillai a4a9381b65 Merge pull request #2408 from AmruthPillai/dependabot/npm_and_yarn/npm_and_yarn-2752d6d8c0
Bump nodemailer from 6.10.1 to 7.0.7 in the npm_and_yarn group across 1 directory
2025-10-08 21:55:09 +02:00
dependabot[bot] 618d42019e Bump nodemailer in the npm_and_yarn group across 1 directory
Bumps the npm_and_yarn group with 1 update in the / directory: [nodemailer](https://github.com/nodemailer/nodemailer).


Updates `nodemailer` from 6.10.1 to 7.0.7
- [Release notes](https://github.com/nodemailer/nodemailer/releases)
- [Changelog](https://github.com/nodemailer/nodemailer/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodemailer/nodemailer/compare/v6.10.1...v7.0.7)

---
updated-dependencies:
- dependency-name: nodemailer
  dependency-version: 7.0.7
  dependency-type: direct:production
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-08 19:51:38 +00:00
Amruth Pillai d9b56cfb5b - fixes #2401 2025-10-08 21:49:07 +02:00
Amruth Pillai 5624dc4f83 Merge pull request #2402 from GouravNG/revert/EnvExampleFile
revert: readded the .env.example file
2025-10-07 21:25:48 +02:00
Gourav Gunaga 313fe47050 Merge pull request #1 from GouravNG/revert/EnvExampleFile
revert: readded the .env.example file
2025-10-07 21:56:25 +05:30
Gourav 772332661f revert: readded the .env.example file 2025-10-07 21:54:59 +05:30
1682 changed files with 376892 additions and 166232 deletions
+26 -56
View File
@@ -1,62 +1,32 @@
# Compiled Output
dist
tmp
/out-tsc
# Project Dependencies
# VCS and editor state
.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
.cursor
.DS_Store
Thumbs.db
.editorconfig
.eslint*
.vite-hooks
# Generated Files
.nx
.swc
fly.toml
stats.html
tools/compose/*
tools/scripts/*
# Local configuration and runtime state
.env*
/data
# Environment Variables
*.env*
!.env.example
# Dependency and package-manager caches
node_modules
**/node_modules
.pnpm-store
# Lingui Compiled Messages
apps/client/src/locales/_build/
apps/client/src/locales/*/messages.mjs
# Build, test, and framework output
dist
**/dist
.nitro
.output
.vercel
.netlify
.tanstack
.turbo
**/.turbo
coverage
reports
# Generated service-worker assets are rebuilt during the web build.
public/sw.js
public/workbox-*.js
-12
View File
@@ -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
+117
View File
@@ -0,0 +1,117 @@
# --- Application ---
# Public port used by the production server and the Vite web server in local development.
PORT="3000"
# Port used by the Hono server in local development. Vite proxies API requests to this port.
SERVER_PORT="3001"
# Public URL where the app is served. Used for auth callbacks, OAuth issuer URLs,
# OpenGraph metadata, and absolute upload URLs.
APP_URL="http://localhost:3000"
# --- Database (PostgreSQL) ---
# PostgreSQL connection URL. In Docker Compose, the hostname is usually `postgres`;
# when running directly on your machine, `localhost` is typical.
DATABASE_URL="postgresql://postgres:postgres@postgres:5432/postgres"
# --- Authentication ---
# Generated using `openssl rand -hex 32`
AUTH_SECRET="change-me-to-a-secure-secret-key-in-production"
# Better Auth Dashboard (optional)
# Enables the Better Auth Dashboard plugin when set, you probably don't need this.
BETTER_AUTH_API_KEY=""
# Social Auth (Google, optional)
# Set both values to enable Google sign-in.
GOOGLE_CLIENT_ID=""
GOOGLE_CLIENT_SECRET=""
# Social Auth (GitHub, optional)
# Set both values to enable GitHub sign-in.
GITHUB_CLIENT_ID=""
GITHUB_CLIENT_SECRET=""
# Social Auth (LinkedIn, optional)
# Set both values to enable LinkedIn sign-in.
LINKEDIN_CLIENT_ID=""
LINKEDIN_CLIENT_SECRET=""
# Custom OAuth Provider (optional)
# Set OAUTH_CLIENT_ID and OAUTH_CLIENT_SECRET plus either OAUTH_DISCOVERY_URL or
# the three manual endpoint URLs below.
OAUTH_PROVIDER_NAME=""
OAUTH_CLIENT_ID=""
OAUTH_CLIENT_SECRET=""
OAUTH_DISCOVERY_URL=""
OAUTH_AUTHORIZATION_URL=""
OAUTH_TOKEN_URL=""
OAUTH_USER_INFO_URL=""
# Space-separated scopes requested from the custom OAuth provider.
OAUTH_SCOPES="openid profile email"
# --- Email (optional) ---
# If SMTP_HOST, SMTP_USER, SMTP_PASS, or SMTP_FROM is missing, the app logs the
# email to the console instead.
SMTP_HOST=""
SMTP_PORT=""
SMTP_USER=""
SMTP_PASS=""
SMTP_FROM="Reactive Resume <noreply@rxresu.me>"
SMTP_SECURE="false"
# --- Storage (optional) ---
# If all S3 keys are disabled, the app uses local filesystem storage instead.
# Make sure to mount this directory to a volume or the host filesystem to ensure data integrity.
# LOCAL_STORAGE_PATH overrides where local uploads/cache are written.
# Defaults to /app/data in the official Docker image; in dev, defaults to <workspace>/data.
# LOCAL_STORAGE_PATH="/app/data"
# Seaweedfs
S3_ACCESS_KEY_ID="seaweedfs"
S3_SECRET_ACCESS_KEY="seaweedfs"
S3_REGION="us-east-1"
S3_ENDPOINT="http://seaweedfs:8333"
S3_BUCKET="reactive-resume"
S3_FORCE_PATH_STYLE="true"
# --- AI Agent Workspace (optional) ---
# Required only for the authenticated /agent workspace and saved AI providers.
REDIS_URL="redis://redis:6379"
ENCRYPTION_SECRET="change-me-to-a-secure-agent-secret-in-production"
# --- Feature Flags ---
# This flag disables new signups, both on the web app and the server.
FLAG_DISABLE_SIGNUPS="false"
# This flag disables email/password login. Disables email verification, forgot password, and reset password flows.
# Users can still sign up via social auth (Google/GitHub/Custom OAuth), unless FLAG_DISABLE_SIGNUPS is also set to true.
FLAG_DISABLE_EMAIL_AUTH="false"
# This flag disables the image processing.
# This is useful if you are using a machine with limited resources, like a Raspberry Pi.
FLAG_DISABLE_IMAGE_PROCESSING="false"
# Allows dynamic OAuth client registration to use any parseable redirect URI,
# including custom schemes, private hosts, and non-loopback http:// URLs.
# WARNING: Enabling this on a public or multi-tenant deployment can enable phishing
# or token exfiltration. Only enable this on a trusted, self-hosted instance.
FLAG_ALLOW_UNSAFE_OAUTH_REDIRECT_URI="false"
# Allows AI providers to be configured with any base URL, including http:// and
# private/loopback addresses (e.g. http://localhost:11434 for a local Ollama instance).
# WARNING: Enabling this on a multi-tenant deployment is a Server-Side Request Forgery (SSRF)
# risk. Only enable this on a trusted, single-tenant self-hosted instance.
FLAG_ALLOW_UNSAFE_AI_BASE_URL="false"
# --- Others ---
# Google Cloud API Key (optional)
# For font-list generation tooling.
# Requires "Google Fonts Developer API" to be enabled.
GOOGLE_CLOUD_API_KEY=""
# Crowdin (optional)
# For translation tooling.
CROWDIN_PROJECT_ID=""
CROWDIN_API_TOKEN=""
-1
View File
@@ -1 +0,0 @@
node_modules
-103
View File
@@ -1,103 +0,0 @@
{
"root": true,
"ignorePatterns": ["**/*"],
"plugins": ["@nx"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"extends": ["plugin:prettier/recommended"],
"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": ["*"]
}
]
}
],
// prettier
"prettier/prettier": [
"warn",
{
"endOfLine": "auto"
}
]
}
},
{
"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"
}
}
]
}
+68
View File
@@ -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": "hello@amruthpillai.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/donate"
}
]
}
}
@@ -3,7 +3,7 @@ name: 🐞 Bug Report
description: Create a bug report to help improve Reactive Resume
title: "[Bug] <title>"
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,14 +44,16 @@ body:
- Bronzor
- Chikorita
- Ditto
- Ditgar
- Gengar
- Glalie
- Kakuna
- Lapras
- Leafish
- Nosepass
- Onyx
- Pikachu
- Rhyhorn
- Scizor
validations:
required: false
@@ -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: [enhancement, 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
+1
View File
@@ -0,0 +1 @@
blank_issues_enabled: false
+41
View File
@@ -0,0 +1,41 @@
name: autofix.ci
on:
pull_request:
push:
branches: ["main"]
permissions:
contents: read
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
autofix:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v6
- name: Install pnpm
uses: pnpm/action-setup@v6
- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: "lts/*"
cache: "pnpm"
- name: Install Dependencies
run: pnpm install --frozen-lockfile
- name: Remove Unused Dependencies
run: pnpm knip --fix
- name: Lint and Format
run: pnpm check
- name: Autofix
uses: autofix-ci/action@7a166d7532b277f34e16238930461bf77f9d7ed8
+36
View File
@@ -0,0 +1,36 @@
name: Sync Translations from Crowdin
on:
push:
branches: ["main"]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
crowdin-sync:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout Repository
uses: actions/checkout@v6
- name: Sync Translations from Crowdin
uses: crowdin/github-action@v2
with:
download_translations: true
localization_branch_name: "l10n"
commit_message: "[skip ci] chore(i18n): sync translations from crowdin"
pull_request_title: "Sync Translations from Crowdin"
pull_request_body: "This is an automated pull request to sync translations from [Crowdin](https://crowdin.com/project/reactive-resume)."
pull_request_labels: "l10n"
pull_request_reviewers: "amruthpillai"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
+251
View File
@@ -0,0 +1,251 @@
name: Build Docker Image
on:
workflow_dispatch:
push:
branches:
- main
tags:
- "v*"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
IMAGE: ${{ github.repository }}
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
mode:
runs-on: ubuntu-latest
outputs:
nightly: ${{ steps.mode.outputs.nightly }}
release: ${{ steps.mode.outputs.release }}
matrix: ${{ steps.mode.outputs.matrix }}
steps:
- name: Determine publishing mode
id: mode
run: |
if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" == "refs/heads/main" ]]; then
echo "nightly=true" >> "$GITHUB_OUTPUT"
echo "release=false" >> "$GITHUB_OUTPUT"
echo 'matrix={"include":[{"platform":"linux/amd64","runner":"ubuntu-latest","arch":"amd64"}]}' >> "$GITHUB_OUTPUT"
else
echo "nightly=false" >> "$GITHUB_OUTPUT"
echo "release=true" >> "$GITHUB_OUTPUT"
echo 'matrix={"include":[{"platform":"linux/amd64","runner":"ubuntu-latest","arch":"amd64"},{"platform":"linux/arm64","runner":"ubuntu-24.04-arm","arch":"arm64"}]}' >> "$GITHUB_OUTPUT"
fi
build:
needs: mode
strategy:
fail-fast: false
matrix: ${{ fromJSON(needs.mode.outputs.matrix) }}
runs-on: ${{ matrix.runner }}
timeout-minutes: 30
permissions:
contents: read
packages: write
id-token: write
attestations: write
steps:
- name: Checkout Repository
uses: actions/checkout@v6
- name: Get version from package.json
id: version
run: echo "version=$(jq -r .version package.json)" >> "$GITHUB_OUTPUT"
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Login to Docker Hub
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v6
with:
images: |
ghcr.io/${{ env.IMAGE }}
docker.io/${{ env.IMAGE }}
tags: |
type=sha,prefix=sha-,suffix=-${{ matrix.arch }}
- name: Build and Push by Digest
id: build
uses: docker/build-push-action@v7
with:
context: .
sbom: true
push: true
provenance: mode=max
platforms: ${{ matrix.platform }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
annotations: ${{ steps.meta.outputs.annotations }}
cache-from: type=gha,scope=${{ env.IMAGE }}-${{ matrix.arch }}
cache-to: type=gha,mode=max,scope=${{ env.IMAGE }}-${{ matrix.arch }}
- name: Export digest
run: |
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@v7
with:
name: digests-${{ matrix.arch }}
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1
merge:
needs:
- mode
- build
timeout-minutes: 30
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
id-token: write
attestations: write
steps:
- name: Checkout Repository
uses: actions/checkout@v6
with:
sparse-checkout: package.json
sparse-checkout-cone-mode: false
- name: Get version from package.json
id: version
run: echo "version=$(jq -r .version package.json)" >> "$GITHUB_OUTPUT"
- name: Download digests
uses: actions/download-artifact@v8
with:
path: /tmp/digests
pattern: digests-*
merge-multiple: true
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Login to Docker Hub
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Parse version components
id: semver
run: |
VERSION="${{ steps.version.outputs.version }}"
MAJOR=$(echo "$VERSION" | cut -d. -f1)
MINOR=$(echo "$VERSION" | cut -d. -f2)
echo "major=$MAJOR" >> "$GITHUB_OUTPUT"
echo "minor=$MINOR" >> "$GITHUB_OUTPUT"
- name: Extract metadata for Docker
id: meta
uses: docker/metadata-action@v6
with:
images: |
ghcr.io/${{ env.IMAGE }}
docker.io/${{ env.IMAGE }}
tags: |
type=sha,prefix=sha-
type=raw,value=nightly,enable=${{ needs.mode.outputs.nightly == 'true' }}
type=raw,value=nightly-{{date 'YYYYMMDDHHmmss' tz='UTC'}},enable=${{ needs.mode.outputs.nightly == 'true' }}
type=raw,value=latest,enable=${{ needs.mode.outputs.release == 'true' }}
type=raw,value=v${{ steps.version.outputs.version }},enable=${{ needs.mode.outputs.release == 'true' }}
type=raw,value=v${{ steps.semver.outputs.major }}.${{ steps.semver.outputs.minor }},enable=${{ needs.mode.outputs.release == 'true' }}
type=raw,value=v${{ steps.semver.outputs.major }},enable=${{ needs.mode.outputs.release == 'true' }}
- name: Create manifest list and push
id: manifest
working-directory: /tmp/digests
run: |
set -euo pipefail
if [[ "${{ needs.mode.outputs.nightly }}" == "true" ]]; then
FINAL_TAG="nightly"
else
FINAL_TAG="v${{ steps.version.outputs.version }}"
fi
docker buildx imagetools create \
$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
--annotation "index:org.opencontainers.image.licenses=MIT" \
--annotation "index:org.opencontainers.image.title=Reactive Resume" \
--annotation "index:org.opencontainers.image.description=A free and open-source resume builder." \
--annotation "index:org.opencontainers.image.vendor=Amruth Pillai" \
--annotation "index:org.opencontainers.image.url=https://rxresu.me" \
--annotation "index:org.opencontainers.image.documentation=https://docs.rxresu.me" \
--annotation "index:org.opencontainers.image.source=https://github.com/amruthpillai/reactive-resume" \
--annotation "index:org.opencontainers.image.version=${{ steps.version.outputs.version }}" \
$(printf 'ghcr.io/${{ env.IMAGE }}@sha256:%s ' *) \
$(printf 'docker.io/${{ env.IMAGE }}@sha256:%s ' *)
# Get the digest of the multi-arch manifest
GHCR_DIGEST=$(docker buildx imagetools inspect ghcr.io/${{ env.IMAGE }}:${FINAL_TAG} --format '{{json .Manifest.Digest}}' | tr -d '"')
DOCKER_DIGEST=$(docker buildx imagetools inspect docker.io/${{ env.IMAGE }}:${FINAL_TAG} --format '{{json .Manifest.Digest}}' | tr -d '"')
echo "final_tag=$FINAL_TAG" >> "$GITHUB_OUTPUT"
echo "ghcr_digest=$GHCR_DIGEST" >> "$GITHUB_OUTPUT"
echo "docker_digest=$DOCKER_DIGEST" >> "$GITHUB_OUTPUT"
- name: Install Cosign
uses: sigstore/cosign-installer@v3
- name: Sign images with Cosign
run: |
# Sign GHCR image
cosign sign --yes ghcr.io/${{ env.IMAGE }}@${{ steps.manifest.outputs.ghcr_digest }}
# Sign Docker Hub image
cosign sign --yes docker.io/${{ env.IMAGE }}@${{ steps.manifest.outputs.docker_digest }}
- name: Inspect image
run: |
docker buildx imagetools inspect ghcr.io/${{ env.IMAGE }}:${{ steps.manifest.outputs.final_tag }}
docker buildx imagetools inspect docker.io/${{ env.IMAGE }}:${{ steps.manifest.outputs.final_tag }}
- name: Redeploy Stack
if: ${{ needs.mode.outputs.release == 'true' }}
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
-48
View File
@@ -1,48 +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: Format
run: pnpm run format
- name: Test
run: pnpm run test
- name: Build
run: pnpm run build
-167
View File
@@ -1,167 +0,0 @@
name: Publish Docker Image
on:
workflow_dispatch:
push:
tags:
- "*"
concurrency:
group: "${{ github.workflow }}-${{ github.ref }}"
cancel-in-progress: true
env:
IMAGE: amruthpillai/reactive-resume
jobs:
build:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
strategy:
matrix:
platform:
- linux/amd64
- linux/arm64
steps:
- name: Checkout Repository
uses: actions/checkout@v4.1.4
- name: Extract version from package.json
id: version
run: echo "version=$(jq -r '.version' package.json)" >> "$GITHUB_OUTPUT"
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.0.0
- name: Login to Docker Hub
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.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.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.3.0
id: build
with:
context: .
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: |
NX_CLOUD_ACCESS_TOKEN=${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
- name: Export Digest
run: |
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"
- name: Upload Digest
uses: actions/upload-artifact@v4.3.3
with:
name: digests-${{ steps.artifact_name.outputs.name }}
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1
merge:
runs-on: ubuntu-latest
needs:
- build
steps:
- name: Checkout Repository
uses: actions/checkout@v4.1.4
- name: Download Digest
uses: actions/download-artifact@v4.1.7
with:
path: /tmp/digests
pattern: digests-*
merge-multiple: true
- name: Login to Docker Hub
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.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.5.1
with:
tags: type=semver,pattern={{version}},prefix=v,value=${{ needs.build.outputs.version }}
images: |
${{ env.IMAGE }}
ghcr.io/${{ env.IMAGE }}
- name: Create Docker Manifest List and Push
working-directory: /tmp/digests
run: |
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ env.IMAGE }}@sha256:%s ' *)
- name: Inspect Image
run: |
docker buildx imagetools inspect ${{ env.IMAGE }}:${{ steps.meta.outputs.version }}
- name: Update Repository Description
uses: peter-evans/dockerhub-description@v4.0.0
with:
repository: ${{ github.repository }}
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}
- name: Deploy the latest image on rxresu.me
run: curl -kX POST ${{ secrets.SERVICE_WEBHOOK }}
- name: Inform about the release on Discord
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"
@@ -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 }}
+45 -40
View File
@@ -1,53 +1,58 @@
# Compiled Output
dist
tmp
/out-tsc
# Project Dependencies
# Dependencies
node_modules
.pnpm-store
# Build Outputs
dist
.vercel
.wrangler
# Environment Variables
.env*
!.env.example
# IDEs and Editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vs/*
*~
*.swp
*.swo
.DS_Store
.idea
.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
# Log Files
logs
*.log*
# System Files
.DS_Store
Thumbs.db
# Generated Files
.nx
.swc
# Turborepo
.turbo
fly.toml
stats.html
# Environment Variables
*.env*
!.env.example
# TanStack Start
.tanstack
# Lingui Compiled Messages
apps/client/src/locales/_build/
apps/client/src/locales/*/messages.*js
# Testing
coverage
reports
# Cache
tmp
temp
.cache
# AI
.codex
.agents
.claude
.cursor
.codegraph
.superpowers
# Local Storage Data
/data
/apps/web/data
# Git Hooks
.vite-hooks/
+17
View File
@@ -0,0 +1,17 @@
// @ts-check
const betaPackages = ["drizzle-zod"];
const rcPackages = ["drizzle-orm", "drizzle-kit"];
/** @type {import('npm-check-updates').RunOptions} */
module.exports = {
upgrade: true,
workspaces: true,
install: "always",
packageManager: "pnpm",
target: (packageName) => {
if (betaPackages.includes(packageName)) return "@beta";
if (rcPackages.includes(packageName)) return "@rc";
return "latest";
},
};
-16
View File
@@ -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"
]
}
-3
View File
@@ -1,3 +0,0 @@
auto-install-peers=true
enable-pre-post-scripts=true
strict-peer-dependencies=false
-7
View File
@@ -1,7 +0,0 @@
/dist
/coverage
/.nx/cache
stats.html
pnpm-lock.yaml
compose-dev.yml
compose.yml
-6
View File
@@ -1,6 +0,0 @@
{
"printWidth": 100,
"endOfLine": "auto",
"plugins": ["prettier-plugin-tailwindcss"],
"tailwindFunctions": ["cn", "cva"]
}
+1 -1
View File
@@ -1,3 +1,3 @@
{
"recommendations": ["nrwl.angular-console", "esbenp.prettier-vscode", "dbaeumer.vscode-eslint"]
"recommendations": ["biomejs.biome", "bradlc.vscode-tailwindcss", "typescriptteam.native-preview"]
}
+29 -7
View File
@@ -1,9 +1,31 @@
{
"css.validate": false,
"i18n-ally.localesPaths": ["apps/client/src/locales"],
"tailwindCSS.experimental.classRegex": [
["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"],
["cn\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"]
],
"typescript.tsdk": "node_modules/typescript/lib"
"biome.enabled": true,
"editor.codeActionsOnSave": {
"source.fixAll.biome": "explicit",
"source.organizeImports.biome": "explicit"
},
"editor.defaultFormatter": "biomejs.biome",
"files.readonlyInclude": {
"**/locales/**.po": true,
"**/routeTree.gen.ts": true,
"pnpm-lock.yaml": true
},
"files.watcherExclude": {
"**/locales/**.po": true,
"**/routeTree.gen.ts": true,
"pnpm-lock.yaml": true
},
"search.exclude": {
"**/locales/**.po": true,
"**/routeTree.gen.ts": true,
"pnpm-lock.yaml": true
},
"tailwindCSS.classFunctions": ["cn", "cva"],
"tailwindCSS.experimental.classRegex": [
["class:\\s*?[\"'`]([^\"'`]*).*?,"],
["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"],
["cn\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"]
],
"tailwindCSS.experimental.configFile": "src/styles/globals.css",
"typescript.experimental.useTsgo": true
}
+142
View File
@@ -0,0 +1,142 @@
# AGENTS.md
## Cursor Cloud specific instructions
### Overview
Reactive Resume is a pnpm monorepo (Turborepo) with two deployable apps: `apps/web` (TanStack Start / React 19 / Vite) and `apps/server` (Hono / Node.js). The production Docker image runs a single Node.js process on port 3000, with `apps/server` mounting the API/auth/MCP/static routes and serving the built web app.
Internal packages are source-consumed through `package.json` export maps that point at `src` files. Do not assume package-local `dist` output exists unless a package explicitly adds it.
### Prerequisites
- **Node.js 24** (matches Dockerfile `ARG NODE_VERSION=24`). Use `nvm install 24 && nvm use 24` if needed.
- **Docker** is required to run PostgreSQL. Start it with `sudo dockerd &` if the daemon isn't running.
- **pnpm 11.1.2** is managed via corepack (`corepack enable`).
### Codebase map
- `apps/web` owns TanStack Start routes, Vite config, PWA setup, oRPC browser client wiring, web features, and the resume builder UI.
- `apps/server` owns the production Hono app, route composition, auth/RPC/MCP/OpenAPI handlers, static uploads, schema JSON, web-dist fallback serving, and startup checks.
- `packages/api` contains oRPC routers, DTOs, rate limiting, and feature-owned API modules under `packages/api/src/features/*`. The router export at `@reactive-resume/api/routers` aggregates those feature routers for `/api/rpc`.
- `packages/auth` contains Better Auth config, auth helper functions, and exported auth types. The server auth adapter in `apps/server/src/http/auth.ts` delegates to `auth.handler`.
- `packages/db` contains the Drizzle client and schema. Migration files live at the repo root in `migrations/`.
- `packages/env` defines server environment validation and auto-loads the root `.env` for app/server code.
- `packages/schema` contains Zod schemas and typed resume/page/template models.
- `packages/pdf` contains the React PDF document, font registration, shared template primitives, template implementations, and browser/server PDF generation adapters. PDF.js viewer UI stays in `apps/web`.
- `packages/resume` contains pure resume-domain behavior such as JSON Patch helpers and social-network icon mapping.
- `packages/docx` contains DOCX export generation.
- `packages/mcp` contains MCP tools, prompts, resources, server-card generation, and tool metadata.
- `packages/ui` contains shared Base UI/shadcn-style components and hooks.
- `packages/fonts`, `packages/email`, `packages/import`, `packages/ai`, `packages/utils`, and `packages/config` provide focused support surfaces. Prefer their existing exports over adding cross-package shortcuts.
- Development-only scripts live in `tooling/`, not under `packages/`, so packages only contain code bundled by the app/runtime.
### Web app conventions
- Routes are file-based under `apps/web/src/routes`. Do not hand-edit `apps/web/src/routeTree.gen.ts`; it is generated by TanStack Router tooling.
- Server-owned HTTP behavior lives in `apps/server/src/{http,rpc,mcp,openapi,static,startup}`. Keep API/RPC/auth/MCP/static route wiring in `apps/server`, not in web routes.
- `apps/web/src/router.tsx` initializes router context with `queryClient`, `orpc`, `theme`, `locale`, `session`, and `flags`. Reuse route context where possible instead of refetching these concerns ad hoc.
- The builder shell lives under `apps/web/src/routes/builder/$resumeId`. The nested preview route is client-only (`ssr: false`), while the public resume route `apps/web/src/routes/$username/$slug.tsx` uses `ssr: "data-only"`.
- Browser-only resume preview code lives under `apps/web/src/features/resume/preview`, and public resume PDF viewer code lives under `apps/web/src/features/resume/public`. Keep PDF.js/canvas/browser APIs out of SSR paths and out of `packages/pdf`.
- The isomorphic oRPC client is in `apps/web/src/libs/orpc/client.ts`; server calls use an in-process router client and browser calls use `/api/rpc` with credentials included.
### Package and feature boundaries
- Workspace dependencies must go through package names and package export maps. Do not import another workspace's `src` tree through repository paths, `@reactive-resume/*/src/*`, or TypeScript path aliases.
- `turbo boundaries` is the executable package-boundary check. Workspace-level `turbo.json` files declare coarse tags:
- `app:web` for the TanStack Start app.
- `app:server` and `runtime:server` for the Node/Hono process.
- `runtime:server` for server-only packages such as API/auth/db/env/email/MCP.
- `runtime:browser` for browser-only shared UI.
- `runtime:universal` for environment-neutral domain packages.
- `role:domain`, `role:infra`, `role:adapter`, `role:api`, `role:rendering`, and `role:tooling` for package intent.
- Browser/server runtime-specific code should live behind explicit export subpaths such as `@reactive-resume/pdf/browser`, `@reactive-resume/pdf/server`, or `@reactive-resume/env/server`. Keep root exports environment-neutral unless the package is intentionally server-only.
- Wildcard exports are allowed only for leaf libraries whose public surface is intentionally file-like, currently `@reactive-resume/ui/components/*`, `@reactive-resume/ui/hooks/*`, and schema resume model files. Prefer explicit exports for packages that own runtime behavior.
- Add new API procedures and business logic inside the owning `packages/api/src/features/*` module. Keep route wiring, DTO usage, helpers, and services colocated by feature/capability, then expose only intentional public surfaces through `packages/api/package.json`. Prefer `protectedProcedure` from `packages/api/src/context.ts` for authenticated procedures.
- Add database columns/tables in `packages/db/src/schema/*`, then generate root-level migrations with `dotenvx run -f .env.local -- pnpm db:generate`.
- Add or change resume data shape in `packages/schema/src/resume/*` first, then update API DTOs, importers, PDF rendering, and web forms that consume that shape.
- Add or rename templates in all relevant places: `packages/schema/src/templates.ts`, `packages/pdf/src/templates/index.ts`, template source under `packages/pdf/src/templates/<name>/`, and static previews under `apps/web/public/templates/{jpg,pdf}`.
- Resume JSON Patch behavior belongs in `@reactive-resume/resume/patch`; do not put resume-domain helpers in `@reactive-resume/utils`.
- DOCX export behavior belongs in `@reactive-resume/docx`; do not put DOCX builders in `@reactive-resume/utils`.
- Shared PDF section filtering lives in `packages/pdf/src/templates/shared/filtering.ts`. Keep template-specific visual exceptions in the owning template directory unless multiple templates need the same behavior.
- `packages/pdf/src/hooks/use-register-fonts.ts` owns React PDF font registration, standard PDF font handling, CJK fallback stacks, and global hyphenation behavior.
- PDF generation helpers live behind `@reactive-resume/pdf/browser` and `@reactive-resume/pdf/server`; locale-specific section-title resolution stays in the caller.
- MCP implementation belongs in `@reactive-resume/mcp`; app packages must not import MCP implementation from another app's source tree.
- `packages/utils` has narrowly exported helpers. If another package needs a utility, add an explicit export path instead of importing private files.
Placement decision tree:
1. If the change is a web route, route loader, or user-facing web workflow, start in `apps/web/src/routes` or `apps/web/src/features`.
2. If the change is a server HTTP route/adapter, startup check, static handler, MCP transport, or OpenAPI/well-known handler, start in `apps/server/src`.
3. If it is authenticated API behavior, put the contract and implementation in the owning `packages/api/src/features/*` module.
4. If it is pure resume data behavior with no DB, HTTP, DOM, or PDF renderer dependency, put it in `packages/resume`.
5. If it renders resume PDFs, put shared React PDF/template code in `packages/pdf`; put PDF.js viewer/canvas UI in `apps/web/src/features/resume`.
6. If it creates DOCX exports, put it in `packages/docx`.
7. If it exposes MCP tools/prompts/resources, put it in `packages/mcp`.
8. If it is a generic UI primitive or hook, put it in `packages/ui`; if it is workflow-specific UI, keep it in the owning web feature.
9. If it is a narrow cross-cutting helper, add an explicit `packages/utils` export only after checking that no domain package is a better owner.
### Database
PostgreSQL runs via Docker Compose:
```
sudo docker compose -f compose.dev.yml up -d postgres
```
The dev default connection string is `postgresql://postgres:postgres@localhost:5432/postgres`.
**Important**: `drizzle-kit` (used by `pnpm db:migrate`) reads `DATABASE_URL` from `process.env` directly — it does **not** auto-load the `.env` file. Run migration commands through `dotenvx`, for example `dotenvx run -f .env.local -- pnpm db:migrate`, so `DATABASE_URL` is present in the process environment.
The production server runs migrations during startup before serving traffic. Manual `pnpm db:migrate` is mainly for first setup, migration debugging, or applying migrations without starting the app.
### Environment
Copy `.env.example` to `.env`. The three required variables are:
- `APP_URL` (default `http://localhost:3000`)
- `DATABASE_URL` (default `postgresql://postgres:postgres@localhost:5432/postgres`)
- `AUTH_SECRET` (any non-empty string)
S3/SeaweedFS is optional. If `S3_ACCESS_KEY_ID`, `S3_SECRET_ACCESS_KEY`, and `S3_BUCKET` are all set, the app uses S3-compatible storage. The checked-in `.env.example` sets SeaweedFS defaults, so either start the `seaweedfs` compose service too or comment out those S3 vars to use local filesystem storage under `<workspace>/data`. `LOCAL_STORAGE_PATH` must be absolute when set.
When running dev servers or migration commands, prefix the command with `dotenvx run -f .env.local --`. For example: `dotenvx run -f .env.local -- pnpm dev`. Tests, typechecks, linters, boundary checks, and `pnpm build` do not need this prefix by default. If one of those commands fails because a specific environment variable is required, rerun it with the `dotenvx run -f .env.local --` prefix.
### Common commands
| Task | Command |
|------|---------|
| Install deps | `pnpm install` |
| Start Postgres only | `sudo docker compose -f compose.dev.yml up -d postgres` |
| Start Postgres + SeaweedFS | `sudo docker compose -f compose.dev.yml up -d postgres seaweedfs seaweedfs_create_bucket` |
| Generate migrations | `dotenvx run -f .env.local -- pnpm db:generate` |
| Run migrations | `dotenvx run -f .env.local -- pnpm db:migrate` |
| Dev server | `dotenvx run -f .env.local -- pnpm dev` (starts on port 3000) |
| Web dev server only | `dotenvx run -f .env.local -- pnpm dev:web` |
| Lint/format | `pnpm check` (Biome) |
| Boundary check | `pnpm exec turbo boundaries` |
| Tests | `pnpm test` (Vitest) |
| Build | `pnpm build` |
| Typecheck | `pnpm typecheck` |
For focused validation, prefer package filters before repo-wide commands, for example:
```
pnpm --filter web typecheck
pnpm --filter @reactive-resume/pdf test
pnpm --filter @reactive-resume/api test
pnpm exec turbo boundaries
```
Vitest test paths are package-relative when running through `pnpm --filter <package> test -- <path>`.
### Gotchas
- The server startup path auto-runs migrations before serving traffic, so `pnpm db:migrate` is mainly needed for first-time setup, migration debugging, or applying migrations without starting the app.
- Email sending requires SMTP config; without it, emails are logged to console. This is fine for dev — the app still functions, but email verification links appear in server logs.
- The `lefthook.yml` pre-commit hook runs `biome check` on staged files. Run `pnpm check` before committing to avoid hook failures.
- `pnpm check` is write-capable (`biome check --write --unsafe .`). Call that out when using it, and use narrower Biome commands if you need a non-mutating inspection.
- Biome uses tabs, double quotes, line width 120, organized import groups, and sorted Tailwind classes for `clsx`, `cva`, and `cn`.
- Most packages use `tsgo --noEmit` for typechecking and `vitest run --passWithNoTests` for tests.
- There may be unrelated local edits in the worktree. Inspect `git status --short` first and avoid reverting files you did not touch.
- **New env vars require a `turbo.json` entry.** Turborepo 2.x runs in strict env mode by default — it filters out env vars that are not listed in `globalEnv` (or task-level `env`/`passThroughEnv`). Any new environment variable added to `packages/env/src/server.ts` must also be added to the `globalEnv` array in `turbo.json`, or the variable will be `undefined` inside child processes at runtime even if it is correctly set in the OS/container environment.
-19
View File
@@ -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).
Symlink
+1
View File
@@ -0,0 +1 @@
AGENTS.md
-132
View File
@@ -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
-85
View File
@@ -1,85 +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.
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.
+347
View File
@@ -0,0 +1,347 @@
---
version: alpha
name: Reactive Resume
description: A monochrome, content-first design system for a free and open-source resume builder. Dark-by-default with light mode support.
colors:
primary: "#343434"
primary-foreground: "#FBFBFB"
secondary: "#F7F7F7"
secondary-foreground: "#343434"
background: "#FFFFFF"
foreground: "#252525"
muted: "#F7F7F7"
muted-foreground: "#8E8E8E"
card: "#FFFFFF"
card-foreground: "#252525"
border: "#EBEBEB"
input: "#EBEBEB"
ring: "#B5B5B5"
destructive: "#DC2626"
on-destructive: "#FFFFFF"
typography:
heading:
fontFamily: IBM Plex Sans Variable
fontSize: 1rem
fontWeight: 500
body:
fontFamily: IBM Plex Sans Variable
fontSize: 0.875rem
fontWeight: 400
body-sm:
fontFamily: IBM Plex Sans Variable
fontSize: 0.75rem
fontWeight: 400
label:
fontFamily: IBM Plex Sans Variable
fontSize: 0.8rem
fontWeight: 500
hero-heading:
fontFamily: IBM Plex Sans Variable
fontSize: 3.75rem
fontWeight: 700
letterSpacing: -0.025em
rounded:
sm: 0.18rem
md: 0.24rem
lg: 0.3rem
xl: 0.42rem
2xl: 0.54rem
3xl: 0.66rem
4xl: 0.78rem
spacing:
xs: 4px
sm: 8px
md: 16px
lg: 24px
xl: 32px
2xl: 48px
components:
button-default:
backgroundColor: "{colors.primary}"
textColor: "{colors.primary-foreground}"
rounded: "{rounded.lg}"
padding: 10px
height: 36px
button-outline:
backgroundColor: "{colors.background}"
textColor: "{colors.foreground}"
rounded: "{rounded.lg}"
padding: 10px
height: 36px
button-secondary:
backgroundColor: "{colors.secondary}"
textColor: "{colors.secondary-foreground}"
rounded: "{rounded.lg}"
padding: 10px
height: 36px
button-ghost:
backgroundColor: "{colors.background}"
textColor: "{colors.foreground}"
rounded: "{rounded.lg}"
padding: 10px
height: 36px
button-destructive:
backgroundColor: "{colors.destructive}"
textColor: "{colors.on-destructive}"
rounded: "{rounded.lg}"
padding: 10px
height: 36px
card:
backgroundColor: "{colors.card}"
textColor: "{colors.card-foreground}"
rounded: "{rounded.lg}"
padding: 16px
input:
backgroundColor: "{colors.background}"
textColor: "{colors.foreground}"
rounded: "{rounded.lg}"
height: 36px
padding: 10px
input-focus:
backgroundColor: "{colors.background}"
textColor: "{colors.foreground}"
rounded: "{rounded.lg}"
height: 36px
padding: 10px
badge:
backgroundColor: "{colors.primary}"
textColor: "{colors.primary-foreground}"
rounded: "{rounded.md}"
padding: 4px
popover:
backgroundColor: "{colors.card}"
textColor: "{colors.card-foreground}"
rounded: "{rounded.xl}"
padding: 4px
sidebar:
backgroundColor: "{colors.muted}"
textColor: "{colors.foreground}"
padding: 8px
sidebar-item:
backgroundColor: "{colors.muted}"
textColor: "{colors.muted-foreground}"
rounded: "{rounded.lg}"
padding: 8px
sidebar-item-active:
backgroundColor: "{colors.primary}"
textColor: "{colors.primary-foreground}"
rounded: "{rounded.lg}"
padding: 8px
tooltip:
backgroundColor: "{colors.primary}"
textColor: "{colors.primary-foreground}"
rounded: "{rounded.md}"
padding: 6px
separator:
backgroundColor: "{colors.border}"
height: 1px
dialog:
backgroundColor: "{colors.card}"
textColor: "{colors.card-foreground}"
rounded: "{rounded.xl}"
padding: 24px
input-invalid:
backgroundColor: "{colors.background}"
textColor: "{colors.destructive}"
rounded: "{rounded.lg}"
height: 36px
padding: 10px
---
## Overview
Reactive Resume is a monochrome, content-first design system built for a resume builder used by tens of thousands of people worldwide. The visual identity prioritizes readability and unobtrusiveness — the user's resume content is always the hero, never the chrome around it.
The system defaults to dark mode with a warm near-black backdrop that makes the resume preview "float" as the visual anchor. Light mode is supported as a full alternative. The authenticated app shell (dashboard, builder, settings) uses an entirely achromatic grayscale palette — the sole chromatic exception is destructive red for dangerous actions. The landing page introduces subtle chromatic accents: blue-tinted spotlight gradients on the hero, a multicolor text-mask animation on hover, and social auth provider brand colors (Google blue, LinkedIn blue) on the login page.
The overall aesthetic is a professional tool UI: clean grid lines, subtle borders, generous whitespace, and typography that steps back to let the content shine. Think "VS Code meets Figma" — a productivity workspace, not a marketing site.
One deliberate counterpoint to the serious UI: all resume templates are named after Pokemon (Azurill, Bronzor, Chikorita, Ditgar, Gengar, Pikachu, etc.). This is an intentional brand choice — playful naming for templates injects personality into an otherwise utilitarian interface, making templates feel collectible and memorable rather than generic ("Template 1", "Modern", "Classic").
## Colors
The palette is rooted in achromatic OKLch values (chroma = 0), producing a pure grayscale scale without warm or cool casts. Colors are defined as CSS custom properties using `oklch()` and consumed through Tailwind CSS 4 theme tokens. Always prefer CSS variables (e.g., `var(--primary)`) or Tailwind tokens (e.g., `bg-primary`) over raw color values. The hex values in this document's YAML front matter are agent-friendly approximations of the canonical OKLch definitions in `packages/ui/src/styles/globals.css` — use hex only where OKLch is unavailable.
- **Primary (#343434 light / #EBEBEB dark):** Used for high-emphasis interactive surfaces — default buttons, selected states, and text selection. In dark mode this inverts to near-white so buttons remain prominent.
- **Foreground (#252525 light / #FBFBFB dark):** Body text and headings. High contrast against the background in both themes.
- **Background (#FFFFFF light / #252525 dark):** The canvas. Pure white in light mode, warm near-black in dark mode.
- **Card (#FFFFFF light / #343434 dark):** Elevated surface for cards, panels, and the builder sidebar. In dark mode, one step lighter than the background to create subtle depth.
- **Muted (#F7F7F7 light / #454545 dark):** De-emphasized backgrounds for secondary UI regions, hover states, and inactive tabs.
- **Muted Foreground (#8E8E8E light / #B5B5B5 dark):** Captions, helper text, timestamps, and metadata. Deliberately low-contrast against the background to recede visually.
- **Border (#EBEBEB light / white at 10% opacity dark):** Thin separator lines. In dark mode, uses transparent white rather than a solid gray to blend naturally with any underlying surface color.
- **Input (#EBEBEB light / white at 15% opacity dark):** Form field borders, slightly more prominent than general borders to make input areas discoverable.
- **Destructive (#DC2626 light / #EF4444 dark):** The only chromatic color in the palette. Reserved exclusively for delete actions, error states, and danger-zone operations. Used at 10% opacity as a background tint with full saturation for text, creating a soft but unmistakable warning.
- **Ring (#B5B5B5 light / #8E8E8E dark):** Focus ring indicator at 50% opacity, surrounding focused interactive elements.
- **Sidebar Primary (dark only, #6366F1):** An indigo value inherited from the shadcn/ui defaults. Not actively used in the current UI — sidebar active states use the standard grayscale primary token instead. Retained in the CSS custom properties for potential future customization.
Resume templates have their own independent color system — users pick primary, text, and background colors per resume through a color picker in the builder's Design panel. These template colors are completely separate from the app shell palette.
## Typography
The entire application uses a single typeface: **IBM Plex Sans Variable**. This is a humanist sans-serif with an extensive weight range (100–900) and excellent readability at small sizes, both on screen and in PDFs.
- **Hero heading (responsive: 2.25rem mobile / 3rem tablet / 3.75rem desktop, weight 700, tracking-tight):** Landing page headline only. Large, bold, and commanding. Scales across three breakpoints.
- **Section heading (1rem / 16px, weight 500):** Used for section titles in the builder sidebar, settings panels, and dashboard cards. Medium weight provides hierarchy without shouting.
- **Body (0.875rem / 14px, weight 400):** The workhorse. All form labels, descriptions, card content, and general UI text.
- **Small body (0.75rem / 12px, weight 400):** Captions, helper text, timestamps, and metadata.
- **Label (0.8rem / ~13px, weight 500):** Button text, badge labels, and form field labels. Slightly heavier than body to denote interactivity.
The resume content itself uses a separate font system — users choose from 1,000+ Google Fonts for their resume headings and body text, with category-aware fallback stacks including CJK support (Noto Sans SC, PingFang SC, Hiragino Sans GB for sans-serif; Noto Serif SC, Songti SC for serif). Standard PDF fonts (Helvetica, Courier, Times-Roman) are available as offline fallbacks.
Font rendering uses `antialiased` (grayscale AA) and `proportional-nums` across the board for clean rendering and properly spaced numerals in dates and phone numbers.
## Layout
### Builder (Three-Panel Workspace)
The core builder uses a resizable three-panel layout powered by `react-resizable-panels`:
- **Left sidebar (default 22%):** Resume section forms — personal info, experience, education, skills, and custom sections. Scrollable with collapsible section groups.
- **Center artboard (default 56%):** Live resume preview rendered via PDF.js canvas. Supports zoom, pan, and pinch gestures via `react-zoom-pan-pinch`. The preview maintains A4 aspect ratio (210:297) with a subtle shadow to simulate a physical page.
- **Right sidebar (default 22%):** Design controls — template picker, font selection, color picker, layout manager (page assignments, section ordering via drag-and-drop).
Panel sizes persist in cookies. On mobile (< 768px), sidebars collapse to 0% width and become toggleable overlays (max 95% width when open). The desktop minimum collapsed width is 48px (icon rail).
### Dashboard
Standard sidebar navigation layout using the `Sidebar` component system. The sidebar contains: logo, resume list link, agent link, settings subnavigation (profile, preferences, authentication, API keys, integrations, danger zone), and a footer with user avatar. Content area shows a responsive grid of resume cards.
### Landing Page
Full-width single-column marketing layout:
1. **Floating builder preview** — A non-interactive screenshot of the builder as a hero visual, creating an immediate "this is what you get" impression.
2. **Hero** — Centered headline, subheadline, and two CTAs (primary "Get Started" with arrow, ghost "Learn More" with icon).
3. **Features grid** — 4-column responsive grid with icon + title + description cards, separated by thin border lines.
4. **Template carousel** — Horizontally scrolling row of template preview thumbnails with Pokemon-themed names.
5. **Testimonials** — Tiled user quotes in a masonry-style grid.
6. **Support / FAQ / Footer** — Accordion FAQ, community section, and a 4-column footer with logo, resource links, community links, and license info.
### Responsive Breakpoints
Mobile detection uses a 768px threshold via `MediaQueryList`. The layout is optimized for workspace productivity on larger screens, with responsive mobile support that adapts the multi-panel builder into a streamlined single-panel experience. Both desktop and mobile are supported experiences — the builder's three-panel layout leverages desktop space, while mobile surfaces the same editing capabilities through collapsible overlays.
### Page Aspect Ratio
A custom Tailwind token `--aspect-page: 210 / 297` enforces A4 paper proportions wherever resume pages are rendered (builder preview, public view, PDF export).
## Animation
Animations use the Motion library (formerly Framer Motion) and follow a consistent choreography pattern:
**Entrance animations** use a fade-up reveal: elements start at `opacity: 0, y: 20-100` and animate to `opacity: 1, y: 0`. The hero section uses a larger y-offset (100px) for dramatic effect; subsequent sections use 20px for subtlety.
**Timing principles:**
- **Base duration:** 0.35s–0.6s for standard section reveals, 0.45s for hero elements, up to 1.1s for the hero video entrance.
- **Stagger pattern:** Sequential delays within a group, typically 0.1s–0.15s apart (hero: 0.55s, 0.7s, 0.82s, 0.95s). For grids, use `index * 0.03`–`0.1` for per-item stagger.
- **Easing:** `easeOut` for entrances (elements decelerate into position). `easeInOut` for looping/ambient animations.
- **Performance:** Apply `will-change-[transform,opacity]` on animated elements and `will-change-transform` on continuously animated elements.
**Hover/interaction animations** are quick (0.2s) and subtle — small scale bumps (`scale: 1.01`), slight y-offsets (`y: -2`), and `active:translate-y-px` for button press.
**Ambient animations** loop infinitely with `easeInOut` — the scroll indicator bounces gently (`y: [0, 5, 0]` over 1.5s).
**Reduced motion:** All CSS transitions and animations collapse to `0.01ms` duration and single iteration when `prefers-reduced-motion: reduce` is active. Motion library animations should also respect this preference.
## Elevation & Depth
Elevation is handled through background color layering rather than drop shadows:
- **Level 0 — Background:** The base canvas (`--background`).
- **Level 1 — Card:** One step lighter in dark mode (`--card`), used for sidebars, panels, and cards.
- **Level 2 — Popover:** Same as card, but appears above the content layer in popovers, dropdowns, and command palette.
- **Level 3 — Overlay:** Backdrop blur (`backdrop-blur-xs` at 0.5px or `backdrop-blur-2xl` at 40px) with `backdrop-saturate-150` for modal overlays, creating a frosted-glass effect over the workspace.
The resume preview page uses a subtle drop shadow to simulate a physical sheet of paper floating above the dark artboard — one of the few places actual shadows appear.
## Shapes
Border radius follows a multiplicative scale from a single `--radius` base of `0.3rem`:
| Token | Value | Usage |
|:------|:------|:------|
| `sm` | 0.18rem (≈3px) | Small badges, inline chips |
| `md` | 0.24rem (≈4px) | XS/SM buttons, compact elements |
| `lg` | 0.3rem (≈5px) | Default buttons, cards, inputs |
| `xl` | 0.42rem (≈7px) | Larger cards, modal corners |
| `2xl` | 0.54rem (≈9px) | Dialog containers |
| `3xl` | 0.66rem (≈11px) | Large panels |
| `4xl` | 0.78rem (≈12px) | Full-page modals |
The radius scale is deliberately tight — the largest value (0.78rem) is still quite subtle. This avoids the "rounded everything" aesthetic and keeps the UI feeling precise and tool-like. Interactive elements consistently use `rounded-lg` as the default.
## Components
### Buttons
Six variants, all sharing `rounded-lg` corners, `font-medium`, `text-sm`, and a 1px `translate-y` on active press (except when the button opens a popup):
- **Default:** Solid primary background. The highest-emphasis action on any screen.
- **Outline:** Transparent with a border. For secondary actions that need clear boundaries.
- **Secondary:** Muted background. For paired actions alongside a primary button.
- **Ghost:** No background or border. For toolbar actions and inline controls where chrome would be noise.
- **Destructive:** Red at 10% opacity background with red text. Visually alarming without being garish.
- **Link:** Underline-on-hover text. For inline navigation within prose.
Size scale: `xs` (28px), `sm` (32px), `default` (36px), `lg` (40px), plus `icon` variants at each size for square icon-only buttons.
### Cards
White/dark surface with foreground text. Composed of `CardHeader`, `CardTitle`, `CardDescription`, `CardContent`, `CardFooter`, and `CardAction` slots. Default vertical padding is `py-4` (compact: `py-3`).
### Forms
Built on TanStack Form with Zod validation. Composed of `FormItem`, `FormLabel`, `FormControl`, `FormMessage`, and `FormDescription`. Validation errors only appear after field touch. Invalid fields get a red destructive border with a ring.
### Dialogs
Centralized dialog manager with 40+ dialog types, all rendered via pattern matching (`ts-pattern`). Dialogs support before-close validation, form blocking for unsaved changes, and confirmation prompts. Used for all CRUD operations on resume sections, settings changes, and import/export flows.
### Command Palette
Triggered by `Cmd+K` / `Ctrl+K`. Built on `cmdk` with fuzzy search via `Fuse.js`. Multi-page navigation (resumes, settings, preferences) with back navigation via Backspace. Screen-reader accessible with `sr-only` headings.
### Toast Notifications
Powered by Sonner, positioned bottom-right with rich colors. Used for auto-save feedback, form submission status, error reporting, and donation prompts. Loading toasts are used during async operations (PDF generation, resume creation) with dismiss-on-complete.
### Drag and Drop
Powered by `@dnd-kit` with `PointerSensor` and `KeyboardSensor`. Used in chip inputs (skill tags, URL lists) and page layout management (section ordering across resume pages). Smooth animations via Motion library.
## Internationalization
The app supports 40+ locales including RTL languages (Arabic, Hebrew, Persian, Urdu, Uyghur, Yiddish). i18n is not an afterthought — it shapes layout decisions:
**Direction:** The `<html>` element receives `dir="rtl"` or `dir="ltr"` based on the active locale, detected via `isRTL()` which checks the language prefix against a known RTL set. All layout mirroring flows from this single attribute.
**Logical properties:** Use CSS logical properties (`ps-`, `pe-`, `ms-`, `me-`, `inline-start`, `inline-end`, `inset-s-`, `inset-e-`) instead of physical (`pl-`, `pr-`, `ml-`, `mr-`, `left`, `right`). Button components already use `has-data-[icon=inline-start]:ps-2` and `has-data-[icon=inline-end]:pe-2` patterns. This ensures correct spacing in both LTR and RTL layouts without separate stylesheets.
**Variable-length text:** Translations can be 30–50% longer than English (German, Finnish) or significantly shorter (CJK). UI elements should accommodate variable text length — avoid fixed widths on buttons and labels. Use `whitespace-nowrap` only where truncation is acceptable, and prefer `min-w-0` with `truncate` over fixed-width containers.
**Icons:** Directional icons (arrows, chevrons, progress indicators) should mirror in RTL contexts. Phosphor Icons provides mirrored variants for directional icons. Non-directional icons (settings gear, checkmark, delete) do not mirror.
**Strings:** All user-facing strings use Lingui macros (`t`, `msg`, `<Trans>`) — never hardcode English text in components. Translation files are `.po` format under `/locale/`.
## Do's and Don'ts
### Do
- **Use the grayscale palette for all app chrome.** The absence of color is the brand. The resume content is the only thing that should be colorful.
- **Default to dark mode.** The dark workspace makes resume previews pop and reduces eye strain during extended editing sessions.
- **Use `text-sm` (14px) as the base text size.** The UI is information-dense — form fields, section labels, metadata — and needs to be scannable without feeling cramped.
- **Keep border radius tight.** Use `rounded-lg` (0.3rem) as the default. The tool should feel precise, not playful.
- **Respect reduced motion preferences.** All animations collapse to 0.01ms when `prefers-reduced-motion: reduce` is active.
- **Use Phosphor Icons consistently.** Regular weight, `size-4` (16px) default. Icons should be functional labels, not decorative.
- **Maintain the three-panel builder proportions.** The center artboard should always dominate. Sidebars are support panels, not equal peers.
- **Use transparent-white borders in dark mode.** `oklch(1 0 0 / 10%)` blends naturally with any surface rather than introducing a distinct gray band.
### Don't
- **Don't introduce accent colors into the app shell.** No blues, greens, or purples for primary actions. The only chromatic color is destructive red. The inherited indigo sidebar-primary token exists in CSS custom properties but is not actively used.
- **Don't use drop shadows for elevation.** Rely on background color layering and border separation. The one exception is the resume page preview shadow.
- **Don't make the UI compete with the resume content.** If a new feature draws more visual attention than the resume preview, it needs to be toned down.
- **Don't use large border radii.** Nothing above `rounded-xl` on standard components. Large pills and full-round shapes conflict with the precision-tool aesthetic.
- **Don't hardcode colors outside the token system.** All colors flow through CSS custom properties so that dark/light mode switching works automatically.
- **Don't use multiple typefaces in the app shell.** IBM Plex Sans Variable is the only UI font. Resume templates have their own font system, but the chrome stays single-family.
- **Don't skip the `data-slot` attribute on components.** It's used for styling hooks and accessibility selectors throughout the component library.
- **Don't forget RTL.** The app supports 40+ locales including Arabic, Hebrew, Persian, and Urdu. Use logical properties (`ps`, `pe`, `ms`, `me`) instead of physical (`pl`, `pr`, `ml`, `mr`).
+59 -30
View File
@@ -1,47 +1,76 @@
ARG NX_CLOUD_ACCESS_TOKEN
# syntax=docker/dockerfile:1.7
# --- Base Image ---
FROM node:lts-bullseye-slim AS base
ARG NX_CLOUD_ACCESS_TOKEN
ARG NODE_VERSION=24
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
FROM node:${NODE_VERSION}-slim AS base
WORKDIR /app
# --- Build Image ---
FROM base AS build
ARG NX_CLOUD_ACCESS_TOKEN
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
PNPM_HOME="/pnpm" \
PATH="/pnpm:$PATH" \
TURBO_TELEMETRY_DISABLED=1
COPY .npmrc package.json pnpm-lock.yaml ./
COPY ./tools/prisma /app/tools/prisma
RUN pnpm install --frozen-lockfile
RUN corepack enable
FROM base AS pruner
COPY . .
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store,sharing=locked \
pnpm dlx turbo@2.9.12 prune web server --docker
ENV NX_CLOUD_ACCESS_TOKEN=$NX_CLOUD_ACCESS_TOKEN
FROM base AS builder
COPY --from=pruner /app/out/json/ ./
COPY --from=pruner /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store,sharing=locked \
pnpm install --frozen-lockfile
RUN pnpm run build
COPY --from=pruner /app/out/full/ ./
RUN rm -rf apps/web/dist apps/server/dist && pnpm turbo run build --filter=web --filter=server --force
# --- Release Image ---
FROM base AS release
ARG NX_CLOUD_ACCESS_TOKEN
FROM base AS runtime-pruner
COPY . .
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store,sharing=locked \
pnpm dlx turbo@2.9.12 prune server --docker
RUN apt update && apt install -y dumb-init --no-install-recommends && rm -rf /var/lib/apt/lists/*
FROM base AS runtime-deps
COPY --from=runtime-pruner /app/out/json/ ./
COPY --from=runtime-pruner /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store,sharing=locked \
pnpm install --prod --frozen-lockfile
COPY --chown=node:node --from=build /app/.npmrc /app/package.json /app/pnpm-lock.yaml ./
RUN pnpm install --prod --frozen-lockfile
FROM node:${NODE_VERSION}-slim AS runtime
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
LABEL maintainer="amruthpillai"
LABEL org.opencontainers.image.licenses="MIT"
LABEL org.opencontainers.image.title="Reactive Resume"
LABEL org.opencontainers.image.description="A free and open-source resume builder."
LABEL org.opencontainers.image.vendor="Amruth Pillai"
LABEL org.opencontainers.image.url="https://rxresu.me"
LABEL org.opencontainers.image.documentation="https://docs.rxresu.me"
LABEL org.opencontainers.image.source="https://github.com/amruthpillai/reactive-resume"
ENV TZ=UTC
ENV PORT=3000
ENV NODE_ENV=production
ENV NODE_ENV="production" \
PORT=3000 \
LOCAL_STORAGE_PATH=/app/data
EXPOSE 3000
WORKDIR /app
CMD [ "dumb-init", "pnpm", "run", "start" ]
RUN mkdir -p /app/apps/server /app/apps/web /app/data && chown node:node /app/data
COPY --from=runtime-deps --chown=node:node /app/node_modules ./node_modules
COPY --from=pruner --chown=node:node /app/package.json /app/pnpm-lock.yaml /app/pnpm-workspace.yaml ./
COPY --from=runtime-deps --chown=node:node /app/apps/server/package.json ./apps/server/package.json
COPY --from=runtime-deps --chown=node:node /app/apps/server/node_modules ./apps/server/node_modules
COPY --from=builder --chown=node:node /app/apps/web/dist ./apps/web/dist
COPY --from=builder --chown=node:node /app/apps/server/dist ./apps/server/dist
COPY --from=pruner --chown=node:node /app/migrations ./migrations
WORKDIR /app
USER node
EXPOSE 3000/tcp
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
CMD ["node", "-e", "fetch(`http://127.0.0.1:${process.env.PORT ?? 3000}/api/health`).then((r) => { if (!r.ok) process.exit(1); }).catch(() => process.exit(1));"]
CMD ["node", "apps/server/dist/index.mjs"]
+43
View File
@@ -0,0 +1,43 @@
# syntax=docker/dockerfile:1.7
ARG NODE_VERSION=24
FROM node:${NODE_VERSION}-slim AS dev
WORKDIR /app
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
PNPM_HOME="/pnpm" \
PATH="/pnpm:$PATH" \
NODE_ENV=development \
TURBO_TELEMETRY_DISABLED=1
RUN corepack enable
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml turbo.json ./
COPY patches ./patches
COPY apps/server/package.json ./apps/server/package.json
COPY apps/web/package.json ./apps/web/package.json
COPY packages/ai/package.json ./packages/ai/package.json
COPY packages/api/package.json ./packages/api/package.json
COPY packages/auth/package.json ./packages/auth/package.json
COPY packages/config/package.json ./packages/config/package.json
COPY packages/db/package.json ./packages/db/package.json
COPY packages/email/package.json ./packages/email/package.json
COPY packages/env/package.json ./packages/env/package.json
COPY packages/fonts/package.json ./packages/fonts/package.json
COPY packages/import/package.json ./packages/import/package.json
COPY packages/pdf/package.json ./packages/pdf/package.json
COPY packages/schema/package.json ./packages/schema/package.json
COPY packages/ui/package.json ./packages/ui/package.json
COPY packages/utils/package.json ./packages/utils/package.json
COPY tooling/package.json ./tooling/package.json
RUN --mount=type=cache,id=reactive-resume-dev-pnpm-store,target=/pnpm/store,sharing=locked \
pnpm install --frozen-lockfile
COPY . .
EXPOSE 3000/tcp 3001/tcp
CMD ["pnpm", "run", "dev"]
+1 -1
View File
@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2023 Amruth Pillai
Copyright (c) 2026 Amruth Pillai
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+232 -93
View File
@@ -1,112 +1,251 @@
<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" />
<div align="center">
<a href="https://rxresu.me">
<img src="apps/web/public/opengraph/banner.jpg" alt="Reactive Resume" />
</a>
### [Warp, built for coding with multiple AI agents.](https://go.warp.dev/Reactive-Resume)
<h1>Reactive Resume</h1>
[Available for MacOS, Linux, & Windows](https://go.warp.dev/Reactive-Resume)<br>
<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/package-json/v/amruthpillai/reactive-resume?style=flat-square" alt="Reactive Resume Version">
<img src="https://img.shields.io/github/stars/amruthpillai/Reactive-Resume?style=flat-square" alt="GitHub 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/aSyA5ZSxpb"><img src="https://img.shields.io/discord/1173518977851473940?style=flat-square&label=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://github.com/sponsors/AmruthPillai"><img src="https://img.shields.io/github/sponsors/AmruthPillai?style=flat-square&label=sponsors" alt="Sponsors" /></a>
<a href="https://opencollective.com/reactive-resume/donate"><img src="https://img.shields.io/opencollective/backers/reactive-resume?style=flat-square&label=donations" alt="Donations" /></a>
</p>
</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="https://i.imgur.com/jKgo04C.jpeg" width="200px" /> | <img src="https://i.imgur.com/DFNQZP2.jpg" width="200px" /> | <img src="https://i.imgur.com/Dwv8Y7f.jpg" width="200px" /> |
| Ditto | Kakuna | Nosepass |
| ----------------------------------------------------------- | ----------------------------------------------------------- | ----------------------------------------------------------- |
| <img src="https://i.imgur.com/6c5lASL.jpg" width="200px" /> | <img src="https://i.imgur.com/268ML3t.jpg" width="200px" /> | <img src="https://i.imgur.com/npRLsPS.jpg" width="200px" /> |
| Onyx | Pikachu | Rhyhorn |
| ----------------------------------------------------------- | ----------------------------------------------------------- | ----------------------------------------------------------- |
| <img src="https://i.imgur.com/cxplXOW.jpg" width="200px" /> | <img src="https://i.imgur.com/Y9f7qsh.jpg" width="200px" /> | <img src="https://i.imgur.com/h4kQxy2.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, DOCX)
- 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**
- Professionally designed templates
- A4 and Letter size support
- Customizable colors, fonts, and spacing
- Custom CSS for advanced styling
**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="apps/web/public/templates/jpg/azurill.jpg" alt="Azurill" width="150" />
<br /><sub><b>Azurill</b></sub>
</td>
<td align="center">
<img src="apps/web/public/templates/jpg/bronzor.jpg" alt="Bronzor" width="150" />
<br /><sub><b>Bronzor</b></sub>
</td>
<td align="center">
<img src="apps/web/public/templates/jpg/chikorita.jpg" alt="Chikorita" width="150" />
<br /><sub><b>Chikorita</b></sub>
</td>
<td align="center">
<img src="apps/web/public/templates/jpg/ditto.jpg" alt="Ditto" width="150" />
<br /><sub><b>Ditto</b></sub>
</td>
</tr>
<tr>
<td align="center">
<img src="apps/web/public/templates/jpg/gengar.jpg" alt="Gengar" width="150" />
<br /><sub><b>Gengar</b></sub>
</td>
<td align="center">
<img src="apps/web/public/templates/jpg/glalie.jpg" alt="Glalie" width="150" />
<br /><sub><b>Glalie</b></sub>
</td>
<td align="center">
<img src="apps/web/public/templates/jpg/kakuna.jpg" alt="Kakuna" width="150" />
<br /><sub><b>Kakuna</b></sub>
</td>
<td align="center">
<img src="apps/web/public/templates/jpg/lapras.jpg" alt="Lapras" width="150" />
<br /><sub><b>Lapras</b></sub>
</td>
</tr>
<tr>
<td align="center">
<img src="apps/web/public/templates/jpg/leafish.jpg" alt="Leafish" width="150" />
<br /><sub><b>Leafish</b></sub>
</td>
<td align="center">
<img src="apps/web/public/templates/jpg/onyx.jpg" alt="Onyx" width="150" />
<br /><sub><b>Onyx</b></sub>
</td>
<td align="center">
<img src="apps/web/public/templates/jpg/pikachu.jpg" alt="Pikachu" width="150" />
<br /><sub><b>Pikachu</b></sub>
</td>
<td align="center">
<img src="apps/web/public/templates/jpg/rhyhorn.jpg" alt="Rhyhorn" width="150" />
<br /><sub><b>Rhyhorn</b></sub>
</td>
</tr>
<tr>
<td align="center">
<img src="apps/web/public/templates/jpg/ditgar.jpg" alt="Ditgar" width="150" />
<br /><sub><b>Ditgar</b></sub>
</td>
<td align="center">
<img src="apps/web/public/templates/jpg/meowth.jpg" alt="Meowth" width="150" />
<br /><sub><b>Meowth</b></sub>
</td>
<td align="center">
<img src="apps/web/public/templates/jpg/scizor.jpg" alt="Scizor" width="150" />
<br /><sub><b>Scizor</b></sub>
</td>
</tr>
</table>
## Quick Start
The quickest way to run Reactive Resume locally:
```bash
# Clone the repository
git clone --depth=1 https://github.com/amruthpillai/reactive-resume.git
cd reactive-resume
# Start all services
docker compose up -d
# Access the app
open http://localhost:3000
```
[![Build with Ona](https://ona.com/build-with-ona.svg)](https://app.ona.com/#https://github.com/amruthpillai/reactive-resume)
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 | Base UI + shadcn-style package |
| 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/self-hosting/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
- **SeaweedFS** (optional) — S3-compatible storage for file uploads
> **From v5.1.0 onwards** — PDF generation now runs entirely client-side via `@react-pdf/renderer`. New deployments no longer require Browserless, Chromium, or any external print service as a dependency. The `PRINTER_*` and `BROWSERLESS_*` environment variables are no longer read and can be removed from your `.env`.
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/self-hosting/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/donate">
<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
## Star History
<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 href="https://www.star-history.com/#amruthpillai/reactive-resume&type=date&legend=top-left">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=amruthpillai/reactive-resume&type=date&theme=dark&legend=top-left" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=amruthpillai/reactive-resume&type=date&legend=top-left" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=amruthpillai/reactive-resume&type=date&legend=top-left" />
</picture>
</a>
## 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.
+25 -5
View File
@@ -2,13 +2,33 @@
## Supported Versions
Reactive Resume provides security updates for version `5.0.x` and newer.
Earlier major versions are no longer supported.
| Version | Supported |
| ------- | ------------------ |
| 4.x.x | :white_check_mark: |
| 3.x.x | :x: |
| 2.x.x | :x: |
| 1.x.x | :x: |
| >= 5.0 | :white_check_mark: |
| 4.x | :x: |
| < 4.0 | :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.
If you believe you have found a security vulnerability, please report it
privately instead of opening a public issue.
Preferred reporting channels:
- Use GitHub's private vulnerability reporting for this repository.
- If that is unavailable, email `hello@amruthpillai.com` with the subject
`Security Vulnerability: Reactive Resume`.
Please include as much detail as possible, including:
- A description of the vulnerability and its impact.
- Steps to reproduce or a proof of concept.
- Affected versions, configuration, or deployment details.
- Any relevant logs, screenshots, or links.
You can expect an initial response within 7 days. If the report is accepted,
we will coordinate remediation and disclosure timing with you before publishing
details publicly.
-46
View File
@@ -1,46 +0,0 @@
{
"extends": ["plugin:@nx/react", "../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"extends": ["plugin:tailwindcss/recommended"],
"settings": {
"tailwindcss": {
"callees": ["cn", "clsx", "cva"],
"config": "tailwind.config.js"
}
},
"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",
// tailwindcss
"tailwindcss/no-custom-classname": "off"
}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
}
]
}
-44
View File
@@ -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</title>
<meta
name="description"
content="A free and open-source resume builder that simplifies the process of creating, updating, and sharing your resume."
/>
<!-- Meta -->
<meta charset="utf-8" />
<meta name="googlebot" content="notranslate" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- Favicon -->
<link
rel="icon"
type="image/svg+xml"
href="/icon/dark.svg"
media="(prefers-color-scheme: light)"
/>
<link
rel="icon"
type="image/svg+xml"
href="/icon/light.svg"
media="(prefers-color-scheme: dark)"
/>
<!-- Styles -->
<link rel="stylesheet" href="/src/styles/main.css" />
</head>
<body>
<div id="root"></div>
<!-- Scripts -->
<script type="module" src="/src/main.tsx"></script>
<!-- Phosphor Icons -->
<script src="https://cdn.jsdelivr.net/npm/@phosphor-icons/web"></script>
</body>
</html>
-10
View File
@@ -1,10 +0,0 @@
const path = require("node:path");
module.exports = {
plugins: {
tailwindcss: {
config: path.join(__dirname, "tailwind.config.js"),
},
autoprefixer: {},
},
};
-64
View File
@@ -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"]
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" fill="none"><rect width="256" height="256" rx="80" style="fill:#000;stroke:none;stroke-width:.396357"/><g fill="#09090b" fill-rule="evenodd" clip-rule="evenodd"><path d="m282.857 257.192-37.64 50.15 39.21.004 17.95-23.88 17.95 23.876h39.21l-75.11-100.256-39.21-.003zm23.775-31.764 13.696-18.345h39.21l-33.234 44.435zM119.537 135.21v129.485h36.626V230.29h19.993c17.99-.01 40.841-3.946 52.762-21.828 4.686-7.152 7.03-15.6 7.03-25.342 0-9.865-2.344-18.374-7.03-25.527-4.687-7.276-11.346-12.825-19.978-16.648-8.51-3.823-18.37-5.735-30.21-5.735zm90.963 95.183s-14.972 6.285-34.681 6.047l21.162 28.255h39.21zm-54.337-28.405h20.348c7.646 0 13.319-1.665 17.018-4.995 3.823-3.33 5.735-7.954 5.735-13.874 0-6.042-1.912-10.729-5.735-14.058-3.7-3.33-9.372-4.995-17.018-4.995h-20.348z" style="fill:#fafafa;fill-opacity:1;stroke-width:.9375" transform="matrix(.91667 0 0 .91667 -91.576 -74.838)"/></g></svg>

Before

Width:  |  Height:  |  Size: 958 B

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none"><path fill="#09090b" fill-rule="evenodd" d="m16.332 15.592-3.764 5.015h3.921l1.795-2.388 1.795 2.388H24L16.489 10.58h-3.921Zm2.377-3.177 1.37-1.834H24l-3.323 4.443zM0 3.393v12.949h3.663v-3.44h1.999c1.799-.002 4.084-.395 5.276-2.183.469-.716.703-1.56.703-2.535 0-.986-.234-1.837-.703-2.552-.469-.728-1.135-1.283-1.998-1.665-.85-.382-1.837-.574-3.02-.574Zm9.096 9.519s-1.497.628-3.468.604l2.116 2.826h3.921zm-5.433-2.84h2.034c.765 0 1.332-.167 1.702-.5.382-.333.574-.796.574-1.388 0-.604-.192-1.073-.574-1.405-.37-.333-.937-.5-1.702-.5H3.663Z" clip-rule="evenodd" style="stroke-width:.09375"/></svg>

Before

Width:  |  Height:  |  Size: 672 B

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none"><path fill="#09090b" fill-rule="evenodd" d="m16.332 15.592-3.764 5.015h3.921l1.795-2.388 1.795 2.388H24L16.489 10.58h-3.921Zm2.377-3.177 1.37-1.834H24l-3.323 4.443zM0 3.393v12.949h3.663v-3.44h1.999c1.799-.002 4.084-.395 5.276-2.183.469-.716.703-1.56.703-2.535 0-.986-.234-1.837-.703-2.552-.469-.728-1.135-1.283-1.998-1.665-.85-.382-1.837-.574-3.02-.574Zm9.096 9.519s-1.497.628-3.468.604l2.116 2.826h3.921zm-5.433-2.84h2.034c.765 0 1.332-.167 1.702-.5.382-.333.574-.796.574-1.388 0-.604-.192-1.073-.574-1.405-.37-.333-.937-.5-1.702-.5H3.663Z" clip-rule="evenodd" style="stroke-width:.09375;fill:#fafafa;fill-opacity:1"/></svg>

Before

Width:  |  Height:  |  Size: 700 B

View File
@@ -1,24 +0,0 @@
import { forwardRef } from "react";
type BrandIconProps = {
slug: string;
};
export const BrandIcon = forwardRef<HTMLImageElement, BrandIconProps>(({ slug }, ref) => {
if (slug.toLowerCase() === "linkedin") {
return (
<img
ref={ref}
alt="LinkedIn"
className="size-4"
src={`${window.location.origin}/support-logos/linkedin.svg`}
/>
);
}
return (
<img ref={ref} alt={slug} className="size-4" src={`https://cdn.simpleicons.org/${slug}`} />
);
});
BrandIcon.displayName = "BrandIcon";
-48
View File
@@ -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 (
<div
data-page={pageNumber}
className={cn("relative bg-background text-foreground", mode === "builder" && "shadow-2xl")}
style={{
fontFamily,
width: `${pageSizeMap[page.format].width * MM_TO_PX}px`,
minHeight: `${pageSizeMap[page.format].height * MM_TO_PX}px`,
}}
>
{mode === "builder" && page.options.pageNumbers && (
<div className={cn("absolute -top-7 left-0 font-bold", isDarkMode && "text-white")}>
Page {pageNumber}
</div>
)}
{children}
{mode === "builder" && page.options.breakLine && (
<div
className="absolute inset-x-0 border-b border-dashed"
style={{
top: `${pageSizeMap[page.format].height * MM_TO_PX}px`,
}}
/>
)}
</div>
);
};
-33
View File
@@ -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 (
<img
src={picture.url}
alt="Profile"
className={cn(
"relative z-20 object-cover",
picture.effects.border && "border-primary",
picture.effects.grayscale && "grayscale",
className,
)}
style={{
maxWidth: `${picture.size}px`,
aspectRatio: `${picture.aspectRatio}`,
borderRadius: `${picture.borderRadius}px`,
borderWidth: `${picture.effects.border ? fontSize / 3 : 0}px`,
}}
/>
);
};
-5
View File
@@ -1,5 +0,0 @@
import { HelmetData } from "react-helmet-async";
export const helmetData = new HelmetData({});
export const helmetContext = helmetData.context;
-14
View File
@@ -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(
<StrictMode>
<RouterProvider router={router} />
</StrictMode>,
);
-74
View File
@@ -1,74 +0,0 @@
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(() => {
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 (
<>
<Helmet>
<title>{name} | Reactive Resume</title>
{metadata.css.visible && (
<style id="custom-css" lang="css">
{metadata.css.value}
</style>
)}
</Helmet>
<Outlet />
</>
);
};
-84
View File
@@ -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<ReactZoomPanPinchRef>(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 (
<TransformWrapper
ref={transformRef}
centerOnInit
maxScale={2}
minScale={0.4}
initialScale={0.8}
limitToBounds={false}
wheel={{ wheelDisabled: wheelPanning }}
panning={{ wheelPanning: wheelPanning }}
>
<TransformComponent
wrapperClass="!w-screen !h-screen"
contentClass="grid items-start justify-center space-x-12 pointer-events-none"
contentStyle={{
width: `${layout.length * (pageSizeMap[format].width * MM_TO_PX + 42)}px`,
gridTemplateColumns: `repeat(${layout.length}, 1fr)`,
}}
>
<AnimatePresence>
{layout.map((columns, pageIndex) => (
<motion.div
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 }}
>
<Page mode="builder" pageNumber={pageIndex + 1}>
<Template isFirstPage={pageIndex === 0} columns={columns as SectionKey[][]} />
</Page>
</motion.div>
))}
</AnimatePresence>
</TransformComponent>
</TransformWrapper>
);
};
-24
View File
@@ -1,24 +0,0 @@
import type { SectionKey } from "@reactive-resume/schema";
import type { Template } from "@reactive-resume/utils";
import { useMemo } from "react";
import { Page } from "../components/page";
import { useArtboardStore } from "../store/artboard";
import { getTemplate } from "../templates";
export const PreviewLayout = () => {
const layout = useArtboardStore((state) => state.resume.metadata.layout);
const template = useArtboardStore((state) => state.resume.metadata.template as Template);
const Template = useMemo(() => getTemplate(template), [template]);
return (
<>
{layout.map((columns, pageIndex) => (
<Page key={pageIndex} mode="preview" pageNumber={pageIndex + 1}>
<Template isFirstPage={pageIndex === 0} columns={columns as SectionKey[][]} />
</Page>
))}
</>
);
};
-39
View File
@@ -1,39 +0,0 @@
import { useEffect } from "react";
import { HelmetProvider } from "react-helmet-async";
import { Outlet } from "react-router";
import { helmetContext } from "../constants/helmet";
import { useArtboardStore } from "../store/artboard";
export const Providers = () => {
const resume = useArtboardStore((state) => state.resume);
const setResume = useArtboardStore((state) => state.setResume);
useEffect(() => {
const handleMessage = (event: MessageEvent) => {
if (event.origin !== window.location.origin) return;
if (event.data.type === "SET_RESUME") setResume(event.data.payload);
};
window.addEventListener("message", handleMessage, false);
return () => {
window.removeEventListener("message", handleMessage, false);
};
}, []);
useEffect(() => {
const resumeData = window.localStorage.getItem("resume");
if (resumeData) setResume(JSON.parse(resumeData));
}, [window.localStorage.getItem("resume")]);
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (!resume) return null;
return (
<HelmetProvider context={helmetContext}>
<Outlet />
</HelmetProvider>
);
};
-17
View File
@@ -1,17 +0,0 @@
import { createBrowserRouter, createRoutesFromChildren, Route } from "react-router";
import { ArtboardPage } from "../pages/artboard";
import { BuilderLayout } from "../pages/builder";
import { PreviewLayout } from "../pages/preview";
import { Providers } from "../providers";
export const routes = createRoutesFromChildren(
<Route element={<Providers />}>
<Route path="artboard" element={<ArtboardPage />}>
<Route path="builder" element={<BuilderLayout />} />
<Route path="preview" element={<PreviewLayout />} />
</Route>
</Route>,
);
export const router = createBrowserRouter(routes);
-14
View File
@@ -1,14 +0,0 @@
import type { ResumeData } from "@reactive-resume/schema";
import { create } from "zustand";
export type ArtboardStore = {
resume: ResumeData;
setResume: (resume: ResumeData) => void;
};
export const useArtboardStore = create<ArtboardStore>()((set) => ({
resume: null as unknown as ResumeData,
setResume: (resume) => {
set({ resume });
},
}));
-29
View File
@@ -1,29 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
* {
font-variant-ligatures: none;
@apply border-current;
}
body {
overflow: hidden;
}
#root {
@apply antialiased;
}
[data-page].hide-icons .ph {
@apply hidden;
}
[data-page].underline-links a {
@apply underline underline-offset-2;
}
.wysiwyg {
@apply prose-foreground prose max-w-none prose-headings:mb-2 prose-headings:mt-0 prose-p:mb-2 prose-p:mt-0 prose-p:leading-normal prose-a:break-all prose-ol:mb-2 prose-ol:mt-0 prose-ul:mb-2 prose-ul:mt-0 prose-li:mb-2 prose-li:mt-0 prose-li:leading-normal prose-img:mb-2 prose-img:mt-0 prose-hr:mb-2 prose-hr:mt-0;
}
-576
View File
@@ -1,576 +0,0 @@
import type {
Award,
Certification,
CustomSection,
CustomSectionGroup,
Interest,
Language,
Profile,
Project,
Publication,
Reference,
SectionKey,
SectionWithItem,
Skill,
URL,
} from "@reactive-resume/schema";
import { Education, Experience, Volunteer } from "@reactive-resume/schema";
import { cn, isEmptyString, isUrl, linearTransform, sanitize } from "@reactive-resume/utils";
import get from "lodash.get";
import React, { Fragment } from "react";
import { BrandIcon } from "../components/brand-icon";
import { Picture } from "../components/picture";
import { useArtboardStore } from "../store/artboard";
import type { TemplateProps } from "../types/template";
const Header = () => {
const basics = useArtboardStore((state) => state.resume.basics);
return (
<div className="flex flex-col items-center justify-center space-y-2 pb-2 text-center">
<Picture />
<div>
<div className="text-2xl font-bold">{basics.name}</div>
<div className="text-base">{basics.headline}</div>
</div>
<div className="flex flex-wrap items-center gap-x-3 gap-y-0.5 text-sm">
{basics.location && (
<div className="flex items-center gap-x-1.5">
<i className="ph ph-bold ph-map-pin text-primary" />
<div>{basics.location}</div>
</div>
)}
{basics.phone && (
<div className="flex items-center gap-x-1.5">
<i className="ph ph-bold ph-phone text-primary" />
<a href={`tel:${basics.phone}`} target="_blank" rel="noreferrer">
{basics.phone}
</a>
</div>
)}
{basics.email && (
<div className="flex items-center gap-x-1.5">
<i className="ph ph-bold ph-at text-primary" />
<a href={`mailto:${basics.email}`} target="_blank" rel="noreferrer">
{basics.email}
</a>
</div>
)}
<Link url={basics.url} />
{basics.customFields.map((item) => (
<div key={item.id} className="flex items-center gap-x-1.5">
<i className={cn(`ph ph-bold ph-${item.icon}`, "text-primary")} />
{isUrl(item.value) ? (
<a href={item.value} target="_blank" rel="noreferrer noopener nofollow">
{item.name || item.value}
</a>
) : (
<span>{[item.name, item.value].filter(Boolean).join(": ")}</span>
)}
</div>
))}
</div>
</div>
);
};
const Summary = () => {
const section = useArtboardStore((state) => state.resume.sections.summary);
if (!section.visible || isEmptyString(section.content)) return null;
return (
<section id={section.id}>
<div className="mb-2 hidden font-bold text-primary group-[.main]:block">
<h4>{section.name}</h4>
</div>
<div className="mb-2 hidden items-center gap-x-2 text-center font-bold text-primary group-[.sidebar]:flex">
<div className="size-1.5 rounded-full border border-primary" />
<h4>{section.name}</h4>
<div className="size-1.5 rounded-full border border-primary" />
</div>
<main className={cn("relative space-y-2", "border-l border-primary pl-4")}>
<div className="absolute left-[-4.5px] top-[8px] hidden size-[8px] rounded-full bg-primary group-[.main]:block" />
<div
dangerouslySetInnerHTML={{ __html: sanitize(section.content) }}
style={{ columns: section.columns }}
className="wysiwyg"
/>
</main>
</section>
);
};
type RatingProps = { level: number };
const Rating = ({ level }: RatingProps) => (
<div className="relative h-1 w-[128px] group-[.sidebar]:mx-auto">
<div className="absolute inset-0 h-1 w-[128px] rounded bg-primary opacity-25" />
<div
className="absolute inset-0 h-1 rounded bg-primary"
style={{ width: linearTransform(level, 0, 5, 0, 128) }}
/>
</div>
);
type LinkProps = {
url: URL;
icon?: React.ReactNode;
iconOnRight?: boolean;
label?: string;
className?: string;
};
const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
if (!isUrl(url.href)) return null;
return (
<div className="flex items-center gap-x-1.5">
{!iconOnRight && (icon ?? <i className="ph ph-bold ph-link text-primary" />)}
<a
href={url.href}
target="_blank"
rel="noreferrer noopener nofollow"
className={cn("inline-block", className)}
>
{label ?? (url.label || url.href)}
</a>
{iconOnRight && (icon ?? <i className="ph ph-bold ph-link text-primary" />)}
</div>
);
};
type LinkedEntityProps = {
name: string;
url: URL;
separateLinks: boolean;
className?: string;
};
const LinkedEntity = ({ name, url, separateLinks, className }: LinkedEntityProps) => {
return !separateLinks && isUrl(url.href) ? (
<Link
url={url}
label={name}
icon={<i className="ph ph-bold ph-globe text-primary" />}
iconOnRight={true}
className={className}
/>
) : (
<div className={className}>{name}</div>
);
};
type SectionProps<T> = {
section: SectionWithItem<T> | CustomSectionGroup;
children?: (item: T) => React.ReactNode;
className?: string;
urlKey?: keyof T;
levelKey?: keyof T;
summaryKey?: keyof T;
keywordsKey?: keyof T;
};
const Section = <T,>({
section,
children,
className,
urlKey,
levelKey,
summaryKey,
keywordsKey,
}: SectionProps<T>) => {
if (!section.visible || section.items.filter((item) => item.visible).length === 0) return null;
return (
<section id={section.id} className="grid">
<div className="mb-2 hidden font-bold text-primary group-[.main]:block">
<h4>{section.name}</h4>
</div>
<div className="mx-auto mb-2 hidden items-center gap-x-2 text-center font-bold text-primary group-[.sidebar]:flex">
<div className="size-1.5 rounded-full border border-primary" />
<h4>{section.name}</h4>
<div className="size-1.5 rounded-full border border-primary" />
</div>
<div
className="grid gap-x-6 gap-y-3 group-[.sidebar]:mx-auto group-[.sidebar]:text-center"
style={{ gridTemplateColumns: `repeat(${section.columns}, 1fr)` }}
>
{section.items
.filter((item) => item.visible)
.map((item) => {
const url = (urlKey && get(item, urlKey)) as URL | undefined;
const level = (levelKey && get(item, levelKey, 0)) as number | undefined;
const summary = (summaryKey && get(item, summaryKey, "")) as string | undefined;
const keywords = (keywordsKey && get(item, keywordsKey, [])) as string[] | undefined;
return (
<div
key={item.id}
className={cn(
"relative space-y-2",
"border-primary group-[.main]:border-l group-[.main]:pl-4",
className,
)}
>
<div>{children?.(item as T)}</div>
{summary !== undefined && !isEmptyString(summary) && (
<div
dangerouslySetInnerHTML={{ __html: sanitize(summary) }}
className="wysiwyg"
/>
)}
{level !== undefined && level > 0 && <Rating level={level} />}
{keywords !== undefined && keywords.length > 0 && (
<p className="text-sm">{keywords.join(", ")}</p>
)}
{url !== undefined && section.separateLinks && <Link url={url} />}
<div className="absolute left-[-4.5px] top-px hidden size-[8px] rounded-full bg-primary group-[.main]:block" />
</div>
);
})}
</div>
</section>
);
};
const Profiles = () => {
const section = useArtboardStore((state) => state.resume.sections.profiles);
return (
<Section<Profile> section={section}>
{(item) => (
<div>
{isUrl(item.url.href) ? (
<Link url={item.url} label={item.username} icon={<BrandIcon slug={item.icon} />} />
) : (
<p>{item.username}</p>
)}
{!item.icon && <p className="text-sm">{item.network}</p>}
</div>
)}
</Section>
);
};
const Experience = () => {
const section = useArtboardStore((state) => state.resume.sections.experience);
return (
<Section<Experience> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div>
<LinkedEntity
name={item.company}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.position}</div>
<div>{item.location}</div>
<div className="font-bold">{item.date}</div>
</div>
)}
</Section>
);
};
const Education = () => {
const section = useArtboardStore((state) => state.resume.sections.education);
return (
<Section<Education> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div>
<LinkedEntity
name={item.institution}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.area}</div>
<div>{item.score}</div>
<div>{item.studyType}</div>
<div className="font-bold">{item.date}</div>
</div>
)}
</Section>
);
};
const Awards = () => {
const section = useArtboardStore((state) => state.resume.sections.awards);
return (
<Section<Award> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div>
<div className="font-bold">{item.title}</div>
<LinkedEntity name={item.awarder} url={item.url} separateLinks={section.separateLinks} />
<div className="font-bold">{item.date}</div>
</div>
)}
</Section>
);
};
const Certifications = () => {
const section = useArtboardStore((state) => state.resume.sections.certifications);
return (
<Section<Certification> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div>
<div className="font-bold">{item.name}</div>
<LinkedEntity name={item.issuer} url={item.url} separateLinks={section.separateLinks} />
<div className="font-bold">{item.date}</div>
</div>
)}
</Section>
);
};
const Skills = () => {
const section = useArtboardStore((state) => state.resume.sections.skills);
return (
<Section<Skill> section={section} levelKey="level" keywordsKey="keywords">
{(item) => (
<div>
<div className="font-bold">{item.name}</div>
<div>{item.description}</div>
</div>
)}
</Section>
);
};
const Interests = () => {
const section = useArtboardStore((state) => state.resume.sections.interests);
return (
<Section<Interest> section={section} keywordsKey="keywords" className="space-y-0.5">
{(item) => <div className="font-bold">{item.name}</div>}
</Section>
);
};
const Publications = () => {
const section = useArtboardStore((state) => state.resume.sections.publications);
return (
<Section<Publication> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div>
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.publisher}</div>
<div className="font-bold">{item.date}</div>
</div>
)}
</Section>
);
};
const Volunteer = () => {
const section = useArtboardStore((state) => state.resume.sections.volunteer);
return (
<Section<Volunteer> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div>
<LinkedEntity
name={item.organization}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.position}</div>
<div>{item.location}</div>
<div className="font-bold">{item.date}</div>
</div>
)}
</Section>
);
};
const Languages = () => {
const section = useArtboardStore((state) => state.resume.sections.languages);
return (
<Section<Language> section={section} levelKey="level">
{(item) => (
<div>
<div className="font-bold">{item.name}</div>
<div>{item.description}</div>
</div>
)}
</Section>
);
};
const Projects = () => {
const section = useArtboardStore((state) => state.resume.sections.projects);
return (
<Section<Project> section={section} urlKey="url" summaryKey="summary" keywordsKey="keywords">
{(item) => (
<div>
<div>
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.description}</div>
<div className="font-bold">{item.date}</div>
</div>
</div>
)}
</Section>
);
};
const References = () => {
const section = useArtboardStore((state) => state.resume.sections.references);
return (
<Section<Reference> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div>
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.description}</div>
</div>
)}
</Section>
);
};
const Custom = ({ id }: { id: string }) => {
const section = useArtboardStore((state) => state.resume.sections.custom[id]);
return (
<Section<CustomSection>
section={section}
urlKey="url"
summaryKey="summary"
keywordsKey="keywords"
>
{(item) => (
<div>
<div>
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.description}</div>
<div className="font-bold">{item.date}</div>
<div>{item.location}</div>
</div>
</div>
)}
</Section>
);
};
const mapSectionToComponent = (section: SectionKey) => {
switch (section) {
case "profiles": {
return <Profiles />;
}
case "summary": {
return <Summary />;
}
case "experience": {
return <Experience />;
}
case "education": {
return <Education />;
}
case "awards": {
return <Awards />;
}
case "certifications": {
return <Certifications />;
}
case "skills": {
return <Skills />;
}
case "interests": {
return <Interests />;
}
case "publications": {
return <Publications />;
}
case "volunteer": {
return <Volunteer />;
}
case "languages": {
return <Languages />;
}
case "projects": {
return <Projects />;
}
case "references": {
return <References />;
}
default: {
if (section.startsWith("custom.")) return <Custom id={section.split(".")[1]} />;
return null;
}
}
};
export const Azurill = ({ columns, isFirstPage = false }: TemplateProps) => {
const [main, sidebar] = columns;
return (
<div className="p-custom space-y-3">
{isFirstPage && <Header />}
<div className="grid grid-cols-3 gap-x-4">
<div className="sidebar group space-y-4">
{sidebar.map((section) => (
<Fragment key={section}>{mapSectionToComponent(section)}</Fragment>
))}
</div>
<div
className={cn("main group space-y-4", sidebar.length > 0 ? "col-span-2" : "col-span-3")}
>
{main.map((section) => (
<Fragment key={section}>{mapSectionToComponent(section)}</Fragment>
))}
</div>
</div>
</div>
);
};
-585
View File
@@ -1,585 +0,0 @@
import type {
Award,
Certification,
CustomSection,
CustomSectionGroup,
Interest,
Language,
Profile,
Project,
Publication,
Reference,
SectionKey,
SectionWithItem,
Skill,
URL,
} from "@reactive-resume/schema";
import { Education, Experience, Volunteer } from "@reactive-resume/schema";
import { cn, isEmptyString, isUrl, sanitize } from "@reactive-resume/utils";
import get from "lodash.get";
import { Fragment } from "react";
import { BrandIcon } from "../components/brand-icon";
import { Picture } from "../components/picture";
import { useArtboardStore } from "../store/artboard";
import type { TemplateProps } from "../types/template";
const Header = () => {
const basics = useArtboardStore((state) => state.resume.basics);
return (
<div className="flex flex-col items-center space-y-2 text-center">
<Picture />
<div>
<div className="text-2xl font-bold">{basics.name}</div>
<div className="text-base">{basics.headline}</div>
</div>
<div className="flex flex-wrap items-center gap-x-2 gap-y-0.5 text-sm">
{basics.location && (
<div className="flex items-center gap-x-1.5">
<i className="ph ph-bold ph-map-pin text-primary" />
<div>{basics.location}</div>
</div>
)}
{basics.phone && (
<div className="flex items-center gap-x-1.5">
<i className="ph ph-bold ph-phone text-primary" />
<a href={`tel:${basics.phone}`} target="_blank" rel="noreferrer">
{basics.phone}
</a>
</div>
)}
{basics.email && (
<div className="flex items-center gap-x-1.5">
<i className="ph ph-bold ph-at text-primary" />
<a href={`mailto:${basics.email}`} target="_blank" rel="noreferrer">
{basics.email}
</a>
</div>
)}
<Link url={basics.url} />
{basics.customFields.map((item) => (
<div key={item.id} className="flex items-center gap-x-1.5">
<i className={cn(`ph ph-bold ph-${item.icon}`, "text-primary")} />
{isUrl(item.value) ? (
<a href={item.value} target="_blank" rel="noreferrer noopener nofollow">
{item.name || item.value}
</a>
) : (
<span>{[item.name, item.value].filter(Boolean).join(": ")}</span>
)}
</div>
))}
</div>
</div>
);
};
const Summary = () => {
const section = useArtboardStore((state) => state.resume.sections.summary);
if (!section.visible || isEmptyString(section.content)) return null;
return (
<section id={section.id} className="grid grid-cols-5 border-t pt-2.5">
<div>
<h4 className="text-base font-bold">{section.name}</h4>
</div>
<div
dangerouslySetInnerHTML={{ __html: sanitize(section.content) }}
style={{ columns: section.columns }}
className="wysiwyg col-span-4"
/>
</section>
);
};
type RatingProps = { level: number };
const Rating = ({ level }: RatingProps) => (
<div className="flex items-center gap-x-1.5">
{Array.from({ length: 5 }).map((_, index) => (
<div
key={index}
className={cn("size-2 rounded-full border border-primary", level > index && "bg-primary")}
/>
))}
</div>
);
type LinkProps = {
url: URL;
icon?: React.ReactNode;
iconOnRight?: boolean;
label?: string;
className?: string;
};
const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
if (!isUrl(url.href)) return null;
return (
<div className="flex items-center gap-x-1.5">
{!iconOnRight && (icon ?? <i className="ph ph-bold ph-link text-primary" />)}
<a
href={url.href}
target="_blank"
rel="noreferrer noopener nofollow"
className={cn("inline-block", className)}
>
{label ?? (url.label || url.href)}
</a>
{iconOnRight && (icon ?? <i className="ph ph-bold ph-link text-primary" />)}
</div>
);
};
type LinkedEntityProps = {
name: string;
url: URL;
separateLinks: boolean;
className?: string;
};
const LinkedEntity = ({ name, url, separateLinks, className }: LinkedEntityProps) => {
return !separateLinks && isUrl(url.href) ? (
<Link
url={url}
label={name}
icon={<i className="ph ph-bold ph-globe text-primary" />}
iconOnRight={true}
className={className}
/>
) : (
<div className={className}>{name}</div>
);
};
type SectionProps<T> = {
section: SectionWithItem<T> | CustomSectionGroup;
children?: (item: T) => React.ReactNode;
className?: string;
urlKey?: keyof T;
levelKey?: keyof T;
summaryKey?: keyof T;
keywordsKey?: keyof T;
};
const Section = <T,>({
section,
children,
className,
urlKey,
levelKey,
summaryKey,
keywordsKey,
}: SectionProps<T>) => {
if (!section.visible || section.items.filter((item) => item.visible).length === 0) return null;
return (
<section id={section.id} className="grid grid-cols-5 border-t pt-2.5">
<div>
<h4 className="text-base font-bold">{section.name}</h4>
</div>
<div
className="col-span-4 grid gap-x-6 gap-y-3"
style={{ gridTemplateColumns: `repeat(${section.columns}, 1fr)` }}
>
{section.items
.filter((item) => item.visible)
.map((item) => {
const url = (urlKey && get(item, urlKey)) as URL | undefined;
const level = (levelKey && get(item, levelKey, 0)) as number | undefined;
const summary = (summaryKey && get(item, summaryKey, "")) as string | undefined;
const keywords = (keywordsKey && get(item, keywordsKey, [])) as string[] | undefined;
return (
<div key={item.id} className={cn("space-y-2", className)}>
<div>
{children?.(item as T)}
{url !== undefined && section.separateLinks && <Link url={url} />}
</div>
{summary !== undefined && !isEmptyString(summary) && (
<div
dangerouslySetInnerHTML={{ __html: sanitize(summary) }}
className="wysiwyg"
/>
)}
{level !== undefined && level > 0 && <Rating level={level} />}
{keywords !== undefined && keywords.length > 0 && (
<p className="text-sm">{keywords.join(", ")}</p>
)}
</div>
);
})}
</div>
</section>
);
};
const Profiles = () => {
const section = useArtboardStore((state) => state.resume.sections.profiles);
return (
<Section<Profile> section={section}>
{(item) => (
<div>
{isUrl(item.url.href) ? (
<Link url={item.url} label={item.username} icon={<BrandIcon slug={item.icon} />} />
) : (
<p>{item.username}</p>
)}
{!item.icon && <p className="text-sm">{item.network}</p>}
</div>
)}
</Section>
);
};
const Experience = () => {
const section = useArtboardStore((state) => state.resume.sections.experience);
return (
<Section<Experience> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-start justify-between">
<div className="text-left">
<LinkedEntity
name={item.company}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.position}</div>
</div>
<div className="shrink-0 text-right">
<div className="font-bold">{item.date}</div>
<div>{item.location}</div>
</div>
</div>
)}
</Section>
);
};
const Education = () => {
const section = useArtboardStore((state) => state.resume.sections.education);
return (
<Section<Education> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-start justify-between">
<div className="text-left">
<LinkedEntity
name={item.institution}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.area}</div>
<div>{item.score}</div>
</div>
<div className="shrink-0 text-right">
<div className="font-bold">{item.date}</div>
<div>{item.studyType}</div>
</div>
</div>
)}
</Section>
);
};
const Awards = () => {
const section = useArtboardStore((state) => state.resume.sections.awards);
return (
<Section<Award> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-start justify-between">
<div className="text-left">
<div className="font-bold">{item.title}</div>
<LinkedEntity
name={item.awarder}
url={item.url}
separateLinks={section.separateLinks}
/>
</div>
<div className="shrink-0 text-right">
<div className="font-bold">{item.date}</div>
</div>
</div>
)}
</Section>
);
};
const Certifications = () => {
const section = useArtboardStore((state) => state.resume.sections.certifications);
return (
<Section<Certification> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-start justify-between">
<div className="text-left">
<div className="font-bold">{item.name}</div>
<LinkedEntity name={item.issuer} url={item.url} separateLinks={section.separateLinks} />
</div>
<div className="shrink-0 text-right">
<div className="font-bold">{item.date}</div>
</div>
</div>
)}
</Section>
);
};
const Skills = () => {
const section = useArtboardStore((state) => state.resume.sections.skills);
return (
<Section<Skill> section={section} levelKey="level" keywordsKey="keywords">
{(item) => (
<div className="space-y-0.5">
<div className="font-bold">{item.name}</div>
<div>{item.description}</div>
</div>
)}
</Section>
);
};
const Interests = () => {
const section = useArtboardStore((state) => state.resume.sections.interests);
return (
<Section<Interest> section={section} keywordsKey="keywords" className="space-y-0.5">
{(item) => <div className="font-bold">{item.name}</div>}
</Section>
);
};
const Publications = () => {
const section = useArtboardStore((state) => state.resume.sections.publications);
return (
<Section<Publication> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-start justify-between">
<div className="text-left">
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.publisher}</div>
</div>
<div className="shrink-0 text-right">
<div className="font-bold">{item.date}</div>
</div>
</div>
)}
</Section>
);
};
const Volunteer = () => {
const section = useArtboardStore((state) => state.resume.sections.volunteer);
return (
<Section<Volunteer> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-start justify-between">
<div className="text-left">
<LinkedEntity
name={item.organization}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.position}</div>
</div>
<div className="shrink-0 text-right">
<div className="font-bold">{item.date}</div>
<div>{item.location}</div>
</div>
</div>
)}
</Section>
);
};
const Languages = () => {
const section = useArtboardStore((state) => state.resume.sections.languages);
return (
<Section<Language> section={section} levelKey="level">
{(item) => (
<div className="space-y-0.5">
<div className="font-bold">{item.name}</div>
<div>{item.description}</div>
</div>
)}
</Section>
);
};
const Projects = () => {
const section = useArtboardStore((state) => state.resume.sections.projects);
return (
<Section<Project> section={section} urlKey="url" summaryKey="summary" keywordsKey="keywords">
{(item) => (
<div className="flex items-start justify-between">
<div className="text-left">
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.description}</div>
</div>
<div className="shrink-0 text-right">
<div className="font-bold">{item.date}</div>
</div>
</div>
)}
</Section>
);
};
const References = () => {
const section = useArtboardStore((state) => state.resume.sections.references);
return (
<Section<Reference> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div>
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.description}</div>
</div>
)}
</Section>
);
};
const Custom = ({ id }: { id: string }) => {
const section = useArtboardStore((state) => state.resume.sections.custom[id]);
return (
<Section<CustomSection>
section={section}
urlKey="url"
summaryKey="summary"
keywordsKey="keywords"
>
{(item) => (
<div className="flex items-start justify-between">
<div className="text-left">
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.description}</div>
</div>
<div className="shrink-0 text-right">
<div className="font-bold">{item.date}</div>
<div>{item.location}</div>
</div>
</div>
)}
</Section>
);
};
const mapSectionToComponent = (section: SectionKey) => {
switch (section) {
case "profiles": {
return <Profiles />;
}
case "summary": {
return <Summary />;
}
case "experience": {
return <Experience />;
}
case "education": {
return <Education />;
}
case "awards": {
return <Awards />;
}
case "certifications": {
return <Certifications />;
}
case "skills": {
return <Skills />;
}
case "interests": {
return <Interests />;
}
case "publications": {
return <Publications />;
}
case "volunteer": {
return <Volunteer />;
}
case "languages": {
return <Languages />;
}
case "projects": {
return <Projects />;
}
case "references": {
return <References />;
}
default: {
if (section.startsWith("custom.")) return <Custom id={section.split(".")[1]} />;
return null;
}
}
};
export const Bronzor = ({ columns, isFirstPage = false }: TemplateProps) => {
const [main, sidebar] = columns;
return (
<div className="p-custom space-y-4">
{isFirstPage && <Header />}
<div className="space-y-4">
{main.map((section) => (
<Fragment key={section}>{mapSectionToComponent(section)}</Fragment>
))}
{sidebar.map((section) => (
<Fragment key={section}>{mapSectionToComponent(section)}</Fragment>
))}
</div>
</div>
);
};
-597
View File
@@ -1,597 +0,0 @@
import type {
Award,
Certification,
CustomSection,
CustomSectionGroup,
Interest,
Language,
Profile,
Project,
Publication,
Reference,
SectionKey,
SectionWithItem,
Skill,
URL,
} from "@reactive-resume/schema";
import { Education, Experience, Volunteer } from "@reactive-resume/schema";
import { cn, isEmptyString, isUrl, sanitize } from "@reactive-resume/utils";
import get from "lodash.get";
import { Fragment } from "react";
import { BrandIcon } from "../components/brand-icon";
import { Picture } from "../components/picture";
import { useArtboardStore } from "../store/artboard";
import type { TemplateProps } from "../types/template";
const Header = () => {
const basics = useArtboardStore((state) => state.resume.basics);
return (
<div className="flex items-center space-x-4">
<Picture />
<div className="space-y-2">
<div>
<div className="text-2xl font-bold">{basics.name}</div>
<div className="text-base">{basics.headline}</div>
</div>
<div className="flex flex-wrap items-center gap-x-2 gap-y-0.5 text-sm">
{basics.location && (
<div className="flex items-center gap-x-1.5">
<i className="ph ph-bold ph-map-pin text-primary" />
<div>{basics.location}</div>
</div>
)}
{basics.phone && (
<div className="flex items-center gap-x-1.5">
<i className="ph ph-bold ph-phone text-primary" />
<a href={`tel:${basics.phone}`} target="_blank" rel="noreferrer">
{basics.phone}
</a>
</div>
)}
{basics.email && (
<div className="flex items-center gap-x-1.5">
<i className="ph ph-bold ph-at text-primary" />
<a href={`mailto:${basics.email}`} target="_blank" rel="noreferrer">
{basics.email}
</a>
</div>
)}
<Link url={basics.url} />
{basics.customFields.map((item) => (
<div key={item.id} className="flex items-center gap-x-1.5">
<i className={cn(`ph ph-bold ph-${item.icon}`, "text-primary")} />
{isUrl(item.value) ? (
<a href={item.value} target="_blank" rel="noreferrer noopener nofollow">
{item.name || item.value}
</a>
) : (
<span>{[item.name, item.value].filter(Boolean).join(": ")}</span>
)}
</div>
))}
</div>
</div>
</div>
);
};
const Summary = () => {
const section = useArtboardStore((state) => state.resume.sections.summary);
if (!section.visible || isEmptyString(section.content)) return null;
return (
<section id={section.id}>
<h4 className="mb-2 border-b pb-0.5 text-sm font-bold">{section.name}</h4>
<div
dangerouslySetInnerHTML={{ __html: sanitize(section.content) }}
style={{ columns: section.columns }}
className="wysiwyg"
/>
</section>
);
};
type RatingProps = { level: number };
const Rating = ({ level }: RatingProps) => (
<div className="flex items-center gap-x-1.5">
{Array.from({ length: 5 }).map((_, index) => (
<div
key={index}
className={cn(
"size-2 rounded-full border border-primary group-[.sidebar]:border-background",
level > index && "bg-primary group-[.sidebar]:bg-background",
)}
/>
))}
</div>
);
type LinkProps = {
url: URL;
icon?: React.ReactNode;
iconOnRight?: boolean;
label?: string;
className?: string;
};
const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
if (!isUrl(url.href)) return null;
return (
<div className="flex items-center gap-x-1.5">
{!iconOnRight &&
(icon ?? <i className="ph ph-bold ph-link text-primary group-[.sidebar]:text-white" />)}
<a
href={url.href}
target="_blank"
rel="noreferrer noopener nofollow"
className={cn("inline-block", className)}
>
{label ?? (url.label || url.href)}
</a>
{iconOnRight &&
(icon ?? <i className="ph ph-bold ph-link text-primary group-[.sidebar]:text-white" />)}
</div>
);
};
type LinkedEntityProps = {
name: string;
url: URL;
separateLinks: boolean;
className?: string;
};
const LinkedEntity = ({ name, url, separateLinks, className }: LinkedEntityProps) => {
return !separateLinks && isUrl(url.href) ? (
<Link
url={url}
label={name}
icon={<i className="ph ph-bold ph-globe text-primary" />}
iconOnRight={true}
className={className}
/>
) : (
<div className={className}>{name}</div>
);
};
type SectionProps<T> = {
section: SectionWithItem<T> | CustomSectionGroup;
children?: (item: T) => React.ReactNode;
className?: string;
urlKey?: keyof T;
levelKey?: keyof T;
summaryKey?: keyof T;
keywordsKey?: keyof T;
};
const Section = <T,>({
section,
children,
className,
urlKey,
levelKey,
summaryKey,
keywordsKey,
}: SectionProps<T>) => {
if (!section.visible || section.items.filter((item) => item.visible).length === 0) return null;
return (
<section id={section.id} className="grid">
<h4 className="mb-2 border-b pb-0.5 text-sm font-bold">{section.name}</h4>
<div
className="grid gap-x-6 gap-y-3"
style={{ gridTemplateColumns: `repeat(${section.columns}, 1fr)` }}
>
{section.items
.filter((item) => item.visible)
.map((item) => {
const url = (urlKey && get(item, urlKey)) as URL | undefined;
const level = (levelKey && get(item, levelKey, 0)) as number | undefined;
const summary = (summaryKey && get(item, summaryKey, "")) as string | undefined;
const keywords = (keywordsKey && get(item, keywordsKey, [])) as string[] | undefined;
return (
<div key={item.id} className={cn("space-y-2", className)}>
<div>
{children?.(item as T)}
{url !== undefined && section.separateLinks && <Link url={url} />}
</div>
{summary !== undefined && !isEmptyString(summary) && (
<div
dangerouslySetInnerHTML={{ __html: sanitize(summary) }}
className="wysiwyg group-[.sidebar]:prose-invert"
/>
)}
{level !== undefined && level > 0 && <Rating level={level} />}
{keywords !== undefined && keywords.length > 0 && (
<p className="text-sm">{keywords.join(", ")}</p>
)}
</div>
);
})}
</div>
</section>
);
};
const Experience = () => {
const section = useArtboardStore((state) => state.resume.sections.experience);
return (
<Section<Experience> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<LinkedEntity
name={item.company}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.position}</div>
</div>
<div className="shrink-0 text-right">
<div className="font-bold">{item.date}</div>
<div>{item.location}</div>
</div>
</div>
)}
</Section>
);
};
const Education = () => {
const section = useArtboardStore((state) => state.resume.sections.education);
return (
<Section<Education> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<LinkedEntity
name={item.institution}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.area}</div>
<div>{item.score}</div>
</div>
<div className="shrink-0 text-right">
<div className="font-bold">{item.date}</div>
<div>{item.studyType}</div>
</div>
</div>
)}
</Section>
);
};
const Profiles = () => {
const section = useArtboardStore((state) => state.resume.sections.profiles);
return (
<Section<Profile> section={section}>
{(item) => (
<div>
{isUrl(item.url.href) ? (
<Link url={item.url} label={item.username} icon={<BrandIcon slug={item.icon} />} />
) : (
<p>{item.username}</p>
)}
{!item.icon && <p className="text-sm">{item.network}</p>}
</div>
)}
</Section>
);
};
const Awards = () => {
const section = useArtboardStore((state) => state.resume.sections.awards);
return (
<Section<Award> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<div className="font-bold">{item.title}</div>
<LinkedEntity
name={item.awarder}
url={item.url}
separateLinks={section.separateLinks}
/>
</div>
<div className="shrink-0 text-right">
<div className="font-bold">{item.date}</div>
</div>
</div>
)}
</Section>
);
};
const Certifications = () => {
const section = useArtboardStore((state) => state.resume.sections.certifications);
return (
<Section<Certification> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<div className="font-bold">{item.name}</div>
<LinkedEntity name={item.issuer} url={item.url} separateLinks={section.separateLinks} />
<div className="font-bold">{item.date}</div>
</div>
</div>
)}
</Section>
);
};
const Skills = () => {
const section = useArtboardStore((state) => state.resume.sections.skills);
return (
<Section<Skill> section={section} levelKey="level" keywordsKey="keywords">
{(item) => (
<div>
<div className="font-bold">{item.name}</div>
<div>{item.description}</div>
</div>
)}
</Section>
);
};
const Interests = () => {
const section = useArtboardStore((state) => state.resume.sections.interests);
return (
<Section<Interest> section={section} keywordsKey="keywords" className="space-y-0.5">
{(item) => <div className="font-bold">{item.name}</div>}
</Section>
);
};
const Publications = () => {
const section = useArtboardStore((state) => state.resume.sections.publications);
return (
<Section<Publication> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.publisher}</div>
</div>
<div className="shrink-0 text-right">
<div className="font-bold">{item.date}</div>
</div>
</div>
)}
</Section>
);
};
const Volunteer = () => {
const section = useArtboardStore((state) => state.resume.sections.volunteer);
return (
<Section<Volunteer> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<LinkedEntity
name={item.organization}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.position}</div>
</div>
<div className="shrink-0 text-right">
<div className="font-bold">{item.date}</div>
<div>{item.location}</div>
</div>
</div>
)}
</Section>
);
};
const Languages = () => {
const section = useArtboardStore((state) => state.resume.sections.languages);
return (
<Section<Language> section={section} levelKey="level">
{(item) => (
<div className="space-y-0.5">
<div className="font-bold">{item.name}</div>
<div>{item.description}</div>
</div>
)}
</Section>
);
};
const Projects = () => {
const section = useArtboardStore((state) => state.resume.sections.projects);
return (
<Section<Project> section={section} urlKey="url" summaryKey="summary" keywordsKey="keywords">
{(item) => (
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.description}</div>
</div>
<div className="shrink-0 text-right">
<div className="font-bold">{item.date}</div>
</div>
</div>
)}
</Section>
);
};
const References = () => {
const section = useArtboardStore((state) => state.resume.sections.references);
return (
<Section<Reference> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div>
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.description}</div>
</div>
)}
</Section>
);
};
const Custom = ({ id }: { id: string }) => {
const section = useArtboardStore((state) => state.resume.sections.custom[id]);
return (
<Section<CustomSection>
section={section}
urlKey="url"
summaryKey="summary"
keywordsKey="keywords"
>
{(item) => (
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.description}</div>
</div>
<div className="shrink-0 text-right">
<div className="font-bold">{item.date}</div>
<div>{item.location}</div>
</div>
</div>
)}
</Section>
);
};
const mapSectionToComponent = (section: SectionKey) => {
switch (section) {
case "profiles": {
return <Profiles />;
}
case "summary": {
return <Summary />;
}
case "experience": {
return <Experience />;
}
case "education": {
return <Education />;
}
case "awards": {
return <Awards />;
}
case "certifications": {
return <Certifications />;
}
case "skills": {
return <Skills />;
}
case "interests": {
return <Interests />;
}
case "publications": {
return <Publications />;
}
case "volunteer": {
return <Volunteer />;
}
case "languages": {
return <Languages />;
}
case "projects": {
return <Projects />;
}
case "references": {
return <References />;
}
default: {
if (section.startsWith("custom.")) return <Custom id={section.split(".")[1]} />;
return null;
}
}
};
export const Chikorita = ({ columns, isFirstPage = false }: TemplateProps) => {
const [main, sidebar] = columns;
return (
<div className="grid min-h-[inherit] grid-cols-3">
<div
className={cn(
"main p-custom group space-y-4",
sidebar.length > 0 ? "col-span-2" : "col-span-3",
)}
>
{isFirstPage && <Header />}
{main.map((section) => (
<Fragment key={section}>{mapSectionToComponent(section)}</Fragment>
))}
</div>
<div
className={cn(
"sidebar p-custom group h-full space-y-4 bg-primary text-background",
sidebar.length === 0 && "hidden",
)}
>
{sidebar.map((section) => (
<Fragment key={section}>{mapSectionToComponent(section)}</Fragment>
))}
</div>
</div>
);
};
-626
View File
@@ -1,626 +0,0 @@
import type {
Award,
Certification,
CustomSection,
CustomSectionGroup,
Interest,
Language,
Profile,
Project,
Publication,
Reference,
SectionKey,
SectionWithItem,
Skill,
URL,
} from "@reactive-resume/schema";
import { Education, Experience, Volunteer } from "@reactive-resume/schema";
import { cn, isEmptyString, isUrl, sanitize } from "@reactive-resume/utils";
import get from "lodash.get";
import { Fragment } from "react";
import { BrandIcon } from "../components/brand-icon";
import { Picture } from "../components/picture";
import { useArtboardStore } from "../store/artboard";
import type { TemplateProps } from "../types/template";
const Header = () => {
const basics = useArtboardStore((state) => state.resume.basics);
return (
<div className="p-custom relative grid grid-cols-3 space-x-4 pb-0">
<Picture className="mx-auto" />
<div className="relative z-10 col-span-2 text-background">
<div className="space-y-0.5">
<h2 className="text-3xl font-bold">{basics.name}</h2>
<p>{basics.headline}</p>
</div>
<div className="col-span-2 col-start-2 mt-10 text-foreground">
<div className="flex flex-wrap items-center gap-x-2 gap-y-0.5 text-sm">
{basics.location && (
<>
<div className="flex items-center gap-x-1.5">
<i className="ph ph-bold ph-map-pin text-primary" />
<div>{basics.location}</div>
</div>
<div className="bg-text size-1 rounded-full last:hidden" />
</>
)}
{basics.phone && (
<>
<div className="flex items-center gap-x-1.5">
<i className="ph ph-bold ph-phone text-primary" />
<a href={`tel:${basics.phone}`} target="_blank" rel="noreferrer">
{basics.phone}
</a>
</div>
<div className="bg-text size-1 rounded-full last:hidden" />
</>
)}
{basics.email && (
<>
<div className="flex items-center gap-x-1.5">
<i className="ph ph-bold ph-at text-primary" />
<a href={`mailto:${basics.email}`} target="_blank" rel="noreferrer">
{basics.email}
</a>
</div>
<div className="bg-text size-1 rounded-full last:hidden" />
</>
)}
{isUrl(basics.url.href) && (
<>
<Link url={basics.url} />
<div className="bg-text size-1 rounded-full last:hidden" />
</>
)}
{basics.customFields.map((item) => (
<Fragment key={item.id}>
<div className="flex items-center gap-x-1.5">
<i className={cn(`ph ph-bold ph-${item.icon}`, "text-primary")} />
{isUrl(item.value) ? (
<a href={item.value} target="_blank" rel="noreferrer noopener nofollow">
{item.name || item.value}
</a>
) : (
<span>{[item.name, item.value].filter(Boolean).join(": ")}</span>
)}
</div>
<div className="bg-text size-1 rounded-full last:hidden" />
</Fragment>
))}
</div>
</div>
</div>
</div>
);
};
const Summary = () => {
const section = useArtboardStore((state) => state.resume.sections.summary);
if (!section.visible || isEmptyString(section.content)) return null;
return (
<section id={section.id}>
<h4 className="mb-2 text-base font-bold">{section.name}</h4>
<div
dangerouslySetInnerHTML={{ __html: sanitize(section.content) }}
style={{ columns: section.columns }}
className="wysiwyg"
/>
</section>
);
};
type RatingProps = { level: number };
const Rating = ({ level }: RatingProps) => (
<div className="flex items-center gap-x-1.5">
{Array.from({ length: 5 }).map((_, index) => (
<div
key={index}
className={cn("h-2 w-4 border border-primary", level > index && "bg-primary")}
/>
))}
</div>
);
type LinkProps = {
url: URL;
icon?: React.ReactNode;
iconOnRight?: boolean;
label?: string;
className?: string;
};
const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
if (!isUrl(url.href)) return null;
return (
<div className="flex items-center gap-x-1.5">
{!iconOnRight && (icon ?? <i className="ph ph-bold ph-link text-primary" />)}
<a
href={url.href}
target="_blank"
rel="noreferrer noopener nofollow"
className={cn("inline-block", className)}
>
{label ?? (url.label || url.href)}
</a>
{iconOnRight && (icon ?? <i className="ph ph-bold ph-link text-primary" />)}
</div>
);
};
type LinkedEntityProps = {
name: string;
url: URL;
separateLinks: boolean;
className?: string;
};
const LinkedEntity = ({ name, url, separateLinks, className }: LinkedEntityProps) => {
return !separateLinks && isUrl(url.href) ? (
<Link
url={url}
label={name}
icon={<i className="ph ph-bold ph-globe text-primary" />}
iconOnRight={true}
className={className}
/>
) : (
<div className={className}>{name}</div>
);
};
type SectionProps<T> = {
section: SectionWithItem<T> | CustomSectionGroup;
children?: (item: T) => React.ReactNode;
className?: string;
urlKey?: keyof T;
levelKey?: keyof T;
summaryKey?: keyof T;
keywordsKey?: keyof T;
};
const Section = <T,>({
section,
children,
className,
urlKey,
levelKey,
summaryKey,
keywordsKey,
}: SectionProps<T>) => {
if (!section.visible || section.items.filter((item) => item.visible).length === 0) return null;
return (
<section id={section.id} className="grid">
<h4 className="mb-2 text-base font-bold">{section.name}</h4>
<div
className="grid gap-x-6 gap-y-3"
style={{ gridTemplateColumns: `repeat(${section.columns}, 1fr)` }}
>
{section.items
.filter((item) => item.visible)
.map((item) => {
const url = (urlKey && get(item, urlKey)) as URL | undefined;
const level = (levelKey && get(item, levelKey, 0)) as number | undefined;
const summary = (summaryKey && get(item, summaryKey, "")) as string | undefined;
const keywords = (keywordsKey && get(item, keywordsKey, [])) as string[] | undefined;
return (
<div
key={item.id}
className={cn("relative space-y-2 pl-4 group-[.sidebar]:pl-0", className)}
>
<div className="relative -ml-4 group-[.sidebar]:ml-0">
<div className="pl-4 group-[.sidebar]:pl-0">
{children?.(item as T)}
{url !== undefined && section.separateLinks && <Link url={url} />}
</div>
<div className="absolute inset-y-0 -left-px border-l-4 border-primary group-[.sidebar]:hidden" />
</div>
{summary !== undefined && !isEmptyString(summary) && (
<div
dangerouslySetInnerHTML={{ __html: sanitize(summary) }}
className="wysiwyg"
/>
)}
{level !== undefined && level > 0 && <Rating level={level} />}
{keywords !== undefined && keywords.length > 0 && (
<p className="text-sm">{keywords.join(", ")}</p>
)}
<div className="absolute inset-y-0 left-0 border-l border-primary group-[.sidebar]:hidden" />
</div>
);
})}
</div>
</section>
);
};
const Profiles = () => {
const section = useArtboardStore((state) => state.resume.sections.profiles);
return (
<Section<Profile> section={section}>
{(item) => (
<div>
{isUrl(item.url.href) ? (
<Link url={item.url} label={item.username} icon={<BrandIcon slug={item.icon} />} />
) : (
<p>{item.username}</p>
)}
{!item.icon && <p className="text-sm">{item.network}</p>}
</div>
)}
</Section>
);
};
const Experience = () => {
const section = useArtboardStore((state) => state.resume.sections.experience);
return (
<Section<Experience> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<LinkedEntity
name={item.company}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.position}</div>
</div>
<div className="shrink-0 text-right">
<div className="font-bold">{item.date}</div>
<div>{item.location}</div>
</div>
</div>
)}
</Section>
);
};
const Education = () => {
const section = useArtboardStore((state) => state.resume.sections.education);
return (
<Section<Education> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<LinkedEntity
name={item.institution}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.area}</div>
<div>{item.score}</div>
</div>
<div className="shrink-0 text-right">
<div className="font-bold">{item.date}</div>
<div>{item.studyType}</div>
</div>
</div>
)}
</Section>
);
};
const Awards = () => {
const section = useArtboardStore((state) => state.resume.sections.awards);
return (
<Section<Award> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<div className="font-bold">{item.title}</div>
<LinkedEntity
name={item.awarder}
url={item.url}
separateLinks={section.separateLinks}
/>
</div>
<div className="shrink-0 text-right">
<div className="font-bold">{item.date}</div>
</div>
</div>
)}
</Section>
);
};
const Certifications = () => {
const section = useArtboardStore((state) => state.resume.sections.certifications);
return (
<Section<Certification> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<div className="font-bold">{item.name}</div>
<LinkedEntity name={item.issuer} url={item.url} separateLinks={section.separateLinks} />
</div>
<div className="shrink-0 text-right">
<div className="font-bold">{item.date}</div>
</div>
</div>
)}
</Section>
);
};
const Skills = () => {
const section = useArtboardStore((state) => state.resume.sections.skills);
return (
<Section<Skill> section={section} levelKey="level" keywordsKey="keywords">
{(item) => (
<div>
<div className="font-bold">{item.name}</div>
<div>{item.description}</div>
</div>
)}
</Section>
);
};
const Interests = () => {
const section = useArtboardStore((state) => state.resume.sections.interests);
return (
<Section<Interest> section={section} className="space-y-0" keywordsKey="keywords">
{(item) => <div className="font-bold">{item.name}</div>}
</Section>
);
};
const Publications = () => {
const section = useArtboardStore((state) => state.resume.sections.publications);
return (
<Section<Publication> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.publisher}</div>
</div>
<div className="shrink-0 text-right">
<div className="font-bold">{item.date}</div>
</div>
</div>
)}
</Section>
);
};
const Volunteer = () => {
const section = useArtboardStore((state) => state.resume.sections.volunteer);
return (
<Section<Volunteer> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<LinkedEntity
name={item.organization}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.position}</div>
</div>
<div className="shrink-0 text-right">
<div className="font-bold">{item.date}</div>
<div>{item.location}</div>
</div>
</div>
)}
</Section>
);
};
const Languages = () => {
const section = useArtboardStore((state) => state.resume.sections.languages);
return (
<Section<Language> section={section} levelKey="level">
{(item) => (
<div>
<div className="font-bold">{item.name}</div>
<div>{item.description}</div>
</div>
)}
</Section>
);
};
const Projects = () => {
const section = useArtboardStore((state) => state.resume.sections.projects);
return (
<Section<Project> section={section} urlKey="url" summaryKey="summary" keywordsKey="keywords">
{(item) => (
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.description}</div>
</div>
<div className="shrink-0 text-right">
<div className="font-bold">{item.date}</div>
</div>
</div>
)}
</Section>
);
};
const References = () => {
const section = useArtboardStore((state) => state.resume.sections.references);
return (
<Section<Reference> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div>
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.description}</div>
</div>
)}
</Section>
);
};
const Custom = ({ id }: { id: string }) => {
const section = useArtboardStore((state) => state.resume.sections.custom[id]);
return (
<Section<CustomSection>
section={section}
urlKey="url"
summaryKey="summary"
keywordsKey="keywords"
>
{(item) => (
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.description}</div>
</div>
<div className="shrink-0 text-right">
<div className="font-bold">{item.date}</div>
<div>{item.location}</div>
</div>
</div>
)}
</Section>
);
};
const mapSectionToComponent = (section: SectionKey) => {
switch (section) {
case "profiles": {
return <Profiles />;
}
case "summary": {
return <Summary />;
}
case "experience": {
return <Experience />;
}
case "education": {
return <Education />;
}
case "awards": {
return <Awards />;
}
case "certifications": {
return <Certifications />;
}
case "skills": {
return <Skills />;
}
case "interests": {
return <Interests />;
}
case "publications": {
return <Publications />;
}
case "volunteer": {
return <Volunteer />;
}
case "languages": {
return <Languages />;
}
case "projects": {
return <Projects />;
}
case "references": {
return <References />;
}
default: {
if (section.startsWith("custom.")) return <Custom id={section.split(".")[1]} />;
return null;
}
}
};
export const Ditto = ({ columns, isFirstPage = false }: TemplateProps) => {
const [main, sidebar] = columns;
return (
<div>
{isFirstPage && (
<div className="relative">
<Header />
<div className="absolute inset-x-0 top-0 h-[85px] w-full bg-primary" />
</div>
)}
<div className="grid grid-cols-3">
<div className="sidebar p-custom group space-y-4">
{sidebar.map((section) => (
<Fragment key={section}>{mapSectionToComponent(section)}</Fragment>
))}
</div>
<div
className={cn(
"main p-custom group space-y-4",
sidebar.length > 0 ? "col-span-2" : "col-span-3",
)}
>
{main.map((section) => (
<Fragment key={section}>{mapSectionToComponent(section)}</Fragment>
))}
</div>
</div>
</div>
);
};
-605
View File
@@ -1,605 +0,0 @@
import type {
Award,
Certification,
CustomSection,
CustomSectionGroup,
Interest,
Language,
Profile,
Project,
Publication,
Reference,
SectionKey,
SectionWithItem,
Skill,
URL,
} from "@reactive-resume/schema";
import { Education, Experience, Volunteer } from "@reactive-resume/schema";
import { cn, hexToRgb, isEmptyString, isUrl, sanitize } from "@reactive-resume/utils";
import get from "lodash.get";
import { Fragment } from "react";
import { BrandIcon } from "../components/brand-icon";
import { Picture } from "../components/picture";
import { useArtboardStore } from "../store/artboard";
import type { TemplateProps } from "../types/template";
const Header = () => {
const basics = useArtboardStore((state) => state.resume.basics);
return (
<div className="p-custom space-y-4 bg-primary text-background">
<Picture className="border-background" />
<div>
<h2 className="text-2xl font-bold">{basics.name}</h2>
<p>{basics.headline}</p>
</div>
<div className="flex flex-col items-start gap-y-2 text-sm">
{basics.location && (
<div className="flex items-center gap-x-1.5">
<i className="ph ph-bold ph-map-pin" />
<div>{basics.location}</div>
</div>
)}
{basics.phone && (
<div className="flex items-center gap-x-1.5">
<i className="ph ph-bold ph-phone" />
<a href={`tel:${basics.phone}`} target="_blank" rel="noreferrer">
{basics.phone}
</a>
</div>
)}
{basics.email && (
<div className="flex items-center gap-x-1.5">
<i className="ph ph-bold ph-at" />
<a href={`mailto:${basics.email}`} target="_blank" rel="noreferrer">
{basics.email}
</a>
</div>
)}
{isUrl(basics.url.href) && <Link url={basics.url} />}
{basics.customFields.map((item) => (
<Fragment key={item.id}>
<div className="flex items-center gap-x-1.5">
<i className={cn(`ph ph-bold ph-${item.icon}`)} />
{isUrl(item.value) ? (
<a href={item.value} target="_blank" rel="noreferrer noopener nofollow">
{item.name || item.value}
</a>
) : (
<span>{[item.name, item.value].filter(Boolean).join(": ")}</span>
)}
</div>
</Fragment>
))}
</div>
</div>
);
};
const Summary = () => {
const section = useArtboardStore((state) => state.resume.sections.summary);
const primaryColor = useArtboardStore((state) => state.resume.metadata.theme.primary);
if (!section.visible || isEmptyString(section.content)) return null;
return (
<div className="p-custom space-y-4" style={{ backgroundColor: hexToRgb(primaryColor, 0.2) }}>
<section id={section.id}>
<div
dangerouslySetInnerHTML={{ __html: sanitize(section.content) }}
style={{ columns: section.columns }}
className="wysiwyg"
/>
</section>
</div>
);
};
type RatingProps = { level: number };
const Rating = ({ level }: RatingProps) => (
<div className="flex items-center gap-x-1">
{Array.from({ length: 5 }).map((_, index) => (
<div
key={index}
className={cn("h-2.5 w-5 border border-primary", level > index && "bg-primary")}
/>
))}
</div>
);
type LinkProps = {
url: URL;
icon?: React.ReactNode;
iconOnRight?: boolean;
label?: string;
className?: string;
};
const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
if (!isUrl(url.href)) return null;
return (
<div className="flex items-center gap-x-1.5">
{!iconOnRight &&
(icon ?? (
<i className="ph ph-bold ph-link text-primary group-[.sidebar]:text-background" />
))}
<a
href={url.href}
target="_blank"
rel="noreferrer noopener nofollow"
className={cn("inline-block", className)}
>
{label ?? (url.label || url.href)}
</a>
{iconOnRight &&
(icon ?? (
<i className="ph ph-bold ph-link text-primary group-[.sidebar]:text-background" />
))}
</div>
);
};
type LinkedEntityProps = {
name: string;
url: URL;
separateLinks: boolean;
className?: string;
};
const LinkedEntity = ({ name, url, separateLinks, className }: LinkedEntityProps) => {
return !separateLinks && isUrl(url.href) ? (
<Link
url={url}
label={name}
icon={<i className="ph ph-bold ph-globe text-primary group-[.sidebar]:text-primary" />}
iconOnRight={true}
className={className}
/>
) : (
<div className={className}>{name}</div>
);
};
type SectionProps<T> = {
section: SectionWithItem<T> | CustomSectionGroup;
children?: (item: T) => React.ReactNode;
className?: string;
urlKey?: keyof T;
levelKey?: keyof T;
summaryKey?: keyof T;
keywordsKey?: keyof T;
};
const Section = <T,>({
section,
children,
className,
urlKey,
levelKey,
summaryKey,
keywordsKey,
}: SectionProps<T>) => {
if (!section.visible || section.items.filter((item) => item.visible).length === 0) return null;
return (
<section id={section.id} className="grid">
<h4 className="mb-2 border-b border-primary text-base font-bold">{section.name}</h4>
<div
className="grid gap-x-6 gap-y-3"
style={{ gridTemplateColumns: `repeat(${section.columns}, 1fr)` }}
>
{section.items
.filter((item) => item.visible)
.map((item) => {
const url = (urlKey && get(item, urlKey)) as URL | undefined;
const level = (levelKey && get(item, levelKey, 0)) as number | undefined;
const summary = (summaryKey && get(item, summaryKey, "")) as string | undefined;
const keywords = (keywordsKey && get(item, keywordsKey, [])) as string[] | undefined;
return (
<div key={item.id} className={cn("space-y-2", className)}>
<div>
{children?.(item as T)}
{url !== undefined && section.separateLinks && <Link url={url} />}
</div>
{summary !== undefined && !isEmptyString(summary) && (
<div
dangerouslySetInnerHTML={{ __html: sanitize(summary) }}
className="wysiwyg"
/>
)}
{level !== undefined && level > 0 && <Rating level={level} />}
{keywords !== undefined && keywords.length > 0 && (
<p className="text-sm">{keywords.join(", ")}</p>
)}
</div>
);
})}
</div>
</section>
);
};
const Profiles = () => {
const section = useArtboardStore((state) => state.resume.sections.profiles);
return (
<Section<Profile> section={section}>
{(item) => (
<div>
{isUrl(item.url.href) ? (
<Link url={item.url} label={item.username} icon={<BrandIcon slug={item.icon} />} />
) : (
<p>{item.username}</p>
)}
{!item.icon && <p className="text-sm">{item.network}</p>}
</div>
)}
</Section>
);
};
const Experience = () => {
const section = useArtboardStore((state) => state.resume.sections.experience);
return (
<Section<Experience> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<LinkedEntity
name={item.company}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.position}</div>
</div>
<div className="shrink-0 text-right group-[.sidebar]:text-left">
<div className="font-bold">{item.date}</div>
<div>{item.location}</div>
</div>
</div>
)}
</Section>
);
};
const Education = () => {
const section = useArtboardStore((state) => state.resume.sections.education);
return (
<Section<Education> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<LinkedEntity
name={item.institution}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.area}</div>
<div>{item.score}</div>
</div>
<div className="shrink-0 text-right group-[.sidebar]:text-left">
<div className="font-bold">{item.date}</div>
<div>{item.studyType}</div>
</div>
</div>
)}
</Section>
);
};
const Awards = () => {
const section = useArtboardStore((state) => state.resume.sections.awards);
return (
<Section<Award> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<div className="font-bold">{item.title}</div>
<LinkedEntity
name={item.awarder}
url={item.url}
separateLinks={section.separateLinks}
/>
</div>
<div className="shrink-0 text-right group-[.sidebar]:text-left">
<div className="font-bold">{item.date}</div>
</div>
</div>
)}
</Section>
);
};
const Certifications = () => {
const section = useArtboardStore((state) => state.resume.sections.certifications);
return (
<Section<Certification> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<div className="font-bold">{item.name}</div>
<LinkedEntity name={item.issuer} url={item.url} separateLinks={section.separateLinks} />
</div>
<div className="shrink-0 text-right group-[.sidebar]:text-left">
<div className="font-bold">{item.date}</div>
</div>
</div>
)}
</Section>
);
};
const Skills = () => {
const section = useArtboardStore((state) => state.resume.sections.skills);
return (
<Section<Skill> section={section} levelKey="level" keywordsKey="keywords">
{(item) => (
<div>
<div className="font-bold">{item.name}</div>
<div>{item.description}</div>
</div>
)}
</Section>
);
};
const Interests = () => {
const section = useArtboardStore((state) => state.resume.sections.interests);
return (
<Section<Interest> section={section} className="space-y-1" keywordsKey="keywords">
{(item) => <div className="font-bold">{item.name}</div>}
</Section>
);
};
const Publications = () => {
const section = useArtboardStore((state) => state.resume.sections.publications);
return (
<Section<Publication> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.publisher}</div>
</div>
<div className="shrink-0 text-right group-[.sidebar]:text-left">
<div className="font-bold">{item.date}</div>
</div>
</div>
)}
</Section>
);
};
const Volunteer = () => {
const section = useArtboardStore((state) => state.resume.sections.volunteer);
return (
<Section<Volunteer> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<LinkedEntity
name={item.organization}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.position}</div>
</div>
<div className="shrink-0 text-right group-[.sidebar]:text-left">
<div className="font-bold">{item.date}</div>
<div>{item.location}</div>
</div>
</div>
)}
</Section>
);
};
const Languages = () => {
const section = useArtboardStore((state) => state.resume.sections.languages);
return (
<Section<Language> section={section} levelKey="level">
{(item) => (
<div>
<div className="font-bold">{item.name}</div>
<div>{item.description}</div>
</div>
)}
</Section>
);
};
const Projects = () => {
const section = useArtboardStore((state) => state.resume.sections.projects);
return (
<Section<Project> section={section} urlKey="url" summaryKey="summary" keywordsKey="keywords">
{(item) => (
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.description}</div>
</div>
<div className="shrink-0 text-right group-[.sidebar]:text-left">
<div className="font-bold">{item.date}</div>
</div>
</div>
)}
</Section>
);
};
const References = () => {
const section = useArtboardStore((state) => state.resume.sections.references);
return (
<Section<Reference> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div>
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.description}</div>
</div>
)}
</Section>
);
};
const Custom = ({ id }: { id: string }) => {
const section = useArtboardStore((state) => state.resume.sections.custom[id]);
return (
<Section<CustomSection>
section={section}
urlKey="url"
summaryKey="summary"
keywordsKey="keywords"
>
{(item) => (
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.description}</div>
</div>
<div className="shrink-0 text-right group-[.sidebar]:text-left">
<div className="font-bold">{item.date}</div>
<div>{item.location}</div>
</div>
</div>
)}
</Section>
);
};
const mapSectionToComponent = (section: SectionKey) => {
switch (section) {
case "profiles": {
return <Profiles />;
}
case "experience": {
return <Experience />;
}
case "education": {
return <Education />;
}
case "awards": {
return <Awards />;
}
case "certifications": {
return <Certifications />;
}
case "skills": {
return <Skills />;
}
case "interests": {
return <Interests />;
}
case "publications": {
return <Publications />;
}
case "volunteer": {
return <Volunteer />;
}
case "languages": {
return <Languages />;
}
case "projects": {
return <Projects />;
}
case "references": {
return <References />;
}
default: {
if (section.startsWith("custom.")) return <Custom id={section.split(".")[1]} />;
return null;
}
}
};
export const Gengar = ({ columns, isFirstPage = false }: TemplateProps) => {
const [main, sidebar] = columns;
const primaryColor = useArtboardStore((state) => state.resume.metadata.theme.primary);
return (
<div className="grid min-h-[inherit] grid-cols-3">
<div
className={cn(
"sidebar group flex flex-col",
!(isFirstPage || sidebar.length > 0) && "hidden",
)}
>
{isFirstPage && <Header />}
<div
className="p-custom flex-1 space-y-4"
style={{ backgroundColor: hexToRgb(primaryColor, 0.2) }}
>
{sidebar.map((section) => (
<Fragment key={section}>{mapSectionToComponent(section)}</Fragment>
))}
</div>
</div>
<div className={cn("main group", sidebar.length > 0 ? "col-span-2" : "col-span-3")}>
{isFirstPage && <Summary />}
<div className="p-custom space-y-4">
{main.map((section) => (
<Fragment key={section}>{mapSectionToComponent(section)}</Fragment>
))}
</div>
</div>
</div>
);
};
-612
View File
@@ -1,612 +0,0 @@
import type {
Award,
Certification,
CustomSection,
CustomSectionGroup,
Interest,
Language,
Profile,
Project,
Publication,
Reference,
SectionKey,
SectionWithItem,
Skill,
URL,
} from "@reactive-resume/schema";
import { Education, Experience, Volunteer } from "@reactive-resume/schema";
import {
cn,
hexToRgb,
isEmptyString,
isUrl,
linearTransform,
sanitize,
} from "@reactive-resume/utils";
import get from "lodash.get";
import { Fragment } from "react";
import { BrandIcon } from "../components/brand-icon";
import { Picture } from "../components/picture";
import { useArtboardStore } from "../store/artboard";
import type { TemplateProps } from "../types/template";
const Header = () => {
const basics = useArtboardStore((state) => state.resume.basics);
return (
<div className="flex flex-col items-center space-y-4 text-center">
<Picture />
<div className="space-y-4">
<div>
<div className="text-2xl font-bold">{basics.name}</div>
<div className="text-base">{basics.headline}</div>
</div>
<div className="flex flex-col items-start gap-y-1.5 rounded border border-primary px-3 py-4 text-left text-sm">
{basics.location && (
<div className="flex items-center gap-x-1.5">
<i className="ph ph-bold ph-map-pin text-primary" />
<div>{basics.location}</div>
</div>
)}
{basics.phone && (
<div className="flex items-center gap-x-1.5">
<i className="ph ph-bold ph-phone text-primary" />
<a href={`tel:${basics.phone}`} target="_blank" rel="noreferrer">
{basics.phone}
</a>
</div>
)}
{basics.email && (
<div className="flex items-center gap-x-1.5">
<i className="ph ph-bold ph-at text-primary" />
<a href={`mailto:${basics.email}`} target="_blank" rel="noreferrer">
{basics.email}
</a>
</div>
)}
<Link url={basics.url} />
{basics.customFields.map((item) => (
<div key={item.id} className="flex items-center gap-x-1.5">
<i className={cn(`ph ph-bold ph-${item.icon} text-primary`)} />
{isUrl(item.value) ? (
<a href={item.value} target="_blank" rel="noreferrer noopener nofollow">
{item.name || item.value}
</a>
) : (
<span>{[item.name, item.value].filter(Boolean).join(": ")}</span>
)}
</div>
))}
</div>
</div>
</div>
);
};
const Summary = () => {
const section = useArtboardStore((state) => state.resume.sections.summary);
if (!section.visible || isEmptyString(section.content)) return null;
return (
<section id={section.id}>
<h4 className="mb-2 border-b pb-0.5 text-sm font-bold">{section.name}</h4>
<div
dangerouslySetInnerHTML={{ __html: sanitize(section.content) }}
style={{ columns: section.columns }}
className="wysiwyg"
/>
</section>
);
};
type RatingProps = { level: number };
const Rating = ({ level }: RatingProps) => {
const primaryColor = useArtboardStore((state) => state.resume.metadata.theme.primary);
return (
<div className="relative">
<div
className="h-2.5 w-full rounded-sm"
style={{ backgroundColor: hexToRgb(primaryColor, 0.4) }}
/>
<div
className="absolute inset-y-0 left-0 h-2.5 w-full rounded-sm bg-primary"
style={{ width: `${linearTransform(level, 0, 5, 0, 100)}%` }}
/>
</div>
);
};
type LinkProps = {
url: URL;
icon?: React.ReactNode;
iconOnRight?: boolean;
label?: string;
className?: string;
};
const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
if (!isUrl(url.href)) return null;
return (
<div className="flex items-center gap-x-1.5">
{!iconOnRight &&
(icon ?? <i className="ph ph-bold ph-link text-primary group-[.sidebar]:text-primary" />)}
<a
href={url.href}
target="_blank"
rel="noreferrer noopener nofollow"
className={cn("inline-block", className)}
>
{label ?? (url.label || url.href)}
</a>
{iconOnRight &&
(icon ?? <i className="ph ph-bold ph-link text-primary group-[.sidebar]:text-primary" />)}
</div>
);
};
type LinkedEntityProps = {
name: string;
url: URL;
separateLinks: boolean;
className?: string;
};
const LinkedEntity = ({ name, url, separateLinks, className }: LinkedEntityProps) => {
return !separateLinks && isUrl(url.href) ? (
<Link
url={url}
label={name}
icon={<i className="ph ph-bold ph-globe text-primary group-[.sidebar]:text-primary" />}
iconOnRight={true}
className={className}
/>
) : (
<div className={className}>{name}</div>
);
};
type SectionProps<T> = {
section: SectionWithItem<T> | CustomSectionGroup;
children?: (item: T) => React.ReactNode;
className?: string;
urlKey?: keyof T;
levelKey?: keyof T;
summaryKey?: keyof T;
keywordsKey?: keyof T;
};
const Section = <T,>({
section,
children,
className,
urlKey,
levelKey,
summaryKey,
keywordsKey,
}: SectionProps<T>) => {
if (!section.visible || section.items.filter((item) => item.visible).length === 0) return null;
return (
<section id={section.id} className="grid">
<h4 className="mb-2 border-b pb-0.5 text-sm font-bold group-[.sidebar]:text-primary">
{section.name}
</h4>
<div
className="grid gap-x-6 gap-y-3"
style={{ gridTemplateColumns: `repeat(${section.columns}, 1fr)` }}
>
{section.items
.filter((item) => item.visible)
.map((item) => {
const url = (urlKey && get(item, urlKey)) as URL | undefined;
const level = (levelKey && get(item, levelKey, 0)) as number | undefined;
const summary = (summaryKey && get(item, summaryKey, "")) as string | undefined;
const keywords = (keywordsKey && get(item, keywordsKey, [])) as string[] | undefined;
return (
<div key={item.id} className={cn("space-y-2", className)}>
<div>
{children?.(item as T)}
{url !== undefined && section.separateLinks && <Link url={url} />}
</div>
{summary !== undefined && !isEmptyString(summary) && (
<div
dangerouslySetInnerHTML={{ __html: sanitize(summary) }}
className="wysiwyg"
/>
)}
{level !== undefined && level > 0 && <Rating level={level} />}
{keywords !== undefined && keywords.length > 0 && (
<p className="text-sm">{keywords.join(", ")}</p>
)}
</div>
);
})}
</div>
</section>
);
};
const Experience = () => {
const section = useArtboardStore((state) => state.resume.sections.experience);
return (
<Section<Experience> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<LinkedEntity
name={item.company}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.position}</div>
</div>
<div className="shrink-0 text-right">
<div className="font-bold">{item.date}</div>
<div>{item.location}</div>
</div>
</div>
)}
</Section>
);
};
const Education = () => {
const section = useArtboardStore((state) => state.resume.sections.education);
return (
<Section<Education> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<LinkedEntity
name={item.institution}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.area}</div>
<div>{item.score}</div>
</div>
<div className="shrink-0 text-right">
<div className="font-bold">{item.date}</div>
<div>{item.studyType}</div>
</div>
</div>
)}
</Section>
);
};
const Profiles = () => {
const section = useArtboardStore((state) => state.resume.sections.profiles);
return (
<Section<Profile> section={section}>
{(item) => (
<div>
{isUrl(item.url.href) ? (
<Link url={item.url} label={item.username} icon={<BrandIcon slug={item.icon} />} />
) : (
<p>{item.username}</p>
)}
{!item.icon && <p className="text-sm">{item.network}</p>}
</div>
)}
</Section>
);
};
const Awards = () => {
const section = useArtboardStore((state) => state.resume.sections.awards);
return (
<Section<Award> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<div className="font-bold">{item.title}</div>
<LinkedEntity
name={item.awarder}
url={item.url}
separateLinks={section.separateLinks}
/>
</div>
<div className="shrink-0 text-right">
<div className="font-bold">{item.date}</div>
</div>
</div>
)}
</Section>
);
};
const Certifications = () => {
const section = useArtboardStore((state) => state.resume.sections.certifications);
return (
<Section<Certification> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<div className="font-bold">{item.name}</div>
<LinkedEntity name={item.issuer} url={item.url} separateLinks={section.separateLinks} />
</div>
<div className="shrink-0 text-right">
<div className="font-bold">{item.date}</div>
</div>
</div>
)}
</Section>
);
};
const Skills = () => {
const section = useArtboardStore((state) => state.resume.sections.skills);
return (
<Section<Skill> section={section} levelKey="level" keywordsKey="keywords">
{(item) => (
<div>
<div className="font-bold">{item.name}</div>
<div>{item.description}</div>
</div>
)}
</Section>
);
};
const Interests = () => {
const section = useArtboardStore((state) => state.resume.sections.interests);
return (
<Section<Interest> section={section} keywordsKey="keywords" className="space-y-0.5">
{(item) => <div className="font-bold">{item.name}</div>}
</Section>
);
};
const Publications = () => {
const section = useArtboardStore((state) => state.resume.sections.publications);
return (
<Section<Publication> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.publisher}</div>
</div>
<div className="shrink-0 text-right">
<div className="font-bold">{item.date}</div>
</div>
</div>
)}
</Section>
);
};
const Volunteer = () => {
const section = useArtboardStore((state) => state.resume.sections.volunteer);
return (
<Section<Volunteer> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<LinkedEntity
name={item.organization}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.position}</div>
</div>
<div className="shrink-0 text-right">
<div className="font-bold">{item.date}</div>
<div>{item.location}</div>
</div>
</div>
)}
</Section>
);
};
const Languages = () => {
const section = useArtboardStore((state) => state.resume.sections.languages);
return (
<Section<Language> section={section} levelKey="level">
{(item) => (
<div className="space-y-0.5">
<div className="font-bold">{item.name}</div>
<div>{item.description}</div>
</div>
)}
</Section>
);
};
const Projects = () => {
const section = useArtboardStore((state) => state.resume.sections.projects);
return (
<Section<Project> section={section} urlKey="url" summaryKey="summary" keywordsKey="keywords">
{(item) => (
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.description}</div>
</div>
<div className="shrink-0 text-right">
<div className="font-bold">{item.date}</div>
</div>
</div>
)}
</Section>
);
};
const References = () => {
const section = useArtboardStore((state) => state.resume.sections.references);
return (
<Section<Reference> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div>
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.description}</div>
</div>
)}
</Section>
);
};
const Custom = ({ id }: { id: string }) => {
const section = useArtboardStore((state) => state.resume.sections.custom[id]);
return (
<Section<CustomSection>
section={section}
urlKey="url"
summaryKey="summary"
keywordsKey="keywords"
>
{(item) => (
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.description}</div>
</div>
<div className="shrink-0 text-right">
<div className="font-bold">{item.date}</div>
<div>{item.location}</div>
</div>
</div>
)}
</Section>
);
};
const mapSectionToComponent = (section: SectionKey) => {
switch (section) {
case "profiles": {
return <Profiles />;
}
case "summary": {
return <Summary />;
}
case "experience": {
return <Experience />;
}
case "education": {
return <Education />;
}
case "awards": {
return <Awards />;
}
case "certifications": {
return <Certifications />;
}
case "skills": {
return <Skills />;
}
case "interests": {
return <Interests />;
}
case "publications": {
return <Publications />;
}
case "volunteer": {
return <Volunteer />;
}
case "languages": {
return <Languages />;
}
case "projects": {
return <Projects />;
}
case "references": {
return <References />;
}
default: {
if (section.startsWith("custom.")) return <Custom id={section.split(".")[1]} />;
return null;
}
}
};
export const Glalie = ({ columns, isFirstPage = false }: TemplateProps) => {
const [main, sidebar] = columns;
const primaryColor = useArtboardStore((state) => state.resume.metadata.theme.primary);
return (
<div className="grid min-h-[inherit] grid-cols-3">
<div
className={cn("sidebar p-custom group space-y-4", sidebar.length === 0 && "hidden")}
style={{ backgroundColor: hexToRgb(primaryColor, 0.2) }}
>
{isFirstPage && <Header />}
{sidebar.map((section) => (
<Fragment key={section}>{mapSectionToComponent(section)}</Fragment>
))}
</div>
<div
className={cn(
"main p-custom group space-y-4",
sidebar.length > 0 ? "col-span-2" : "col-span-3",
)}
>
{main.map((section) => (
<Fragment key={section}>{mapSectionToComponent(section)}</Fragment>
))}
</div>
</div>
);
};
-58
View File
@@ -1,58 +0,0 @@
import type { Template } from "@reactive-resume/utils";
import { Azurill } from "./azurill";
import { Bronzor } from "./bronzor";
import { Chikorita } from "./chikorita";
import { Ditto } from "./ditto";
import { Gengar } from "./gengar";
import { Glalie } from "./glalie";
import { Kakuna } from "./kakuna";
import { Leafish } from "./leafish";
import { Nosepass } from "./nosepass";
import { Onyx } from "./onyx";
import { Pikachu } from "./pikachu";
import { Rhyhorn } from "./rhyhorn";
export const getTemplate = (template: Template) => {
switch (template) {
case "azurill": {
return Azurill;
}
case "bronzor": {
return Bronzor;
}
case "chikorita": {
return Chikorita;
}
case "ditto": {
return Ditto;
}
case "gengar": {
return Gengar;
}
case "glalie": {
return Glalie;
}
case "kakuna": {
return Kakuna;
}
case "leafish": {
return Leafish;
}
case "nosepass": {
return Nosepass;
}
case "onyx": {
return Onyx;
}
case "pikachu": {
return Pikachu;
}
case "rhyhorn": {
return Rhyhorn;
}
default: {
return Onyx;
}
}
};
-541
View File
@@ -1,541 +0,0 @@
import type {
Award,
Certification,
CustomSection,
CustomSectionGroup,
Interest,
Language,
Project,
Publication,
Reference,
SectionKey,
SectionWithItem,
Skill,
URL,
} from "@reactive-resume/schema";
import { Education, Experience, Volunteer } from "@reactive-resume/schema";
import { cn, isEmptyString, isUrl, sanitize } from "@reactive-resume/utils";
import get from "lodash.get";
import React, { Fragment } from "react";
import { BrandIcon } from "../components/brand-icon";
import { Picture } from "../components/picture";
import { useArtboardStore } from "../store/artboard";
import type { TemplateProps } from "../types/template";
const Header = () => {
const basics = useArtboardStore((state) => state.resume.basics);
const profiles = useArtboardStore((state) => state.resume.sections.profiles);
return (
<div className="flex flex-col items-center justify-center space-y-2 pb-2 text-center">
<Picture />
<div>
<div className="text-2xl font-bold">{basics.name}</div>
<div className="text-base">{basics.headline}</div>
</div>
<div className="flex flex-wrap items-center gap-x-3 gap-y-0.5 text-sm">
{basics.location && (
<div className="flex items-center gap-x-1.5">
<i className="ph ph-bold ph-map-pin text-primary" />
<div>{basics.location}</div>
</div>
)}
{basics.phone && (
<div className="flex items-center gap-x-1.5">
<i className="ph ph-bold ph-phone text-primary" />
<a href={`tel:${basics.phone}`} target="_blank" rel="noreferrer">
{basics.phone}
</a>
</div>
)}
{basics.email && (
<div className="flex items-center gap-x-1.5">
<i className="ph ph-bold ph-at text-primary" />
<a href={`mailto:${basics.email}`} target="_blank" rel="noreferrer">
{basics.email}
</a>
</div>
)}
<Link url={basics.url} />
{basics.customFields.map((item) => (
<div key={item.id} className="flex items-center gap-x-1.5">
<i className={cn(`ph ph-bold ph-${item.icon}`, "text-primary")} />
{isUrl(item.value) ? (
<a href={item.value} target="_blank" rel="noreferrer noopener nofollow">
{item.name || item.value}
</a>
) : (
<span>{[item.name, item.value].filter(Boolean).join(": ")}</span>
)}
</div>
))}
</div>
{profiles.visible && profiles.items.length > 0 && (
<div className="flex items-center gap-x-3 gap-y-0.5">
{profiles.items
.filter((item) => item.visible)
.map((item) => (
<div key={item.id} className="flex items-center gap-x-2">
<Link
url={item.url}
label={item.username}
className="text-sm"
icon={<BrandIcon slug={item.icon} />}
/>
</div>
))}
</div>
)}
</div>
);
};
const Summary = () => {
const section = useArtboardStore((state) => state.resume.sections.summary);
if (!section.visible || isEmptyString(section.content)) return null;
return (
<section id={section.id}>
<h4 className="mb-2 border-b border-primary text-center font-bold text-primary">
{section.name}
</h4>
<div
dangerouslySetInnerHTML={{ __html: sanitize(section.content) }}
style={{ columns: section.columns }}
className="wysiwyg"
/>
</section>
);
};
type RatingProps = { level: number };
const Rating = ({ level }: RatingProps) => (
<div className="flex items-center gap-x-1.5">
{Array.from({ length: 5 }).map((_, index) => (
<div
key={index}
className={cn("h-3 w-5 rounded border-2 border-primary", level > index && "bg-primary")}
/>
))}
</div>
);
type LinkProps = {
url: URL;
icon?: React.ReactNode;
iconOnRight?: boolean;
label?: string;
className?: string;
};
const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
if (!isUrl(url.href)) return null;
return (
<div className="flex items-center gap-x-1.5">
{!iconOnRight && (icon ?? <i className="ph ph-bold ph-link text-primary" />)}
<a
href={url.href}
target="_blank"
rel="noreferrer noopener nofollow"
className={cn("inline-block", className)}
>
{label ?? (url.label || url.href)}
</a>
{iconOnRight && (icon ?? <i className="ph ph-bold ph-link text-primary" />)}
</div>
);
};
type LinkedEntityProps = {
name: string;
url: URL;
separateLinks: boolean;
className?: string;
};
const LinkedEntity = ({ name, url, separateLinks, className }: LinkedEntityProps) => {
return !separateLinks && isUrl(url.href) ? (
<Link
url={url}
label={name}
icon={<i className="ph ph-bold ph-globe text-primary" />}
iconOnRight={true}
className={className}
/>
) : (
<div className={className}>{name}</div>
);
};
type SectionProps<T> = {
section: SectionWithItem<T> | CustomSectionGroup;
children?: (item: T) => React.ReactNode;
className?: string;
urlKey?: keyof T;
levelKey?: keyof T;
summaryKey?: keyof T;
keywordsKey?: keyof T;
};
const Section = <T,>({
section,
children,
className,
urlKey,
levelKey,
summaryKey,
keywordsKey,
}: SectionProps<T>) => {
if (!section.visible || section.items.filter((item) => item.visible).length === 0) return null;
return (
<section id={section.id} className="grid">
<h4 className="mb-2 border-b border-primary text-center font-bold text-primary">
{section.name}
</h4>
<div
className="grid gap-x-6 gap-y-3"
style={{ gridTemplateColumns: `repeat(${section.columns}, 1fr)` }}
>
{section.items
.filter((item) => item.visible)
.map((item) => {
const url = (urlKey && get(item, urlKey)) as URL | undefined;
const level = (levelKey && get(item, levelKey, 0)) as number | undefined;
const summary = (summaryKey && get(item, summaryKey, "")) as string | undefined;
const keywords = (keywordsKey && get(item, keywordsKey, [])) as string[] | undefined;
return (
<div key={item.id} className={cn("space-y-2", className)}>
<div>{children?.(item as T)}</div>
{summary !== undefined && !isEmptyString(summary) && (
<div
dangerouslySetInnerHTML={{ __html: sanitize(summary) }}
className="wysiwyg"
/>
)}
{level !== undefined && level > 0 && <Rating level={level} />}
{keywords !== undefined && keywords.length > 0 && (
<p className="text-sm">{keywords.join(", ")}</p>
)}
{url !== undefined && section.separateLinks && <Link url={url} />}
</div>
);
})}
</div>
</section>
);
};
const Experience = () => {
const section = useArtboardStore((state) => state.resume.sections.experience);
return (
<Section<Experience> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div>
<LinkedEntity
name={item.company}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.position}</div>
<div>{item.location}</div>
<div className="font-bold">{item.date}</div>
</div>
)}
</Section>
);
};
const Education = () => {
const section = useArtboardStore((state) => state.resume.sections.education);
return (
<Section<Education> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div>
<LinkedEntity
name={item.institution}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.area}</div>
<div>{item.score}</div>
<div>{item.studyType}</div>
<div className="font-bold">{item.date}</div>
</div>
)}
</Section>
);
};
const Awards = () => {
const section = useArtboardStore((state) => state.resume.sections.awards);
return (
<Section<Award> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div>
<div className="font-bold">{item.title}</div>
<LinkedEntity name={item.awarder} url={item.url} separateLinks={section.separateLinks} />
<div className="font-bold">{item.date}</div>
</div>
)}
</Section>
);
};
const Certifications = () => {
const section = useArtboardStore((state) => state.resume.sections.certifications);
return (
<Section<Certification> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div>
<div className="font-bold">{item.name}</div>
<LinkedEntity name={item.issuer} url={item.url} separateLinks={section.separateLinks} />
<div className="font-bold">{item.date}</div>
</div>
)}
</Section>
);
};
const Skills = () => {
const section = useArtboardStore((state) => state.resume.sections.skills);
return (
<Section<Skill> section={section} levelKey="level" keywordsKey="keywords">
{(item) => (
<div>
<div className="font-bold">{item.name}</div>
<div>{item.description}</div>
</div>
)}
</Section>
);
};
const Interests = () => {
const section = useArtboardStore((state) => state.resume.sections.interests);
return (
<Section<Interest> section={section} keywordsKey="keywords" className="space-y-0.5">
{(item) => <div className="font-bold">{item.name}</div>}
</Section>
);
};
const Publications = () => {
const section = useArtboardStore((state) => state.resume.sections.publications);
return (
<Section<Publication> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div>
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.publisher}</div>
<div className="font-bold">{item.date}</div>
</div>
)}
</Section>
);
};
const Volunteer = () => {
const section = useArtboardStore((state) => state.resume.sections.volunteer);
return (
<Section<Volunteer> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div>
<LinkedEntity
name={item.organization}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.position}</div>
<div>{item.location}</div>
<div className="font-bold">{item.date}</div>
</div>
)}
</Section>
);
};
const Languages = () => {
const section = useArtboardStore((state) => state.resume.sections.languages);
return (
<Section<Language> section={section} levelKey="level">
{(item) => (
<div>
<div className="font-bold">{item.name}</div>
<div>{item.description}</div>
</div>
)}
</Section>
);
};
const Projects = () => {
const section = useArtboardStore((state) => state.resume.sections.projects);
return (
<Section<Project> section={section} urlKey="url" summaryKey="summary" keywordsKey="keywords">
{(item) => (
<div>
<div>
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.description}</div>
<div className="font-bold">{item.date}</div>
</div>
</div>
)}
</Section>
);
};
const References = () => {
const section = useArtboardStore((state) => state.resume.sections.references);
return (
<Section<Reference> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div>
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.description}</div>
</div>
)}
</Section>
);
};
const Custom = ({ id }: { id: string }) => {
const section = useArtboardStore((state) => state.resume.sections.custom[id]);
return (
<Section<CustomSection>
section={section}
urlKey="url"
summaryKey="summary"
keywordsKey="keywords"
>
{(item) => (
<div>
<div>
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.description}</div>
<div>{item.location}</div>
<div className="font-bold">{item.date}</div>
</div>
</div>
)}
</Section>
);
};
const mapSectionToComponent = (section: SectionKey) => {
switch (section) {
case "summary": {
return <Summary />;
}
case "experience": {
return <Experience />;
}
case "education": {
return <Education />;
}
case "awards": {
return <Awards />;
}
case "certifications": {
return <Certifications />;
}
case "skills": {
return <Skills />;
}
case "interests": {
return <Interests />;
}
case "publications": {
return <Publications />;
}
case "volunteer": {
return <Volunteer />;
}
case "languages": {
return <Languages />;
}
case "projects": {
return <Projects />;
}
case "references": {
return <References />;
}
default: {
if (section.startsWith("custom.")) return <Custom id={section.split(".")[1]} />;
return null;
}
}
};
export const Kakuna = ({ columns, isFirstPage = false }: TemplateProps) => {
const [main, sidebar] = columns;
return (
<div className="p-custom space-y-4">
{isFirstPage && <Header />}
<div className="space-y-4">
{main.map((section) => (
<Fragment key={section}>{mapSectionToComponent(section)}</Fragment>
))}
{sidebar.map((section) => (
<Fragment key={section}>{mapSectionToComponent(section)}</Fragment>
))}
</div>
</div>
);
};
-537
View File
@@ -1,537 +0,0 @@
import type {
Award,
Certification,
CustomSection,
CustomSectionGroup,
Interest,
Language,
Project,
Publication,
Reference,
SectionKey,
SectionWithItem,
Skill,
URL,
} from "@reactive-resume/schema";
import { Education, Experience, Volunteer } from "@reactive-resume/schema";
import { cn, hexToRgb, isEmptyString, isUrl, sanitize } from "@reactive-resume/utils";
import get from "lodash.get";
import React, { Fragment } from "react";
import { BrandIcon } from "../components/brand-icon";
import { Picture } from "../components/picture";
import { useArtboardStore } from "../store/artboard";
import type { TemplateProps } from "../types/template";
const Header = () => {
const basics = useArtboardStore((state) => state.resume.basics);
const section = useArtboardStore((state) => state.resume.sections.summary);
const profiles = useArtboardStore((state) => state.resume.sections.profiles);
const primaryColor = useArtboardStore((state) => state.resume.metadata.theme.primary);
return (
<div>
<div
className="p-custom flex items-center space-x-8"
style={{ backgroundColor: hexToRgb(primaryColor, 0.2) }}
>
<div className="space-y-3">
<div>
<div className="text-3xl font-bold">{basics.name}</div>
<div className="text-base font-medium text-primary">{basics.headline}</div>
</div>
<div
dangerouslySetInnerHTML={{ __html: sanitize(section.content) }}
style={{ columns: section.columns }}
className="wysiwyg"
/>
</div>
<Picture />
</div>
<div className="p-custom space-y-3" style={{ backgroundColor: hexToRgb(primaryColor, 0.4) }}>
<div className="flex flex-wrap items-center gap-x-3 gap-y-0.5 text-sm">
{basics.location && (
<div className="flex items-center gap-x-1.5">
<i className="ph ph-bold ph-map-pin text-primary" />
<div>{basics.location}</div>
</div>
)}
{basics.phone && (
<div className="flex items-center gap-x-1.5">
<i className="ph ph-bold ph-phone text-primary" />
<a href={`tel:${basics.phone}`} target="_blank" rel="noreferrer">
{basics.phone}
</a>
</div>
)}
{basics.email && (
<div className="flex items-center gap-x-1.5">
<i className="ph ph-bold ph-at text-primary" />
<a href={`mailto:${basics.email}`} target="_blank" rel="noreferrer">
{basics.email}
</a>
</div>
)}
<Link url={basics.url} />
{basics.customFields.map((item) => (
<div key={item.id} className="flex items-center gap-x-1.5">
<i className={cn(`ph ph-bold ph-${item.icon}`, "text-primary")} />
{isUrl(item.value) ? (
<a href={item.value} target="_blank" rel="noreferrer noopener nofollow">
{item.name || item.value}
</a>
) : (
<span>{[item.name, item.value].filter(Boolean).join(": ")}</span>
)}
</div>
))}
</div>
{profiles.visible && profiles.items.length > 0 && (
<div className="flex items-center gap-x-3 gap-y-0.5">
{profiles.items
.filter((item) => item.visible)
.map((item) => (
<div key={item.id} className="flex items-center gap-x-2">
<Link
url={item.url}
label={item.username}
className="text-sm"
icon={<BrandIcon slug={item.icon} />}
/>
</div>
))}
</div>
)}
</div>
</div>
);
};
type RatingProps = { level: number };
const Rating = ({ level }: RatingProps) => (
<div className="flex items-center gap-x-1.5">
{Array.from({ length: 5 }).map((_, index) => (
<div
key={index}
className={cn("h-3 w-6 border-2 border-primary", level > index && "bg-primary")}
/>
))}
</div>
);
type LinkProps = {
url: URL;
icon?: React.ReactNode;
iconOnRight?: boolean;
label?: string;
className?: string;
};
const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
if (!isUrl(url.href)) return null;
return (
<div className="flex items-center gap-x-1.5">
{!iconOnRight && (icon ?? <i className="ph ph-bold ph-link text-primary" />)}
<a
href={url.href}
target="_blank"
rel="noreferrer noopener nofollow"
className={cn("inline-block", className)}
>
{label ?? (url.label || url.href)}
</a>
{iconOnRight && (icon ?? <i className="ph ph-bold ph-link text-primary" />)}
</div>
);
};
type LinkedEntityProps = {
name: string;
url: URL;
separateLinks: boolean;
className?: string;
};
const LinkedEntity = ({ name, url, separateLinks, className }: LinkedEntityProps) => {
return !separateLinks && isUrl(url.href) ? (
<Link
url={url}
label={name}
icon={<i className="ph ph-bold ph-globe text-primary" />}
iconOnRight={true}
className={className}
/>
) : (
<div className={className}>{name}</div>
);
};
type SectionProps<T> = {
section: SectionWithItem<T> | CustomSectionGroup;
children?: (item: T) => React.ReactNode;
className?: string;
urlKey?: keyof T;
levelKey?: keyof T;
summaryKey?: keyof T;
keywordsKey?: keyof T;
};
const Section = <T,>({
section,
children,
className,
urlKey,
levelKey,
summaryKey,
keywordsKey,
}: SectionProps<T>) => {
if (!section.visible || section.items.filter((item) => item.visible).length === 0) return null;
return (
<section id={section.id} className="grid">
<h4 className="mb-2 border-b border-primary text-left font-bold text-primary">
{section.name}
</h4>
<div
className="grid gap-x-6 gap-y-3"
style={{ gridTemplateColumns: `repeat(${section.columns}, 1fr)` }}
>
{section.items
.filter((item) => item.visible)
.map((item) => {
const url = (urlKey && get(item, urlKey)) as URL | undefined;
const level = (levelKey && get(item, levelKey, 0)) as number | undefined;
const summary = (summaryKey && get(item, summaryKey, "")) as string | undefined;
const keywords = (keywordsKey && get(item, keywordsKey, [])) as string[] | undefined;
return (
<div key={item.id} className={cn("space-y-2", className)}>
<div>{children?.(item as T)}</div>
{summary !== undefined && !isEmptyString(summary) && (
<div
dangerouslySetInnerHTML={{ __html: sanitize(summary) }}
className="wysiwyg"
/>
)}
{level !== undefined && level > 0 && <Rating level={level} />}
{keywords !== undefined && keywords.length > 0 && (
<p className="text-sm">{keywords.join(", ")}</p>
)}
{url !== undefined && section.separateLinks && <Link url={url} />}
</div>
);
})}
</div>
</section>
);
};
const Experience = () => {
const section = useArtboardStore((state) => state.resume.sections.experience);
return (
<Section<Experience> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div>
<LinkedEntity
name={item.company}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.position}</div>
<div>{item.location}</div>
<div className="font-bold">{item.date}</div>
</div>
)}
</Section>
);
};
const Education = () => {
const section = useArtboardStore((state) => state.resume.sections.education);
return (
<Section<Education> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div>
<LinkedEntity
name={item.institution}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.area}</div>
<div>{item.score}</div>
<div>{item.studyType}</div>
<div className="font-bold">{item.date}</div>
</div>
)}
</Section>
);
};
const Awards = () => {
const section = useArtboardStore((state) => state.resume.sections.awards);
return (
<Section<Award> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div>
<div className="font-bold">{item.title}</div>
<LinkedEntity name={item.awarder} url={item.url} separateLinks={section.separateLinks} />
<div className="font-bold">{item.date}</div>
</div>
)}
</Section>
);
};
const Certifications = () => {
const section = useArtboardStore((state) => state.resume.sections.certifications);
return (
<Section<Certification> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div>
<div className="font-bold">{item.name}</div>
<LinkedEntity name={item.issuer} url={item.url} separateLinks={section.separateLinks} />
<div className="font-bold">{item.date}</div>
</div>
)}
</Section>
);
};
const Skills = () => {
const section = useArtboardStore((state) => state.resume.sections.skills);
return (
<Section<Skill> section={section} levelKey="level" keywordsKey="keywords">
{(item) => (
<div>
<div className="font-bold">{item.name}</div>
<div>{item.description}</div>
</div>
)}
</Section>
);
};
const Interests = () => {
const section = useArtboardStore((state) => state.resume.sections.interests);
return (
<Section<Interest> section={section} keywordsKey="keywords" className="space-y-0.5">
{(item) => <div className="font-bold">{item.name}</div>}
</Section>
);
};
const Publications = () => {
const section = useArtboardStore((state) => state.resume.sections.publications);
return (
<Section<Publication> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div>
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.publisher}</div>
<div className="font-bold">{item.date}</div>
</div>
)}
</Section>
);
};
const Volunteer = () => {
const section = useArtboardStore((state) => state.resume.sections.volunteer);
return (
<Section<Volunteer> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div>
<LinkedEntity
name={item.organization}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.position}</div>
<div>{item.location}</div>
<div className="font-bold">{item.date}</div>
</div>
)}
</Section>
);
};
const Languages = () => {
const section = useArtboardStore((state) => state.resume.sections.languages);
return (
<Section<Language> section={section} levelKey="level">
{(item) => (
<div>
<div className="font-bold">{item.name}</div>
<div>{item.description}</div>
</div>
)}
</Section>
);
};
const Projects = () => {
const section = useArtboardStore((state) => state.resume.sections.projects);
return (
<Section<Project> section={section} urlKey="url" summaryKey="summary" keywordsKey="keywords">
{(item) => (
<div>
<div>
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.description}</div>
<div className="font-bold">{item.date}</div>
</div>
</div>
)}
</Section>
);
};
const References = () => {
const section = useArtboardStore((state) => state.resume.sections.references);
return (
<Section<Reference> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div>
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.description}</div>
</div>
)}
</Section>
);
};
const Custom = ({ id }: { id: string }) => {
const section = useArtboardStore((state) => state.resume.sections.custom[id]);
return (
<Section<CustomSection>
section={section}
urlKey="url"
summaryKey="summary"
keywordsKey="keywords"
>
{(item) => (
<div>
<div>
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.description}</div>
<div>{item.location}</div>
<div className="font-bold">{item.date}</div>
</div>
</div>
)}
</Section>
);
};
const mapSectionToComponent = (section: SectionKey) => {
switch (section) {
case "experience": {
return <Experience />;
}
case "education": {
return <Education />;
}
case "awards": {
return <Awards />;
}
case "certifications": {
return <Certifications />;
}
case "skills": {
return <Skills />;
}
case "interests": {
return <Interests />;
}
case "publications": {
return <Publications />;
}
case "volunteer": {
return <Volunteer />;
}
case "languages": {
return <Languages />;
}
case "projects": {
return <Projects />;
}
case "references": {
return <References />;
}
default: {
if (section.startsWith("custom.")) return <Custom id={section.split(".")[1]} />;
return null;
}
}
};
export const Leafish = ({ columns, isFirstPage = false }: TemplateProps) => {
const [main, sidebar] = columns;
return (
<div>
{isFirstPage && <Header />}
<div className="p-custom grid grid-cols-2 items-start space-x-6">
<div className={cn("grid gap-y-4", sidebar.length === 0 && "col-span-2")}>
{main.map((section) => (
<Fragment key={section}>{mapSectionToComponent(section)}</Fragment>
))}
</div>
<div className={cn("grid gap-y-4", sidebar.length === 0 && "hidden")}>
{sidebar.map((section) => (
<Fragment key={section}>{mapSectionToComponent(section)}</Fragment>
))}
</div>
</div>
</div>
);
};
-600
View File
@@ -1,600 +0,0 @@
import type {
Award,
Certification,
CustomSection,
CustomSectionGroup,
Interest,
Language,
Profile,
Project,
Publication,
Reference,
SectionKey,
SectionWithItem,
Skill,
URL,
} from "@reactive-resume/schema";
import { Education, Experience, Volunteer } from "@reactive-resume/schema";
import { cn, isEmptyString, isUrl, sanitize } from "@reactive-resume/utils";
import get from "lodash.get";
import { Fragment } from "react";
import { BrandIcon } from "../components/brand-icon";
import { Picture } from "../components/picture";
import { useArtboardStore } from "../store/artboard";
import type { TemplateProps } from "../types/template";
const Header = () => {
const basics = useArtboardStore((state) => state.resume.basics);
return (
<div className="grid grid-cols-4 gap-x-6">
<div className="mt-1 space-y-2 text-right">
<Picture className="ml-auto" />
</div>
<div className="col-span-3 space-y-2">
<div>
<div className="text-2xl font-bold">{basics.name}</div>
<div className="text-base">{basics.headline}</div>
</div>
<div className="space-y-1 text-sm">
{basics.location && (
<div className="flex items-center gap-x-1.5">
<i className="ph ph-bold ph-map-pin text-primary" />
<div>{basics.location}</div>
</div>
)}
{basics.phone && (
<div className="flex items-center gap-x-1.5">
<i className="ph ph-bold ph-phone text-primary" />
<a href={`tel:${basics.phone}`} target="_blank" rel="noreferrer">
{basics.phone}
</a>
</div>
)}
{basics.email && (
<div className="flex items-center gap-x-1.5">
<i className="ph ph-bold ph-at text-primary" />
<a href={`mailto:${basics.email}`} target="_blank" rel="noreferrer">
{basics.email}
</a>
</div>
)}
<Link url={basics.url} />
</div>
<div className="flex flex-wrap gap-x-3 text-sm">
{basics.customFields.map((item) => (
<div key={item.id} className="flex items-center gap-x-1.5">
<i className={cn(`ph ph-bold ph-${item.icon}`, "text-primary")} />
{isUrl(item.value) ? (
<a href={item.value} target="_blank" rel="noreferrer noopener nofollow">
{item.name || item.value}
</a>
) : (
<>
<span className="text-primary">{item.name}</span>
<span>{item.value}</span>
</>
)}
</div>
))}
</div>
</div>
</div>
);
};
const Summary = () => {
const section = useArtboardStore((state) => state.resume.sections.summary);
if (!section.visible || isEmptyString(section.content)) return null;
return (
<section id={section.id} className="grid grid-cols-4 gap-x-6">
<div className="text-right">
<h4 className="font-medium text-primary">{section.name}</h4>
</div>
<div className="col-span-3">
<div className="relative">
<hr className="mt-3 border-primary pb-3" />
<div className="absolute bottom-3 right-0 size-3 bg-primary" />
</div>
<div
dangerouslySetInnerHTML={{ __html: sanitize(section.content) }}
style={{ columns: section.columns }}
className="wysiwyg"
/>
</div>
</section>
);
};
type LinkProps = {
url: URL;
icon?: React.ReactNode;
iconOnRight?: boolean;
label?: string;
className?: string;
};
const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
if (!isUrl(url.href)) return null;
return (
<div className="flex items-center gap-x-1.5">
{!iconOnRight && (icon ?? <i className="ph ph-bold ph-link text-primary" />)}
<a
href={url.href}
target="_blank"
rel="noreferrer noopener nofollow"
className={cn("inline-block", className)}
>
{label ?? (url.label || url.href)}
</a>
{iconOnRight && (icon ?? <i className="ph ph-bold ph-link text-primary" />)}
</div>
);
};
type LinkedEntityProps = {
name: string;
url: URL;
separateLinks: boolean;
className?: string;
};
const LinkedEntity = ({ name, url, separateLinks, className }: LinkedEntityProps) => {
return !separateLinks && isUrl(url.href) ? (
<Link
url={url}
label={name}
icon={<i className="ph ph-bold ph-globe text-primary" />}
iconOnRight={true}
className={className}
/>
) : (
<div className={className}>{name}</div>
);
};
type SectionProps<T> = {
section: SectionWithItem<T> | CustomSectionGroup;
children?: (item: T) => React.ReactNode;
urlKey?: keyof T;
dateKey?: keyof T;
levelKey?: keyof T;
summaryKey?: keyof T;
keywordsKey?: keyof T;
};
const Section = <T,>({
section,
children,
urlKey,
dateKey,
summaryKey,
keywordsKey,
}: SectionProps<T>) => {
if (!section.visible || section.items.filter((item) => item.visible).length === 0) return null;
return (
<section id={section.id} className={cn("grid", dateKey !== undefined && "gap-y-4")}>
<div className="grid grid-cols-4 gap-x-6">
<div className="text-right">
<h4 className="font-medium text-primary">{section.name}</h4>
</div>
<div className="col-span-3">
<div className="relative">
<hr className="mt-3 border-primary" />
<div className="absolute bottom-0 right-0 size-3 bg-primary" />
</div>
</div>
</div>
{dateKey !== undefined && (
<div className="grid grid-cols-4 gap-x-6 gap-y-4">
{section.items
.filter((item) => item.visible)
.map((item) => {
const url = (urlKey && get(item, urlKey)) as URL | undefined;
const date = (dateKey && get(item, dateKey, "")) as string | undefined;
const summary = (summaryKey && get(item, summaryKey, "")) as string | undefined;
const keywords = (keywordsKey && get(item, keywordsKey, [])) as string[] | undefined;
return (
<Fragment key={item.id}>
<div className="text-right font-medium text-primary">{date}</div>
<div className="col-span-3 space-y-1">
{children?.(item as T)}
{url !== undefined && section.separateLinks && <Link url={url} />}
{summary !== undefined && !isEmptyString(summary) && (
<div
dangerouslySetInnerHTML={{ __html: sanitize(summary) }}
className="wysiwyg"
/>
)}
{keywords !== undefined && keywords.length > 0 && (
<p className="text-sm">{keywords.join(", ")}</p>
)}
</div>
</Fragment>
);
})}
</div>
)}
{dateKey === undefined && (
<div className="grid grid-cols-4 gap-x-6">
<div
className="col-span-3 col-start-2 grid gap-x-6 gap-y-3"
style={{ gridTemplateColumns: `repeat(${section.columns}, 1fr)` }}
>
{section.items
.filter((item) => item.visible)
.map((item) => {
const url = (urlKey && get(item, urlKey)) as URL | undefined;
const summary = (summaryKey && get(item, summaryKey, "")) as string | undefined;
const keywords = (keywordsKey && get(item, keywordsKey, [])) as
| string[]
| undefined;
return (
<div key={item.id}>
{children?.(item as T)}
{url !== undefined && section.separateLinks && <Link url={url} />}
{summary !== undefined && !isEmptyString(summary) && (
<div
dangerouslySetInnerHTML={{ __html: sanitize(summary) }}
className="wysiwyg"
/>
)}
{keywords !== undefined && keywords.length > 0 && (
<p className="text-sm">{keywords.join(", ")}</p>
)}
</div>
);
})}
</div>
</div>
)}
</section>
);
};
const Profiles = () => {
const section = useArtboardStore((state) => state.resume.sections.profiles);
return (
<Section<Profile> section={section}>
{(item) => (
<div>
{isUrl(item.url.href) ? (
<Link url={item.url} label={item.username} icon={<BrandIcon slug={item.icon} />} />
) : (
<p>{item.username}</p>
)}
{!item.icon && <p className="text-sm">{item.network}</p>}
</div>
)}
</Section>
);
};
const Experience = () => {
const section = useArtboardStore((state) => state.resume.sections.experience);
return (
<Section<Experience> section={section} urlKey="url" dateKey="date" summaryKey="summary">
{(item) => (
<div>
<LinkedEntity
name={item.company}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.position}</div>
<div>{item.location}</div>
</div>
)}
</Section>
);
};
const Education = () => {
const section = useArtboardStore((state) => state.resume.sections.education);
return (
<Section<Education> section={section} urlKey="url" dateKey="date" summaryKey="summary">
{(item) => (
<div>
<LinkedEntity
name={item.institution}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.area}</div>
<div>{item.studyType}</div>
<div>{item.score}</div>
</div>
)}
</Section>
);
};
const Awards = () => {
const section = useArtboardStore((state) => state.resume.sections.awards);
return (
<Section<Award> section={section} urlKey="url" dateKey="date" summaryKey="summary">
{(item) => (
<div>
<div className="font-bold">{item.title}</div>
<LinkedEntity name={item.awarder} url={item.url} separateLinks={section.separateLinks} />
</div>
)}
</Section>
);
};
const Certifications = () => {
const section = useArtboardStore((state) => state.resume.sections.certifications);
return (
<Section<Certification> section={section} urlKey="url" dateKey="date" summaryKey="summary">
{(item) => (
<div>
<div className="font-bold">{item.name}</div>
<LinkedEntity name={item.issuer} url={item.url} separateLinks={section.separateLinks} />
</div>
)}
</Section>
);
};
const Skills = () => {
const section = useArtboardStore((state) => state.resume.sections.skills);
return (
<Section<Skill> section={section} levelKey="level" keywordsKey="keywords">
{(item) => (
<div>
<div className="font-bold">{item.name}</div>
<div>{item.description}</div>
</div>
)}
</Section>
);
};
const Interests = () => {
const section = useArtboardStore((state) => state.resume.sections.interests);
return (
<Section<Interest> section={section} keywordsKey="keywords">
{(item) => <div className="font-bold">{item.name}</div>}
</Section>
);
};
const Publications = () => {
const section = useArtboardStore((state) => state.resume.sections.publications);
return (
<Section<Publication> section={section} urlKey="url" dateKey="date" summaryKey="summary">
{(item) => (
<div>
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.publisher}</div>
</div>
)}
</Section>
);
};
const Volunteer = () => {
const section = useArtboardStore((state) => state.resume.sections.volunteer);
return (
<Section<Volunteer> section={section} urlKey="url" dateKey="date" summaryKey="summary">
{(item) => (
<div>
<LinkedEntity
name={item.organization}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.position}</div>
<div>{item.location}</div>
</div>
)}
</Section>
);
};
const Languages = () => {
const section = useArtboardStore((state) => state.resume.sections.languages);
return (
<Section<Language> section={section} levelKey="level">
{(item) => (
<div>
<div className="font-bold">{item.name}</div>
<div>{item.description}</div>
</div>
)}
</Section>
);
};
const Projects = () => {
const section = useArtboardStore((state) => state.resume.sections.projects);
return (
<Section<Project>
section={section}
urlKey="url"
dateKey="date"
summaryKey="summary"
keywordsKey="keywords"
>
{(item) => (
<div>
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.description}</div>
</div>
)}
</Section>
);
};
const References = () => {
const section = useArtboardStore((state) => state.resume.sections.references);
return (
<Section<Reference> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div>
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.description}</div>
</div>
)}
</Section>
);
};
const Custom = ({ id }: { id: string }) => {
const section = useArtboardStore((state) => state.resume.sections.custom[id]);
return (
<Section<CustomSection>
section={section}
urlKey="url"
dateKey="date"
summaryKey="summary"
keywordsKey="keywords"
>
{(item) => (
<div>
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.description}</div>
<div>{item.location}</div>
</div>
)}
</Section>
);
};
const mapSectionToComponent = (section: SectionKey) => {
switch (section) {
case "profiles": {
return <Profiles />;
}
case "summary": {
return <Summary />;
}
case "experience": {
return <Experience />;
}
case "education": {
return <Education />;
}
case "awards": {
return <Awards />;
}
case "certifications": {
return <Certifications />;
}
case "skills": {
return <Skills />;
}
case "interests": {
return <Interests />;
}
case "publications": {
return <Publications />;
}
case "volunteer": {
return <Volunteer />;
}
case "languages": {
return <Languages />;
}
case "projects": {
return <Projects />;
}
case "references": {
return <References />;
}
default: {
if (section.startsWith("custom.")) return <Custom id={section.split(".")[1]} />;
return null;
}
}
};
export const Nosepass = ({ columns, isFirstPage = false }: TemplateProps) => {
const name = useArtboardStore((state) => state.resume.basics.name);
const [main, sidebar] = columns;
return (
<div className="p-custom space-y-6">
<div className="flex items-center justify-between">
<img alt="Europass Logo" className="h-[42px]" src="/assets/europass.png" />
<p className="font-medium text-primary">Curriculum Vitae</p>
<p className="font-medium text-primary">{name}</p>
</div>
{isFirstPage && <Header />}
<div className="space-y-4">
{main.map((section) => (
<Fragment key={section}>{mapSectionToComponent(section)}</Fragment>
))}
{sidebar.map((section) => (
<Fragment key={section}>{mapSectionToComponent(section)}</Fragment>
))}
</div>
</div>
);
};
-579
View File
@@ -1,579 +0,0 @@
import type {
Award,
Certification,
CustomSection,
CustomSectionGroup,
Interest,
Language,
Project,
Publication,
Reference,
SectionKey,
SectionWithItem,
Skill,
URL,
} from "@reactive-resume/schema";
import { Education, Experience, Volunteer } from "@reactive-resume/schema";
import { cn, isEmptyString, isUrl, sanitize } from "@reactive-resume/utils";
import get from "lodash.get";
import React, { Fragment } from "react";
import { BrandIcon } from "../components/brand-icon";
import { Picture } from "../components/picture";
import { useArtboardStore } from "../store/artboard";
import type { TemplateProps } from "../types/template";
const Header = () => {
const basics = useArtboardStore((state) => state.resume.basics);
const profiles = useArtboardStore((state) => state.resume.sections.profiles);
return (
<div className="flex items-center justify-between space-x-4 border-b border-primary pb-5">
<Picture />
<div className="flex-1 space-y-2">
<div>
<div className="text-2xl font-bold">{basics.name}</div>
<div className="text-base">{basics.headline}</div>
</div>
<div className="flex flex-wrap items-center gap-x-2 gap-y-0.5 text-sm">
{basics.location && (
<div className="flex items-center gap-x-1.5">
<i className="ph ph-bold ph-map-pin text-primary" />
<div>{basics.location}</div>
</div>
)}
{basics.phone && (
<div className="flex items-center gap-x-1.5">
<i className="ph ph-bold ph-phone text-primary" />
<a href={`tel:${basics.phone}`} target="_blank" rel="noreferrer">
{basics.phone}
</a>
</div>
)}
{basics.email && (
<div className="flex items-center gap-x-1.5">
<i className="ph ph-bold ph-at text-primary" />
<a href={`mailto:${basics.email}`} target="_blank" rel="noreferrer">
{basics.email}
</a>
</div>
)}
<Link url={basics.url} />
{basics.customFields.map((item) => (
<div key={item.id} className="flex items-center gap-x-1.5">
<i className={cn(`ph ph-bold ph-${item.icon}`, "text-primary")} />
{isUrl(item.value) ? (
<a href={item.value} target="_blank" rel="noreferrer noopener nofollow">
{item.name || item.value}
</a>
) : (
<span>{[item.name, item.value].filter(Boolean).join(": ")}</span>
)}
</div>
))}
</div>
</div>
{profiles.visible && profiles.items.length > 0 && (
<div
className="grid gap-x-4 gap-y-1 text-right"
style={{ gridTemplateColumns: `repeat(${profiles.columns}, auto)` }}
>
{profiles.items
.filter((item) => item.visible)
.map((item) => (
<div key={item.id} className="flex items-center gap-x-2">
<Link
url={item.url}
label={item.username}
className="text-sm"
icon={<BrandIcon slug={item.icon} />}
/>
</div>
))}
</div>
)}
</div>
);
};
const Summary = () => {
const section = useArtboardStore((state) => state.resume.sections.summary);
if (!section.visible || isEmptyString(section.content)) return null;
return (
<section id={section.id}>
<h4 className="font-bold text-primary">{section.name}</h4>
<div
dangerouslySetInnerHTML={{ __html: sanitize(section.content) }}
style={{ columns: section.columns }}
className="wysiwyg"
/>
</section>
);
};
type RatingProps = { level: number };
const Rating = ({ level }: RatingProps) => (
<div className="flex items-center gap-x-1.5">
{Array.from({ length: 5 }).map((_, index) => (
<div
key={index}
className={cn("size-3 rounded border-2 border-primary", level > index && "bg-primary")}
/>
))}
</div>
);
type LinkProps = {
url: URL;
icon?: React.ReactNode;
iconOnRight?: boolean;
label?: string;
className?: string;
};
const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
if (!isUrl(url.href)) return null;
return (
<div className="flex items-center gap-x-1.5">
{!iconOnRight && (icon ?? <i className="ph ph-bold ph-link text-primary" />)}
<a
href={url.href}
target="_blank"
rel="noreferrer noopener nofollow"
className={cn("inline-block", className)}
>
{label ?? (url.label || url.href)}
</a>
{iconOnRight && (icon ?? <i className="ph ph-bold ph-link text-primary" />)}
</div>
);
};
type LinkedEntityProps = {
name: string;
url: URL;
separateLinks: boolean;
className?: string;
};
const LinkedEntity = ({ name, url, separateLinks, className }: LinkedEntityProps) => {
return !separateLinks && isUrl(url.href) ? (
<Link
url={url}
label={name}
icon={<i className="ph ph-bold ph-globe text-primary" />}
iconOnRight={true}
className={className}
/>
) : (
<div className={className}>{name}</div>
);
};
type SectionProps<T> = {
section: SectionWithItem<T> | CustomSectionGroup;
children?: (item: T) => React.ReactNode;
className?: string;
urlKey?: keyof T;
levelKey?: keyof T;
summaryKey?: keyof T;
keywordsKey?: keyof T;
};
const Section = <T,>({
section,
children,
className,
urlKey,
levelKey,
summaryKey,
keywordsKey,
}: SectionProps<T>) => {
if (!section.visible || section.items.filter((item) => item.visible).length === 0) return null;
return (
<section id={section.id} className="grid">
<h4 className="font-bold text-primary">{section.name}</h4>
<div
className="grid gap-x-6 gap-y-3"
style={{ gridTemplateColumns: `repeat(${section.columns}, 1fr)` }}
>
{section.items
.filter((item) => item.visible)
.map((item) => {
const url = (urlKey && get(item, urlKey)) as URL | undefined;
const level = (levelKey && get(item, levelKey, 0)) as number | undefined;
const summary = (summaryKey && get(item, summaryKey, "")) as string | undefined;
const keywords = (keywordsKey && get(item, keywordsKey, [])) as string[] | undefined;
return (
<div key={item.id} className={cn("space-y-2", className)}>
<div>
{children?.(item as T)}
{url !== undefined && section.separateLinks && <Link url={url} />}
</div>
{summary !== undefined && !isEmptyString(summary) && (
<div
dangerouslySetInnerHTML={{ __html: sanitize(summary) }}
className="wysiwyg"
/>
)}
{level !== undefined && level > 0 && <Rating level={level} />}
{keywords !== undefined && keywords.length > 0 && (
<p className="text-sm">{keywords.join(", ")}</p>
)}
</div>
);
})}
</div>
</section>
);
};
const Experience = () => {
const section = useArtboardStore((state) => state.resume.sections.experience);
return (
<Section<Experience> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-start justify-between">
<div className="text-left">
<LinkedEntity
name={item.company}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.position}</div>
</div>
<div className="shrink-0 text-right">
<div className="font-bold">{item.date}</div>
<div>{item.location}</div>
</div>
</div>
)}
</Section>
);
};
const Education = () => {
const section = useArtboardStore((state) => state.resume.sections.education);
return (
<Section<Education> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-start justify-between">
<div className="text-left">
<LinkedEntity
name={item.institution}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.area}</div>
<div>{item.score}</div>
</div>
<div className="shrink-0 text-right">
<div className="font-bold">{item.date}</div>
<div>{item.studyType}</div>
</div>
</div>
)}
</Section>
);
};
const Awards = () => {
const section = useArtboardStore((state) => state.resume.sections.awards);
return (
<Section<Award> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-start justify-between">
<div className="text-left">
<div className="font-bold">{item.title}</div>
<LinkedEntity
name={item.awarder}
url={item.url}
separateLinks={section.separateLinks}
/>
</div>
<div className="shrink-0 text-right">
<div className="font-bold">{item.date}</div>
</div>
</div>
)}
</Section>
);
};
const Certifications = () => {
const section = useArtboardStore((state) => state.resume.sections.certifications);
return (
<Section<Certification> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-start justify-between">
<div className="text-left">
<div className="font-bold">{item.name}</div>
<LinkedEntity name={item.issuer} url={item.url} separateLinks={section.separateLinks} />
</div>
<div className="shrink-0 text-right">
<div className="font-bold">{item.date}</div>
</div>
</div>
)}
</Section>
);
};
const Skills = () => {
const section = useArtboardStore((state) => state.resume.sections.skills);
return (
<Section<Skill> section={section} levelKey="level" keywordsKey="keywords">
{(item) => (
<div>
<div className="font-bold">{item.name}</div>
<div>{item.description}</div>
</div>
)}
</Section>
);
};
const Interests = () => {
const section = useArtboardStore((state) => state.resume.sections.interests);
return (
<Section<Interest> section={section} keywordsKey="keywords" className="space-y-0.5">
{(item) => <div className="font-bold">{item.name}</div>}
</Section>
);
};
const Publications = () => {
const section = useArtboardStore((state) => state.resume.sections.publications);
return (
<Section<Publication> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-start justify-between">
<div className="text-left">
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.publisher}</div>
</div>
<div className="shrink-0 text-right">
<div className="font-bold">{item.date}</div>
</div>
</div>
)}
</Section>
);
};
const Volunteer = () => {
const section = useArtboardStore((state) => state.resume.sections.volunteer);
return (
<Section<Volunteer> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-start justify-between">
<div className="text-left">
<LinkedEntity
name={item.organization}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.position}</div>
</div>
<div className="shrink-0 text-right">
<div className="font-bold">{item.date}</div>
<div>{item.location}</div>
</div>
</div>
)}
</Section>
);
};
const Languages = () => {
const section = useArtboardStore((state) => state.resume.sections.languages);
return (
<Section<Language> section={section} levelKey="level">
{(item) => (
<div className="space-y-0.5">
<div className="font-bold">{item.name}</div>
<div>{item.description}</div>
</div>
)}
</Section>
);
};
const Projects = () => {
const section = useArtboardStore((state) => state.resume.sections.projects);
return (
<Section<Project> section={section} urlKey="url" summaryKey="summary" keywordsKey="keywords">
{(item) => (
<div className="flex items-start justify-between">
<div className="text-left">
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.description}</div>
</div>
<div className="shrink-0 text-right">
<div className="font-bold">{item.date}</div>
</div>
</div>
)}
</Section>
);
};
const References = () => {
const section = useArtboardStore((state) => state.resume.sections.references);
return (
<Section<Reference> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div>
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.description}</div>
</div>
)}
</Section>
);
};
const Custom = ({ id }: { id: string }) => {
const section = useArtboardStore((state) => state.resume.sections.custom[id]);
return (
<Section<CustomSection>
section={section}
urlKey="url"
summaryKey="summary"
keywordsKey="keywords"
>
{(item) => (
<div className="flex items-start justify-between">
<div className="text-left">
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.description}</div>
</div>
<div className="shrink-0 text-right">
<div className="font-bold">{item.date}</div>
<div>{item.location}</div>
</div>
</div>
)}
</Section>
);
};
const mapSectionToComponent = (section: SectionKey) => {
switch (section) {
case "summary": {
return <Summary />;
}
case "experience": {
return <Experience />;
}
case "education": {
return <Education />;
}
case "awards": {
return <Awards />;
}
case "certifications": {
return <Certifications />;
}
case "skills": {
return <Skills />;
}
case "interests": {
return <Interests />;
}
case "publications": {
return <Publications />;
}
case "volunteer": {
return <Volunteer />;
}
case "languages": {
return <Languages />;
}
case "projects": {
return <Projects />;
}
case "references": {
return <References />;
}
default: {
if (section.startsWith("custom.")) return <Custom id={section.split(".")[1]} />;
return null;
}
}
};
export const Onyx = ({ columns, isFirstPage = false }: TemplateProps) => {
const [main, sidebar] = columns;
return (
<div className="p-custom space-y-4">
{isFirstPage && <Header />}
{main.map((section) => (
<Fragment key={section}>{mapSectionToComponent(section)}</Fragment>
))}
{sidebar.map((section) => (
<Fragment key={section}>{mapSectionToComponent(section)}</Fragment>
))}
</div>
);
};
-622
View File
@@ -1,622 +0,0 @@
import type {
Award,
Certification,
CustomSection,
CustomSectionGroup,
Interest,
Language,
Profile,
Project,
Publication,
Reference,
SectionKey,
SectionWithItem,
Skill,
URL,
} from "@reactive-resume/schema";
import { Education, Experience, Volunteer } from "@reactive-resume/schema";
import { cn, isEmptyString, isUrl, sanitize } from "@reactive-resume/utils";
import get from "lodash.get";
import { Fragment } from "react";
import { BrandIcon } from "../components/brand-icon";
import { Picture } from "../components/picture";
import { useArtboardStore } from "../store/artboard";
import type { TemplateProps } from "../types/template";
const Header = () => {
const basics = useArtboardStore((state) => state.resume.basics);
const borderRadius = useArtboardStore((state) => state.resume.basics.picture.borderRadius);
return (
<div
className="summary group bg-primary px-6 pb-7 pt-6 text-background"
style={{ borderRadius: `calc(${borderRadius}px - 2px)` }}
>
<div className="col-span-2 space-y-2.5">
<div>
<h2 className="text-2xl font-bold">{basics.name}</h2>
<p>{basics.headline}</p>
</div>
<hr className="border-background opacity-50" />
<div className="flex flex-wrap items-center gap-x-2 gap-y-0.5 text-sm">
{basics.location && (
<>
<div className="flex items-center gap-x-1.5">
<i className="ph ph-bold ph-map-pin" />
<div>{basics.location}</div>
</div>
<div className="size-1 rounded-full bg-background last:hidden" />
</>
)}
{basics.phone && (
<>
<div className="flex items-center gap-x-1.5">
<i className="ph ph-bold ph-phone" />
<a href={`tel:${basics.phone}`} target="_blank" rel="noreferrer">
{basics.phone}
</a>
</div>
<div className="size-1 rounded-full bg-background last:hidden" />
</>
)}
{basics.email && (
<>
<div className="flex items-center gap-x-1.5">
<i className="ph ph-bold ph-at" />
<a href={`mailto:${basics.email}`} target="_blank" rel="noreferrer">
{basics.email}
</a>
</div>
<div className="size-1 rounded-full bg-background last:hidden" />
</>
)}
{isUrl(basics.url.href) && (
<>
<Link url={basics.url} />
<div className="size-1 rounded-full bg-background last:hidden" />
</>
)}
{basics.customFields.map((item) => (
<Fragment key={item.id}>
<div className="flex items-center gap-x-1.5">
<i className={cn(`ph ph-bold ph-${item.icon}`)} />
{isUrl(item.value) ? (
<a href={item.value} target="_blank" rel="noreferrer noopener nofollow">
{item.name || item.value}
</a>
) : (
<span>{[item.name, item.value].filter(Boolean).join(": ")}</span>
)}
</div>
<div className="size-1 rounded-full bg-background last:hidden" />
</Fragment>
))}
</div>
</div>
</div>
);
};
const Summary = () => {
const section = useArtboardStore((state) => state.resume.sections.summary);
if (!section.visible || isEmptyString(section.content)) return null;
return (
<section id={section.id}>
<h4 className="mb-2 border-b border-primary text-base font-bold">{section.name}</h4>
<div
dangerouslySetInnerHTML={{ __html: sanitize(section.content) }}
style={{ columns: section.columns }}
className="wysiwyg"
/>
</section>
);
};
type RatingProps = { level: number };
const Rating = ({ level }: RatingProps) => (
<div className="flex items-center gap-x-1.5">
{Array.from({ length: 5 }).map((_, index) => (
<i
key={index}
className={cn(
"ph ph-diamond text-primary",
level > index && "ph-fill",
level <= index && "ph-bold",
)}
/>
// <div
// key={index}
// className={cn("h-2 w-4 border border-primary", level > index && "bg-primary")}
// />
))}
</div>
);
type LinkProps = {
url: URL;
icon?: React.ReactNode;
iconOnRight?: boolean;
label?: string;
className?: string;
};
const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
if (!isUrl(url.href)) return null;
return (
<div className="flex items-center gap-x-1.5">
{!iconOnRight &&
(icon ?? (
<i className="ph ph-bold ph-link text-primary group-[.summary]:text-background" />
))}
<a
href={url.href}
target="_blank"
rel="noreferrer noopener nofollow"
className={cn("inline-block", className)}
>
{label ?? (url.label || url.href)}
</a>
{iconOnRight &&
(icon ?? (
<i className="ph ph-bold ph-link text-primary group-[.summary]:text-background" />
))}
</div>
);
};
type LinkedEntityProps = {
name: string;
url: URL;
separateLinks: boolean;
className?: string;
};
const LinkedEntity = ({ name, url, separateLinks, className }: LinkedEntityProps) => {
return !separateLinks && isUrl(url.href) ? (
<Link
url={url}
label={name}
icon={<i className="ph ph-bold ph-globe text-primary group-[.summary]:text-background" />}
iconOnRight={true}
className={className}
/>
) : (
<div className={className}>{name}</div>
);
};
type SectionProps<T> = {
section: SectionWithItem<T> | CustomSectionGroup;
children?: (item: T) => React.ReactNode;
className?: string;
urlKey?: keyof T;
levelKey?: keyof T;
summaryKey?: keyof T;
keywordsKey?: keyof T;
};
const Section = <T,>({
section,
children,
className,
urlKey,
levelKey,
summaryKey,
keywordsKey,
}: SectionProps<T>) => {
if (!section.visible || section.items.filter((item) => item.visible).length === 0) return null;
return (
<section id={section.id} className="grid">
<h4 className="mb-2 border-b border-primary text-base font-bold">{section.name}</h4>
<div
className="grid gap-x-6 gap-y-3"
style={{ gridTemplateColumns: `repeat(${section.columns}, 1fr)` }}
>
{section.items
.filter((item) => item.visible)
.map((item) => {
const url = (urlKey && get(item, urlKey)) as URL | undefined;
const level = (levelKey && get(item, levelKey, 0)) as number | undefined;
const summary = (summaryKey && get(item, summaryKey, "")) as string | undefined;
const keywords = (keywordsKey && get(item, keywordsKey, [])) as string[] | undefined;
return (
<div key={item.id} className={cn("space-y-2", className)}>
<div>
{children?.(item as T)}
{url !== undefined && section.separateLinks && <Link url={url} />}
</div>
{summary !== undefined && !isEmptyString(summary) && (
<div
dangerouslySetInnerHTML={{ __html: sanitize(summary) }}
className="wysiwyg"
/>
)}
{level !== undefined && level > 0 && <Rating level={level} />}
{keywords !== undefined && keywords.length > 0 && (
<p className="text-sm">{keywords.join(", ")}</p>
)}
</div>
);
})}
</div>
</section>
);
};
const Profiles = () => {
const section = useArtboardStore((state) => state.resume.sections.profiles);
return (
<Section<Profile> section={section}>
{(item) => (
<div>
{isUrl(item.url.href) ? (
<Link url={item.url} label={item.username} icon={<BrandIcon slug={item.icon} />} />
) : (
<p>{item.username}</p>
)}
{!item.icon && <p className="text-sm">{item.network}</p>}
</div>
)}
</Section>
);
};
const Experience = () => {
const section = useArtboardStore((state) => state.resume.sections.experience);
return (
<Section<Experience> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<LinkedEntity
name={item.company}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.position}</div>
</div>
<div className="shrink-0 text-right group-[.sidebar]:text-left">
<div className="font-bold">{item.date}</div>
<div>{item.location}</div>
</div>
</div>
)}
</Section>
);
};
const Education = () => {
const section = useArtboardStore((state) => state.resume.sections.education);
return (
<Section<Education> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<LinkedEntity
name={item.institution}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.area}</div>
<div>{item.score}</div>
</div>
<div className="shrink-0 text-right group-[.sidebar]:text-left">
<div className="font-bold">{item.date}</div>
<div>{item.studyType}</div>
</div>
</div>
)}
</Section>
);
};
const Awards = () => {
const section = useArtboardStore((state) => state.resume.sections.awards);
return (
<Section<Award> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<div className="font-bold">{item.title}</div>
<LinkedEntity
name={item.awarder}
url={item.url}
separateLinks={section.separateLinks}
/>
</div>
<div className="shrink-0 text-right group-[.sidebar]:text-left">
<div className="font-bold">{item.date}</div>
</div>
</div>
)}
</Section>
);
};
const Certifications = () => {
const section = useArtboardStore((state) => state.resume.sections.certifications);
return (
<Section<Certification> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<div className="font-bold">{item.name}</div>
<LinkedEntity name={item.issuer} url={item.url} separateLinks={section.separateLinks} />
</div>
<div className="shrink-0 text-right group-[.sidebar]:text-left">
<div className="font-bold">{item.date}</div>
</div>
</div>
)}
</Section>
);
};
const Skills = () => {
const section = useArtboardStore((state) => state.resume.sections.skills);
return (
<Section<Skill> section={section} levelKey="level" keywordsKey="keywords">
{(item) => (
<div>
<div className="font-bold">{item.name}</div>
<div>{item.description}</div>
</div>
)}
</Section>
);
};
const Interests = () => {
const section = useArtboardStore((state) => state.resume.sections.interests);
return (
<Section<Interest> section={section} className="space-y-1" keywordsKey="keywords">
{(item) => <div className="font-bold">{item.name}</div>}
</Section>
);
};
const Publications = () => {
const section = useArtboardStore((state) => state.resume.sections.publications);
return (
<Section<Publication> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.publisher}</div>
</div>
<div className="shrink-0 text-right group-[.sidebar]:text-left">
<div className="font-bold">{item.date}</div>
</div>
</div>
)}
</Section>
);
};
const Volunteer = () => {
const section = useArtboardStore((state) => state.resume.sections.volunteer);
return (
<Section<Volunteer> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<LinkedEntity
name={item.organization}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.position}</div>
</div>
<div className="shrink-0 text-right group-[.sidebar]:text-left">
<div className="font-bold">{item.date}</div>
<div>{item.location}</div>
</div>
</div>
)}
</Section>
);
};
const Languages = () => {
const section = useArtboardStore((state) => state.resume.sections.languages);
return (
<Section<Language> section={section} levelKey="level">
{(item) => (
<div>
<div className="font-bold">{item.name}</div>
<div>{item.description}</div>
</div>
)}
</Section>
);
};
const Projects = () => {
const section = useArtboardStore((state) => state.resume.sections.projects);
return (
<Section<Project> section={section} urlKey="url" summaryKey="summary" keywordsKey="keywords">
{(item) => (
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.description}</div>
</div>
<div className="shrink-0 text-right group-[.sidebar]:text-left">
<div className="font-bold">{item.date}</div>
</div>
</div>
)}
</Section>
);
};
const References = () => {
const section = useArtboardStore((state) => state.resume.sections.references);
return (
<Section<Reference> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div>
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.description}</div>
</div>
)}
</Section>
);
};
const Custom = ({ id }: { id: string }) => {
const section = useArtboardStore((state) => state.resume.sections.custom[id]);
return (
<Section<CustomSection>
section={section}
urlKey="url"
summaryKey="summary"
keywordsKey="keywords"
>
{(item) => (
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.description}</div>
</div>
<div className="shrink-0 text-right group-[.sidebar]:text-left">
<div className="font-bold">{item.date}</div>
<div>{item.location}</div>
</div>
</div>
)}
</Section>
);
};
const mapSectionToComponent = (section: SectionKey) => {
switch (section) {
case "profiles": {
return <Profiles />;
}
case "summary": {
return <Summary />;
}
case "experience": {
return <Experience />;
}
case "education": {
return <Education />;
}
case "awards": {
return <Awards />;
}
case "certifications": {
return <Certifications />;
}
case "skills": {
return <Skills />;
}
case "interests": {
return <Interests />;
}
case "publications": {
return <Publications />;
}
case "volunteer": {
return <Volunteer />;
}
case "languages": {
return <Languages />;
}
case "projects": {
return <Projects />;
}
case "references": {
return <References />;
}
default: {
if (section.startsWith("custom.")) return <Custom id={section.split(".")[1]} />;
return null;
}
}
};
export const Pikachu = ({ columns, isFirstPage = false }: TemplateProps) => {
const [main, sidebar] = columns;
return (
<div className="p-custom grid grid-cols-3 space-x-6">
<div className="sidebar group space-y-4">
{isFirstPage && <Picture className="w-full !max-w-none" />}
{sidebar.map((section) => (
<Fragment key={section}>{mapSectionToComponent(section)}</Fragment>
))}
</div>
<div className={cn("main group space-y-4", sidebar.length > 0 ? "col-span-2" : "col-span-3")}>
{isFirstPage && <Header />}
{main.map((section) => (
<Fragment key={section}>{mapSectionToComponent(section)}</Fragment>
))}
</div>
</div>
);
};
-582
View File
@@ -1,582 +0,0 @@
import type {
Award,
Certification,
CustomSection,
CustomSectionGroup,
Interest,
Language,
Profile,
Project,
Publication,
Reference,
SectionKey,
SectionWithItem,
Skill,
URL,
} from "@reactive-resume/schema";
import { Education, Experience, Volunteer } from "@reactive-resume/schema";
import { cn, isEmptyString, isUrl, sanitize } from "@reactive-resume/utils";
import get from "lodash.get";
import { Fragment } from "react";
import { BrandIcon } from "../components/brand-icon";
import { Picture } from "../components/picture";
import { useArtboardStore } from "../store/artboard";
import type { TemplateProps } from "../types/template";
const Header = () => {
const basics = useArtboardStore((state) => state.resume.basics);
return (
<div className="flex items-center space-x-4">
<Picture />
<div className="space-y-0.5">
<div className="text-2xl font-bold">{basics.name}</div>
<div className="text-base">{basics.headline}</div>
<div className="flex flex-wrap items-center gap-x-2 gap-y-0.5 text-sm">
{basics.location && (
<div className="flex items-center gap-x-1.5 border-r pr-2 last:border-r-0 last:pr-0">
<i className="ph ph-bold ph-map-pin text-primary" />
<div>{basics.location}</div>
</div>
)}
{basics.phone && (
<div className="flex items-center gap-x-1.5 border-r pr-2 last:border-r-0 last:pr-0">
<i className="ph ph-bold ph-phone text-primary" />
<a href={`tel:${basics.phone}`} target="_blank" rel="noreferrer">
{basics.phone}
</a>
</div>
)}
{basics.email && (
<div className="flex items-center gap-x-1.5 border-r pr-2 last:border-r-0 last:pr-0">
<i className="ph ph-bold ph-at text-primary" />
<a href={`mailto:${basics.email}`} target="_blank" rel="noreferrer">
{basics.email}
</a>
</div>
)}
<Link url={basics.url} />
{basics.customFields.map((item) => (
<div
key={item.id}
className="flex items-center gap-x-1.5 border-r pr-2 last:border-r-0 last:pr-0"
>
<i className={cn(`ph ph-bold ph-${item.icon}`, "text-primary")} />
{isUrl(item.value) ? (
<a href={item.value} target="_blank" rel="noreferrer noopener nofollow">
{item.name || item.value}
</a>
) : (
<span>{[item.name, item.value].filter(Boolean).join(": ")}</span>
)}
</div>
))}
</div>
</div>
</div>
);
};
const Summary = () => {
const section = useArtboardStore((state) => state.resume.sections.summary);
if (!section.visible || isEmptyString(section.content)) return null;
return (
<section id={section.id}>
<h4 className="mb-2 border-b pb-0.5 text-sm font-bold">{section.name}</h4>
<div
dangerouslySetInnerHTML={{ __html: sanitize(section.content) }}
style={{ columns: section.columns }}
className="wysiwyg"
/>
</section>
);
};
type RatingProps = { level: number };
const Rating = ({ level }: RatingProps) => (
<div className="flex items-center gap-x-1.5">
{Array.from({ length: 5 }).map((_, index) => (
<div
key={index}
className={cn("size-2 rounded-full border border-primary", level > index && "bg-primary")}
/>
))}
</div>
);
type LinkProps = {
url: URL;
icon?: React.ReactNode;
iconOnRight?: boolean;
label?: string;
className?: string;
};
const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
if (!isUrl(url.href)) return null;
return (
<div className="flex items-center gap-x-1.5 border-r pr-2 last:border-r-0 last:pr-0">
{!iconOnRight && (icon ?? <i className="ph ph-bold ph-link text-primary" />)}
<a
href={url.href}
target="_blank"
rel="noreferrer noopener nofollow"
className={cn("inline-block", className)}
>
{label ?? (url.label || url.href)}
</a>
{iconOnRight && (icon ?? <i className="ph ph-bold ph-link text-primary" />)}
</div>
);
};
type LinkedEntityProps = {
name: string;
url: URL;
separateLinks: boolean;
className?: string;
};
const LinkedEntity = ({ name, url, separateLinks, className }: LinkedEntityProps) => {
return !separateLinks && isUrl(url.href) ? (
<Link
url={url}
label={name}
icon={<i className="ph ph-bold ph-globe text-primary" />}
iconOnRight={true}
className={className}
/>
) : (
<div className={className}>{name}</div>
);
};
type SectionProps<T> = {
section: SectionWithItem<T> | CustomSectionGroup;
children?: (item: T) => React.ReactNode;
className?: string;
urlKey?: keyof T;
levelKey?: keyof T;
summaryKey?: keyof T;
keywordsKey?: keyof T;
};
const Section = <T,>({
section,
children,
className,
urlKey,
levelKey,
summaryKey,
keywordsKey,
}: SectionProps<T>) => {
if (!section.visible || section.items.filter((item) => item.visible).length === 0) return null;
return (
<section id={section.id} className="grid">
<h4 className="mb-2 border-b pb-0.5 text-sm font-bold">{section.name}</h4>
<div
className="grid gap-x-6 gap-y-3"
style={{ gridTemplateColumns: `repeat(${section.columns}, 1fr)` }}
>
{section.items
.filter((item) => item.visible)
.map((item) => {
const url = (urlKey && get(item, urlKey)) as URL | undefined;
const level = (levelKey && get(item, levelKey, 0)) as number | undefined;
const summary = (summaryKey && get(item, summaryKey, "")) as string | undefined;
const keywords = (keywordsKey && get(item, keywordsKey, [])) as string[] | undefined;
return (
<div key={item.id} className={cn("space-y-2", className)}>
<div>
{children?.(item as T)}
{url !== undefined && section.separateLinks && <Link url={url} />}
</div>
{summary !== undefined && !isEmptyString(summary) && (
<div
dangerouslySetInnerHTML={{ __html: sanitize(summary) }}
className="wysiwyg"
/>
)}
{level !== undefined && level > 0 && <Rating level={level} />}
{keywords !== undefined && keywords.length > 0 && (
<p className="text-sm">{keywords.join(", ")}</p>
)}
</div>
);
})}
</div>
</section>
);
};
const Profiles = () => {
const section = useArtboardStore((state) => state.resume.sections.profiles);
return (
<Section<Profile> section={section}>
{(item) => (
<div>
{isUrl(item.url.href) ? (
<Link url={item.url} label={item.username} icon={<BrandIcon slug={item.icon} />} />
) : (
<p>{item.username}</p>
)}
{!item.icon && <p className="text-sm">{item.network}</p>}
</div>
)}
</Section>
);
};
const Experience = () => {
const section = useArtboardStore((state) => state.resume.sections.experience);
return (
<Section<Experience> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-start justify-between">
<div className="text-left">
<LinkedEntity
name={item.company}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.position}</div>
</div>
<div className="shrink-0 text-right">
<div className="font-bold">{item.date}</div>
<div>{item.location}</div>
</div>
</div>
)}
</Section>
);
};
const Education = () => {
const section = useArtboardStore((state) => state.resume.sections.education);
return (
<Section<Education> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-start justify-between">
<div className="text-left">
<LinkedEntity
name={item.institution}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.area}</div>
<div>{item.score}</div>
</div>
<div className="shrink-0 text-right">
<div className="font-bold">{item.date}</div>
<div>{item.studyType}</div>
</div>
</div>
)}
</Section>
);
};
const Awards = () => {
const section = useArtboardStore((state) => state.resume.sections.awards);
return (
<Section<Award> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-start justify-between">
<div className="text-left">
<div className="font-bold">{item.title}</div>
<LinkedEntity
name={item.awarder}
url={item.url}
separateLinks={section.separateLinks}
/>
</div>
<div className="shrink-0 text-right">
<div className="font-bold">{item.date}</div>
</div>
</div>
)}
</Section>
);
};
const Certifications = () => {
const section = useArtboardStore((state) => state.resume.sections.certifications);
return (
<Section<Certification> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-start justify-between">
<div className="text-left">
<div className="font-bold">{item.name}</div>
<LinkedEntity name={item.issuer} url={item.url} separateLinks={section.separateLinks} />
</div>
<div className="shrink-0 text-right">
<div className="font-bold">{item.date}</div>
</div>
</div>
)}
</Section>
);
};
const Skills = () => {
const section = useArtboardStore((state) => state.resume.sections.skills);
return (
<Section<Skill> section={section} levelKey="level" keywordsKey="keywords">
{(item) => (
<div>
<div className="font-bold">{item.name}</div>
<div>{item.description}</div>
</div>
)}
</Section>
);
};
const Interests = () => {
const section = useArtboardStore((state) => state.resume.sections.interests);
return (
<Section<Interest> section={section} keywordsKey="keywords" className="space-y-0.5">
{(item) => <div className="font-bold">{item.name}</div>}
</Section>
);
};
const Publications = () => {
const section = useArtboardStore((state) => state.resume.sections.publications);
return (
<Section<Publication> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-start justify-between">
<div className="text-left">
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.publisher}</div>
</div>
<div className="shrink-0 text-right">
<div className="font-bold">{item.date}</div>
</div>
</div>
)}
</Section>
);
};
const Volunteer = () => {
const section = useArtboardStore((state) => state.resume.sections.volunteer);
return (
<Section<Volunteer> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-start justify-between">
<div className="text-left">
<LinkedEntity
name={item.organization}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.position}</div>
</div>
<div className="shrink-0 text-right">
<div className="font-bold">{item.date}</div>
<div>{item.location}</div>
</div>
</div>
)}
</Section>
);
};
const Languages = () => {
const section = useArtboardStore((state) => state.resume.sections.languages);
return (
<Section<Language> section={section} levelKey="level">
{(item) => (
<div className="space-y-0.5">
<div className="font-bold">{item.name}</div>
<div>{item.description}</div>
</div>
)}
</Section>
);
};
const Projects = () => {
const section = useArtboardStore((state) => state.resume.sections.projects);
return (
<Section<Project> section={section} urlKey="url" summaryKey="summary" keywordsKey="keywords">
{(item) => (
<div className="flex items-start justify-between">
<div className="text-left">
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.description}</div>
</div>
<div className="shrink-0 text-right">
<div className="font-bold">{item.date}</div>
</div>
</div>
)}
</Section>
);
};
const References = () => {
const section = useArtboardStore((state) => state.resume.sections.references);
return (
<Section<Reference> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div>
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.description}</div>
</div>
)}
</Section>
);
};
const Custom = ({ id }: { id: string }) => {
const section = useArtboardStore((state) => state.resume.sections.custom[id]);
return (
<Section<CustomSection>
section={section}
urlKey="url"
summaryKey="summary"
keywordsKey="keywords"
>
{(item) => (
<div className="flex items-start justify-between">
<div className="text-left">
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.description}</div>
</div>
<div className="shrink-0 text-right">
<div className="font-bold">{item.date}</div>
<div>{item.location}</div>
</div>
</div>
)}
</Section>
);
};
const mapSectionToComponent = (section: SectionKey) => {
switch (section) {
case "profiles": {
return <Profiles />;
}
case "summary": {
return <Summary />;
}
case "experience": {
return <Experience />;
}
case "education": {
return <Education />;
}
case "awards": {
return <Awards />;
}
case "certifications": {
return <Certifications />;
}
case "skills": {
return <Skills />;
}
case "interests": {
return <Interests />;
}
case "publications": {
return <Publications />;
}
case "volunteer": {
return <Volunteer />;
}
case "languages": {
return <Languages />;
}
case "projects": {
return <Projects />;
}
case "references": {
return <References />;
}
default: {
if (section.startsWith("custom.")) return <Custom id={section.split(".")[1]} />;
return null;
}
}
};
export const Rhyhorn = ({ columns, isFirstPage = false }: TemplateProps) => {
const [main, sidebar] = columns;
return (
<div className="p-custom space-y-4">
{isFirstPage && <Header />}
{main.map((section) => (
<Fragment key={section}>{mapSectionToComponent(section)}</Fragment>
))}
{sidebar.map((section) => (
<Fragment key={section}>{mapSectionToComponent(section)}</Fragment>
))}
</div>
);
};
-6
View File
@@ -1,6 +0,0 @@
import type { SectionKey } from "@reactive-resume/schema";
export type TemplateProps = {
columns: SectionKey[][];
isFirstPage?: boolean;
};
-51
View File
@@ -1,51 +0,0 @@
const { createGlobPatternsForDependencies } = require("@nx/react/tailwind");
const { join } = require("path");
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: "class",
content: [
join(__dirname, "{src,pages,components,app}/**/*!(*.stories|*.spec).{ts,tsx,html}"),
...createGlobPatternsForDependencies(__dirname),
],
theme: {
extend: {
colors: {
foreground: "var(--color-foreground)",
primary: "var(--color-primary)",
background: "var(--color-background)",
},
lineHeight: {
tight: "calc(var(--line-height) - 0.5)",
snug: "calc(var(--line-height) - 0.3)",
normal: "var(--line-height)",
relaxed: "calc(var(--line-height) + 0.3)",
loose: "calc(var(--line-height) + 0.5)",
},
spacing: { custom: "var(--margin)" },
typography: () => ({
foreground: {
css: {
"--tw-prose-body": "var(--color-foreground)",
"--tw-prose-headings": "var(--color-foreground)",
"--tw-prose-lead": "var(--color-foreground)",
"--tw-prose-links": "var(--color-foreground)",
"--tw-prose-bold": "var(--color-foreground)",
"--tw-prose-counters": "var(--color-foreground)",
"--tw-prose-bullets": "var(--color-foreground)",
"--tw-prose-hr": "var(--color-foreground)",
"--tw-prose-quotes": "var(--color-foreground)",
"--tw-prose-quote-borders": "var(--color-foreground)",
"--tw-prose-captions": "var(--color-foreground)",
"--tw-prose-code": "var(--color-foreground)",
"--tw-prose-pre-code": "var(--color-foreground)",
"--tw-prose-pre-bg": "var(--color-background)",
"--tw-prose-th-borders": "var(--color-foreground)",
"--tw-prose-td-borders": "var(--color-foreground)",
},
},
}),
},
},
plugins: [require("@tailwindcss/typography")],
};
-23
View File
@@ -1,23 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"types": [
"node",
"@nx/react/typings/cssmodule.d.ts",
"@nx/react/typings/image.d.ts",
"vite/client"
]
},
"exclude": [
"src/**/*.spec.ts",
"src/**/*.test.ts",
"src/**/*.spec.tsx",
"src/**/*.test.tsx",
"src/**/*.spec.js",
"src/**/*.test.js",
"src/**/*.spec.jsx",
"src/**/*.test.jsx"
],
"include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"]
}
-18
View File
@@ -1,18 +0,0 @@
{
"compilerOptions": {
"jsx": "react-jsx",
"allowJs": false,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true,
"types": ["vite/client"]
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.app.json"
}
],
"extends": "../../tsconfig.base.json"
}
-30
View File
@@ -1,30 +0,0 @@
/// <reference types='vitest' />
import { nxViteTsPaths } from "@nx/vite/plugins/nx-tsconfig-paths.plugin";
import react from "@vitejs/plugin-react-swc";
import { defineConfig, searchForWorkspaceRoot } from "vite";
export default defineConfig({
base: "/artboard/",
cacheDir: "../../node_modules/.vite/artboard",
build: {
sourcemap: true,
emptyOutDir: true,
},
server: {
host: true,
port: 6173,
fs: { allow: [searchForWorkspaceRoot(process.cwd())] },
},
plugins: [react(), nxViteTsPaths()],
resolve: {
alias: {
"@/artboard/": `${searchForWorkspaceRoot(process.cwd())}/apps/artboard/src/`,
},
},
});
-127
View File
@@ -1,127 +0,0 @@
{
"extends": ["plugin:@nx/react", "../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx"],
"extends": [
"plugin:tailwindcss/recommended",
"plugin:@tanstack/eslint-plugin-query/recommended"
],
"parserOptions": {
"projectService": "./apps/client/tsconfig.json"
},
"settings": {
"tailwindcss": {
"callees": ["cn", "clsx", "cva"],
"config": "tailwind.config.js",
"whitelist": ["ph", "ph\\-.*", "si", "si\\-.*"]
}
},
"plugins": ["lingui"],
"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",
// lingui
"lingui/no-unlocalized-strings": [
2,
{
"ignore": [
// Ignore strings which are a single "word" (no spaces)
// and doesn't start with an uppercase letter
"^(?![A-Z])\\S+$",
// Ignore UPPERCASE literals
// Example: const test = "FOO"
"^[A-Z0-9_-]+$"
],
"ignoreNames": [
// Ignore matching className (case-insensitive)
{ "regex": { "pattern": "className", "flags": "i" } },
// Ignore UPPERCASE names
// Example: test.FOO = "ola!"
{ "regex": { "pattern": "^[A-Z0-9_-]+$" } },
"id",
"src",
"srcSet",
"styleName",
"placeholder",
"alt",
"type",
"width",
"height",
"displayName",
"Authorization"
],
"ignoreFunctions": [
"cn",
"cva",
"track",
"Error",
"console.*",
"*headers.set",
"*.addEventListener",
"*.removeEventListener",
"*.postMessage",
"*.getElementById",
"*.dispatch",
"*.commit",
"*.includes",
"*.indexOf",
"*.endsWith",
"*.startsWith",
"require"
],
// Following settings require typed linting https://typescript-eslint.io/getting-started/typed-linting/
"useTsTypes": true,
"ignoreMethodsOnTypes": [
// Ignore specified methods on Map and Set types
"Map.get",
"Map.has",
"Set.has"
]
}
],
"lingui/t-call-in-function": 2,
"lingui/no-single-variables-to-translate": 2,
"lingui/no-expression-in-message": 2,
"lingui/no-single-tag-to-translate": 2,
"lingui/no-trans-inside-trans": 2,
"lingui/text-restrictions": [
2,
{
"rules": [
{
"patterns": ["''", "\"", "’", "“"],
"message": "This string contains forbidden characters"
}
]
}
]
}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
}
]
}
-47
View File
@@ -1,47 +0,0 @@
<!doctype html>
<html lang="en-US" translate="no">
<head>
<base href="/" />
<!-- SEO -->
<title>Reactive Resume - A free and open-source resume builder</title>
<meta
name="description"
content="A free and open-source resume builder that simplifies the process of creating, updating, and sharing your resume."
/>
<!-- Meta -->
<meta charset="utf-8" />
<meta name="googlebot" content="notranslate" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- White Flash Prevention Script -->
<script type="text/javascript" src="/scripts/initialize-theme.js"></script>
<!-- Favicon -->
<link
rel="icon"
type="image/svg+xml"
href="/icon/dark.svg"
media="(prefers-color-scheme: light)"
/>
<link
rel="icon"
type="image/svg+xml"
href="/icon/light.svg"
media="(prefers-color-scheme: dark)"
/>
<!-- Styles -->
<link rel="stylesheet" href="/src/styles/main.css" />
</head>
<body class="bg-background text-sm text-foreground antialiased print:m-0 print:bg-white">
<div id="root"></div>
<!-- Scripts -->
<script type="module" src="/src/main.tsx"></script>
<!-- Phosphor Icons -->
<script src="https://cdn.jsdelivr.net/npm/@phosphor-icons/web"></script>
</body>
</html>
-10
View File
@@ -1,10 +0,0 @@
const path = require("node:path");
module.exports = {
plugins: {
"postcss-import": {},
"tailwindcss/nesting": {},
tailwindcss: { config: path.join(__dirname, "tailwind.config.js") },
autoprefixer: {},
},
};
-80
View File
@@ -1,80 +0,0 @@
{
"name": "client",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "apps/client/src",
"projectType": "application",
"targets": {
"build": {
"executor": "@nx/vite:build",
"outputs": ["{options.outputPath}"],
"defaultConfiguration": "production",
"dependsOn": ["^build"],
"options": {
"outputPath": "dist/apps/client"
},
"configurations": {
"development": {
"mode": "development"
},
"production": {
"mode": "production"
}
}
},
"serve": {
"executor": "@nx/vite:dev-server",
"defaultConfiguration": "development",
"options": {
"buildTarget": "client:build",
"proxyConfig": "apps/client/proxy.conf.json"
},
"configurations": {
"development": {
"buildTarget": "client:build:development",
"hmr": true
},
"production": {
"buildTarget": "client:build:production",
"hmr": false
}
}
},
"preview": {
"executor": "@nx/vite:preview-server",
"defaultConfiguration": "development",
"options": {
"buildTarget": "client:build"
},
"configurations": {
"development": {
"buildTarget": "client:build:development"
},
"production": {
"buildTarget": "client:build:production"
}
}
},
"test": {
"executor": "@nx/vite:test",
"outputs": ["{options.reportsDirectory}"],
"options": {
"passWithNoTests": true,
"reportsDirectory": "../../coverage/apps/client"
}
},
"lint": {
"executor": "@nx/eslint:lint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": ["apps/client/**/*.{ts,tsx,js,jsx}"]
}
},
"serve-static": {
"executor": "@nx/web:file-server",
"options": {
"buildTarget": "client:build"
}
}
},
"tags": ["frontend"]
}
-10
View File
@@ -1,10 +0,0 @@
{
"/api": {
"target": "http://localhost:3000",
"secure": false
},
"/artboard": {
"target": "http://localhost:6173",
"secure": false
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 363 KiB

@@ -1,33 +0,0 @@
<svg width="498" height="151" viewBox="0 0 498 151" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_177_125)">
<path
d="M309.08 117.21C280.235 138.472 238.425 149.816 202.427 149.816C151.952 149.816 106.512 131.147 72.1348 100.098C69.4339 97.6559 71.8539 94.3284 75.095 96.2298C112.195 117.815 158.067 130.801 205.452 130.801C237.409 130.801 272.564 124.19 304.889 110.469C309.772 108.395 313.856 113.667 309.08 117.21Z"
fill="#09090b" />
<path
d="M321.072 103.49C317.399 98.7795 296.699 101.264 287.408 102.366C284.578 102.712 284.146 100.249 286.695 98.477C303.182 86.8739 330.234 90.223 333.389 94.1123C336.543 98.0232 332.567 125.14 317.075 138.083C314.698 140.071 312.429 139.012 313.488 136.376C316.967 127.69 324.767 108.222 321.072 103.49Z"
fill="#09090b" />
<path
d="M288.057 16.5637V5.28474C288.057 3.57776 289.353 2.43258 290.909 2.43258H341.405C343.025 2.43258 344.322 3.59937 344.322 5.28474V14.9432C344.3 16.5637 342.939 18.6813 340.519 22.0304L314.353 59.3894C324.076 59.1517 334.339 60.5994 343.155 65.5691C345.143 66.6926 345.683 68.3348 345.834 69.9554V81.9906C345.834 83.6328 344.019 85.5558 342.118 84.5619C326.582 76.4159 305.947 75.53 288.77 84.6483C287.019 85.599 285.183 83.6976 285.183 82.0554V70.6252C285.183 68.7886 285.204 65.6555 287.041 62.8682L317.356 19.3943H290.974C289.353 19.3943 288.057 18.2491 288.057 16.5637Z"
fill="#09090b" />
<path
d="M103.854 86.9387H88.4916C87.0223 86.8307 85.8555 85.7287 85.7474 84.3242V5.47922C85.7474 3.90188 87.0655 2.64865 88.7076 2.64865H103.033C104.524 2.71343 105.713 3.85866 105.821 5.28474V15.5914H106.102C109.84 5.63045 116.862 0.984887 126.326 0.984887C135.941 0.984887 141.948 5.63045 146.269 15.5914C149.986 5.63045 158.434 0.984887 167.488 0.984887C173.927 0.984887 180.971 3.64258 185.271 9.60619C190.132 16.2396 189.138 25.8765 189.138 34.3249L189.117 84.0865C189.117 85.6639 187.799 86.9387 186.157 86.9387H170.815C169.281 86.8307 168.05 85.599 168.05 84.0865V42.298C168.05 38.9705 168.352 30.6733 167.617 27.5186C166.472 22.2249 163.037 20.7339 158.586 20.7339C154.869 20.7339 150.98 23.2188 149.403 27.1945C147.825 31.1703 147.976 37.8253 147.976 42.298V84.0865C147.976 85.6639 146.658 86.9387 145.016 86.9387H129.675C128.119 86.8307 126.909 85.599 126.909 84.0865L126.888 42.298C126.888 33.5039 128.335 20.5611 117.424 20.5611C106.382 20.5611 106.815 33.1797 106.815 42.298V84.0865C106.815 85.6639 105.496 86.9387 103.854 86.9387Z"
fill="#09090b" />
<path
d="M387.796 0.984887C410.591 0.984887 422.929 20.5611 422.929 45.4527C422.929 69.5016 409.295 88.5808 387.796 88.5808C365.411 88.5808 353.224 69.0046 353.224 44.61C353.224 20.0641 365.562 0.984887 387.796 0.984887ZM387.925 17.0823C376.603 17.0823 375.89 32.5099 375.89 42.1252C375.89 51.762 375.739 72.3322 387.796 72.3322C399.701 72.3322 400.263 55.7378 400.263 45.6255C400.263 38.9705 399.982 31.019 397.973 24.7097C396.244 19.2215 392.809 17.0823 387.925 17.0823Z"
fill="#09090b" />
<path
d="M452.488 86.9387H437.19C435.656 86.8307 434.424 85.599 434.424 84.0865L434.403 5.21991C434.532 3.77223 435.807 2.64865 437.363 2.64865H451.602C452.942 2.71343 454.044 3.62098 454.346 4.8526V16.9095H454.627C458.927 6.12742 464.955 0.984887 475.565 0.984887C482.457 0.984887 489.177 3.46973 493.499 10.276C497.518 16.5853 497.518 27.1945 497.518 34.8219V84.4539C497.345 85.8367 496.07 86.9387 494.557 86.9387H479.151C477.747 86.8307 476.58 85.7935 476.429 84.4539V41.6282C476.429 33.0069 477.423 20.3882 466.814 20.3882C463.076 20.3882 459.64 22.8947 457.933 26.6976C455.772 31.516 455.491 36.3128 455.491 41.6282V84.0865C455.47 85.6639 454.13 86.9387 452.488 86.9387Z"
fill="#09090b" />
<path
d="M247.802 49.2772V45.9497C236.696 45.9497 224.963 48.3265 224.963 61.4205C224.963 68.0539 228.399 72.5482 234.297 72.5482C238.619 72.5482 242.487 69.8905 244.928 65.5691C247.953 60.2537 247.802 55.2624 247.802 49.2772ZM263.294 86.7226C262.279 87.6301 260.81 87.6949 259.664 87.0899C254.565 82.8549 253.658 80.8887 250.849 76.8481C242.422 85.4478 236.458 88.019 225.525 88.019C212.604 88.019 202.535 80.046 202.535 64.0782C202.535 51.6108 209.298 43.1191 218.913 38.9705C227.254 35.2973 238.9 34.649 247.802 33.6335V31.6456C247.802 27.994 248.083 23.6725 245.944 20.5179C244.064 17.6873 240.477 16.5205 237.323 16.5205C231.467 16.5205 226.238 19.5239 224.963 25.7468C224.704 27.1297 223.688 28.491 222.305 28.5558L207.396 26.9568C206.143 26.676 204.76 25.6604 205.106 23.7374C208.542 5.67368 224.855 0.228627 239.462 0.228627C246.938 0.228627 256.704 2.2165 262.603 7.87761C270.079 14.8568 269.366 24.1695 269.366 34.3033V58.2442C269.366 65.4394 272.348 68.5941 275.157 72.4834C276.151 73.8663 276.367 75.53 275.114 76.5672C271.981 79.1817 266.406 84.0433 263.338 86.7658L263.294 86.7226Z"
fill="#09090b" />
<path
d="M46.4006 49.2772V45.9497C35.2944 45.9497 23.5617 48.3265 23.5617 61.4205C23.5617 68.0539 26.9972 72.5482 32.896 72.5482C37.2175 72.5482 41.0852 69.8905 43.5268 65.5691C46.5518 60.2537 46.4006 55.2624 46.4006 49.2772ZM61.893 86.7226C60.8775 87.6301 59.4081 87.6949 58.263 87.0899C53.1637 82.8549 52.2561 80.8887 49.4472 76.8481C41.0203 85.4478 35.0567 88.019 24.1234 88.019C11.2023 88.019 1.1333 80.046 1.1333 64.0782C1.1333 51.6108 7.89638 43.1191 17.5116 38.9705C25.852 35.2973 37.4984 34.649 46.4006 33.6335V31.6456C46.4006 27.994 46.6815 23.6725 44.5423 20.5179C42.6625 17.6873 39.0757 16.5205 35.921 16.5205C30.0655 16.5205 24.8365 19.5239 23.5617 25.7468C23.3024 27.1297 22.2868 28.491 20.904 28.5558L5.99494 26.9568C4.74172 26.676 3.35885 25.6604 3.70456 23.7374C7.14012 5.67368 23.4536 0.228627 38.0602 0.228627C45.5363 0.228627 55.3028 2.2165 61.2015 7.87761C68.6777 14.8568 67.9646 24.1695 67.9646 34.3033V58.2442C67.9646 65.4394 70.9464 68.5941 73.7554 72.4834C74.7493 73.8663 74.9654 75.53 73.7122 76.5672C70.5791 79.1817 65.0044 84.0433 61.9362 86.7658L61.893 86.7226Z"
fill="#09090b" />
</g>
<defs>
<clipPath id="clip0_177_125">
<rect width="496.978" height="150" fill="#09090b" transform="translate(0.833313 0.0258408)" />
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 6.0 KiB

@@ -1,25 +0,0 @@
<svg width="452" height="151" viewBox="0 0 452 151" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_177_106)">
<path
d="M193.468 78.927C193.468 100.308 176.742 116.063 156.215 116.063C135.689 116.063 118.962 100.308 118.962 78.927C118.962 57.3955 135.689 41.7911 156.215 41.7911C176.742 41.7911 193.468 57.3955 193.468 78.927ZM177.161 78.927C177.161 65.5661 167.467 56.4244 156.215 56.4244C144.964 56.4244 135.27 65.5661 135.27 78.927C135.27 92.1539 144.964 101.429 156.215 101.429C167.467 101.429 177.161 92.1371 177.161 78.927Z"
fill="#09090b" />
<path
d="M273.835 78.927C273.835 100.308 257.108 116.063 236.582 116.063C216.055 116.063 199.328 100.308 199.328 78.927C199.328 57.4123 216.055 41.7911 236.582 41.7911C257.108 41.7911 273.835 57.3955 273.835 78.927ZM257.527 78.927C257.527 65.5661 247.833 56.4244 236.582 56.4244C225.33 56.4244 215.636 65.5661 215.636 78.927C215.636 92.1539 225.33 101.429 236.582 101.429C247.833 101.429 257.527 92.1371 257.527 78.927Z"
fill="#09090b" />
<path
d="M350.852 44.0347V110.705C350.852 138.13 334.678 149.331 315.558 149.331C297.559 149.331 286.727 137.293 282.641 127.448L296.839 121.538C299.368 127.582 305.562 134.714 315.541 134.714C327.78 134.714 335.365 127.163 335.365 112.949V107.608H334.796C331.146 112.111 324.114 116.046 315.24 116.046C296.672 116.046 279.661 99.8723 279.661 79.0609C279.661 58.0987 296.672 41.7911 315.24 41.7911C324.097 41.7911 331.129 45.7257 334.796 50.0956H335.365V44.0514H350.852V44.0347ZM336.52 79.0609C336.52 65.9846 327.797 56.4244 316.696 56.4244C305.445 56.4244 296.019 65.9846 296.019 79.0609C296.019 92.0032 305.445 101.429 316.696 101.429C327.797 101.429 336.52 92.0032 336.52 79.0609Z"
fill="#09090b" />
<path d="M376.385 4.95665V113.786H360.479V4.95665H376.385Z" fill="#09090b" />
<path
d="M438.367 91.1493L451.025 99.5877C446.94 105.632 437.095 116.046 420.084 116.046C398.988 116.046 383.233 99.7384 383.233 78.9102C383.233 56.8263 399.122 41.7744 418.259 41.7744C437.53 41.7744 446.957 57.1109 450.037 65.3986L451.728 69.6179L402.085 90.1782C405.886 97.6288 411.796 101.429 420.084 101.429C428.389 101.429 434.148 97.3442 438.367 91.1493ZM399.407 77.7884L432.591 64.009C430.766 59.3712 425.274 56.1398 418.812 56.1398C410.524 56.1398 398.988 63.4565 399.407 77.7884Z"
fill="#09090b" />
<path
d="M58.7548 69.2663V53.5112H111.847C112.366 56.257 112.634 59.5051 112.634 63.0211C112.634 74.8417 109.402 89.4583 98.9881 99.8724C88.8586 110.42 75.9163 116.046 58.7715 116.046C26.9935 116.046 0.271729 90.1615 0.271729 58.3834C0.271729 26.6053 26.9935 0.72068 58.7715 0.72068C76.3516 0.72068 88.8753 7.61877 98.2849 16.6097L87.1676 27.7271C80.4202 21.3982 71.2785 16.4758 58.7548 16.4758C35.5491 16.4758 17.3997 35.1776 17.3997 58.3834C17.3997 81.5891 35.5491 100.291 58.7548 100.291C73.8067 100.291 82.3791 94.2467 87.8708 88.755C92.3244 84.3014 95.2544 77.9391 96.4097 69.2495L58.7548 69.2663Z"
fill="#09090b" />
</g>
<defs>
<clipPath id="clip0_177_106">
<rect width="451.667" height="150" fill="#09090b" transform="translate(0.166702 0.0258408)" />
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 3.1 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 7.6 KiB

@@ -1,12 +0,0 @@
<svg width="500" height="151" viewBox="0 0 500 151" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_177_123)">
<path
d="M72.4341 56.5258C72.4341 65.0258 65.4341 72.0258 56.9341 72.0258C48.4341 72.0258 41.4341 65.0258 41.4341 56.5258C41.4341 48.0258 48.4341 41.0258 56.9341 41.0258C65.4341 41.0258 72.4341 48.0258 72.4341 56.5258ZM56.9341 78.0258C48.4341 78.0258 41.4341 85.0258 41.4341 93.5258C41.4341 102.026 48.4341 109.026 56.9341 109.026C65.4341 109.026 72.4341 102.026 72.4341 93.5258C72.4341 85.0258 65.4341 78.0258 56.9341 78.0258ZM150.434 75.0258C150.434 116.526 116.934 150.026 75.4341 150.026C33.9341 150.026 0.434082 116.526 0.434082 75.0258C0.434082 33.5258 33.9341 0.0258408 75.4341 0.0258408C116.934 0.0258408 150.434 33.5258 150.434 75.0258ZM130.434 75.0258C130.434 44.5258 105.934 20.0258 75.4341 20.0258C44.9341 20.0258 20.4341 44.5258 20.4341 75.0258C20.4341 105.526 44.9341 130.026 75.4341 130.026C105.934 130.026 130.434 105.526 130.434 75.0258ZM93.9341 78.0258C85.4341 78.0258 78.4341 85.0258 78.4341 93.5258C78.4341 102.026 85.4341 109.026 93.9341 109.026C102.434 109.026 109.434 102.026 109.434 93.5258C109.434 85.0258 102.434 78.0258 93.9341 78.0258ZM93.9341 41.0258C85.4341 41.0258 78.4341 48.0258 78.4341 56.5258C78.4341 65.0258 85.4341 72.0258 93.9341 72.0258C102.434 72.0258 109.434 65.0258 109.434 56.5258C109.434 48.0258 102.434 41.0258 93.9341 41.0258ZM351.934 29.5258C352.434 29.5258 352.934 30.0258 353.434 30.5258V46.5258C353.434 47.5258 352.434 48.0258 351.934 48.0258H325.434C324.434 48.0258 323.934 47.0258 323.934 46.5258V31.0258C323.934 30.0258 324.934 29.5258 325.434 29.5258H351.934ZM351.434 52.0258H300.434C299.934 52.0258 298.934 52.5258 298.934 53.5258L292.434 78.5258L291.934 80.0258L283.934 53.5258C283.934 53.0258 282.934 52.0258 282.434 52.0258H262.434C261.934 52.0258 260.934 52.5258 260.934 53.5258L253.434 78.5258L252.934 80.0258L252.434 78.5258L249.434 66.0258L246.434 53.5258C246.434 53.0258 245.434 52.0258 244.934 52.0258H204.934V30.5258C204.934 30.0258 203.934 29.0258 202.934 29.5258L177.934 37.5258C176.934 37.5258 176.434 38.0258 176.434 39.0258V52.5258H169.934C169.434 52.5258 168.434 53.0258 168.434 54.0258V73.0258C168.434 73.5258 168.934 74.5258 169.934 74.5258H176.434V98.0258C176.434 114.526 185.434 122.026 201.934 122.026C208.934 122.026 215.434 120.526 219.934 118.026V98.0258C219.934 97.0258 218.934 96.5258 218.434 97.0258C215.934 98.0258 213.434 98.5258 211.434 98.5258C206.934 98.5258 204.434 96.5258 204.434 91.5258V74.5258H218.934C219.434 74.5258 220.434 74.0258 220.434 73.0258V57.0258L239.434 120.026C239.434 120.526 240.434 121.526 240.934 121.526H261.934C262.434 121.526 263.434 121.026 263.434 120.026L272.434 92.0258L276.934 106.526L280.934 120.026C280.934 120.526 281.934 121.526 282.434 121.526H303.434C303.934 121.526 304.934 121.026 304.934 120.026L323.934 57.0258V120.026C323.934 120.526 324.434 121.526 325.434 121.526H350.934C351.434 121.526 352.434 121.026 352.434 120.026V53.5258C352.434 53.0258 351.934 52.0258 351.434 52.0258ZM384.934 29.5258H359.434C358.934 29.5258 357.934 30.0258 357.934 31.0258V119.526C357.934 120.026 358.434 121.026 359.434 121.026H384.934C385.434 121.026 386.434 120.526 386.434 119.526V30.5258C386.434 30.0258 385.934 29.5258 384.934 29.5258ZM418.934 29.5258H392.434C391.934 29.5258 390.934 30.0258 390.934 31.0258V46.5258C390.934 47.0258 391.434 48.0258 392.434 48.0258H418.934C419.434 48.0258 420.434 47.5258 420.434 46.5258V30.5258C420.434 30.0258 419.934 29.5258 418.934 29.5258ZM418.434 52.0258H392.934C392.434 52.0258 391.434 52.5258 391.434 53.5258V119.026C391.434 119.526 391.934 120.526 392.934 120.526H418.434C418.934 120.526 419.934 120.026 419.934 119.026V53.5258C419.934 53.0258 419.434 52.0258 418.434 52.0258ZM498.934 86.0258C498.934 105.026 482.934 121.526 460.434 121.526C438.434 121.526 422.434 105.026 422.434 86.0258C422.434 67.0258 438.434 50.5258 460.934 50.5258C482.934 50.5258 498.934 67.0258 498.934 86.0258ZM471.934 86.5258C471.934 79.5258 466.934 74.0258 460.934 74.5258C454.434 74.5258 449.934 80.0258 449.934 86.5258C449.934 93.0258 454.934 98.5258 460.934 98.5258C467.434 98.5258 471.934 93.0258 471.934 86.5258Z"
fill="#09090b" />
</g>
<defs>
<clipPath id="clip0_177_123">
<rect width="499" height="150" fill="#09090b" transform="translate(0.434082 0.0258408)" />
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 4.3 KiB

@@ -1,14 +0,0 @@
<svg width="781" height="151" viewBox="0 0 781 151" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M324.116 41.9481C318.271 38.7333 309.581 37.1694 297.548 37.1651C281.083 37.1712 265.8 40.5866 260.015 42.7425C258.709 43.1615 257.312 44.289 257.289 46.2185L257.293 46.644L258.913 54.8322L258.953 54.9846C259.461 56.6518 260.849 57.3995 262.114 57.3995H262.12C262.527 57.3821 262.844 57.3037 263.138 57.2286L265.879 56.5887C273.051 54.793 283.891 52.0811 295.996 52.0811C303.339 52.0811 308.275 52.721 311.584 54.1183C316.953 56.1512 316.987 59.6218 317.041 65.3668V73.9599C316.426 73.949 315.388 73.9381 313.789 73.9381C307.86 73.9381 296.203 74.2134 284.738 76.0515C267.242 78.7385 252.766 82.9262 252.857 107.032C252.864 137.632 281.246 137.69 294.886 137.718H294.888C304.147 137.716 312.69 136.349 318.269 135.195C331.002 132.238 334.253 128.195 334.082 115.543V64.6594C334.101 56.8989 334.124 47.2404 324.116 41.9481ZM317.126 120.552C310.349 122.752 302.934 123.865 295.081 123.865V124.58L295.072 123.865C282.524 123.769 270.27 122.334 270.254 106.329C270.335 93.599 277.127 92.0841 285.724 90.1644L286.321 90.0316C293.969 88.397 313.167 88.0901 317.128 88.0444L317.126 120.552ZM475.964 64.6594V115.543C476.134 128.195 472.883 132.238 460.151 135.195C454.571 136.349 446.028 137.716 436.768 137.718C423.129 137.69 394.745 137.632 394.738 107.032C394.647 82.9262 409.124 78.7385 426.619 76.0515C438.084 74.2134 449.742 73.9381 455.669 73.9381C457.269 73.9381 458.306 73.949 458.922 73.9599V65.3668C458.868 59.6218 458.834 56.1512 453.466 54.1183C450.156 52.721 445.22 52.0811 437.877 52.0811C425.771 52.0811 414.932 54.793 407.76 56.5887L405.02 57.2286C404.725 57.3038 404.408 57.3821 404 57.3995H403.995C402.73 57.3995 401.342 56.6518 400.834 54.9846L400.794 54.8322L399.174 46.644L399.17 46.2185C399.193 44.289 400.59 43.1615 401.896 42.7425C407.679 40.5866 422.964 37.1716 439.429 37.1651C451.462 37.1694 460.152 38.7333 465.997 41.9481C476.004 47.2404 475.982 56.8989 475.964 64.6594ZM459.009 88.0444C455.048 88.0901 435.85 88.397 428.201 90.0316L427.606 90.1644C419.008 92.0841 412.216 93.599 412.136 106.329C412.151 122.334 424.405 123.769 436.953 123.865L436.963 124.58V123.865C444.816 123.865 452.23 122.752 459.007 120.552L459.009 88.0444ZM732.972 37.1651C693.446 37.2183 690.094 63.1891 690.057 87.5318C690.094 111.785 693.446 137.665 732.972 137.718H732.975C772.471 137.665 775.838 111.855 775.893 87.5286C775.852 62.0737 772.501 37.2184 732.972 37.1651ZM732.974 123.51C709 123.405 707.795 111.099 707.632 87.5362C707.795 63.8443 708.998 51.476 732.971 51.3715C756.951 51.476 758.152 63.8443 758.315 87.5253C758.152 111.099 756.947 123.405 732.974 123.51ZM662.037 134.136L661.004 134.398C655.477 135.799 647.904 137.718 634.631 137.718H634.602C598.02 137.665 590.841 119.212 590.801 87.3544C590.845 50.8154 602.428 37.214 633.536 37.1651C644.511 37.1651 652.072 38.5842 657.497 39.8945L657.464 10.4641C657.419 9.17337 658.003 7.48327 661.058 6.90104L670.774 4.42302H671.304C673.536 4.47857 674.326 6.4679 674.331 8.15365V118.735C674.481 125.717 673.659 131.708 662.037 134.136ZM657.473 54.0574C654.229 53.2325 645.173 51.1941 634.976 51.1941C616.247 51.366 608.392 56.4538 608.196 87.5351C608.416 121.664 618.578 123.508 635.144 123.688L635.154 124.403V123.688C645.464 123.688 654.31 121.538 657.465 120.669L657.473 54.0574ZM570.156 135.413H561.466C559.138 135.409 557.383 133.651 557.377 131.324V65.7216C557.22 54.6244 553.904 51.538 541.954 51.3715C528.435 51.3715 514.751 54.7637 510.237 55.9934V131.322C510.233 133.69 508.589 135.41 506.327 135.413H497.461C495.132 135.409 493.374 133.651 493.371 131.324V57.7434C493.242 50.9329 493.882 46.225 503.57 43.0897C512.844 39.8869 530.595 37.1716 542.319 37.1651C564.666 37.1934 574.211 45.2554 574.241 64.1251V131.322C574.238 133.651 572.481 135.41 570.156 135.413ZM373.331 137.186H373.338C378.332 137.088 382.026 136.101 383.472 134.477C383.986 133.901 384.223 133.236 384.159 132.58C384.125 132.04 384.125 132.04 382.755 125.623L382.723 125.469C382.426 123.791 381.228 123.028 380.241 123.028C380.149 123.028 380.059 123.034 380.107 123.04C380.107 123.04 378.853 122.979 377.614 122.979H377.602C374.043 122.866 371.092 122.348 371.031 117.139V8.15478C371.028 6.33731 369.932 4.47308 367.828 4.42302L367.208 4.42825L357.475 6.92259C355.817 7.09236 354.091 8.27097 354.165 10.4617V117.494C354.183 130.169 360.986 137.163 373.323 137.186L373.331 137.186ZM235.87 135.413H166.139C163.811 135.409 162.054 133.651 162.049 131.324V122.812C162.001 120.726 162.67 119.796 163.955 118.204L219.457 53.6776H165.076C162.747 53.6723 160.989 51.9145 160.986 49.5889V43.5598C160.99 41.2331 162.747 39.4755 165.075 39.4711H235.515C237.842 39.4755 239.599 41.2331 239.604 43.5587V52.2476C239.637 53.818 239.093 55.15 237.839 56.5474L182.197 121.204H235.869C238.196 121.211 239.954 122.967 239.958 125.294V131.322C239.954 133.651 238.196 135.41 235.87 135.413Z"
fill="#09090b" />
<path
d="M22.3275 28.7189C10.2759 28.7189 4.07923 61.6253 4.72458 92.5194C4.7073 92.5151 4.68744 92.5238 4.67017 92.5194C5.31443 119.636 11.3109 145.628 23.1982 145.628C68.8157 145.628 120.79 102.608 120.79 87.1868C120.79 83.3289 116.86 75.9057 111.594 70.0464C111.587 70.0504 111.573 70.0424 111.567 70.0464C111.209 69.6307 110.829 69.216 110.451 68.7949C94.2009 50.6641 60.4881 28.7189 22.3275 28.7189Z"
fill="#09090b" />
<path
d="M4.71592 92.5183C38.9903 100.567 75.5815 91.5062 111.031 70.3783C111.21 70.2717 111.388 70.165 111.567 70.0573C111.209 69.6416 110.842 69.2226 110.464 68.8014C94.214 50.6706 60.4806 28.7211 22.32 28.7211C10.2683 28.7211 4.07056 61.6242 4.71592 92.5183ZM111.567 70.0573C111.575 70.0538 111.581 70.0495 111.587 70.0454L111.567 70.0573Z"
fill="#09090b" />
<path
d="M4.68018 92.5107C5.32444 119.627 11.323 145.629 23.2103 145.629C68.8279 145.629 120.798 102.621 120.798 87.1999C120.798 83.3419 116.854 75.9046 111.588 70.0453C75.9539 91.4181 39.1461 100.616 4.68018 92.5107Z"
fill="#09090b" />
</svg>

Before

Width:  |  Height:  |  Size: 5.9 KiB

@@ -1,33 +0,0 @@
<svg width="498" height="151" viewBox="0 0 498 151" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_177_125)">
<path
d="M309.08 117.21C280.235 138.472 238.425 149.816 202.427 149.816C151.952 149.816 106.512 131.147 72.1348 100.098C69.4339 97.6559 71.8539 94.3284 75.095 96.2298C112.195 117.815 158.067 130.801 205.452 130.801C237.409 130.801 272.564 124.19 304.889 110.469C309.772 108.395 313.856 113.667 309.08 117.21Z"
fill="#fafafa" />
<path
d="M321.072 103.49C317.399 98.7795 296.699 101.264 287.408 102.366C284.578 102.712 284.146 100.249 286.695 98.477C303.182 86.8739 330.234 90.223 333.389 94.1123C336.543 98.0232 332.567 125.14 317.075 138.083C314.698 140.071 312.429 139.012 313.488 136.376C316.967 127.69 324.767 108.222 321.072 103.49Z"
fill="#fafafa" />
<path
d="M288.057 16.5637V5.28474C288.057 3.57776 289.353 2.43258 290.909 2.43258H341.405C343.025 2.43258 344.322 3.59937 344.322 5.28474V14.9432C344.3 16.5637 342.939 18.6813 340.519 22.0304L314.353 59.3894C324.076 59.1517 334.339 60.5994 343.155 65.5691C345.143 66.6926 345.683 68.3348 345.834 69.9554V81.9906C345.834 83.6328 344.019 85.5558 342.118 84.5619C326.582 76.4159 305.947 75.53 288.77 84.6483C287.019 85.599 285.183 83.6976 285.183 82.0554V70.6252C285.183 68.7886 285.204 65.6555 287.041 62.8682L317.356 19.3943H290.974C289.353 19.3943 288.057 18.2491 288.057 16.5637Z"
fill="#fafafa" />
<path
d="M103.854 86.9387H88.4916C87.0223 86.8307 85.8555 85.7287 85.7474 84.3242V5.47922C85.7474 3.90188 87.0655 2.64865 88.7076 2.64865H103.033C104.524 2.71343 105.713 3.85866 105.821 5.28474V15.5914H106.102C109.84 5.63045 116.862 0.984887 126.326 0.984887C135.941 0.984887 141.948 5.63045 146.269 15.5914C149.986 5.63045 158.434 0.984887 167.488 0.984887C173.927 0.984887 180.971 3.64258 185.271 9.60619C190.132 16.2396 189.138 25.8765 189.138 34.3249L189.117 84.0865C189.117 85.6639 187.799 86.9387 186.157 86.9387H170.815C169.281 86.8307 168.05 85.599 168.05 84.0865V42.298C168.05 38.9705 168.352 30.6733 167.617 27.5186C166.472 22.2249 163.037 20.7339 158.586 20.7339C154.869 20.7339 150.98 23.2188 149.403 27.1945C147.825 31.1703 147.976 37.8253 147.976 42.298V84.0865C147.976 85.6639 146.658 86.9387 145.016 86.9387H129.675C128.119 86.8307 126.909 85.599 126.909 84.0865L126.888 42.298C126.888 33.5039 128.335 20.5611 117.424 20.5611C106.382 20.5611 106.815 33.1797 106.815 42.298V84.0865C106.815 85.6639 105.496 86.9387 103.854 86.9387Z"
fill="#fafafa" />
<path
d="M387.796 0.984887C410.591 0.984887 422.929 20.5611 422.929 45.4527C422.929 69.5016 409.295 88.5808 387.796 88.5808C365.411 88.5808 353.224 69.0046 353.224 44.61C353.224 20.0641 365.562 0.984887 387.796 0.984887ZM387.925 17.0823C376.603 17.0823 375.89 32.5099 375.89 42.1252C375.89 51.762 375.739 72.3322 387.796 72.3322C399.701 72.3322 400.263 55.7378 400.263 45.6255C400.263 38.9705 399.982 31.019 397.973 24.7097C396.244 19.2215 392.809 17.0823 387.925 17.0823Z"
fill="#fafafa" />
<path
d="M452.488 86.9387H437.19C435.656 86.8307 434.424 85.599 434.424 84.0865L434.403 5.21991C434.532 3.77223 435.807 2.64865 437.363 2.64865H451.602C452.942 2.71343 454.044 3.62098 454.346 4.8526V16.9095H454.627C458.927 6.12742 464.955 0.984887 475.565 0.984887C482.457 0.984887 489.177 3.46973 493.499 10.276C497.518 16.5853 497.518 27.1945 497.518 34.8219V84.4539C497.345 85.8367 496.07 86.9387 494.557 86.9387H479.151C477.747 86.8307 476.58 85.7935 476.429 84.4539V41.6282C476.429 33.0069 477.423 20.3882 466.814 20.3882C463.076 20.3882 459.64 22.8947 457.933 26.6976C455.772 31.516 455.491 36.3128 455.491 41.6282V84.0865C455.47 85.6639 454.13 86.9387 452.488 86.9387Z"
fill="#fafafa" />
<path
d="M247.802 49.2772V45.9497C236.696 45.9497 224.963 48.3265 224.963 61.4205C224.963 68.0539 228.399 72.5482 234.297 72.5482C238.619 72.5482 242.487 69.8905 244.928 65.5691C247.953 60.2537 247.802 55.2624 247.802 49.2772ZM263.294 86.7226C262.279 87.6301 260.81 87.6949 259.664 87.0899C254.565 82.8549 253.658 80.8887 250.849 76.8481C242.422 85.4478 236.458 88.019 225.525 88.019C212.604 88.019 202.535 80.046 202.535 64.0782C202.535 51.6108 209.298 43.1191 218.913 38.9705C227.254 35.2973 238.9 34.649 247.802 33.6335V31.6456C247.802 27.994 248.083 23.6725 245.944 20.5179C244.064 17.6873 240.477 16.5205 237.323 16.5205C231.467 16.5205 226.238 19.5239 224.963 25.7468C224.704 27.1297 223.688 28.491 222.305 28.5558L207.396 26.9568C206.143 26.676 204.76 25.6604 205.106 23.7374C208.542 5.67368 224.855 0.228627 239.462 0.228627C246.938 0.228627 256.704 2.2165 262.603 7.87761C270.079 14.8568 269.366 24.1695 269.366 34.3033V58.2442C269.366 65.4394 272.348 68.5941 275.157 72.4834C276.151 73.8663 276.367 75.53 275.114 76.5672C271.981 79.1817 266.406 84.0433 263.338 86.7658L263.294 86.7226Z"
fill="#fafafa" />
<path
d="M46.4006 49.2772V45.9497C35.2944 45.9497 23.5617 48.3265 23.5617 61.4205C23.5617 68.0539 26.9972 72.5482 32.896 72.5482C37.2175 72.5482 41.0852 69.8905 43.5268 65.5691C46.5518 60.2537 46.4006 55.2624 46.4006 49.2772ZM61.893 86.7226C60.8775 87.6301 59.4081 87.6949 58.263 87.0899C53.1637 82.8549 52.2561 80.8887 49.4472 76.8481C41.0203 85.4478 35.0567 88.019 24.1234 88.019C11.2023 88.019 1.1333 80.046 1.1333 64.0782C1.1333 51.6108 7.89638 43.1191 17.5116 38.9705C25.852 35.2973 37.4984 34.649 46.4006 33.6335V31.6456C46.4006 27.994 46.6815 23.6725 44.5423 20.5179C42.6625 17.6873 39.0757 16.5205 35.921 16.5205C30.0655 16.5205 24.8365 19.5239 23.5617 25.7468C23.3024 27.1297 22.2868 28.491 20.904 28.5558L5.99494 26.9568C4.74172 26.676 3.35885 25.6604 3.70456 23.7374C7.14012 5.67368 23.4536 0.228627 38.0602 0.228627C45.5363 0.228627 55.3028 2.2165 61.2015 7.87761C68.6777 14.8568 67.9646 24.1695 67.9646 34.3033V58.2442C67.9646 65.4394 70.9464 68.5941 73.7554 72.4834C74.7493 73.8663 74.9654 75.53 73.7122 76.5672C70.5791 79.1817 65.0044 84.0433 61.9362 86.7658L61.893 86.7226Z"
fill="#fafafa" />
</g>
<defs>
<clipPath id="clip0_177_125">
<rect width="496.978" height="150" fill="#fafafa" transform="translate(0.833313 0.0258408)" />
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 6.0 KiB

@@ -1,25 +0,0 @@
<svg width="452" height="151" viewBox="0 0 452 151" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_177_106)">
<path
d="M193.468 78.927C193.468 100.308 176.742 116.063 156.215 116.063C135.689 116.063 118.962 100.308 118.962 78.927C118.962 57.3955 135.689 41.7911 156.215 41.7911C176.742 41.7911 193.468 57.3955 193.468 78.927ZM177.161 78.927C177.161 65.5661 167.467 56.4244 156.215 56.4244C144.964 56.4244 135.27 65.5661 135.27 78.927C135.27 92.1539 144.964 101.429 156.215 101.429C167.467 101.429 177.161 92.1371 177.161 78.927Z"
fill="#fafafa" />
<path
d="M273.835 78.927C273.835 100.308 257.108 116.063 236.582 116.063C216.055 116.063 199.328 100.308 199.328 78.927C199.328 57.4123 216.055 41.7911 236.582 41.7911C257.108 41.7911 273.835 57.3955 273.835 78.927ZM257.527 78.927C257.527 65.5661 247.833 56.4244 236.582 56.4244C225.33 56.4244 215.636 65.5661 215.636 78.927C215.636 92.1539 225.33 101.429 236.582 101.429C247.833 101.429 257.527 92.1371 257.527 78.927Z"
fill="#fafafa" />
<path
d="M350.852 44.0347V110.705C350.852 138.13 334.678 149.331 315.558 149.331C297.559 149.331 286.727 137.293 282.641 127.448L296.839 121.538C299.368 127.582 305.562 134.714 315.541 134.714C327.78 134.714 335.365 127.163 335.365 112.949V107.608H334.796C331.146 112.111 324.114 116.046 315.24 116.046C296.672 116.046 279.661 99.8723 279.661 79.0609C279.661 58.0987 296.672 41.7911 315.24 41.7911C324.097 41.7911 331.129 45.7257 334.796 50.0956H335.365V44.0514H350.852V44.0347ZM336.52 79.0609C336.52 65.9846 327.797 56.4244 316.696 56.4244C305.445 56.4244 296.019 65.9846 296.019 79.0609C296.019 92.0032 305.445 101.429 316.696 101.429C327.797 101.429 336.52 92.0032 336.52 79.0609Z"
fill="#fafafa" />
<path d="M376.385 4.95665V113.786H360.479V4.95665H376.385Z" fill="#fafafa" />
<path
d="M438.367 91.1493L451.025 99.5877C446.94 105.632 437.095 116.046 420.084 116.046C398.988 116.046 383.233 99.7384 383.233 78.9102C383.233 56.8263 399.122 41.7744 418.259 41.7744C437.53 41.7744 446.957 57.1109 450.037 65.3986L451.728 69.6179L402.085 90.1782C405.886 97.6288 411.796 101.429 420.084 101.429C428.389 101.429 434.148 97.3442 438.367 91.1493ZM399.407 77.7884L432.591 64.009C430.766 59.3712 425.274 56.1398 418.812 56.1398C410.524 56.1398 398.988 63.4565 399.407 77.7884Z"
fill="#fafafa" />
<path
d="M58.7548 69.2663V53.5112H111.847C112.366 56.257 112.634 59.5051 112.634 63.0211C112.634 74.8417 109.402 89.4583 98.9881 99.8724C88.8586 110.42 75.9163 116.046 58.7715 116.046C26.9935 116.046 0.271729 90.1615 0.271729 58.3834C0.271729 26.6053 26.9935 0.72068 58.7715 0.72068C76.3516 0.72068 88.8753 7.61877 98.2849 16.6097L87.1676 27.7271C80.4202 21.3982 71.2785 16.4758 58.7548 16.4758C35.5491 16.4758 17.3997 35.1776 17.3997 58.3834C17.3997 81.5891 35.5491 100.291 58.7548 100.291C73.8067 100.291 82.3791 94.2467 87.8708 88.755C92.3244 84.3014 95.2544 77.9391 96.4097 69.2495L58.7548 69.2663Z"
fill="#fafafa" />
</g>
<defs>
<clipPath id="clip0_177_106">
<rect width="451.667" height="150" fill="#fafafa" transform="translate(0.166702 0.0258408)" />
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 3.1 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 7.6 KiB

@@ -1,12 +0,0 @@
<svg width="500" height="151" viewBox="0 0 500 151" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_177_123)">
<path
d="M72.4341 56.5258C72.4341 65.0258 65.4341 72.0258 56.9341 72.0258C48.4341 72.0258 41.4341 65.0258 41.4341 56.5258C41.4341 48.0258 48.4341 41.0258 56.9341 41.0258C65.4341 41.0258 72.4341 48.0258 72.4341 56.5258ZM56.9341 78.0258C48.4341 78.0258 41.4341 85.0258 41.4341 93.5258C41.4341 102.026 48.4341 109.026 56.9341 109.026C65.4341 109.026 72.4341 102.026 72.4341 93.5258C72.4341 85.0258 65.4341 78.0258 56.9341 78.0258ZM150.434 75.0258C150.434 116.526 116.934 150.026 75.4341 150.026C33.9341 150.026 0.434082 116.526 0.434082 75.0258C0.434082 33.5258 33.9341 0.0258408 75.4341 0.0258408C116.934 0.0258408 150.434 33.5258 150.434 75.0258ZM130.434 75.0258C130.434 44.5258 105.934 20.0258 75.4341 20.0258C44.9341 20.0258 20.4341 44.5258 20.4341 75.0258C20.4341 105.526 44.9341 130.026 75.4341 130.026C105.934 130.026 130.434 105.526 130.434 75.0258ZM93.9341 78.0258C85.4341 78.0258 78.4341 85.0258 78.4341 93.5258C78.4341 102.026 85.4341 109.026 93.9341 109.026C102.434 109.026 109.434 102.026 109.434 93.5258C109.434 85.0258 102.434 78.0258 93.9341 78.0258ZM93.9341 41.0258C85.4341 41.0258 78.4341 48.0258 78.4341 56.5258C78.4341 65.0258 85.4341 72.0258 93.9341 72.0258C102.434 72.0258 109.434 65.0258 109.434 56.5258C109.434 48.0258 102.434 41.0258 93.9341 41.0258ZM351.934 29.5258C352.434 29.5258 352.934 30.0258 353.434 30.5258V46.5258C353.434 47.5258 352.434 48.0258 351.934 48.0258H325.434C324.434 48.0258 323.934 47.0258 323.934 46.5258V31.0258C323.934 30.0258 324.934 29.5258 325.434 29.5258H351.934ZM351.434 52.0258H300.434C299.934 52.0258 298.934 52.5258 298.934 53.5258L292.434 78.5258L291.934 80.0258L283.934 53.5258C283.934 53.0258 282.934 52.0258 282.434 52.0258H262.434C261.934 52.0258 260.934 52.5258 260.934 53.5258L253.434 78.5258L252.934 80.0258L252.434 78.5258L249.434 66.0258L246.434 53.5258C246.434 53.0258 245.434 52.0258 244.934 52.0258H204.934V30.5258C204.934 30.0258 203.934 29.0258 202.934 29.5258L177.934 37.5258C176.934 37.5258 176.434 38.0258 176.434 39.0258V52.5258H169.934C169.434 52.5258 168.434 53.0258 168.434 54.0258V73.0258C168.434 73.5258 168.934 74.5258 169.934 74.5258H176.434V98.0258C176.434 114.526 185.434 122.026 201.934 122.026C208.934 122.026 215.434 120.526 219.934 118.026V98.0258C219.934 97.0258 218.934 96.5258 218.434 97.0258C215.934 98.0258 213.434 98.5258 211.434 98.5258C206.934 98.5258 204.434 96.5258 204.434 91.5258V74.5258H218.934C219.434 74.5258 220.434 74.0258 220.434 73.0258V57.0258L239.434 120.026C239.434 120.526 240.434 121.526 240.934 121.526H261.934C262.434 121.526 263.434 121.026 263.434 120.026L272.434 92.0258L276.934 106.526L280.934 120.026C280.934 120.526 281.934 121.526 282.434 121.526H303.434C303.934 121.526 304.934 121.026 304.934 120.026L323.934 57.0258V120.026C323.934 120.526 324.434 121.526 325.434 121.526H350.934C351.434 121.526 352.434 121.026 352.434 120.026V53.5258C352.434 53.0258 351.934 52.0258 351.434 52.0258ZM384.934 29.5258H359.434C358.934 29.5258 357.934 30.0258 357.934 31.0258V119.526C357.934 120.026 358.434 121.026 359.434 121.026H384.934C385.434 121.026 386.434 120.526 386.434 119.526V30.5258C386.434 30.0258 385.934 29.5258 384.934 29.5258ZM418.934 29.5258H392.434C391.934 29.5258 390.934 30.0258 390.934 31.0258V46.5258C390.934 47.0258 391.434 48.0258 392.434 48.0258H418.934C419.434 48.0258 420.434 47.5258 420.434 46.5258V30.5258C420.434 30.0258 419.934 29.5258 418.934 29.5258ZM418.434 52.0258H392.934C392.434 52.0258 391.434 52.5258 391.434 53.5258V119.026C391.434 119.526 391.934 120.526 392.934 120.526H418.434C418.934 120.526 419.934 120.026 419.934 119.026V53.5258C419.934 53.0258 419.434 52.0258 418.434 52.0258ZM498.934 86.0258C498.934 105.026 482.934 121.526 460.434 121.526C438.434 121.526 422.434 105.026 422.434 86.0258C422.434 67.0258 438.434 50.5258 460.934 50.5258C482.934 50.5258 498.934 67.0258 498.934 86.0258ZM471.934 86.5258C471.934 79.5258 466.934 74.0258 460.934 74.5258C454.434 74.5258 449.934 80.0258 449.934 86.5258C449.934 93.0258 454.934 98.5258 460.934 98.5258C467.434 98.5258 471.934 93.0258 471.934 86.5258Z"
fill="#fafafa" />
</g>
<defs>
<clipPath id="clip0_177_123">
<rect width="499" height="150" fill="#fafafa" transform="translate(0.434082 0.0258408)" />
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 4.3 KiB

@@ -1,14 +0,0 @@
<svg width="781" height="151" viewBox="0 0 781 151" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M324.116 41.9481C318.271 38.7333 309.581 37.1694 297.548 37.1651C281.083 37.1712 265.8 40.5866 260.015 42.7425C258.709 43.1615 257.312 44.289 257.289 46.2185L257.293 46.644L258.913 54.8322L258.953 54.9846C259.461 56.6518 260.849 57.3995 262.114 57.3995H262.12C262.527 57.3821 262.844 57.3037 263.138 57.2286L265.879 56.5887C273.051 54.793 283.891 52.0811 295.996 52.0811C303.339 52.0811 308.275 52.721 311.584 54.1183C316.953 56.1512 316.987 59.6218 317.041 65.3668V73.9599C316.426 73.949 315.388 73.9381 313.789 73.9381C307.86 73.9381 296.203 74.2134 284.738 76.0515C267.242 78.7385 252.766 82.9262 252.857 107.032C252.864 137.632 281.246 137.69 294.886 137.718H294.888C304.147 137.716 312.69 136.349 318.269 135.195C331.002 132.238 334.253 128.195 334.082 115.543V64.6594C334.101 56.8989 334.124 47.2404 324.116 41.9481ZM317.126 120.552C310.349 122.752 302.934 123.865 295.081 123.865V124.58L295.072 123.865C282.524 123.769 270.27 122.334 270.254 106.329C270.335 93.599 277.127 92.0841 285.724 90.1644L286.321 90.0316C293.969 88.397 313.167 88.0901 317.128 88.0444L317.126 120.552ZM475.964 64.6594V115.543C476.134 128.195 472.883 132.238 460.151 135.195C454.571 136.349 446.028 137.716 436.768 137.718C423.129 137.69 394.745 137.632 394.738 107.032C394.647 82.9262 409.124 78.7385 426.619 76.0515C438.084 74.2134 449.742 73.9381 455.669 73.9381C457.269 73.9381 458.306 73.949 458.922 73.9599V65.3668C458.868 59.6218 458.834 56.1512 453.466 54.1183C450.156 52.721 445.22 52.0811 437.877 52.0811C425.771 52.0811 414.932 54.793 407.76 56.5887L405.02 57.2286C404.725 57.3038 404.408 57.3821 404 57.3995H403.995C402.73 57.3995 401.342 56.6518 400.834 54.9846L400.794 54.8322L399.174 46.644L399.17 46.2185C399.193 44.289 400.59 43.1615 401.896 42.7425C407.679 40.5866 422.964 37.1716 439.429 37.1651C451.462 37.1694 460.152 38.7333 465.997 41.9481C476.004 47.2404 475.982 56.8989 475.964 64.6594ZM459.009 88.0444C455.048 88.0901 435.85 88.397 428.201 90.0316L427.606 90.1644C419.008 92.0841 412.216 93.599 412.136 106.329C412.151 122.334 424.405 123.769 436.953 123.865L436.963 124.58V123.865C444.816 123.865 452.23 122.752 459.007 120.552L459.009 88.0444ZM732.972 37.1651C693.446 37.2183 690.094 63.1891 690.057 87.5318C690.094 111.785 693.446 137.665 732.972 137.718H732.975C772.471 137.665 775.838 111.855 775.893 87.5286C775.852 62.0737 772.501 37.2184 732.972 37.1651ZM732.974 123.51C709 123.405 707.795 111.099 707.632 87.5362C707.795 63.8443 708.998 51.476 732.971 51.3715C756.951 51.476 758.152 63.8443 758.315 87.5253C758.152 111.099 756.947 123.405 732.974 123.51ZM662.037 134.136L661.004 134.398C655.477 135.799 647.904 137.718 634.631 137.718H634.602C598.02 137.665 590.841 119.212 590.801 87.3544C590.845 50.8154 602.428 37.214 633.536 37.1651C644.511 37.1651 652.072 38.5842 657.497 39.8945L657.464 10.4641C657.419 9.17337 658.003 7.48327 661.058 6.90104L670.774 4.42302H671.304C673.536 4.47857 674.326 6.4679 674.331 8.15365V118.735C674.481 125.717 673.659 131.708 662.037 134.136ZM657.473 54.0574C654.229 53.2325 645.173 51.1941 634.976 51.1941C616.247 51.366 608.392 56.4538 608.196 87.5351C608.416 121.664 618.578 123.508 635.144 123.688L635.154 124.403V123.688C645.464 123.688 654.31 121.538 657.465 120.669L657.473 54.0574ZM570.156 135.413H561.466C559.138 135.409 557.383 133.651 557.377 131.324V65.7216C557.22 54.6244 553.904 51.538 541.954 51.3715C528.435 51.3715 514.751 54.7637 510.237 55.9934V131.322C510.233 133.69 508.589 135.41 506.327 135.413H497.461C495.132 135.409 493.374 133.651 493.371 131.324V57.7434C493.242 50.9329 493.882 46.225 503.57 43.0897C512.844 39.8869 530.595 37.1716 542.319 37.1651C564.666 37.1934 574.211 45.2554 574.241 64.1251V131.322C574.238 133.651 572.481 135.41 570.156 135.413ZM373.331 137.186H373.338C378.332 137.088 382.026 136.101 383.472 134.477C383.986 133.901 384.223 133.236 384.159 132.58C384.125 132.04 384.125 132.04 382.755 125.623L382.723 125.469C382.426 123.791 381.228 123.028 380.241 123.028C380.149 123.028 380.059 123.034 380.107 123.04C380.107 123.04 378.853 122.979 377.614 122.979H377.602C374.043 122.866 371.092 122.348 371.031 117.139V8.15478C371.028 6.33731 369.932 4.47308 367.828 4.42302L367.208 4.42825L357.475 6.92259C355.817 7.09236 354.091 8.27097 354.165 10.4617V117.494C354.183 130.169 360.986 137.163 373.323 137.186L373.331 137.186ZM235.87 135.413H166.139C163.811 135.409 162.054 133.651 162.049 131.324V122.812C162.001 120.726 162.67 119.796 163.955 118.204L219.457 53.6776H165.076C162.747 53.6723 160.989 51.9145 160.986 49.5889V43.5598C160.99 41.2331 162.747 39.4755 165.075 39.4711H235.515C237.842 39.4755 239.599 41.2331 239.604 43.5587V52.2476C239.637 53.818 239.093 55.15 237.839 56.5474L182.197 121.204H235.869C238.196 121.211 239.954 122.967 239.958 125.294V131.322C239.954 133.651 238.196 135.41 235.87 135.413Z"
fill="#fafafa" />
<path
d="M22.3275 28.7189C10.2759 28.7189 4.07923 61.6253 4.72458 92.5194C4.7073 92.5151 4.68744 92.5238 4.67017 92.5194C5.31443 119.636 11.3109 145.628 23.1982 145.628C68.8157 145.628 120.79 102.608 120.79 87.1868C120.79 83.3289 116.86 75.9057 111.594 70.0464C111.587 70.0504 111.573 70.0424 111.567 70.0464C111.209 69.6307 110.829 69.216 110.451 68.7949C94.2009 50.6641 60.4881 28.7189 22.3275 28.7189Z"
fill="#fafafa" />
<path
d="M4.71592 92.5183C38.9903 100.567 75.5815 91.5062 111.031 70.3783C111.21 70.2717 111.388 70.165 111.567 70.0573C111.209 69.6416 110.842 69.2226 110.464 68.8014C94.214 50.6706 60.4806 28.7211 22.32 28.7211C10.2683 28.7211 4.07056 61.6242 4.71592 92.5183ZM111.567 70.0573C111.575 70.0538 111.581 70.0495 111.587 70.0454L111.567 70.0573Z"
fill="#fafafa" />
<path
d="M4.68018 92.5107C5.32444 119.627 11.323 145.629 23.2103 145.629C68.8279 145.629 120.798 102.621 120.798 87.1999C120.798 83.3419 116.854 75.9046 111.588 70.0453C75.9539 91.4181 39.1461 100.616 4.68018 92.5107Z"
fill="#fafafa" />
</svg>

Before

Width:  |  Height:  |  Size: 5.9 KiB

-8
View File
@@ -1,8 +0,0 @@
<svg width="256" height="256" viewBox="0 0 256 256" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M173.611 166.311L132.877 219.804H173.524L193.973 191.813L213.183 219.804H256L215.673 165.707L215.15 165.046L207.461 155.332L195.329 140.004L195.258 139.915L193.813 138.089L193.923 138.001L176.286 112.861H134.061L173.611 166.311ZM199.89 133.554L214.959 112.861H254.619L219.874 158.8L199.89 133.554Z"
fill="#09090B" />
<path fill-rule="evenodd" clip-rule="evenodd"
d="M0 36.1959V174.314H39.0678V137.614H60.3938L60.4323 137.671C60.8436 137.653 61.2518 137.634 61.6569 137.614C75.0665 136.968 85.1471 135.549 96.3849 131.385C96.7596 131.246 97.1355 131.104 97.5128 130.959L97.4591 130.881C105.816 126.86 112.331 121.344 117.006 114.331C122.005 106.702 124.504 97.6915 124.504 87.2997C124.504 76.7764 122.005 67.7 117.006 60.0706C112.007 52.3097 104.904 46.3903 95.6964 42.3125C86.62 38.2347 75.7678 36.1959 63.1399 36.1959H0ZM102.156 137.725L64.8705 144.175L85.4361 174.314H127.266L102.156 137.725ZM39.0678 107.426H60.7721C68.9277 107.426 74.9786 105.65 78.9248 102.098C83.0026 98.5465 85.0415 93.6137 85.0415 87.2997C85.0415 80.8542 83.0026 75.8556 78.9248 72.304C74.9786 68.7523 68.9277 66.9765 60.7721 66.9765H39.0678V107.426Z"
fill="#09090B" />
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

-8
View File
@@ -1,8 +0,0 @@
<svg width="256" height="256" viewBox="0 0 256 256" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M173.611 166.311L132.877 219.804H173.524L193.973 191.813L213.183 219.804H256L215.673 165.707L215.15 165.046L207.461 155.332L195.329 140.004L195.258 139.915L193.813 138.089L193.923 138.001L176.286 112.861H134.061L173.611 166.311ZM199.89 133.554L214.959 112.861H254.619L219.874 158.8L199.89 133.554Z"
fill="#FAFAFA" />
<path fill-rule="evenodd" clip-rule="evenodd"
d="M0 36.1959V174.314H39.0678V137.614H60.3938L60.4323 137.671C60.8436 137.653 61.2517 137.634 61.6567 137.614C75.0665 136.968 85.1471 135.549 96.385 131.385C96.7596 131.246 97.1355 131.104 97.5128 130.959L97.4591 130.881C105.816 126.86 112.331 121.344 117.006 114.331C122.005 106.702 124.504 97.6915 124.504 87.2997C124.504 76.7764 122.005 67.7 117.006 60.0706C112.007 52.3097 104.904 46.3903 95.6964 42.3125C86.62 38.2347 75.7679 36.1959 63.1399 36.1959H0ZM102.156 137.725L64.8705 144.175L85.4361 174.314H127.266L102.156 137.725ZM39.0678 107.426H60.7721C68.9277 107.426 74.9786 105.65 78.9248 102.098C83.0026 98.5465 85.0415 93.6137 85.0415 87.2997C85.0415 80.8542 83.0026 75.8556 78.9248 72.304C74.9786 68.7523 68.9277 66.9765 60.7721 66.9765H39.0678V107.426Z"
fill="#FAFAFA" />
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 7.5 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 867 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 910 KiB

@@ -1,15 +0,0 @@
(function initializeTheme() {
try {
if (
localStorage.theme === "dark" ||
// eslint-disable-next-line lingui/no-unlocalized-strings
window.matchMedia("(prefers-color-scheme: dark)").matches
) {
document.documentElement.classList.add("dark");
} else {
document.documentElement.classList.remove("dark");
}
} catch {
// pass
}
})();
-155
View File
@@ -1,155 +0,0 @@
code[class*="language-"],
pre[class*="language-"] {
color: #f8f8f2;
background: none;
font-family: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
word-wrap: normal;
line-height: 1.5;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
/* Code blocks */
pre[class*="language-"] {
padding: 1em;
margin: 0.5em 0;
overflow: auto;
border-radius: 0.3em;
}
:not(pre) > code[class*="language-"],
pre[class*="language-"] {
background: #2b2b2b;
}
/* Inline code */
:not(pre) > code[class*="language-"] {
padding: 0.1em;
border-radius: 0.3em;
white-space: normal;
}
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: #d4d0ab;
}
.token.punctuation {
color: #fefefe;
}
.token.property,
.token.tag,
.token.constant,
.token.symbol,
.token.deleted {
color: #ffa07a;
}
.token.boolean,
.token.number {
color: #00e0e0;
}
.token.selector,
.token.attr-name,
.token.string,
.token.char,
.token.builtin,
.token.inserted {
color: #abe338;
}
.token.operator,
.token.entity,
.token.url,
.language-css .token.string,
.style .token.string,
.token.variable {
color: #00e0e0;
}
.token.atrule,
.token.attr-value,
.token.function {
color: #ffd700;
}
.token.keyword {
color: #00e0e0;
}
.token.regex,
.token.important {
color: #ffd700;
}
.token.important,
.token.bold {
font-weight: bold;
}
.token.italic {
font-style: italic;
}
.token.entity {
cursor: help;
}
@media screen and (forced-colors: active) {
code[class*="language-"],
pre[class*="language-"] {
color: windowText;
background: window;
}
:not(pre) > code[class*="language-"],
pre[class*="language-"] {
background: window;
}
.token.important {
background: highlight;
color: window;
font-weight: normal;
}
.token.atrule,
.token.attr-value,
.token.function,
.token.keyword,
.token.operator,
.token.selector {
font-weight: bold;
}
.token.attr-value,
.token.comment,
.token.doctype,
.token.function,
.token.keyword,
.token.operator,
.token.property,
.token.string {
color: highlight;
}
.token.attr-value,
.token.url {
font-weight: normal;
}
}

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