mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-09-29 16:24:22 +10:00
chore(copy): rewrite marketing, app, and docs copy to read less AI-generated
Rewrites the landing page, in-app microcopy, and public docs, then fixes what the rewrite exposed: stale template counts, a broken quickstart anchor, out-of-sync FAQ structured data, dead error-hint branches in the MCP tools, and wrong-sense translations across all 53 locales. Adds GLOSSARY.md so translators get the right sense of the ambiguous UI terms.
This commit is contained in:
@@ -15,7 +15,7 @@
|
|||||||
{
|
{
|
||||||
"guid": "reactive-resume",
|
"guid": "reactive-resume",
|
||||||
"name": "Reactive Resume",
|
"name": "Reactive Resume",
|
||||||
"description": "A free and open-source resume builder that simplifies the process of creating, updating, and sharing your resume.",
|
"description": "A free and open-source resume builder that makes it easy to create, update, and share your resume.",
|
||||||
"webpageUrl": {
|
"webpageUrl": {
|
||||||
"url": "https://rxresu.me"
|
"url": "https://rxresu.me"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ temp
|
|||||||
.claude
|
.claude
|
||||||
.cursor
|
.cursor
|
||||||
.opencode
|
.opencode
|
||||||
|
.codegraph
|
||||||
.superpowers
|
.superpowers
|
||||||
.worktrees
|
.worktrees
|
||||||
.migration
|
.migration
|
||||||
|
|||||||
@@ -46,169 +46,96 @@ Rules:
|
|||||||
|
|
||||||
## Agent skills
|
## Agent skills
|
||||||
|
|
||||||
### Issue tracker
|
- Issues and specs: GitHub Issues for `amruthpillai/reactive-resume`. See `docs/agents/issue-tracker.md`.
|
||||||
|
- Domain docs use a multi-context layout. See `docs/agents/domain.md`.
|
||||||
|
|
||||||
Issues and specs are tracked in GitHub Issues for `amruthpillai/reactive-resume`. See `docs/agents/issue-tracker.md`.
|
## Overview
|
||||||
|
|
||||||
### Domain docs
|
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; `apps/server` mounts the API/auth/MCP/static routes and serves the built web app.
|
||||||
|
|
||||||
This repository uses a multi-context domain-doc layout. See `docs/agents/domain.md`.
|
Internal packages are source-consumed through `package.json` export maps pointing at `src` files. Do not assume package-local `dist` output exists unless a package explicitly adds it.
|
||||||
|
|
||||||
## Cursor Cloud specific instructions
|
Prerequisites: **Node.js 24** (matches Dockerfile `ARG NODE_VERSION=24`), **pnpm 11.21.0** ([install guide](https://pnpm.io/installation)), and **Docker** for PostgreSQL (`sudo dockerd &` if the daemon isn't running).
|
||||||
|
|
||||||
### Overview
|
## Ownership map
|
||||||
|
|
||||||
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.
|
Where each concern lives, and where new code for it goes:
|
||||||
|
|
||||||
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.
|
| Area | Owner |
|
||||||
|
|------|-------|
|
||||||
|
| Web routes, loaders, user-facing workflows | `apps/web/src/routes`, `apps/web/src/features` (file-based; never hand-edit `routeTree.gen.ts`) |
|
||||||
|
| Server HTTP routes/adapters, startup checks, static handlers, MCP transport, OpenAPI/well-known | `apps/server/src/{http,rpc,mcp,openapi,static,startup}` |
|
||||||
|
| Authenticated API contracts + business logic | `packages/api/src/features/*` (oRPC routers, DTOs, rate limiting; aggregated at `@reactive-resume/api/routers` for `/api/rpc`) |
|
||||||
|
| Auth | `packages/auth` (Better Auth config/helpers/types; `apps/server/src/http/auth.ts` delegates to `auth.handler`) |
|
||||||
|
| DB client + schema | `packages/db` (Drizzle; migrations at repo root `migrations/`) |
|
||||||
|
| Server env validation | `packages/env` (auto-loads root `.env`) |
|
||||||
|
| Resume/page/template Zod schemas | `packages/schema` |
|
||||||
|
| Pure resume-domain behavior (no DB/HTTP/DOM/renderer deps) | `packages/resume` (JSON Patch helpers, social-network icons) |
|
||||||
|
| Resume PDF rendering | `packages/pdf` (React PDF document, font registration, template primitives, browser/server adapters) |
|
||||||
|
| PDF.js viewer/canvas UI | `apps/web/src/features/resume` — never in `packages/pdf` |
|
||||||
|
| DOCX export | `packages/docx` |
|
||||||
|
| MCP tools/prompts/resources/server-card | `packages/mcp` |
|
||||||
|
| Generic UI primitives + hooks | `packages/ui` (Base UI/shadcn-style); workflow-specific UI stays in the owning web feature |
|
||||||
|
| Focused support surfaces | `packages/fonts`, `packages/email`, `packages/import`, `packages/ai`, `packages/utils`, `packages/config` — prefer existing exports over cross-package shortcuts |
|
||||||
|
| Dev-only scripts | `tooling/`, not `packages/`, so packages only hold runtime-bundled code |
|
||||||
|
|
||||||
### Prerequisites
|
Narrow cross-cutting helpers go in `packages/utils` only after checking no domain package is a better owner. Specifically: resume JSON Patch behavior belongs in `@reactive-resume/resume/patch` and DOCX builders in `@reactive-resume/docx` — not in `@reactive-resume/utils`.
|
||||||
|
|
||||||
- **Node.js 24** (matches Dockerfile `ARG NODE_VERSION=24`). Use `nvm install 24 && nvm use 24` if needed.
|
## Web app conventions
|
||||||
- **Docker** is required to run PostgreSQL. Start it with `sudo dockerd &` if the daemon isn't running.
|
|
||||||
- **pnpm 11.21.0**. Install pnpm directly using the [official installation guide](https://pnpm.io/installation).
|
|
||||||
|
|
||||||
### Codebase map
|
- `apps/web/src/router.tsx` initializes router context with `queryClient`, `orpc`, `theme`, `locale`, `session`, and `flags`. Reuse route context instead of refetching these ad hoc.
|
||||||
|
- Builder shell: `apps/web/src/routes/builder/$resumeId`. Its nested preview route is client-only (`ssr: false`); the public resume route `apps/web/src/routes/$username/$slug.tsx` uses `ssr: "data-only"`.
|
||||||
|
- Browser-only preview code: `apps/web/src/features/resume/preview`. Public PDF viewer: `apps/web/src/features/resume/public`. Keep PDF.js/canvas/browser APIs out of SSR paths.
|
||||||
|
- Isomorphic oRPC client: `apps/web/src/libs/orpc/client.ts` — server calls use an in-process router client, browser calls use `/api/rpc` with credentials included.
|
||||||
|
- For React components with explicit props, use a named props type (e.g. `type FooProps = {...}` with `function Foo(props: FooProps)`) rather than inline object annotations, especially with more than one field or with generics.
|
||||||
|
|
||||||
- `apps/web` owns TanStack Start routes, Vite config, PWA setup, oRPC browser client wiring, web features, and the resume builder UI.
|
## Package boundaries
|
||||||
- `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
|
`pnpm exec turbo boundaries` is the executable check. Rules:
|
||||||
|
|
||||||
- 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.
|
- Workspace deps go through package names and export maps. Never import another workspace's `src` tree via repo paths, `@reactive-resume/*/src/*`, or TS path aliases.
|
||||||
- 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.
|
- Workspace `turbo.json` files declare coarse tags: `app:web`, `app:server`, `runtime:server` (server-only packages: API/auth/db/env/email/MCP), `runtime:browser` (browser-only shared UI), `runtime:universal` (environment-neutral domain packages), plus `role:domain|infra|adapter|api|rendering|tooling` for intent.
|
||||||
- `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.
|
- Runtime-specific code lives behind explicit export subpaths (`@reactive-resume/pdf/browser`, `@reactive-resume/pdf/server`, `@reactive-resume/env/server`). Keep root exports environment-neutral unless the package is intentionally server-only.
|
||||||
- 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"`.
|
- Wildcard exports are allowed only for leaf libraries with an intentionally file-like surface — currently `@reactive-resume/ui/components/*`, `@reactive-resume/ui/hooks/*`, and schema resume model files. Prefer explicit exports for packages owning runtime behavior.
|
||||||
- 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`.
|
- Prefer `protectedProcedure` from `packages/api/src/context.ts` for authenticated procedures. Expose only intentional public surfaces through `packages/api/package.json`.
|
||||||
- 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.
|
- Shared PDF section filtering: `packages/pdf/src/templates/shared/filtering.ts`. Template-specific visual exceptions stay in the owning template directory unless multiple templates need the behavior. `packages/pdf/src/hooks/use-register-fonts.ts` owns font registration, standard PDF fonts, CJK fallback stacks, and global hyphenation.
|
||||||
- For React components with explicit props, prefer a named TypeScript props type over inline object annotations in the function signature, especially once the props include more than one field or generics. For example:
|
|
||||||
|
|
||||||
```ts
|
Multi-place changes:
|
||||||
type IntentSelectFieldProps<TValue extends string> = {
|
|
||||||
label: string;
|
|
||||||
id: string;
|
|
||||||
value: TValue | undefined;
|
|
||||||
options: readonly ComboboxOption<TValue>[];
|
|
||||||
onChange: (value: TValue | undefined) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
function IntentSelectField<TValue extends string>(props: IntentSelectFieldProps<TValue>) {
|
- **Resume data shape**: `packages/schema/src/resume/*` first, then API DTOs, importers, PDF rendering, and web forms consuming it.
|
||||||
// ...
|
- **New template**: `packages/schema/src/templates.ts`, `packages/pdf/src/templates/index.ts`, source under `packages/pdf/src/templates/<name>/`, and previews under `apps/web/public/templates/{jpg,pdf}`.
|
||||||
}
|
- **New DB column/table**: `packages/db/src/schema/*`, then `dotenvx run -f .env.local -- pnpm db:generate`.
|
||||||
```
|
- **New env var**: `packages/env/src/server.ts` **and** the `globalEnv` array in `turbo.json`. Turborepo 2.x strict env mode filters out unlisted vars, so the variable will be `undefined` in child processes at runtime even when correctly set in the OS/container environment.
|
||||||
|
|
||||||
### Package and feature boundaries
|
## Environment and database
|
||||||
|
|
||||||
- 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.
|
Copy `.env.example` to `.env.local`. Three required vars: `APP_URL` (default `http://localhost:3000`), `DATABASE_URL` (default `postgresql://postgres:postgres@localhost:5432/postgres`), `AUTH_SECRET` (any non-empty string).
|
||||||
- `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:
|
- **S3/SeaweedFS optional.** If `S3_ACCESS_KEY_ID`, `S3_SECRET_ACCESS_KEY`, and `S3_BUCKET` are all set, the app uses S3-compatible storage. `.env.example` ships SeaweedFS defaults, so either start the `seaweedfs` compose service or comment those vars out to use local filesystem storage under `<workspace>/data`. `LOCAL_STORAGE_PATH` must be absolute when set.
|
||||||
|
- **`REDIS_URL` and `ENCRYPTION_SECRET`** are optional for core resume flows but both required for saved AI providers and the authenticated `/agent` workspace. Host-run dev uses `REDIS_URL=redis://localhost:6379`; the container-run app uses `redis://redis:6379`.
|
||||||
|
- **`drizzle-kit` (used by `pnpm db:migrate`) reads `DATABASE_URL` from `process.env` directly** — it does not auto-load `.env`. Run migration commands through `dotenvx`.
|
||||||
|
- The production server auto-runs migrations at startup before serving traffic, so manual `pnpm db:migrate` is mainly for first setup, migration debugging, or applying migrations without starting the app.
|
||||||
|
|
||||||
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`.
|
## Commands
|
||||||
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
|
Prefix dev servers and migration commands with `dotenvx run -f .env.local --`. Tests, typechecks, linters, boundary checks, and `pnpm build` do not need it; if one fails on a missing env var, rerun it with the prefix.
|
||||||
|
|
||||||
PostgreSQL runs via Docker Compose:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
sudo docker compose -f compose.dev.yml up -d postgres
|
sudo docker compose -f compose.dev.yml up -d postgres # DB only
|
||||||
|
sudo docker compose -f compose.dev.yml up -d postgres redis seaweedfs seaweedfs_create_bucket # full infra
|
||||||
|
dotenvx run -f .env.local -- pnpm dev # port 3000 (dev:web for web only)
|
||||||
|
dotenvx run -f .env.local -- pnpm db:generate # db:migrate to apply
|
||||||
|
pnpm check # Biome — WRITE-CAPABLE (--write --unsafe)
|
||||||
|
pnpm test | pnpm typecheck | pnpm build | pnpm exec turbo boundaries
|
||||||
```
|
```
|
||||||
|
|
||||||
The dev default connection string is `postgresql://postgres:postgres@localhost:5432/postgres`.
|
Prefer package filters over repo-wide runs, e.g. `pnpm --filter web typecheck`, `pnpm --filter @reactive-resume/pdf test`. Vitest paths are package-relative under `pnpm --filter <package> test -- <path>`.
|
||||||
|
|
||||||
**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.
|
## Gotchas
|
||||||
|
|
||||||
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.
|
- Email sending needs SMTP config; without it emails are logged to console. Dev still works — verification links appear in server logs.
|
||||||
|
- `lefthook.yml` pre-commit runs `biome check` on staged files. Run `pnpm check` before committing.
|
||||||
### Environment
|
- `pnpm check` is write-capable. Call that out when using it, and use narrower Biome commands for a non-mutating inspection.
|
||||||
|
- Biome: tabs, double quotes, line width 120, organized import groups, sorted Tailwind classes for `clsx`, `cva`, `cn`.
|
||||||
Copy `.env.example` to `.env.local`. The three required variables are:
|
- Most packages typecheck with `tsgo --noEmit` and test with `vitest run --passWithNoTests`.
|
||||||
|
- There may be unrelated local edits in the worktree. Check `git status --short` first; do not revert files you did not touch.
|
||||||
- `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.
|
|
||||||
|
|
||||||
`REDIS_URL` and `ENCRYPTION_SECRET` are optional for core resume flows, but both are required for saved AI providers and the authenticated `/agent` workspace. Start the `redis` compose service and set both vars in `.env.local` when working on those features. For host-run development, use `REDIS_URL=redis://localhost:6379`; the container-run app uses `REDIS_URL=redis://redis:6379`.
|
|
||||||
|
|
||||||
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` |
|
|
||||||
| Start full dev infrastructure | `sudo docker compose -f compose.dev.yml up -d postgres redis 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.
|
|
||||||
|
|||||||
+256
@@ -0,0 +1,256 @@
|
|||||||
|
# Glossary
|
||||||
|
|
||||||
|
What the recurring terms in Reactive Resume's interface actually mean.
|
||||||
|
|
||||||
|
This file exists because most of the interface is translated from short, standalone strings.
|
||||||
|
A translator, human or machine, sees `Board` or `Resume` with no surrounding sentence, picks the
|
||||||
|
most common English sense, and gets it wrong. Every entry below has been mistranslated that way
|
||||||
|
in at least one shipped locale.
|
||||||
|
|
||||||
|
**If you are translating, read the term here before translating it.** When the English word has
|
||||||
|
a common sense that is *not* the one used here, that wrong sense is listed explicitly.
|
||||||
|
|
||||||
|
Terms are grouped by the part of the product they belong to. Source references point at where the
|
||||||
|
string is defined, so you can read the surrounding code when this file is not enough.
|
||||||
|
|
||||||
|
## Always left untranslated
|
||||||
|
|
||||||
|
Product and technology names stay in English (or in the locale's established transliteration, if
|
||||||
|
the catalog already uses one consistently):
|
||||||
|
|
||||||
|
Reactive Resume, GitHub, Crowdin, Docker, PostgreSQL, Better Auth, TanStack, Microsoft Word,
|
||||||
|
PDF, DOCX, JSON, CSV, API, MCP, oRPC, SSO, CSS, URL, JSON Resume.
|
||||||
|
|
||||||
|
Template names are proper nouns and are never translated: Azurill, Bronzor, Chikorita, Ditgar,
|
||||||
|
Ditto, Gengar, Glalie, Kakuna, Lapras, Leafish, Meowth, Onyx, Pikachu, Rhyhorn, Scizor.
|
||||||
|
|
||||||
|
## The document
|
||||||
|
|
||||||
|
**Resume** — the job-application document the app builds. Always a noun.
|
||||||
|
Not the verb "to resume", "to continue", or "to restart". This is the single most common
|
||||||
|
mistranslation in the catalogs: many locales render the standalone `Resume` label as the verb.
|
||||||
|
In `application-form-sheet.tsx` the label marks the resume attached to a job application.
|
||||||
|
Where a locale's normal word for this document is CV, use CV.
|
||||||
|
|
||||||
|
**Resumes** — plural of the above. A list of the user's documents.
|
||||||
|
|
||||||
|
**Cover letter** — the letter accompanying a resume. Stored as a resume section, not a separate
|
||||||
|
document.
|
||||||
|
|
||||||
|
**Builder** — the editor where a resume is composed. A tool, not a construction worker or a
|
||||||
|
person who builds.
|
||||||
|
|
||||||
|
**Template** — a visual design for a resume. Not a "model" in the machine-learning sense, and
|
||||||
|
not a "sample" or "example" document. Beware in languages where the natural word for template
|
||||||
|
is also the word for model: the app uses "model" separately, for AI models.
|
||||||
|
|
||||||
|
**Section** — one block of a resume, such as Experience or Education. Not a legal section or a
|
||||||
|
document chapter.
|
||||||
|
|
||||||
|
**Item** — one entry inside a section, for example a single job or a single degree. Generic on
|
||||||
|
purpose. Not "product", "article", or "column".
|
||||||
|
|
||||||
|
**Page** — one physical page of the rendered resume. Not a web page.
|
||||||
|
|
||||||
|
**Columns** — the column count of a resume layout. Not database or spreadsheet columns.
|
||||||
|
|
||||||
|
**Slug** — the URL-safe identifier in a resume's public address. Usually kept in English or
|
||||||
|
transliterated; never translated as "snail".
|
||||||
|
|
||||||
|
### Resume section names
|
||||||
|
|
||||||
|
These are the built-in section presets, defined in `apps/web/src/libs/resume/section.tsx` and
|
||||||
|
`apps/web/src/dialogs/resume/sections/custom.tsx`. Translate them the way a resume in the target
|
||||||
|
language would label them:
|
||||||
|
|
||||||
|
**Basics** — name, contact details, and headline. Not "fundamentals" or "basic settings".
|
||||||
|
|
||||||
|
**Summary** — the short personal statement at the top of a resume. Not a summary of the app, and
|
||||||
|
not an AI-generated abstract.
|
||||||
|
|
||||||
|
**Profiles** — links to the user's social and professional accounts (LinkedIn, GitHub). Plural.
|
||||||
|
Distinct from **Profile**, below, which is the user's own account page. These two are different
|
||||||
|
things and several catalogs have collapsed them into one word.
|
||||||
|
|
||||||
|
**Volunteer** — volunteering experience. A noun naming a section, not the verb "to volunteer".
|
||||||
|
|
||||||
|
Also: Experience, Education, Skills, Languages, Awards, Certifications, Interests, Projects,
|
||||||
|
Publications, References, Custom.
|
||||||
|
|
||||||
|
## The application tracker
|
||||||
|
|
||||||
|
**Applications** — job applications the user has submitted. Not software applications, apps, or
|
||||||
|
programs. Frequently mistranslated as the software sense.
|
||||||
|
|
||||||
|
**Board** — the kanban board view of applications, arranged in columns by stage. Not a board of
|
||||||
|
directors, a committee, a plank, or a noticeboard.
|
||||||
|
|
||||||
|
**Stage** — where an application sits in the pipeline (applied, interviewing, offer, rejected).
|
||||||
|
Not a theatre stage or a phase of construction.
|
||||||
|
|
||||||
|
**Source** — where the user found the job listing (a job board, a referral, a company site).
|
||||||
|
Singular, and specific to one application. Not a source code file and not a data source.
|
||||||
|
|
||||||
|
**Pipeline** — the sequence of stages an application moves through. A recruiting funnel, not a
|
||||||
|
physical pipe, duct, conduit, or oil pipeline. Seven locales translated it as plumbing.
|
||||||
|
|
||||||
|
**Table** — the table view of applications, one of the view options next to Board and List. Not a
|
||||||
|
piece of furniture.
|
||||||
|
|
||||||
|
**Archive** — a verb in this context: to move an application out of the active list. Not the
|
||||||
|
noun "an archive". It is a menu action and pairs with **Unarchive**; almost every locale had the
|
||||||
|
noun here.
|
||||||
|
|
||||||
|
**Applied on** — the date the user submitted the application. "Applied" is the job-application
|
||||||
|
verb, not "applied a substance onto a surface" and not "applied a patch".
|
||||||
|
|
||||||
|
**Mark rejected / Mark as…** — "Mark" is the verb, to set a status. It is not the given name Mark.
|
||||||
|
|
||||||
|
**Match score** — how well a resume matches a job description. A degree of correspondence, not a
|
||||||
|
sporting fixture.
|
||||||
|
|
||||||
|
**Fit**, as in "Score my fit" or "Strong fit" — how well the user suits the role. Not physical
|
||||||
|
fitness, and not how clothing fits.
|
||||||
|
|
||||||
|
**A stretch** — a role the user is unlikely to get, an ambitious application. Not a stretching
|
||||||
|
exercise.
|
||||||
|
|
||||||
|
**Notes** — the user's free-text notes on an application. Compare **Note** in the ATS checker,
|
||||||
|
which is not the same thing.
|
||||||
|
|
||||||
|
**Timeline** — the dated history of one application.
|
||||||
|
|
||||||
|
## The AI agent
|
||||||
|
|
||||||
|
**Threads** — conversations with the AI agent. The chat sense, as in a message thread. Not
|
||||||
|
sewing thread, not string, not yarn, and not a CPU thread. Several locales use the textile word.
|
||||||
|
|
||||||
|
**Provider** — a third-party AI service the user configures, such as OpenAI or Anthropic. A
|
||||||
|
service supplier. Not a healthcare provider, and not a person who provides for a family.
|
||||||
|
|
||||||
|
**Model** — the specific AI model chosen from a provider, such as Claude Sonnet or GPT. Not a
|
||||||
|
**Template** (several locales used the same word for both), not a device model or product
|
||||||
|
variant, and not a "style" or "pattern".
|
||||||
|
|
||||||
|
**Working resume** — the resume a thread is currently editing. "Working" describes the draft
|
||||||
|
being worked on, not the user's employment. It is not their work history, not a "job resume",
|
||||||
|
and not a *functional résumé*, which is a real and different résumé format.
|
||||||
|
|
||||||
|
**Tailor** — a verb: to adapt a resume to a specific job description. Nothing to do with
|
||||||
|
dressmaking or sewing.
|
||||||
|
|
||||||
|
**Sources** — the citations the agent attaches to an answer. Plural, and distinct from **Source**
|
||||||
|
in the application tracker above.
|
||||||
|
|
||||||
|
**Draft** — a working copy of a resume the agent edits. A noun.
|
||||||
|
|
||||||
|
**Patch** — a set of JSON Patch operations the agent proposes. Kept in English in most catalogs.
|
||||||
|
Not a cloth patch, a scrap of fabric, an adhesive bandage, or a connector.
|
||||||
|
|
||||||
|
## The ATS checker
|
||||||
|
|
||||||
|
**ATS** — applicant tracking system: recruiting software that parses resumes. Spell it out on
|
||||||
|
first use in languages where the acronym is unfamiliar. It is not a drug test, a transmission,
|
||||||
|
or any other expansion of the letters; at least one catalog translated `ATS Check` as a test for
|
||||||
|
amphetamines.
|
||||||
|
|
||||||
|
**Readability, Layout, Sections, Contact details, Dates, Writing** — the six check categories, in
|
||||||
|
`apps/web/src/features/ats-checker/messages.ts`. "Layout" here means page geometry and reading
|
||||||
|
order, not the builder's layout settings.
|
||||||
|
|
||||||
|
**Blocker, Warning, Tip** — the three severity levels of a finding.
|
||||||
|
|
||||||
|
**Note** — the label for an informational finding, in
|
||||||
|
`apps/web/src/routes/builder/$resumeId/-sidebar/right/sections/ats-check.tsx`. A severity label,
|
||||||
|
not a written note. Unrelated to **Notes** in the application tracker.
|
||||||
|
|
||||||
|
**Parse / parsing** — software reading text out of the PDF.
|
||||||
|
|
||||||
|
## Account and security
|
||||||
|
|
||||||
|
**Passkey / Passkeys** — a WebAuthn credential that replaces a password, stored on the user's
|
||||||
|
device or security key. **It is not a password.** Many catalogs translate it with their word for
|
||||||
|
"password", which is actively confusing: both appear together on the security settings page, so
|
||||||
|
the user cannot tell which credential a message refers to. If the target language has no
|
||||||
|
established term, keep "passkey" in English rather than reusing the word for password.
|
||||||
|
|
||||||
|
**Password** — the ordinary secret. Distinct from the above, always.
|
||||||
|
|
||||||
|
**Two-factor authentication (2FA)** — a second verification step at sign-in.
|
||||||
|
|
||||||
|
**Backup codes** — single-use codes for signing in when the second factor is unavailable.
|
||||||
|
|
||||||
|
**API key** — a token for programmatic access. **Key** on its own, in `ai-section.tsx`, means the
|
||||||
|
AI provider's API key. Not a physical door key, not a keyboard key, and not the adjective "key"
|
||||||
|
in the sense of crucial or main.
|
||||||
|
|
||||||
|
**Session** — an active sign-in on one device.
|
||||||
|
|
||||||
|
**Sign in / Sign out** — the app's chosen verbs. Prefer the locale's equivalent of "sign in"
|
||||||
|
over "log in" where both exist, and keep whichever the catalog already uses consistently.
|
||||||
|
|
||||||
|
## Navigation and app shell
|
||||||
|
|
||||||
|
**Dashboard** — the main page after signing in, listing resumes and applications. Not a vehicle
|
||||||
|
dashboard, an instrument panel, or a control panel in the machinery sense.
|
||||||
|
|
||||||
|
**Profile** — the user's own account settings page. Distinct from **Profiles**, the resume
|
||||||
|
section, above.
|
||||||
|
|
||||||
|
**Lock / Unlock** — verbs: to make a resume read-only, and to release it.
|
||||||
|
|
||||||
|
**Tags** — user-defined labels for organizing resumes and applications.
|
||||||
|
|
||||||
|
**Custom** — in `color-picker.tsx`, a user-chosen color as opposed to a preset. An adjective.
|
||||||
|
|
||||||
|
**Public URL** — the shareable address of a published resume. Use one term consistently; the
|
||||||
|
English strings say "public URL" rather than "public link".
|
||||||
|
|
||||||
|
## Verbs that read as adjectives or nouns
|
||||||
|
|
||||||
|
Button labels and `aria-label` strings are usually **imperative verbs**: they say what the
|
||||||
|
control does. Read as a noun or an adjective, they turn into nonsense. This is the most common
|
||||||
|
error in the catalogs after the ambiguous nouns above.
|
||||||
|
|
||||||
|
**Open** — the verb. `Open AI agent` means *open the AI agent panel*; it does not describe an
|
||||||
|
agent that is "open", and it is **not a reference to OpenAI, the company**. Around forty-five of
|
||||||
|
the fifty-three catalogs got this wrong, split between "an open AI agent" and a transliteration
|
||||||
|
of *OpenAI*. The same applies to `Open in builder`.
|
||||||
|
|
||||||
|
**Close** — likewise the verb, as in `Close AI assistant`. Not "an assistant for closing things",
|
||||||
|
and not the adjective "close/nearby".
|
||||||
|
|
||||||
|
The app names two different surfaces here, and both strings are real: **AI agent** is the
|
||||||
|
full workspace at `/agent`, opened from the builder dock (`Open AI agent`), while **AI assistant**
|
||||||
|
is the panel that slides out inside the builder (`Open AI assistant`, `Close AI assistant`).
|
||||||
|
Translate them as two distinct names, the way the English does.
|
||||||
|
|
||||||
|
**Clear** — the verb, to empty a field or remove filters. Not the adjective "transparent",
|
||||||
|
"obvious", or "clear-cut".
|
||||||
|
|
||||||
|
**Lock / Unlock** — verbs. `Unlock` is specifically the opposite of `Lock`, not a synonym for
|
||||||
|
`Open`; several catalogs collapsed the two and produced two identical menu items.
|
||||||
|
|
||||||
|
**Archive / Unarchive**, **Mark**, **Tailor**, **Duplicate**, **Import**, **Export**, **Share**,
|
||||||
|
**Star** — all verbs when they appear as a control label. Check the `#:` source reference if you
|
||||||
|
are unsure whether a given string is a button or a heading.
|
||||||
|
|
||||||
|
## Message syntax
|
||||||
|
|
||||||
|
These are not words to translate, and breaking them breaks the interface:
|
||||||
|
|
||||||
|
- `{name}`, `{count}`, `{email}`, `{MAX_IMPORT}`, `{overflow}` — value placeholders. Keep the
|
||||||
|
spelling exactly, keep every one that appears in the source, and add none.
|
||||||
|
- `{count, plural, one {# item} other {# items}}` — ICU plurals. Translate only the text inside
|
||||||
|
the inner braces, keep the `#`, and use the plural categories your language actually needs
|
||||||
|
(Arabic and the Slavic languages legitimately have more than English).
|
||||||
|
- `<0>…</0>`, `<1>…</1>`, `<0/>` — indexes pointing at interface elements such as links and bold
|
||||||
|
spans. Keep every index and keep the pairs matched. You may move a tag inside the sentence for
|
||||||
|
word order, as long as it still wraps the corresponding words.
|
||||||
|
|
||||||
|
A missing or renamed placeholder is a runtime error, not a style problem.
|
||||||
|
|
||||||
|
## Adding to this file
|
||||||
|
|
||||||
|
When a translator asks what a term means, the answer belongs here. When you add a term, say what
|
||||||
|
it means in this app and, if the English word is ambiguous, say plainly which sense is wrong.
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
<h1>Reactive Resume</h1>
|
<h1>Reactive Resume</h1>
|
||||||
|
|
||||||
<p>Reactive Resume is a free and open-source resume builder that simplifies the process of creating, updating, and sharing your resume.</p>
|
<p>Reactive Resume is a free and open-source resume builder that makes it easy to create, update, and share your resume.</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<a href="https://rxresu.me"><strong>Get Started</strong></a>
|
<a href="https://rxresu.me"><strong>Get Started</strong></a>
|
||||||
@@ -27,13 +27,13 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
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.
|
Pick a template, fill in your details, and export to PDF. Basic use needs no account. If you want more control, you can run the whole application on your own infrastructure.
|
||||||
|
|
||||||
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.
|
You own your data. The codebase is open source under the MIT license, with no tracking, no ads, and no hidden costs.
|
||||||
|
|
||||||
## Sponsors
|
## Sponsors
|
||||||
|
|
||||||
Reactive Resume stays free, open-source, and independent because companies choose to support the work behind it. Thank you to every sponsor who helps fund hosting, maintenance, and continued development for the community.
|
Sponsors pay for hosting, maintenance, and ongoing development, which is what keeps Reactive Resume free and independent. Thank you to everyone who chips in.
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<a href="https://www.atlascloud.ai/?utm_source=github&utm_medium=link&utm_campaign=reactive-resume">
|
<a href="https://www.atlascloud.ai/?utm_source=github&utm_medium=link&utm_campaign=reactive-resume">
|
||||||
@@ -49,16 +49,16 @@ If your company would like to sponsor Reactive Resume, email [hello@amruthpillai
|
|||||||
|
|
||||||
**Resume Building**
|
**Resume Building**
|
||||||
|
|
||||||
- Real-time preview as you type
|
- Live preview as you type
|
||||||
- Multiple export formats (PDF, JSON, DOCX)
|
- Multiple export formats (PDF, JSON, DOCX)
|
||||||
- Drag-and-drop section ordering
|
- Drag-and-drop section ordering
|
||||||
- Custom sections for any content type
|
- Custom sections for any content type
|
||||||
- Rich text editor with formatting support
|
- Rich text editor
|
||||||
|
|
||||||
**Templates**
|
**Templates**
|
||||||
|
|
||||||
- Professionally designed templates
|
- 15 templates to choose from
|
||||||
- A4 and Letter size support
|
- A4 and Letter page sizes
|
||||||
- Customizable colors, fonts, and spacing
|
- Customizable colors, fonts, and spacing
|
||||||
- Structured Style Rules for section and text styling
|
- Structured Style Rules for section and text styling
|
||||||
|
|
||||||
@@ -75,7 +75,7 @@ If your company would like to sponsor Reactive Resume, email [hello@amruthpillai
|
|||||||
- Multi-language support
|
- Multi-language support
|
||||||
- Share resumes via unique links
|
- Share resumes via unique links
|
||||||
- Import from JSON Resume format
|
- Import from JSON Resume format
|
||||||
- Dark mode support
|
- Dark mode
|
||||||
- Passkey and two-factor authentication
|
- Passkey and two-factor authentication
|
||||||
|
|
||||||
## Templates
|
## Templates
|
||||||
@@ -187,14 +187,14 @@ For detailed setup instructions, environment configuration, and self-hosting gui
|
|||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
Comprehensive guides are available at [docs.rxresu.me](https://docs.rxresu.me):
|
The full documentation lives at [docs.rxresu.me](https://docs.rxresu.me):
|
||||||
|
|
||||||
| Guide | Description |
|
| Guide | Description |
|
||||||
| ---------------------------------------------------------------------------- | -------------------------------- |
|
| ---------------------------------------------------------------------------- | -------------------------------- |
|
||||||
| [Getting Started](https://docs.rxresu.me/getting-started) | First-time setup and basic usage |
|
| [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 |
|
| [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 |
|
| [Development setup](https://docs.rxresu.me/contributing/development) | Local development environment |
|
||||||
| [Project Architecture](https://docs.rxresu.me/contributing/architecture) | Codebase structure and patterns |
|
| [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 |
|
| [Exporting Your Resume](https://docs.rxresu.me/guides/exporting-your-resume) | PDF and JSON export options |
|
||||||
|
|
||||||
## Self-Hosting
|
## Self-Hosting
|
||||||
@@ -204,7 +204,7 @@ Reactive Resume can be self-hosted using Docker. The stack includes:
|
|||||||
- **PostgreSQL** — Database for storing user data and resumes
|
- **PostgreSQL** — Database for storing user data and resumes
|
||||||
- **SeaweedFS** (optional) — S3-compatible storage for file uploads
|
- **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`.
|
> **From v5.1.0 onwards** — PDF generation runs entirely client-side via `@react-pdf/renderer`. New deployments no longer need Browserless, Chromium, or any external print service. 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:
|
Pull the latest image from Docker Hub or GitHub Container Registry:
|
||||||
|
|
||||||
@@ -220,7 +220,7 @@ See the [self-hosting guide](https://docs.rxresu.me/self-hosting/docker) for com
|
|||||||
|
|
||||||
## Support
|
## 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:
|
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>
|
<p>
|
||||||
<a href="https://github.com/sponsors/AmruthPillai">
|
<a href="https://github.com/sponsors/AmruthPillai">
|
||||||
@@ -251,7 +251,7 @@ Other ways to support:
|
|||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Contributions make open-source thrive. Whether fixing a typo or adding a feature, all contributions are welcome.
|
Every contribution helps, whether it is a typo fix or a new feature.
|
||||||
|
|
||||||
1. Fork the repository
|
1. Fork the repository
|
||||||
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
||||||
@@ -259,7 +259,7 @@ Contributions make open-source thrive. Whether fixing a typo or adding a feature
|
|||||||
4. Push to the branch (`git push origin feature/amazing-feature`)
|
4. Push to the branch (`git push origin feature/amazing-feature`)
|
||||||
5. Open a Pull Request
|
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.
|
See the [development setup guide](https://docs.rxresu.me/contributing/development) for how to run the project locally.
|
||||||
|
|
||||||
Maintainers review the [`status: needs triage` queue](https://github.com/amruthpillai/reactive-resume/issues?q=is%3Aissue+is%3Aopen+label%3A%22status%3A+needs+triage%22)
|
Maintainers review the [`status: needs triage` queue](https://github.com/amruthpillai/reactive-resume/issues?q=is%3Aissue+is%3Aopen+label%3A%22status%3A+needs+triage%22)
|
||||||
weekly. Triaged bugs become `status: confirmed`; feature proposals become `status: accepted`; reports that need details become
|
weekly. Triaged bugs become `status: confirmed`; feature proposals become `status: accepted`; reports that need details become
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ describe("web app fallback classification", () => {
|
|||||||
<title>Reactive Resume — A free and open-source resume builder</title>
|
<title>Reactive Resume — A free and open-source resume builder</title>
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
name="description"
|
||||||
content="Reactive Resume is a free and open-source resume builder that simplifies the process of creating, updating, and sharing your resume."
|
content="Reactive Resume is a free and open-source resume builder that makes it easy to create, update, and share your resume."
|
||||||
>
|
>
|
||||||
</head>
|
</head>
|
||||||
<body><div id="app"></div></body>
|
<body><div id="app"></div></body>
|
||||||
|
|||||||
@@ -70,38 +70,36 @@ const BASE_SECURITY_HEADERS = {
|
|||||||
const ROOT_TITLE = "Reactive Resume — A free and open-source resume builder";
|
const ROOT_TITLE = "Reactive Resume — A free and open-source resume builder";
|
||||||
// Keep under ~120 characters so Google's mobile SERP snippet is not truncated at 3 lines.
|
// Keep under ~120 characters so Google's mobile SERP snippet is not truncated at 3 lines.
|
||||||
const ROOT_DESCRIPTION =
|
const ROOT_DESCRIPTION =
|
||||||
"Free, open-source resume builder. Create, update, and share a professional resume in minutes — no ads, no paywall.";
|
"Free, open-source resume builder. Create, update, and share your resume, with no ads and no paywall.";
|
||||||
const ROOT_POSTER_PATH = "/videos/timelapse-v1.webp";
|
const ROOT_POSTER_PATH = "/videos/timelapse-v1.webp";
|
||||||
const ROOT_FAQ_ITEMS = [
|
const ROOT_FAQ_ITEMS = [
|
||||||
{
|
{
|
||||||
question: "Is Reactive Resume really free?",
|
question: "Is Reactive Resume really free?",
|
||||||
answer:
|
answer:
|
||||||
"Yes! Reactive Resume is completely free to use, with no hidden costs, premium tiers, or subscription fees. It's open-source and will always remain free.",
|
"Yes. Reactive Resume is free to use, with no hidden costs, premium tiers, or subscription fees. It's open source, and it will stay free.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
question: "How is my data protected?",
|
question: "How is my data protected?",
|
||||||
answer:
|
answer:
|
||||||
"Your data is stored securely and is never shared with third parties. You can also self-host Reactive Resume on your own servers for complete control over your data.",
|
"Your data is stored securely and never shared with third parties. If you want full control over it, you can self-host Reactive Resume on your own servers.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
question: "Can I export my resume to PDF?",
|
question: "Can I export my resume to PDF?",
|
||||||
answer:
|
answer: "Yes. One click exports your resume to PDF, with your formatting and styling intact.",
|
||||||
"Absolutely! You can export your resume to PDF with a single click. The exported PDF maintains all your formatting and styling perfectly.",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
question: "Is Reactive Resume available in multiple languages?",
|
question: "Is Reactive Resume available in multiple languages?",
|
||||||
answer:
|
answer:
|
||||||
"Yes, Reactive Resume is available in multiple languages. You can choose your preferred language in the settings page, or using the language switcher in the top right corner. If you don't see your language, or you would like to improve the existing translations, you can contribute to the translations on Crowdin.",
|
"Yes. Pick your language on the settings page, or with the language switcher in the top right corner. If your language is missing, or the existing translation could be better, you can contribute to the translations on Crowdin.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
question: "What makes Reactive Resume different from other resume builders?",
|
question: "What makes Reactive Resume different from other resume builders?",
|
||||||
answer:
|
answer:
|
||||||
"Reactive Resume is open-source, privacy-focused, and completely free. Unlike other resume builders, it doesn't show ads, track your data, or limit your features behind a paywall.",
|
"Reactive Resume is open source, private, and free. It shows no ads, doesn't track what you do, and doesn't lock features behind a paywall.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
question: "How do I share my resume?",
|
question: "How do I share my resume?",
|
||||||
answer:
|
answer: "Share it with a public URL, put a password on that URL, or download the PDF and send it yourself.",
|
||||||
"You can share your resume via a unique public URL, protect it with a password, or download it as a PDF to share directly. The choice is yours!",
|
|
||||||
},
|
},
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
@@ -169,9 +167,9 @@ function createRootSeoMarkup(canonicalUrl: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ATS_CHECKER_TITLE = "ATS Checker - Reactive Resume";
|
const ATS_CHECKER_TITLE = "ATS Checker - Reactive Resume";
|
||||||
// Kept under ~120 characters so Google's mobile SERP snippet is not truncated at 3 lines.
|
// Keep under ~120 characters so Google's mobile SERP snippet is not truncated at 3 lines.
|
||||||
const ATS_CHECKER_DESCRIPTION =
|
const ATS_CHECKER_DESCRIPTION =
|
||||||
"Check whether software can read your resume PDF. Runs in your browser, so your file is never uploaded.";
|
"Check whether software can read your resume PDF. Runs entirely in your browser, so your file is never uploaded.";
|
||||||
|
|
||||||
function createAtsCheckerSeoMarkup(origin: string) {
|
function createAtsCheckerSeoMarkup(origin: string) {
|
||||||
const canonicalUrl = `${origin}/ats-checker`;
|
const canonicalUrl = `${origin}/ats-checker`;
|
||||||
|
|||||||
+443
-450
File diff suppressed because it is too large
Load Diff
+394
-401
File diff suppressed because it is too large
Load Diff
+372
-379
File diff suppressed because it is too large
Load Diff
+373
-380
File diff suppressed because it is too large
Load Diff
+375
-382
File diff suppressed because it is too large
Load Diff
+361
-368
File diff suppressed because it is too large
Load Diff
+371
-378
File diff suppressed because it is too large
Load Diff
+367
-374
File diff suppressed because it is too large
Load Diff
+362
-369
File diff suppressed because it is too large
Load Diff
+352
-359
File diff suppressed because it is too large
Load Diff
+346
-353
File diff suppressed because it is too large
Load Diff
+330
-337
File diff suppressed because it is too large
Load Diff
+330
-336
File diff suppressed because it is too large
Load Diff
+344
-351
File diff suppressed because it is too large
Load Diff
+339
-346
File diff suppressed because it is too large
Load Diff
+357
-364
File diff suppressed because it is too large
Load Diff
+347
-354
File diff suppressed because it is too large
Load Diff
+342
-349
File diff suppressed because it is too large
Load Diff
+344
-351
File diff suppressed because it is too large
Load Diff
+352
-359
File diff suppressed because it is too large
Load Diff
+350
-357
File diff suppressed because it is too large
Load Diff
+349
-356
File diff suppressed because it is too large
Load Diff
+379
-386
File diff suppressed because it is too large
Load Diff
+343
-350
File diff suppressed because it is too large
Load Diff
+343
-350
File diff suppressed because it is too large
Load Diff
+348
-355
File diff suppressed because it is too large
Load Diff
+341
-348
File diff suppressed because it is too large
Load Diff
+341
-348
File diff suppressed because it is too large
Load Diff
+339
-346
File diff suppressed because it is too large
Load Diff
+370
-377
File diff suppressed because it is too large
Load Diff
+372
-379
File diff suppressed because it is too large
Load Diff
+362
-369
File diff suppressed because it is too large
Load Diff
+372
-379
File diff suppressed because it is too large
Load Diff
+343
-350
File diff suppressed because it is too large
Load Diff
+358
-365
File diff suppressed because it is too large
Load Diff
+350
-357
File diff suppressed because it is too large
Load Diff
+377
-384
File diff suppressed because it is too large
Load Diff
+374
-381
File diff suppressed because it is too large
Load Diff
+376
-383
File diff suppressed because it is too large
Load Diff
+371
-378
File diff suppressed because it is too large
Load Diff
+365
-372
File diff suppressed because it is too large
Load Diff
+378
-385
File diff suppressed because it is too large
Load Diff
+377
-384
File diff suppressed because it is too large
Load Diff
+396
-403
File diff suppressed because it is too large
Load Diff
+361
-368
File diff suppressed because it is too large
Load Diff
+400
-407
File diff suppressed because it is too large
Load Diff
+343
-350
File diff suppressed because it is too large
Load Diff
+357
-364
File diff suppressed because it is too large
Load Diff
+349
-356
File diff suppressed because it is too large
Load Diff
+352
-359
File diff suppressed because it is too large
Load Diff
+346
-353
File diff suppressed because it is too large
Load Diff
+357
-364
File diff suppressed because it is too large
Load Diff
+418
-425
File diff suppressed because it is too large
Load Diff
+415
-422
File diff suppressed because it is too large
Load Diff
+252
-258
File diff suppressed because it is too large
Load Diff
@@ -15,7 +15,7 @@
|
|||||||
{
|
{
|
||||||
"guid": "reactive-resume",
|
"guid": "reactive-resume",
|
||||||
"name": "Reactive Resume",
|
"name": "Reactive Resume",
|
||||||
"description": "A free and open-source resume builder that simplifies the process of creating, updating, and sharing your resume.",
|
"description": "A free and open-source resume builder that makes it easy to create, update, and share your resume.",
|
||||||
"webpageUrl": {
|
"webpageUrl": {
|
||||||
"url": "https://rxresu.me"
|
"url": "https://rxresu.me"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -309,7 +309,7 @@ function useEditorToolbarState(editor: Editor) {
|
|||||||
// Link
|
// Link
|
||||||
isLink: ctx.editor.isActive("link") ?? false,
|
isLink: ctx.editor.isActive("link") ?? false,
|
||||||
setLink: async () => {
|
setLink: async () => {
|
||||||
const url = await prompt(t`Please enter the URL you want to link to:`, {
|
const url = await prompt(t`Enter the URL you want to link to:`, {
|
||||||
defaultValue: "https://",
|
defaultValue: "https://",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -96,8 +96,8 @@ const CreateApiKeyForm = ({ setApiKey }: CreateApiKeyFormProps) => {
|
|||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
<Trans>
|
<Trans>
|
||||||
This will generate a new API key to access the Reactive Resume API to allow machines to interact with your
|
This creates a new API key, which lets other programs read and change your resume data through the Reactive
|
||||||
resume data.
|
Resume API.
|
||||||
</Trans>
|
</Trans>
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
@@ -130,9 +130,7 @@ const CreateApiKeyForm = ({ setApiKey }: CreateApiKeyFormProps) => {
|
|||||||
/>
|
/>
|
||||||
<FormMessage errors={field.state.meta.errors} />
|
<FormMessage errors={field.state.meta.errors} />
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
<Trans>
|
<Trans>Names help you tell keys apart, so name it after what you will use it for.</Trans>
|
||||||
Tip: Give your API key a name, corresponding to the purpose of the key, to help you identify it later.
|
|
||||||
</Trans>
|
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
@@ -231,7 +229,7 @@ const CopyApiKeyForm = ({ apiKey }: CopyApiKeyFormProps) => {
|
|||||||
</InputGroup>
|
</InputGroup>
|
||||||
|
|
||||||
<span className="font-medium text-muted-foreground text-sm">
|
<span className="font-medium text-muted-foreground text-sm">
|
||||||
<Trans>For security reasons, this key will only be displayed once.</Trans>
|
<Trans>This key is shown only once. Copy it before you close this dialog.</Trans>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ export function ChangePasswordDialog(_: DialogProps<"auth.change-password">) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
toast.add({ type: "success", description: t`Your password has been updated successfully.`, id: toastId });
|
toast.add({ type: "success", description: t`Your password has been updated.`, id: toastId });
|
||||||
void queryClient.invalidateQueries({ queryKey: ["auth", "accounts"] });
|
void queryClient.invalidateQueries({ queryKey: ["auth", "accounts"] });
|
||||||
closeDialog();
|
closeDialog();
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ export function DisableTwoFactorDialog(_: DialogProps<"auth.two-factor.disable">
|
|||||||
|
|
||||||
toast.add({
|
toast.add({
|
||||||
type: "success",
|
type: "success",
|
||||||
description: t`Two-factor authentication has been disabled successfully.`,
|
description: t`Two-factor authentication is now disabled.`,
|
||||||
id: toastId,
|
id: toastId,
|
||||||
});
|
});
|
||||||
void router.invalidate();
|
void router.invalidate();
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ export function EnableTwoFactorDialog(_: DialogProps<"auth.two-factor.enable">)
|
|||||||
setStep("verify");
|
setStep("verify");
|
||||||
toast.close(toastId);
|
toast.close(toastId);
|
||||||
} else {
|
} else {
|
||||||
toast.add({ type: "error", description: t`Failed to setup two-factor authentication.`, id: toastId });
|
toast.add({ type: "error", description: t`Could not set up two-factor authentication.`, id: toastId });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -133,7 +133,7 @@ export function EnableTwoFactorDialog(_: DialogProps<"auth.two-factor.enable">)
|
|||||||
});
|
});
|
||||||
|
|
||||||
const onConfirmBackup = () => {
|
const onConfirmBackup = () => {
|
||||||
toast.add({ type: "success", description: t`Two-factor authentication has been setup successfully.` });
|
toast.add({ type: "success", description: t`Two-factor authentication is now enabled.` });
|
||||||
void router.invalidate();
|
void router.invalidate();
|
||||||
closeDialog();
|
closeDialog();
|
||||||
onReset();
|
onReset();
|
||||||
@@ -373,8 +373,8 @@ function TwoFactorDialogDescription({ step }: TwoFactorStepProps) {
|
|||||||
return match(step)
|
return match(step)
|
||||||
.with("enable", () => (
|
.with("enable", () => (
|
||||||
<Trans>
|
<Trans>
|
||||||
Enter your password to confirm setting up two-factor authentication. When enabled, you'll need to enter a code
|
Enter your password to confirm setting up two-factor authentication. Once it is on, you need a code from your
|
||||||
from your authenticator app every time you log in.
|
authenticator app every time you sign in.
|
||||||
</Trans>
|
</Trans>
|
||||||
))
|
))
|
||||||
.with("verify", () => (
|
.with("verify", () => (
|
||||||
|
|||||||
@@ -201,7 +201,7 @@ export function ImportResumeDialog(_: DialogProps<"resume.import">) {
|
|||||||
toast.add({
|
toast.add({
|
||||||
type: "success",
|
type: "success",
|
||||||
title: null,
|
title: null,
|
||||||
description: t`Your resume has been imported successfully.`,
|
description: t`Your resume has been imported.`,
|
||||||
id: toastId,
|
id: toastId,
|
||||||
});
|
});
|
||||||
closeDialog();
|
closeDialog();
|
||||||
@@ -288,8 +288,8 @@ export function ImportResumeDialog(_: DialogProps<"resume.import">) {
|
|||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
<Trans>
|
<Trans>
|
||||||
Continue where you left off by importing an existing resume you created using Reactive Resume or any another
|
Continue where you left off by importing a resume you built in Reactive Resume or another resume builder.
|
||||||
resume builder. Supported formats include PDF, Microsoft Word, as well as JSON files from Reactive Resume.
|
Supported formats are PDF, Microsoft Word, and JSON files from Reactive Resume or JSON Resume.
|
||||||
</Trans>
|
</Trans>
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
@@ -401,7 +401,7 @@ export function ImportResumeDialog(_: DialogProps<"resume.import">) {
|
|||||||
/>
|
/>
|
||||||
{!field.state.value && (
|
{!field.state.value && (
|
||||||
<p className="text-muted-foreground text-xs">
|
<p className="text-muted-foreground text-xs">
|
||||||
<Trans>We couldn't detect the format automatically — please choose it above.</Trans>
|
<Trans>We couldn't detect the format automatically. Choose it above.</Trans>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<FormMessage errors={field.state.meta.errors} />
|
<FormMessage errors={field.state.meta.errors} />
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ export function CreateResumeDialog(_: DialogProps<"resume.create">) {
|
|||||||
createResume(value, {
|
createResume(value, {
|
||||||
onSuccess: (id) => {
|
onSuccess: (id) => {
|
||||||
didCreateRef.current = true;
|
didCreateRef.current = true;
|
||||||
toast.add({ type: "success", description: t`Your resume has been created successfully.`, id: toastId });
|
toast.add({ type: "success", description: t`Your resume has been created.`, id: toastId });
|
||||||
closeDialog();
|
closeDialog();
|
||||||
void navigate({ to: "/builder/$resumeId", params: { resumeId: id } });
|
void navigate({ to: "/builder/$resumeId", params: { resumeId: id } });
|
||||||
},
|
},
|
||||||
@@ -117,7 +117,7 @@ export function CreateResumeDialog(_: DialogProps<"resume.create">) {
|
|||||||
createResume(data, {
|
createResume(data, {
|
||||||
onSuccess: (id) => {
|
onSuccess: (id) => {
|
||||||
didCreateRef.current = true;
|
didCreateRef.current = true;
|
||||||
toast.add({ type: "success", description: t`Your resume has been created successfully.`, id: toastId });
|
toast.add({ type: "success", description: t`Your resume has been created.`, id: toastId });
|
||||||
closeDialog();
|
closeDialog();
|
||||||
void navigate({ to: "/builder/$resumeId", params: { resumeId: id } });
|
void navigate({ to: "/builder/$resumeId", params: { resumeId: id } });
|
||||||
},
|
},
|
||||||
@@ -215,7 +215,7 @@ export function UpdateResumeDialog({ data }: DialogProps<"resume.update">) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
toast.add({ type: "success", description: t`Your resume has been updated successfully.`, id: toastId });
|
toast.add({ type: "success", description: t`Your resume has been updated.`, id: toastId });
|
||||||
closeDialog();
|
closeDialog();
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
@@ -285,7 +285,7 @@ export function DuplicateResumeDialog({ data }: DialogProps<"resume.duplicate">)
|
|||||||
|
|
||||||
duplicateResume(value, {
|
duplicateResume(value, {
|
||||||
onSuccess: (id) => {
|
onSuccess: (id) => {
|
||||||
toast.add({ type: "success", description: t`Your resume has been duplicated successfully.`, id: toastId });
|
toast.add({ type: "success", description: t`Your resume has been duplicated.`, id: toastId });
|
||||||
closeDialog();
|
closeDialog();
|
||||||
|
|
||||||
if (!data.shouldRedirect) return;
|
if (!data.shouldRedirect) return;
|
||||||
@@ -378,7 +378,7 @@ const ResumeForm = withForm({
|
|||||||
</div>
|
</div>
|
||||||
<FormMessage errors={field.state.meta.errors} />
|
<FormMessage errors={field.state.meta.errors} />
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
<Trans>Tip: You can name the resume referring to the position you are applying for.</Trans>
|
<Trans>Name the resume after the position you are applying for.</Trans>
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ describe("TemplateGalleryDialog", () => {
|
|||||||
it("renders the documented title and intro copy", () => {
|
it("renders the documented title and intro copy", () => {
|
||||||
renderGallery();
|
renderGallery();
|
||||||
expect(screen.getByText("Template Gallery")).toBeInTheDocument();
|
expect(screen.getByText("Template Gallery")).toBeInTheDocument();
|
||||||
expect(screen.getByText(/range of resume templates/)).toBeInTheDocument();
|
expect(screen.getByText(/Resume templates for different professions/)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders one tile per template", () => {
|
it("renders one tile per template", () => {
|
||||||
|
|||||||
@@ -54,11 +54,7 @@ export function TemplateGalleryDialog(_: DialogProps<"resume.template.gallery">)
|
|||||||
<Trans>Template Gallery</Trans>
|
<Trans>Template Gallery</Trans>
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription className="leading-relaxed">
|
<DialogDescription className="leading-relaxed">
|
||||||
<Trans>
|
<Trans>Resume templates for different professions and tastes. Pick the one that suits you.</Trans>
|
||||||
Here's a range of resume templates for different professions and personalities. Whether you prefer modern or
|
|
||||||
classic, bold or simple, there is a design to match you. Look through the options below and choose a
|
|
||||||
template that fits your style.
|
|
||||||
</Trans>
|
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
|
|||||||
@@ -349,7 +349,7 @@ export function ApplicationFormSheet({ open, onOpenChange, application }: Props)
|
|||||||
onChange={(value) => set("resumeFile", value)}
|
onChange={(value) => set("resumeFile", value)}
|
||||||
/>
|
/>
|
||||||
<p className="text-[11px] text-muted-foreground">
|
<p className="text-[11px] text-muted-foreground">
|
||||||
<Trans>Linking a Reactive Resume enables AI match scoring and tailoring.</Trans>
|
<Trans>Link a Reactive Resume to use AI match scoring and tailoring.</Trans>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Field>
|
</Field>
|
||||||
|
|||||||
@@ -125,8 +125,8 @@ export function ImportApplicationsSheet({ open, onOpenChange }: Props) {
|
|||||||
{overflow > 0 && (
|
{overflow > 0 && (
|
||||||
<p className="mt-1.5 text-amber-600 text-xs dark:text-amber-500">
|
<p className="mt-1.5 text-amber-600 text-xs dark:text-amber-500">
|
||||||
<Trans>
|
<Trans>
|
||||||
Only the first {MAX_IMPORT} rows import at once — {overflow} left out. Split the file to import the
|
Only the first {MAX_IMPORT} rows import at once, leaving out {overflow}. Split the file to import
|
||||||
rest.
|
the rest.
|
||||||
</Trans>
|
</Trans>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export function ApplicationInsights({ applications }: { applications: Applicatio
|
|||||||
if (insights.total === 0) {
|
if (insights.total === 0) {
|
||||||
return (
|
return (
|
||||||
<p className="py-16 text-center text-muted-foreground text-sm">
|
<p className="py-16 text-center text-muted-foreground text-sm">
|
||||||
<Trans>No applications yet — add a few to see your funnel and reply rates.</Trans>
|
<Trans>No applications yet. Add a few to see your funnel and reply rates.</Trans>
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ export function AiReviewResults({ review }: AiReviewResultsProps) {
|
|||||||
<p className="text-muted-foreground text-xs leading-normal">
|
<p className="text-muted-foreground text-xs leading-normal">
|
||||||
<Trans>
|
<Trans>
|
||||||
This review is a language model's opinion of your writing. It does not change the score above, and it can be
|
This review is a language model's opinion of your writing. It does not change the score above, and it can be
|
||||||
wrong. Read it as a second opinion, not a verdict.
|
wrong. Treat it as a second opinion, not a verdict.
|
||||||
</Trans>
|
</Trans>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -257,7 +257,7 @@ export function getPdfFindingMessage(code: PdfRuleCode): PdfFindingMessage {
|
|||||||
}))
|
}))
|
||||||
.with("MULTIPLE_EMAILS", () => ({
|
.with("MULTIPLE_EMAILS", () => ({
|
||||||
title: t`More than one email address appears.`,
|
title: t`More than one email address appears.`,
|
||||||
action: t`Keep a single address, so nothing has to guess which one to use.`,
|
action: t`Keep a single address, so software does not have to guess which one to use.`,
|
||||||
}))
|
}))
|
||||||
.with("NO_PROFESSIONAL_LINK", () => ({
|
.with("NO_PROFESSIONAL_LINK", () => ({
|
||||||
title: t`There is no link to a profile, portfolio or repository.`,
|
title: t`There is no link to a profile, portfolio or repository.`,
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ export function ParsePreview() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<figcaption className="border-t px-3 py-2 text-muted-foreground text-xs leading-normal">
|
<figcaption className="border-t px-3 py-2 text-muted-foreground text-xs leading-normal">
|
||||||
<Trans>A clean single-column export keeps every fact. Columns and images are where they get lost.</Trans>
|
<Trans>A single-column export keeps every fact. Columns and images are where facts get lost.</Trans>
|
||||||
</figcaption>
|
</figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export function JdCoverage({ jd }: JdCoverageProps) {
|
|||||||
</Trans>
|
</Trans>
|
||||||
</p>
|
</p>
|
||||||
<p className="text-muted-foreground text-xs leading-normal">
|
<p className="text-muted-foreground text-xs leading-normal">
|
||||||
<Trans>Counted separately from the parse score. Coverage is not a prediction of anything.</Trans>
|
<Trans>Counted separately from the parse score. Coverage does not predict anything.</Trans>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ export function AtsPdfReportView({ report, className }: ReportViewProps) {
|
|||||||
<p className="text-muted-foreground text-xs leading-normal">
|
<p className="text-muted-foreground text-xs leading-normal">
|
||||||
<Trans>
|
<Trans>
|
||||||
This measures how faithfully software can extract this file's text. It does not predict whether an application
|
This measures how faithfully software can extract this file's text. It does not predict whether an application
|
||||||
will be rejected. No tool can. Your file was read in this browser and never uploaded.
|
will be rejected, and no tool can. Your file was read in this browser and never uploaded.
|
||||||
</Trans>
|
</Trans>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export function ScoreHeader({ report }: ScoreHeaderProps) {
|
|||||||
<TooltipContent side="bottom" className="max-w-64">
|
<TooltipContent side="bottom" className="max-w-64">
|
||||||
<Trans>
|
<Trans>
|
||||||
Some checks need information this file does not carry: page contents that could not be inspected, or
|
Some checks need information this file does not carry: page contents that could not be inspected, or
|
||||||
text in a language these checks do not cover. They are counted as neither passed nor failed.
|
text in a language these checks do not cover. They count as neither a pass nor a fail.
|
||||||
</Trans>
|
</Trans>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ export function LoginPage({ disableEmailAuth, disableSignups }: Props) {
|
|||||||
/>
|
/>
|
||||||
<FormMessage errors={field.state.meta.errors} />
|
<FormMessage errors={field.state.meta.errors} />
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
<Trans>You can also use your username to login.</Trans>
|
<Trans>You can also sign in with your username.</Trans>
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export function ResetPasswordPage({ token }: Props) {
|
|||||||
|
|
||||||
toast.add({
|
toast.add({
|
||||||
type: "success",
|
type: "success",
|
||||||
description: t`Your password has been reset successfully. You can now sign in with your new password.`,
|
description: t`Your password has been reset. You can now sign in with your new password.`,
|
||||||
id: toastId,
|
id: toastId,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ export function ResetPasswordPage({ token }: Props) {
|
|||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<div className="text-muted-foreground">
|
<div className="text-muted-foreground">
|
||||||
<Trans>Please enter a new password for your account</Trans>
|
<Trans>Enter a new password for your account</Trans>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -79,11 +79,11 @@ export function ResumePasswordPage({ redirectPath }: Props) {
|
|||||||
<>
|
<>
|
||||||
<div className="space-y-4 text-center">
|
<div className="space-y-4 text-center">
|
||||||
<h1 className="font-semibold text-2xl tracking-tight">
|
<h1 className="font-semibold text-2xl tracking-tight">
|
||||||
<Trans>The resume you are trying to access is password protected</Trans>
|
<Trans>This resume is password protected</Trans>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<div className="text-muted-foreground leading-relaxed">
|
<div className="text-muted-foreground leading-relaxed">
|
||||||
<Trans>Please enter the password shared with you by the owner of the resume to continue.</Trans>
|
<Trans>Enter the password the resume owner shared with you.</Trans>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ function TwoFactorVerificationPage({ backupCode = false }: TwoFactorVerification
|
|||||||
{backupCode ? (
|
{backupCode ? (
|
||||||
<Trans comment="Secondary navigation button on backup-code verification screen">Go Back</Trans>
|
<Trans comment="Secondary navigation button on backup-code verification screen">Go Back</Trans>
|
||||||
) : (
|
) : (
|
||||||
<Trans comment="Secondary navigation button on 2FA verification screen">Back to Login</Trans>
|
<Trans comment="Secondary navigation button on 2FA verification screen">Back to sign in</Trans>
|
||||||
)}
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ export function CommandPalette() {
|
|||||||
<CommandList key={commandListPage} data-command-page={commandListPage}>
|
<CommandList key={commandListPage} data-command-page={commandListPage}>
|
||||||
<CommandEmpty>
|
<CommandEmpty>
|
||||||
<Trans comment="Empty-state message when no command palette results match the search query">
|
<Trans comment="Empty-state message when no command palette results match the search query">
|
||||||
The command you're looking for doesn't exist.
|
No commands match that search.
|
||||||
</Trans>
|
</Trans>
|
||||||
</CommandEmpty>
|
</CommandEmpty>
|
||||||
|
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ describe("ResumesCommandGroup", () => {
|
|||||||
|
|
||||||
expect(useCommandPaletteStore.getState().pages).toEqual([page]);
|
expect(useCommandPaletteStore.getState().pages).toEqual([page]);
|
||||||
expect(await screen.findByText(createLabel)).toBeInTheDocument();
|
expect(await screen.findByText(createLabel)).toBeInTheDocument();
|
||||||
expect(screen.queryByText("The command you're looking for doesn't exist.")).not.toBeInTheDocument();
|
expect(screen.queryByText("No commands match that search.")).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ export function ResumeDownloadDialog({ resume, trigger }: ResumeDownloadDialogPr
|
|||||||
<Trans>Download</Trans>
|
<Trans>Download</Trans>
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
<Trans>Export your resume or cover letter in the format you need.</Trans>
|
<Trans>Pick a format for your resume or cover letter.</Trans>
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
@@ -168,7 +168,7 @@ export function ResumeDownloadDialog({ resume, trigger }: ResumeDownloadDialogPr
|
|||||||
<FormatRow
|
<FormatRow
|
||||||
icon={<MarkdownLogoIcon className="size-5" />}
|
icon={<MarkdownLogoIcon className="size-5" />}
|
||||||
title="Markdown"
|
title="Markdown"
|
||||||
description={<Trans>Plain text, ideal for AI tools and quick edits.</Trans>}
|
description={<Trans>Plain text, for AI tools and quick edits.</Trans>}
|
||||||
action={
|
action={
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ export function useResumeExport(resume: ExportableResume | undefined, exportOpti
|
|||||||
const blob = await buildDocx(data, resolveTitle);
|
const blob = await buildDocx(data, resolveTitle);
|
||||||
downloadWithAnchor(blob, generateFilename(getTargetExportName(resume, target), "docx"));
|
downloadWithAnchor(blob, generateFilename(getTargetExportName(resume, target), "docx"));
|
||||||
} catch {
|
} catch {
|
||||||
toast.add({ type: "error", description: t`There was a problem while generating the DOCX, please try again.` });
|
toast.add({ type: "error", description: t`Could not generate the DOCX. Please try again.` });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[resume],
|
[resume],
|
||||||
@@ -92,7 +92,7 @@ export function useResumeExport(resume: ExportableResume | undefined, exportOpti
|
|||||||
if (target === "cover-letter" && !resumeHasCoverLetter(resume.data)) return;
|
if (target === "cover-letter" && !resumeHasCoverLetter(resume.data)) return;
|
||||||
const toastId = toast.add({
|
const toastId = toast.add({
|
||||||
type: "loading",
|
type: "loading",
|
||||||
description: t`Please wait while your PDF is being generated...`,
|
description: t`Generating your PDF...`,
|
||||||
});
|
});
|
||||||
setIsExporting(true);
|
setIsExporting(true);
|
||||||
try {
|
try {
|
||||||
@@ -108,7 +108,7 @@ export function useResumeExport(resume: ExportableResume | undefined, exportOpti
|
|||||||
);
|
);
|
||||||
downloadWithAnchor(blob, generateFilename(getTargetExportName(resume, target), "pdf"));
|
downloadWithAnchor(blob, generateFilename(getTargetExportName(resume, target), "pdf"));
|
||||||
} catch {
|
} catch {
|
||||||
toast.add({ type: "error", description: t`There was a problem while generating the PDF, please try again.` });
|
toast.add({ type: "error", description: t`Could not generate the PDF. Please try again.` });
|
||||||
} finally {
|
} finally {
|
||||||
setIsExporting(false);
|
setIsExporting(false);
|
||||||
toast.close(toastId);
|
toast.close(toastId);
|
||||||
@@ -147,7 +147,7 @@ export function useResumeExport(resume: ExportableResume | undefined, exportOpti
|
|||||||
} catch {
|
} catch {
|
||||||
toast.add({
|
toast.add({
|
||||||
type: "error",
|
type: "error",
|
||||||
description: t`There was a problem while preparing your resume for printing, please try again.`,
|
description: t`Could not prepare your resume for printing. Please try again.`,
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setIsExporting(false);
|
setIsExporting(false);
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export function LegacyStylesheetBanner({ disabled, onActivate }: LegacyStyleshee
|
|||||||
</AlertTitle>
|
</AlertTitle>
|
||||||
<AlertDescription className="space-y-3">
|
<AlertDescription className="space-y-3">
|
||||||
<p>
|
<p>
|
||||||
<Trans>Your legacy styles remain active until you explicitly activate this Semantic CSS draft.</Trans>
|
<Trans>Your legacy styles stay active until you activate this Semantic CSS draft.</Trans>
|
||||||
</p>
|
</p>
|
||||||
<Button type="button" size="sm" disabled={disabled} onClick={onActivate}>
|
<Button type="button" size="sm" disabled={disabled} onClick={onActivate}>
|
||||||
<Trans>Activate Semantic CSS</Trans>
|
<Trans>Activate Semantic CSS</Trans>
|
||||||
|
|||||||
@@ -37,11 +37,11 @@ export function PasskeysSection() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
toast.add({ type: "success", description: t`Passkey registered successfully.` });
|
toast.add({ type: "success", description: t`Passkey registered.` });
|
||||||
await queryClient.invalidateQueries({ queryKey: ["auth", "passkeys"] });
|
await queryClient.invalidateQueries({ queryKey: ["auth", "passkeys"] });
|
||||||
|
|
||||||
const name = await prompt(t`Enter a name for your passkey.`, {
|
const name = await prompt(t`Enter a name for your passkey.`, {
|
||||||
description: t`This will help you identify it later, if you plan to have multiple passkeys.`,
|
description: t`Names help you tell passkeys apart if you register more than one.`,
|
||||||
defaultValue: "",
|
defaultValue: "",
|
||||||
confirmText: t({
|
confirmText: t({
|
||||||
comment: "Passkey rename prompt confirm action in authentication settings",
|
comment: "Passkey rename prompt confirm action in authentication settings",
|
||||||
@@ -93,7 +93,7 @@ export function PasskeysSection() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
toast.add({ type: "success", description: t`Passkey deleted successfully.` });
|
toast.add({ type: "success", description: t`Passkey deleted.` });
|
||||||
await queryClient.invalidateQueries({ queryKey: ["auth", "passkeys"] });
|
await queryClient.invalidateQueries({ queryKey: ["auth", "passkeys"] });
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: () => {
|
||||||
|
|||||||
@@ -463,7 +463,7 @@ function CreateProviderForm() {
|
|||||||
|
|
||||||
if (tested.testStatus === "success") {
|
if (tested.testStatus === "success") {
|
||||||
setForm(emptyForm);
|
setForm(emptyForm);
|
||||||
setResult({ ok: true, message: t`Connection verified — provider is ready to use.` });
|
setResult({ ok: true, message: t`Connection verified. The provider is ready to use.` });
|
||||||
} else {
|
} else {
|
||||||
// ponytail: provider stays persisted on failure so it shows in the list; a re-save creates a new row.
|
// ponytail: provider stays persisted on failure so it shows in the list; a re-save creates a new row.
|
||||||
setResult({
|
setResult({
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export function AccountSettingsPage() {
|
|||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" });
|
const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" });
|
||||||
downloadWithAnchor(blob, generateFilename("reactive-resume-export", "json"));
|
downloadWithAnchor(blob, generateFilename("reactive-resume-export", "json"));
|
||||||
toast.add({ type: "success", description: t`Your data has been exported successfully.` });
|
toast.add({ type: "success", description: t`Your data has been exported.` });
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
toast.add({
|
toast.add({
|
||||||
@@ -65,7 +65,7 @@ export function AccountSettingsPage() {
|
|||||||
|
|
||||||
deleteAccount(undefined, {
|
deleteAccount(undefined, {
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
toast.add({ type: "success", description: t`Your account has been deleted successfully.`, id: toastId });
|
toast.add({ type: "success", description: t`Your account has been deleted.`, id: toastId });
|
||||||
await authClient.signOut();
|
await authClient.signOut();
|
||||||
void navigate({ to: "/" });
|
void navigate({ to: "/" });
|
||||||
},
|
},
|
||||||
@@ -113,7 +113,7 @@ export function AccountSettingsPage() {
|
|||||||
<hr className="border-border" />
|
<hr className="border-border" />
|
||||||
|
|
||||||
<p className="leading-relaxed">
|
<p className="leading-relaxed">
|
||||||
<Trans>To delete your account, you need to enter the confirmation text and click the button below.</Trans>
|
<Trans>To delete your account, type the confirmation text below, then click the button.</Trans>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export function ApiKeysSettingsPage() {
|
|||||||
|
|
||||||
const onDelete = async (id: string) => {
|
const onDelete = async (id: string) => {
|
||||||
const confirmation = await confirm(t`Are you sure you want to delete this API key?`, {
|
const confirmation = await confirm(t`Are you sure you want to delete this API key?`, {
|
||||||
description: t`The API key will no longer be able to access your data after deletion. This action cannot be undone.`,
|
description: t`Once deleted, the key can no longer access your data. This cannot be undone.`,
|
||||||
confirmText: t({
|
confirmText: t({
|
||||||
comment: "API key deletion confirmation dialog confirm action in settings",
|
comment: "API key deletion confirmation dialog confirm action in settings",
|
||||||
message: "Delete",
|
message: "Delete",
|
||||||
@@ -62,7 +62,7 @@ export function ApiKeysSettingsPage() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
toast.add({ type: "success", description: t`The API key has been deleted successfully.`, id: toastId });
|
toast.add({ type: "success", description: t`The API key has been deleted.`, id: toastId });
|
||||||
void queryClient.invalidateQueries({ queryKey: ["auth", "api-keys"] });
|
void queryClient.invalidateQueries({ queryKey: ["auth", "api-keys"] });
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -85,8 +85,8 @@ export function ApiKeysSettingsPage() {
|
|||||||
|
|
||||||
<p className="text-muted-foreground leading-relaxed">
|
<p className="text-muted-foreground leading-relaxed">
|
||||||
<Trans>
|
<Trans>
|
||||||
Explore the API documentation to learn how to integrate Reactive Resume with your applications. Find
|
The API documentation shows how to connect Reactive Resume to your own applications. It covers the
|
||||||
detailed endpoints, request examples, and authentication methods.
|
endpoints, the request format, and authentication.
|
||||||
</Trans>
|
</Trans>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ export function ProfileSettingsPage({ session }: Props) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
toast.add({ type: "success", description: t`Your profile has been updated successfully.` });
|
toast.add({ type: "success", description: t`Your profile has been updated.` });
|
||||||
form.reset({ name: value.name, username: value.username, email: session.user.email });
|
form.reset({ name: value.name, username: value.username, email: session.user.email });
|
||||||
void router.invalidate();
|
void router.invalidate();
|
||||||
|
|
||||||
@@ -90,7 +90,7 @@ export function ProfileSettingsPage({ session }: Props) {
|
|||||||
|
|
||||||
toast.add({
|
toast.add({
|
||||||
type: "success",
|
type: "success",
|
||||||
description: t`A confirmation link has been sent to your current email address. Please check your inbox to confirm the change.`,
|
description: t`We sent a confirmation link to your new email address. Open it to confirm the change.`,
|
||||||
});
|
});
|
||||||
form.reset({ name: value.name, username: value.username, email: session.user.email });
|
form.reset({ name: value.name, username: value.username, email: session.user.email });
|
||||||
void router.invalidate();
|
void router.invalidate();
|
||||||
@@ -129,7 +129,7 @@ export function ProfileSettingsPage({ session }: Props) {
|
|||||||
|
|
||||||
toast.add({
|
toast.add({
|
||||||
type: "success",
|
type: "success",
|
||||||
description: t`A new verification link has been sent to your email address. Please check your inbox to verify your account.`,
|
description: t`We sent a new verification link to your email address. Open it to verify your account.`,
|
||||||
id: toastId,
|
id: toastId,
|
||||||
});
|
});
|
||||||
void router.invalidate();
|
void router.invalidate();
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ export function UserDropdownMenu({ children }: Props) {
|
|||||||
|
|
||||||
<DropdownMenuItem onClick={handleLogout}>
|
<DropdownMenuItem onClick={handleLogout}>
|
||||||
<SignOutIcon />
|
<SignOutIcon />
|
||||||
<Trans comment="User menu action to sign out of current account">Logout</Trans>
|
<Trans comment="User menu action to sign out of current account">Sign out</Trans>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ export const getRootStructuredData = (canonicalUrl: string): JsonLd[] => [
|
|||||||
name: appName,
|
name: appName,
|
||||||
url: canonicalUrl,
|
url: canonicalUrl,
|
||||||
description:
|
description:
|
||||||
"Reactive Resume is a free and open-source resume builder that simplifies the process of creating, updating, and sharing your resume.",
|
"Reactive Resume is a free and open-source resume builder that makes it easy to create, update, and share your resume.",
|
||||||
applicationCategory: "BusinessApplication",
|
applicationCategory: "BusinessApplication",
|
||||||
operatingSystem: "Web",
|
operatingSystem: "Web",
|
||||||
isAccessibleForFree: true,
|
isAccessibleForFree: true,
|
||||||
@@ -112,31 +112,29 @@ const homeFaqJsonLdItems = [
|
|||||||
{
|
{
|
||||||
question: "Is Reactive Resume really free?",
|
question: "Is Reactive Resume really free?",
|
||||||
answer:
|
answer:
|
||||||
"Yes! Reactive Resume is completely free to use, with no hidden costs, premium tiers, or subscription fees. It's open-source and will always remain free.",
|
"Yes. Reactive Resume is free to use, with no hidden costs, premium tiers, or subscription fees. It's open source, and it will stay free.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
question: "How is my data protected?",
|
question: "How is my data protected?",
|
||||||
answer:
|
answer:
|
||||||
"Your data is stored securely and is never shared with third parties. You can also self-host Reactive Resume on your own servers for complete control over your data.",
|
"Your data is stored securely and never shared with third parties. If you want full control over it, you can self-host Reactive Resume on your own servers.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
question: "Can I export my resume to PDF?",
|
question: "Can I export my resume to PDF?",
|
||||||
answer:
|
answer: "Yes. One click exports your resume to PDF, with your formatting and styling intact.",
|
||||||
"Absolutely! You can export your resume to PDF with a single click. The exported PDF maintains all your formatting and styling perfectly.",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
question: "Is Reactive Resume available in multiple languages?",
|
question: "Is Reactive Resume available in multiple languages?",
|
||||||
answer:
|
answer:
|
||||||
"Yes, Reactive Resume is available in multiple languages. You can choose your preferred language in the settings page, or using the language switcher in the top right corner. If you don't see your language, or you would like to improve the existing translations, you can contribute to the translations on Crowdin.",
|
"Yes. Pick your language on the settings page, or with the language switcher in the top right corner. If your language is missing, or the existing translation could be better, you can contribute to the translations on Crowdin.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
question: "What makes Reactive Resume different from other resume builders?",
|
question: "What makes Reactive Resume different from other resume builders?",
|
||||||
answer:
|
answer:
|
||||||
"Reactive Resume is open-source, privacy-focused, and completely free. Unlike other resume builders, it doesn't show ads, track your data, or limit your features behind a paywall.",
|
"Reactive Resume is open source, private, and free. It shows no ads, doesn't track what you do, and doesn't lock features behind a paywall.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
question: "How do I share my resume?",
|
question: "How do I share my resume?",
|
||||||
answer:
|
answer: "Share it with a public URL, put a password on that URL, or download the PDF and send it yourself.",
|
||||||
"You can share your resume via a unique public URL, protect it with a password, or download it as a PDF to share directly. The choice is yours!",
|
|
||||||
},
|
},
|
||||||
] as const;
|
] as const;
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ const tagline = "A free and open-source resume builder";
|
|||||||
const title = `${appName} — ${tagline}`;
|
const title = `${appName} — ${tagline}`;
|
||||||
// Keep under ~120 characters so Google's mobile SERP snippet is not truncated at 3 lines.
|
// Keep under ~120 characters so Google's mobile SERP snippet is not truncated at 3 lines.
|
||||||
const description =
|
const description =
|
||||||
"Free, open-source resume builder. Create, update, and share a professional resume in minutes — no ads, no paywall.";
|
"Free, open-source resume builder. Create, update, and share your resume, with no ads and no paywall.";
|
||||||
const iconContextValue: IconProps = { size: 16, weight: "regular" };
|
const iconContextValue: IconProps = { size: 16, weight: "regular" };
|
||||||
|
|
||||||
export const Route = createRootRouteWithContext<RouterContext>()({
|
export const Route = createRootRouteWithContext<RouterContext>()({
|
||||||
|
|||||||
@@ -161,8 +161,8 @@ export const DonationBanner = () => (
|
|||||||
transition={{ duration: 0.6, delay: 0.2 }}
|
transition={{ duration: 0.6, delay: 0.2 }}
|
||||||
>
|
>
|
||||||
<Trans>
|
<Trans>
|
||||||
Reactive Resume is a free and open-source project, built with love and maintained by me and a community of
|
Reactive Resume is a free and open-source project, maintained by me and a community of contributors. Your
|
||||||
contributors. Your donations help keep the lights on and the code flowing.
|
donations cover the running costs and keep development going.
|
||||||
</Trans>
|
</Trans>
|
||||||
</m.p>
|
</m.p>
|
||||||
</m.div>
|
</m.div>
|
||||||
@@ -172,19 +172,19 @@ export const DonationBanner = () => (
|
|||||||
<FeatureCard
|
<FeatureCard
|
||||||
icon={RocketIcon}
|
icon={RocketIcon}
|
||||||
title={t`Long-term Sustainability`}
|
title={t`Long-term Sustainability`}
|
||||||
description={t`Your support ensures the project remains free and accessible for everyone, now and in the future.`}
|
description={t`Your support keeps the project free and open to everyone, now and later.`}
|
||||||
delay={0.3}
|
delay={0.3}
|
||||||
/>
|
/>
|
||||||
<FeatureCard
|
<FeatureCard
|
||||||
icon={WrenchIcon}
|
icon={WrenchIcon}
|
||||||
title={t`Ongoing Maintenance`}
|
title={t`Ongoing Maintenance`}
|
||||||
description={t`Contributions fund bug fixes, security updates, and continuous improvements to keep the app running smoothly.`}
|
description={t`Donations pay for bug fixes, security updates, and the ordinary work of keeping the app running.`}
|
||||||
delay={0.4}
|
delay={0.4}
|
||||||
/>
|
/>
|
||||||
<FeatureCard
|
<FeatureCard
|
||||||
icon={UsersIcon}
|
icon={UsersIcon}
|
||||||
title={t`Grow the Team`}
|
title={t`Grow the Team`}
|
||||||
description={t`Help me bring more experienced contributors on board, reducing the burden on a single maintainer and accelerating development.`}
|
description={t`Help me bring more experienced contributors on board, so the work doesn't rest on one maintainer.`}
|
||||||
delay={0.5}
|
delay={0.5}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -233,7 +233,7 @@ export const DonationBanner = () => (
|
|||||||
transition={{ duration: 0.6, delay: 0.8 }}
|
transition={{ duration: 0.6, delay: 0.8 }}
|
||||||
>
|
>
|
||||||
<Trans>
|
<Trans>
|
||||||
Every contribution, big or small, makes a huge difference to the project.
|
Every contribution helps, however small.
|
||||||
<br />
|
<br />
|
||||||
Thank you for your support!
|
Thank you for your support!
|
||||||
</Trans>
|
</Trans>
|
||||||
|
|||||||
@@ -15,23 +15,22 @@ type FAQItemData = {
|
|||||||
const getFaqItems = (): FAQItemData[] => [
|
const getFaqItems = (): FAQItemData[] => [
|
||||||
{
|
{
|
||||||
question: t`Is Reactive Resume really free?`,
|
question: t`Is Reactive Resume really free?`,
|
||||||
answer: t`Yes! Reactive Resume is completely free to use, with no hidden costs, premium tiers, or subscription fees. It's open-source and will always remain free.`,
|
answer: t`Yes. Reactive Resume is free to use, with no hidden costs, premium tiers, or subscription fees. It's open source, and it will stay free.`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
question: t`How is my data protected?`,
|
question: t`How is my data protected?`,
|
||||||
answer: t`Your data is stored securely and is never shared with third parties. You can also self-host Reactive Resume on your own servers for complete control over your data.`,
|
answer: t`Your data is stored securely and never shared with third parties. If you want full control over it, you can self-host Reactive Resume on your own servers.`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
question: t`Can I export my resume to PDF?`,
|
question: t`Can I export my resume to PDF?`,
|
||||||
answer: t`Absolutely! You can export your resume to PDF with a single click. The exported PDF maintains all your formatting and styling perfectly.`,
|
answer: t`Yes. One click exports your resume to PDF, with your formatting and styling intact.`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
question: t`Is Reactive Resume available in multiple languages?`,
|
question: t`Is Reactive Resume available in multiple languages?`,
|
||||||
answer: (
|
answer: (
|
||||||
<Trans>
|
<Trans>
|
||||||
Yes, Reactive Resume is available in multiple languages. You can choose your preferred language in the settings
|
Yes. Pick your language on the settings page, or with the language switcher in the top right corner. If your
|
||||||
page, or using the language switcher in the top right corner. If you don't see your language, or you would like
|
language is missing, or the existing translation could be better, you can{" "}
|
||||||
to improve the existing translations, you can{" "}
|
|
||||||
<a
|
<a
|
||||||
href={crowdinUrl}
|
href={crowdinUrl}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -52,11 +51,11 @@ const getFaqItems = (): FAQItemData[] => [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
question: t`What makes Reactive Resume different from other resume builders?`,
|
question: t`What makes Reactive Resume different from other resume builders?`,
|
||||||
answer: t`Reactive Resume is open-source, privacy-focused, and completely free. Unlike other resume builders, it doesn't show ads, track your data, or limit your features behind a paywall.`,
|
answer: t`Reactive Resume is open source, private, and free. It shows no ads, doesn't track what you do, and doesn't lock features behind a paywall.`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
question: t`How do I share my resume?`,
|
question: t`How do I share my resume?`,
|
||||||
answer: t`You can share your resume via a unique public URL, protect it with a password, or download it as a PDF to share directly. The choice is yours!`,
|
answer: t`Share it with a public URL, put a password on that URL, or download the PDF and send it yourself.`,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user