Compare commits

..
19 Commits
Author SHA1 Message Date
Amruth Pillai 269dbc600f 📦 v5.0.10 · Changelog: https://docs.rxresu.me/changelog 2026-02-24 10:55:58 +01:00
Amruth Pillai 9b29a44429 fix(get-section-component): display titles for summary-type custom sections (#2744) 2026-02-24 10:48:44 +01:00
YeungandAmruth Pillai e9fa161148 fix(ai): prevent Edge/password-manager autofill and save prompts in AI settings (#2719)
* fix(ai): prevent password autofill in AI settings

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

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

* update code style

---------

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

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

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

* refactor(auth): rename originWith to withHostname

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

* update dependencies, update code style

---------

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

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

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

https://claude.ai/code/session_01NZ48uTqAAbNn3wAsSMsMGV

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* New translations en-us.po (Nepali)
[ci skip]
2026-02-10 00:18:14 +01:00
111 changed files with 4210 additions and 2417 deletions
+4
View File
@@ -66,6 +66,10 @@ FLAG_DISABLE_SIGNUPS="false"
# This flag disables email/password login. Disables email verification, forgot password, and reset password flows. Users can still sign up via social auth (Google/GitHub/Custom OAuth), unless FLAG_DISABLE_SIGNUPS is also set to true.
FLAG_DISABLE_EMAIL_AUTH="false"
# This flag disables the image processing.
# This is useful if you are using a machine with limited resources, like a Raspberry Pi.
FLAG_DISABLE_IMAGE_PROCESSING="false"
# --- Others ---
# Google Cloud API Key (optional)
# This is not used within Reactive Resume, but in src/scripts/fonts/generate.ts to generate a list of fonts served by Google Fonts.
+82 -16
View File
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Project Overview
Reactive Resume is a free, open-source resume builder built with TanStack Start (React 19 + Vite), using ORPC for type-safe RPC APIs, Drizzle ORM with PostgreSQL, and Better Auth for authentication.
Reactive Resume is a free, open-source resume builder built with TanStack Start (React 19 + Vite 8), using ORPC for type-safe RPC APIs, Drizzle ORM with PostgreSQL, Nitro as the server runtime, and Better Auth for authentication. It is a PWA with 47 supported locales and 13 resume templates.
## Development Commands
@@ -18,10 +18,10 @@ pnpm build
# Start production server
pnpm start
# Linting (uses Biome)
# Linting and formatting (uses Biome)
pnpm lint
# Type checking
# Type checking (uses tsgo)
pnpm typecheck
# Database operations
@@ -33,10 +33,12 @@ pnpm db:studio # Open Drizzle Studio
# Extract i18n strings for translation
pnpm lingui:extract
# Find unused exports
# Find unused exports / dead code
dotenvx run -- pnpm knip
```
**There is no test framework configured.** No unit, integration, or E2E tests exist in the codebase.
## Local Development Setup
1. Copy `.env.example` to `.env` and configure environment variables
@@ -48,27 +50,52 @@ dotenvx run -- pnpm knip
- Adminer for DB management (port 8080)
3. Run `pnpm dev`
Database migrations run automatically on server startup via the Nitro plugin at `plugins/1.migrate.ts`.
## Architecture
### Directory Structure
- `src/routes/` - TanStack Router file-based routing
- `src/integrations/` - External service integrations (auth, database, ORPC, AI, email)
- `src/integrations/orpc/router` - oRPC server routers
- `src/integrations/orpc/services` - oRPC server services
- `src/integrations/` - External service integrations (auth, database, ORPC, AI, email, import)
- `src/integrations/orpc/router/` - oRPC server routers (procedure definitions)
- `src/integrations/orpc/services/` - oRPC server services (business logic)
- `src/integrations/orpc/dto/` - Data transfer objects
- `src/integrations/orpc/context.ts` - Auth and request context setup
- `src/components/` - React components organized by feature
- `src/components/ui/` - Shadcn UI components (Radix + Phosphor icons)
- `src/schema/` - Zod schemas for validation
- `plugins/` - Nitro server plugins (eg. auto-migration on startup)
- `src/hooks/` - Custom React hooks
- `plugins/` - Nitro server plugins (auto-migration on startup)
- `migrations/` - Drizzle database migrations
- `locales/` - i18n translation files (managed by Lingui)
- `docs/` - Documentation (Mintlify)
### Key Integrations (`src/integrations/`)
- **auth/** - Better Auth configuration and client
- **auth/** - Better Auth configuration (session-based + API key via `x-api-key` header)
- **drizzle/** - Database schema and client (PostgreSQL)
- **orpc/** - Type-safe RPC router with procedures for ai, auth, flags, printer, resume, statistics, storage
- **query/** - TanStack Query client configuration
- **ai/** - AI provider integrations (OpenAI, Anthropic, Google Gemini, Ollama)
- **email/** - Nodemailer integration (falls back to console logging if SMTP is not configured)
- **import/** - Resume file parsing/import
### ORPC Procedure Types
Three procedure types exist in `src/integrations/orpc/context.ts`:
- `publicProcedure` - No authentication required
- `protectedProcedure` - Requires authenticated user (session or API key)
- `serverOnlyProcedure` - Server-side calls only
Procedures follow this pattern:
```ts
const handler = protectedProcedure
.route({ method: "GET", path: "/resumes/{id}", tags: ["Resumes"], ... })
.input(schema)
.output(schema)
.handler(async ({ context, input }) => { ... })
```
### Resume Data Model
@@ -80,14 +107,17 @@ The resume schema is defined in `src/schema/resume/data.ts`. Key concepts:
### Resume Templates
Templates are React components in `src/components/resume/templates/`. Each template (azurill, bronzor, chikorita, etc.) renders the resume data with different visual styles. Templates use shared components from `src/components/resume/shared/`.
13 templates in `src/components/resume/templates/` (Pokemon-themed names):
azurill, bronzor, chikorita, ditgar, ditto, gengar, glalie, kakuna, lapras, leafish, onyx, pikachu, rhyhorn
Shared rendering components live in `src/components/resume/shared/`.
### Database Schema
Defined in `src/integrations/drizzle/schema.ts`:
- `user`, `session`, `account`, `verification`, `twoFactor`, `passkey`, `apikey` - Better Auth tables
- `resume` - Stores Resume Data as JSONB (defined in `src/schema/resume/data.ts`)
- `resumeStatistics` - Views/Download for Resume Tracking
- `resumeStatistics` - Views/Download tracking
### Routing
@@ -98,19 +128,39 @@ Uses TanStack Router with file-based routing. Key routes:
- `/builder/$resumeId/` - Resume editor
- `/printer/$resumeId/` - PDF rendering endpoint
- `/api/` - Public API endpoints
- `/mcp/` - MCP server endpoint for LLM integration
Routes use `createFileRoute()` with `beforeLoad()` for auth guards and `loader()` for server-side data fetching.
### MCP Server
An MCP (Model Context Protocol) server is available at `/mcp/` for LLM-based resume interaction. It requires an `x-api-key` header for authentication. Configuration is in `src/routes/mcp/` with helper modules for resources, prompts, and tools.
### State Management
- **Zustand** - Client-side state (resume editor state in `src/components/resume/store/`)
- **Zundo** - Undo/redo history for resume edits (built on Zustand)
- **TanStack Query** - Server state and caching (configured via ORPC integration)
### Global Providers
Defined in `src/routes/__root.tsx`:
- I18nProvider (Lingui), ThemeProvider, MotionConfig, IconContext (Phosphor Icons)
- ConfirmDialogProvider, PromptDialogProvider, DialogManager, CommandPalette, Toaster
## Code Style
- Uses Biome for linting and formatting
- Uses **Biome** for linting and formatting (`biome.json`)
- Tab indentation, double quotes, 120 character line width
- Imports are auto-organized; unused imports are errors
- a11y rules are disabled
- Path alias: `@/` maps to `src/`
- Tailwind CSS v4 with sorted class names (enforced by Biome)
- Uses `cn()` utility for conditional class names
- Tailwind CSS v4 with sorted class names (enforced by Biome's `useSortedClasses`)
- Uses `cn()` utility (from `@/utils/style`) for conditional class names
- Uses `cva()` for component variants
- Shadcn UI components in `src/components/ui/` (Radix UI + Phosphor icons, zinc base color)
- i18n strings use Lingui macros: `<Trans>`, `t`, `msg`
- TypeScript strict mode enabled; `noUnusedLocals` and `noUnusedParameters` enforced
## Environment Variables
@@ -119,5 +169,21 @@ Key variables (see `.env.example` for full list):
- `DATABASE_URL` - PostgreSQL connection string
- `AUTH_SECRET` - Secret for authentication
- `PRINTER_ENDPOINT` - WebSocket endpoint for PDF printer service
- `S3_*` - S3-compatible storage configuration
- `FLAG_*` - Feature flags
- `PRINTER_APP_URL` - Internal URL for printer to reach the app (important for Docker)
- `S3_*` - S3-compatible storage configuration (falls back to local `/data` filesystem)
- `SMTP_*` - Email configuration (falls back to console logging)
- `GOOGLE_CLIENT_ID/SECRET` - Google OAuth (optional)
- `GITHUB_CLIENT_ID/SECRET` - GitHub OAuth (optional)
- `OAUTH_*` - Custom OAuth provider (optional)
- `FLAG_DEBUG_PRINTER` - Debug PDF printing endpoint
- `FLAG_DISABLE_SIGNUPS` - Block new account registration
- `FLAG_DISABLE_EMAIL_AUTH` - Disable email/password login
- `FLAG_DISABLE_IMAGE_PROCESSING` - Disable image processing
## Build & Deployment
- **Build output**: `.output/` directory (Nitro server bundle)
- **Production start**: `node .output/server/index.mjs`
- **Docker**: Multi-stage Dockerfile with Node 24-slim base
- **Health check**: `GET /api/health`
- **PWA**: Configured via vite-plugin-pwa with auto-update, standalone display, dark theme
+1 -1
View File
@@ -18,7 +18,7 @@
<img src="https://img.shields.io/github/stars/amruthpillai/Reactive-Resume?style=flat-square" alt="GitHub Stars">
<img src="https://img.shields.io/github/license/amruthpillai/Reactive-Resume?style=flat-square" alt="License" />
<img src="https://img.shields.io/docker/pulls/amruthpillai/reactive-resume?style=flat-square" alt="Docker Pulls" />
<a href="https://discord.gg/EE8yFqW4"><img src="https://img.shields.io/discord/1173518977851473940?style=flat-square&label=discord" alt="Discord" /></a>
<a href="https://discord.gg/aSyA5ZSxpb"><img src="https://img.shields.io/discord/1173518977851473940?style=flat-square&label=discord" alt="Discord" /></a>
<a href="https://crowdin.com/project/reactive-resume"><img src="https://badges.crowdin.net/reactive-resume/localized.svg?style=flat-square" alt="Crowdin" /></a>
<a href="https://github.com/sponsors/AmruthPillai"><img src="https://img.shields.io/github/sponsors/AmruthPillai?style=flat-square&label=sponsors" alt="Sponsors" /></a>
<a href="https://opencollective.com/reactive-resume"><img src="https://img.shields.io/opencollective/backers/reactive-resume?style=flat-square&label=donations" alt="Donations" /></a>
+5 -5
View File
@@ -53,11 +53,11 @@ services:
# As an alternative to browserless, you can also use a lightweight image like chromedp/headless-shell:latest
# See https://docs.rxresu.me/self-hosting/docker#alternative-printer-options for more information.
# chrome:
# image: chromedp/headless-shell:latest
# restart: unless-stopped
# ports:
# - "9222:9222"
chrome:
image: chromedp/headless-shell:latest
restart: unless-stopped
ports:
- "9222:9222"
seaweedfs:
image: chrislusf/seaweedfs:latest
+28
View File
@@ -4,6 +4,34 @@ description: "List of all notable changes and updates to Reactive Resume"
rss: true
---
<Update label="v5.0.10" description="24th February 2026">
- **Fixes**
- Show section titles for summary-type custom sections in the resume builder. [#2744](https://github.com/amruthpillai/reactive-resume/pull/2744)
- Prevent browser password managers and Edge autofill/save prompts from appearing on AI settings API key fields. [#2719](https://github.com/amruthpillai/reactive-resume/pull/2719)
- Replace deprecated Tailwind CSS classes: use `inset-s-*`/`inset-e-*` instead of `start-*`/`end-*`.
- Fix PDF downloader to work correctly in offline mode. [#2743](https://github.com/amruthpillai/reactive-resume/pull/2743)
- Make bold formatting visible for `<strong>` in the resume rich text editor (uses plain `font-weight: bold` fallback if the CSS variable is unset). Fixes [#2730](https://github.com/amruthpillai/reactive-resume/issues/2730)
- Prevent credentials sign-in from dropping `session_token` Set-Cookie, improves login reliability. [#2718](https://github.com/amruthpillai/reactive-resume/pull/2718)
- Remove redundant `resume-` prefix from download filename; add spacing between pages in shared view. [#2709](https://github.com/amruthpillai/reactive-resume/pull/2709)
- Normalize autocomplete tokens for login and register forms. [#2714](https://github.com/amruthpillai/reactive-resume/pull/2714)
- Fix improper chips reordering and update dependency/translations. [#2711](https://github.com/amruthpillai/reactive-resume/issues/2711)
- Fix issue with clipping of heading in Lapras resume template.
- Remove error-causing plugins in oRPC integration.
- Remove duplicate database indexes; add index for `created_at` on user and resume tables.
- **Features & Improvements**
- Add new feature flag: `FLAG_DISABLE_IMAGE_PROCESSING` (allows disabling of image processing site-wide).
- Update Discord invite link in the app and documentation.
- Add comprehensive codebase and architecture documentation in `CLAUDE.md`.
- Sync latest translations from Crowdin (updated and added multiple languages).
- **Other**
- Refactor: rename auth utility from `originWith` to `withHostname`, preserve localhost/127.0.0.1 sibling trust.
- General dependency updates and code style improvements.
- Documentation and README updates.
</Update>
<Update label="v5.0.9" description="9th February 2026">
- Add Computer Modern web fonts to the font selector, allowing the user to choose from a variety of "Computer Modern" (LaTeX) fonts.
- This lets you create a resume that looks just like a LaTeX document, with the same fonts and styles.
+1 -1
View File
@@ -104,7 +104,7 @@ Reactive Resume is built with modern web technologies:
<Card title="Reddit" icon="reddit" href="https://reddit.com/r/reactiveresume">
Join our Reddit community to get help and connect with other users.
</Card>
<Card title="Discord" icon="discord" href="https://discord.gg/EE8yFqW4">
<Card title="Discord" icon="discord" href="https://discord.gg/aSyA5ZSxpb">
Join our Discord server to get help and connect with other users.
</Card>
<Card title="Sponsor" icon="heart" href="https://opencollective.com/reactive-resume">
+1
View File
@@ -193,6 +193,7 @@ Here's a complete list of environment variables you can configure:
| `FLAG_DEBUG_PRINTER` | Used for debugging the printer route | `false` |
| `FLAG_DISABLE_SIGNUPS` | Disables new user signups | `false` |
| `FLAG_DISABLE_EMAIL_AUTH` | Disables email/password login (SSO only) | `false` |
| `FLAG_DISABLE_IMAGE_PROCESSING` | Disables image processing | `false` |
> **Note:** Some variables are only required for using related features (OAuth, SMTP, S3, etc.) and can be left unset if unused.
+1
View File
@@ -363,6 +363,7 @@ openssl rand -hex 32
- **`FLAG_DEBUG_PRINTER`**: Bypasses the printer-only access restriction (useful when debugging `/printer/{resumeId}`). Recommended: keep `"false"` in production.
- **`FLAG_DISABLE_SIGNUPS`**: Disables new signups (web app and server). Useful for private instances.
- **`FLAG_DISABLE_EMAIL_AUTH`**: Disables email/password login entirely. Also disables email verification, forgot password, and reset password flows. Users can still sign up via social auth (Google/GitHub/Custom OAuth), unless FLAG_DISABLE_SIGNUPS is also set to true. Useful when only SSO is required.
- **`FLAG_DISABLE_IMAGE_PROCESSING`**: Disables image processing. This is useful if you are using a machine with limited resources, like a Raspberry Pi.
</Accordion>
</AccordionGroup>
-3
View File
@@ -1,7 +1,4 @@
import { defineConfig } from "drizzle-kit";
import invariant from "tiny-invariant";
invariant(process.env.DATABASE_URL, "DATABASE_URL is not set");
export default defineConfig({
out: "./migrations",
+1 -1
View File
@@ -5,5 +5,5 @@
"project": ["src/**/*.{js,jsx,ts,tsx,mdx,css}"],
"ignoreBinaries": ["mint"],
"ignoreDependencies": ["npm-check-updates", "node-addon-api", "node-gyp"],
"ignore": ["src/schema/**/*.ts", "src/components/**/*.{ts,tsx}", "src/routeTree.gen.ts"]
"ignore": ["src/schema/**/*.ts", "src/components/**/*.{ts,tsx}"]
}
BIN
View File
Binary file not shown.
+1 -1
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: am\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-02-09 00:59\n"
"PO-Revision-Date: 2026-02-09 22:31\n"
"Last-Translator: \n"
"Language-Team: Amharic\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+1 -1
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: ar\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-02-09 00:58\n"
"PO-Revision-Date: 2026-02-09 22:30\n"
"Last-Translator: \n"
"Language-Team: Arabic\n"
"Plural-Forms: nplurals=6; plural=(n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5);\n"
+1 -1
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: az\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-02-09 00:58\n"
"PO-Revision-Date: 2026-02-09 22:31\n"
"Last-Translator: \n"
"Language-Team: Azerbaijani\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+1 -1
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: bg\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-02-09 00:58\n"
"PO-Revision-Date: 2026-02-09 22:31\n"
"Last-Translator: \n"
"Language-Team: Bulgarian\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+1 -1
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: bn\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-02-09 00:58\n"
"PO-Revision-Date: 2026-02-09 22:31\n"
"Last-Translator: \n"
"Language-Team: Bengali\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+1 -1
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: ca\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-02-09 00:58\n"
"PO-Revision-Date: 2026-02-09 22:31\n"
"Last-Translator: \n"
"Language-Team: Catalan\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+1 -1
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: cs\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-02-09 00:58\n"
"PO-Revision-Date: 2026-02-09 22:31\n"
"Last-Translator: \n"
"Language-Team: Czech\n"
"Plural-Forms: nplurals=4; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 3;\n"
+1 -1
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: da\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-02-09 00:58\n"
"PO-Revision-Date: 2026-02-09 22:31\n"
"Last-Translator: \n"
"Language-Team: Danish\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+1 -1
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: de\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-02-09 00:58\n"
"PO-Revision-Date: 2026-02-09 22:31\n"
"Last-Translator: \n"
"Language-Team: German\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+1 -1
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: el\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-02-09 00:58\n"
"PO-Revision-Date: 2026-02-09 22:31\n"
"Last-Translator: \n"
"Language-Team: Greek\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+9 -9
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: es\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-02-09 00:58\n"
"PO-Revision-Date: 2026-02-17 09:51\n"
"Last-Translator: \n"
"Language-Team: Spanish\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -209,7 +209,7 @@ msgstr "Añadir fila antes"
#: src/components/ai/chat.tsx
msgid "Added"
msgstr ""
msgstr "Añadido"
#: src/utils/locale.ts
msgid "Afrikaans"
@@ -222,7 +222,7 @@ msgstr "IA"
#: src/components/ai/chat.tsx
msgid "AI Chat"
msgstr ""
msgstr "Chat con IA"
#: src/utils/locale.ts
msgid "Albanian"
@@ -337,7 +337,7 @@ msgstr "Inteligencia artificial"
#: src/components/ai/chat.tsx
msgid "Ask me to update your resume..."
msgstr ""
msgstr "Pídame que actualice su currículum..."
#: src/routes/builder/$resumeId/-sidebar/left/sections/picture.tsx
msgid "Aspect Ratio"
@@ -535,7 +535,7 @@ msgstr "Círculo"
#: src/components/ai/chat.tsx
msgid "Clear chat history"
msgstr ""
msgstr "Limpiar historial del chat"
#: src/components/ui/multiple-combobox.tsx
msgid "Clear selection"
@@ -946,7 +946,7 @@ msgstr "Neerlandés"
#: src/components/ai/chat.tsx
msgid "e.g. Change my name to..."
msgstr ""
msgstr "ej. Cambiar mi nombre a..."
#: src/utils/resume/section.tsx
msgid "Education"
@@ -1893,7 +1893,7 @@ msgstr "Quitar contraseña"
#: src/components/ai/chat.tsx
msgid "Removed"
msgstr ""
msgstr "Eliminado"
#: src/routes/builder/$resumeId/-sidebar/right/sections/sharing.tsx
msgid "Removing password protection..."
@@ -2357,7 +2357,7 @@ msgstr "Ha habido un problema al generar el PDF, inténtalo de nuevo dentro de u
#: src/components/ai/chat.tsx
msgid "Thinking..."
msgstr ""
msgstr "Pensando..."
#: src/routes/builder/$resumeId/-components/header.tsx
#: src/routes/dashboard/resumes/-components/menus/context-menu.tsx
@@ -2624,7 +2624,7 @@ msgstr "Actualiza tu contraseña"
#: src/components/ai/chat.tsx
msgid "Updated"
msgstr ""
msgstr "Actualizado"
#: src/dialogs/auth/change-password.tsx
msgid "Updating your password..."
+1 -1
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: fa\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-02-09 00:58\n"
"PO-Revision-Date: 2026-02-09 22:31\n"
"Last-Translator: \n"
"Language-Team: Persian\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+1 -1
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: fi\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-02-09 00:58\n"
"PO-Revision-Date: 2026-02-09 22:31\n"
"Last-Translator: \n"
"Language-Team: Finnish\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+9 -9
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: fr\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-02-09 00:58\n"
"PO-Revision-Date: 2026-02-11 19:56\n"
"Last-Translator: \n"
"Language-Team: French\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
@@ -209,7 +209,7 @@ msgstr "Ajouter une ligne avant"
#: src/components/ai/chat.tsx
msgid "Added"
msgstr ""
msgstr "Ajouté"
#: src/utils/locale.ts
msgid "Afrikaans"
@@ -222,7 +222,7 @@ msgstr "IA"
#: src/components/ai/chat.tsx
msgid "AI Chat"
msgstr ""
msgstr "Chat avec l'IA"
#: src/utils/locale.ts
msgid "Albanian"
@@ -337,7 +337,7 @@ msgstr "Intelligence artificielle"
#: src/components/ai/chat.tsx
msgid "Ask me to update your resume..."
msgstr ""
msgstr "Demandez-moi de mettre à jour votre CV."
#: src/routes/builder/$resumeId/-sidebar/left/sections/picture.tsx
msgid "Aspect Ratio"
@@ -535,7 +535,7 @@ msgstr "Cercle"
#: src/components/ai/chat.tsx
msgid "Clear chat history"
msgstr ""
msgstr "Effacer l'historique de chat"
#: src/components/ui/multiple-combobox.tsx
msgid "Clear selection"
@@ -946,7 +946,7 @@ msgstr "Néerlandais"
#: src/components/ai/chat.tsx
msgid "e.g. Change my name to..."
msgstr ""
msgstr "Par ex. : Changer mon nom en"
#: src/utils/resume/section.tsx
msgid "Education"
@@ -1893,7 +1893,7 @@ msgstr "Supprimer le mot de passe"
#: src/components/ai/chat.tsx
msgid "Removed"
msgstr ""
msgstr "Supprimé"
#: src/routes/builder/$resumeId/-sidebar/right/sections/sharing.tsx
msgid "Removing password protection..."
@@ -2357,7 +2357,7 @@ msgstr "Un problème est survenu lors de la génération du PDF, veuillez réess
#: src/components/ai/chat.tsx
msgid "Thinking..."
msgstr ""
msgstr "Réflexion..."
#: src/routes/builder/$resumeId/-components/header.tsx
#: src/routes/dashboard/resumes/-components/menus/context-menu.tsx
@@ -2624,7 +2624,7 @@ msgstr "Modifier votre mot de passe"
#: src/components/ai/chat.tsx
msgid "Updated"
msgstr ""
msgstr "Mis à jour"
#: src/dialogs/auth/change-password.tsx
msgid "Updating your password..."
+1 -1
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: he\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-02-09 00:58\n"
"PO-Revision-Date: 2026-02-09 22:31\n"
"Last-Translator: \n"
"Language-Team: Hebrew\n"
"Plural-Forms: nplurals=4; plural=n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3;\n"
+1 -1
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: hi\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-02-09 00:58\n"
"PO-Revision-Date: 2026-02-09 22:31\n"
"Last-Translator: \n"
"Language-Team: Hindi\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+1 -1
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: hu\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-02-09 00:58\n"
"PO-Revision-Date: 2026-02-09 22:31\n"
"Last-Translator: \n"
"Language-Team: Hungarian\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+1 -1
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: id\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-02-09 00:58\n"
"PO-Revision-Date: 2026-02-09 22:31\n"
"Last-Translator: \n"
"Language-Team: Indonesian\n"
"Plural-Forms: nplurals=1; plural=0;\n"
+1 -1
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: it\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-02-09 00:58\n"
"PO-Revision-Date: 2026-02-09 22:31\n"
"Last-Translator: \n"
"Language-Team: Italian\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+1 -1
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: ja\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-02-09 00:58\n"
"PO-Revision-Date: 2026-02-09 22:31\n"
"Last-Translator: \n"
"Language-Team: Japanese\n"
"Plural-Forms: nplurals=1; plural=0;\n"
+1 -1
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: km\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-02-09 00:58\n"
"PO-Revision-Date: 2026-02-09 22:31\n"
"Last-Translator: \n"
"Language-Team: Khmer\n"
"Plural-Forms: nplurals=1; plural=0;\n"
+1 -1
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: kn\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-02-09 00:58\n"
"PO-Revision-Date: 2026-02-09 22:31\n"
"Last-Translator: \n"
"Language-Team: Kannada\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+1 -1
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: ko\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-02-09 00:58\n"
"PO-Revision-Date: 2026-02-09 22:31\n"
"Last-Translator: \n"
"Language-Team: Korean\n"
"Plural-Forms: nplurals=1; plural=0;\n"
+1 -1
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: lt\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-02-09 00:58\n"
"PO-Revision-Date: 2026-02-09 22:31\n"
"Last-Translator: \n"
"Language-Team: Lithuanian\n"
"Plural-Forms: nplurals=4; plural=(n%10==1 && (n%100>19 || n%100<11) ? 0 : (n%10>=2 && n%10<=9) && (n%100>19 || n%100<11) ? 1 : n%1!=0 ? 2: 3);\n"
+1 -1
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: lv\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-02-09 00:58\n"
"PO-Revision-Date: 2026-02-09 22:31\n"
"Last-Translator: \n"
"Language-Team: Latvian\n"
"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2;\n"
+1 -1
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: ml\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-02-09 00:58\n"
"PO-Revision-Date: 2026-02-09 22:31\n"
"Last-Translator: \n"
"Language-Team: Malayalam\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+1 -1
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: mr\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-02-09 00:58\n"
"PO-Revision-Date: 2026-02-09 22:31\n"
"Last-Translator: \n"
"Language-Team: Marathi\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+1 -1
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: ms\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-02-09 00:58\n"
"PO-Revision-Date: 2026-02-09 22:31\n"
"Last-Translator: \n"
"Language-Team: Malay\n"
"Plural-Forms: nplurals=1; plural=0;\n"
+1 -1
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: ne\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-02-09 00:59\n"
"PO-Revision-Date: 2026-02-09 22:31\n"
"Last-Translator: \n"
"Language-Team: Nepali\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+1 -1
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: nl\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-02-09 00:58\n"
"PO-Revision-Date: 2026-02-09 22:31\n"
"Last-Translator: \n"
"Language-Team: Dutch\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+1 -1
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: no\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-02-09 00:58\n"
"PO-Revision-Date: 2026-02-09 22:31\n"
"Last-Translator: \n"
"Language-Team: Norwegian\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+1 -1
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: or\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-02-09 00:58\n"
"PO-Revision-Date: 2026-02-09 22:31\n"
"Last-Translator: \n"
"Language-Team: Odia\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+1 -1
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: pl\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-02-09 00:58\n"
"PO-Revision-Date: 2026-02-09 22:31\n"
"Last-Translator: \n"
"Language-Team: Polish\n"
"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n"
+1 -1
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: pt\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-02-09 00:58\n"
"PO-Revision-Date: 2026-02-09 22:31\n"
"Last-Translator: \n"
"Language-Team: Portuguese, Brazilian\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+1 -1
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: pt\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-02-09 00:58\n"
"PO-Revision-Date: 2026-02-09 22:31\n"
"Last-Translator: \n"
"Language-Team: Portuguese\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+1 -1
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: ro\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-02-09 00:58\n"
"PO-Revision-Date: 2026-02-09 22:30\n"
"Last-Translator: \n"
"Language-Team: Romanian\n"
"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : (n==0 || (n%100>0 && n%100<20)) ? 1 : 2);\n"
+1 -1
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: ru\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-02-09 00:58\n"
"PO-Revision-Date: 2026-02-09 22:31\n"
"Last-Translator: \n"
"Language-Team: Russian\n"
"Plural-Forms: nplurals=4; plural=((n%10==1 && n%100!=11) ? 0 : ((n%10 >= 2 && n%10 <=4 && (n%100 < 12 || n%100 > 14)) ? 1 : ((n%10 == 0 || (n%10 >= 5 && n%10 <=9)) || (n%100 >= 11 && n%100 <= 14)) ? 2 : 3));\n"
+1 -1
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: sk\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-02-09 00:58\n"
"PO-Revision-Date: 2026-02-09 22:31\n"
"Last-Translator: \n"
"Language-Team: Slovak\n"
"Plural-Forms: nplurals=4; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 3;\n"
+1 -44
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: sl\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-02-09 00:58\n"
"PO-Revision-Date: 2026-02-09 22:31\n"
"Last-Translator: \n"
"Language-Team: Slovenian\n"
"Plural-Forms: nplurals=4; plural=n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3;\n"
@@ -211,15 +211,6 @@ msgstr "Dodaj vrstico pred"
msgid "Added"
msgstr ""
#. placeholder {0}: passkey.createdAt.toLocaleDateString()
#: src/routes/dashboard/settings/authentication/-components/passkeys.tsx
msgid "Added on {0}"
msgstr "Dodano {0}"
#: src/routes/dashboard/settings/authentication/-components/passkeys.tsx
msgid "Adding your passkey..."
msgstr "Dodajanje vaše ključa za prijavo..."
#: src/utils/locale.ts
msgid "Afrikaans"
msgstr "Afriščina"
@@ -313,10 +304,6 @@ msgstr ""
msgid "Are you sure you want to delete this API key?"
msgstr "Ali ste prepričani, da želite izbrisati ta API ključ?"
#: src/routes/dashboard/settings/authentication/-components/passkeys.tsx
msgid "Are you sure you want to delete this passkey?"
msgstr "Ali ste prepričani, da želite izbrisati ta ključ za prijavo?"
#: src/routes/builder/$resumeId/-components/header.tsx
#: src/routes/dashboard/resumes/-components/menus/context-menu.tsx
#: src/routes/dashboard/resumes/-components/menus/dropdown-menu.tsx
@@ -840,10 +827,6 @@ msgstr "Brišem vaš račun..."
msgid "Deleting your API key..."
msgstr "Brišem vaš API ključ ..."
#: src/routes/dashboard/settings/authentication/-components/passkeys.tsx
msgid "Deleting your passkey..."
msgstr "Brišem vaš varnostni ključ..."
#: src/routes/builder/$resumeId/-components/header.tsx
#: src/routes/dashboard/resumes/-components/menus/context-menu.tsx
#: src/routes/dashboard/resumes/-components/menus/dropdown-menu.tsx
@@ -1701,10 +1684,6 @@ msgstr "Stran {pageNumber} od {totalNumberOfPages}"
msgid "Paragraph"
msgstr "Odstavek"
#: src/routes/dashboard/settings/authentication/-components/passkeys.tsx
msgid "Passkeys"
msgstr "Ključi za preverjanje pristnosti"
#: src/routes/_home/-sections/features.tsx
msgid "Passkeys & 2FA"
msgstr "Ključi za prijavo in dvofaktorska avtentikacija"
@@ -1904,10 +1883,6 @@ msgstr "Reference"
msgid "Refresh"
msgstr "Osveži"
#: src/routes/dashboard/settings/authentication/-components/passkeys.tsx
msgid "Register New Device"
msgstr "Registriraj novo napravo"
#: src/routes/auth/forgot-password.tsx
msgid "Remember your password? <0><1>Sign in now <2/></1></0>"
msgstr "Se spomnite gesla? <0><1>Prijavite se zdaj <2/></1></0>"
@@ -2141,7 +2116,6 @@ msgstr "Prijavite se z GitHub, Google ali poljubnim ponudnikom OAuth."
msgid "Sign up"
msgstr "Registracija"
#: src/routes/auth/-components/social-auth.tsx
#: src/routes/auth/-components/social-auth.tsx
#: src/routes/auth/-components/social-auth.tsx
#: src/routes/auth/login.tsx
@@ -2741,10 +2715,6 @@ msgstr "Prostovoljec"
msgid "Website"
msgstr "Spletna stran"
#: src/routes/dashboard/settings/authentication/-components/passkeys.tsx
msgid "What do you want to call this passkey?"
msgstr "Kako bi želeli poimenovati ta dostopni ključ?"
#: src/routes/builder/$resumeId/-sidebar/left/shared/section-menu.tsx
msgid "What do you want to rename this section to?"
msgstr "Kako želite preimenovati ta razdelek?"
@@ -2791,11 +2761,6 @@ msgstr "Za prijavo lahko uporabite tudi svoje uporabniško ime."
msgid "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!"
msgstr "Svoj življenjepis lahko delite prek edinstvenega javnega URL-ja, ga zaščitite z geslom ali prenesete kot PDF za neposredno deljenje. Izbira je vaša!"
#. placeholder {0}: passkey.name ?? "(WebAuthn Device)"
#: src/routes/dashboard/settings/authentication/-components/passkeys.tsx
msgid "You cannot use the passkey \"{0}\" anymore to sign in after deletion. This action cannot be undone."
msgstr "Ključa \"{0}\" po izbrisu ne boste mogli več uporabiti za prijavo. To dejanje je nepovratno."
#: src/hooks/use-form-blocker.tsx
msgid "You have unsaved changes that will be lost."
msgstr ""
@@ -2829,14 +2794,6 @@ msgstr "Vaši podatki so shranjeni lokalno"
msgid "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."
msgstr "Vaši podatki so varno shranjeni in se nikoli ne delijo s tretjimi osebami. Reactive Življenjepis lahko tudi gostite sami na svojih strežnikih za popoln nadzor nad svojimi podatki."
#: src/routes/dashboard/settings/authentication/-components/passkeys.tsx
msgid "Your passkey has been added successfully."
msgstr "Vaš dostopni ključ je bil uspešno dodan."
#: src/routes/dashboard/settings/authentication/-components/passkeys.tsx
msgid "Your passkey has been deleted successfully."
msgstr "Vaš dostopni ključ je bil uspešno izbrisan."
#: src/routes/auth/reset-password.tsx
msgid "Your password has been reset successfully. You can now sign in with your new password."
msgstr "Vaše geslo je bilo uspešno ponastavljeno. Zdaj se lahko prijavite z novim geslom."
+1 -1
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: sq\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-02-09 00:58\n"
"PO-Revision-Date: 2026-02-09 22:31\n"
"Last-Translator: \n"
"Language-Team: Albanian\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+1 -1
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: sr\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-02-09 00:58\n"
"PO-Revision-Date: 2026-02-09 22:31\n"
"Last-Translator: \n"
"Language-Team: Serbian (Cyrillic)\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
+1 -1
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: sv\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-02-09 00:58\n"
"PO-Revision-Date: 2026-02-09 22:31\n"
"Last-Translator: \n"
"Language-Team: Swedish\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+1 -1
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: ta\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-02-09 00:58\n"
"PO-Revision-Date: 2026-02-09 22:31\n"
"Last-Translator: \n"
"Language-Team: Tamil\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+1 -1
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: te\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-02-09 00:58\n"
"PO-Revision-Date: 2026-02-09 22:31\n"
"Last-Translator: \n"
"Language-Team: Telugu\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+1 -1
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: th\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-02-09 00:58\n"
"PO-Revision-Date: 2026-02-09 22:31\n"
"Last-Translator: \n"
"Language-Team: Thai\n"
"Plural-Forms: nplurals=1; plural=0;\n"
+1 -1
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: tr\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-02-09 00:58\n"
"PO-Revision-Date: 2026-02-09 22:31\n"
"Last-Translator: \n"
"Language-Team: Turkish\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+1 -1
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: uk\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-02-09 00:58\n"
"PO-Revision-Date: 2026-02-09 22:31\n"
"Last-Translator: \n"
"Language-Team: Ukrainian\n"
"Plural-Forms: nplurals=4; plural=((n%10==1 && n%100!=11) ? 0 : ((n%10 >= 2 && n%10 <=4 && (n%100 < 12 || n%100 > 14)) ? 1 : ((n%10 == 0 || (n%10 >= 5 && n%10 <=9)) || (n%100 >= 11 && n%100 <= 14)) ? 2 : 3));\n"
+1 -1
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: uz\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-02-09 00:58\n"
"PO-Revision-Date: 2026-02-09 22:31\n"
"Last-Translator: \n"
"Language-Team: Uzbek\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+1 -1
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: vi\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-02-09 00:58\n"
"PO-Revision-Date: 2026-02-09 22:31\n"
"Last-Translator: \n"
"Language-Team: Vietnamese\n"
"Plural-Forms: nplurals=1; plural=0;\n"
+1 -1
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: zh\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-02-09 00:58\n"
"PO-Revision-Date: 2026-02-09 22:31\n"
"Last-Translator: \n"
"Language-Team: Chinese Simplified\n"
"Plural-Forms: nplurals=1; plural=0;\n"
+1 -1
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: zh\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-02-09 00:58\n"
"PO-Revision-Date: 2026-02-09 22:31\n"
"Last-Translator: \n"
"Language-Team: Chinese Traditional\n"
"Plural-Forms: nplurals=1; plural=0;\n"
@@ -0,0 +1,6 @@
DROP INDEX "user_email_index";--> statement-breakpoint
DROP INDEX "user_username_index";--> statement-breakpoint
DROP INDEX "resume_statistics_resume_id_index";--> statement-breakpoint
DROP INDEX "verification_identifier_index";--> statement-breakpoint
CREATE INDEX "resume_created_at_index" ON "resume" ("created_at");--> statement-breakpoint
CREATE INDEX "user_created_at_index" ON "user" ("created_at");
File diff suppressed because it is too large Load Diff
+54 -55
View File
@@ -1,10 +1,10 @@
{
"name": "reactive-resume",
"description": "Reactive Resume is a free and open-source resume builder that simplifies the process of creating, updating, and sharing your resume.",
"version": "5.0.9",
"version": "5.0.10",
"license": "MIT",
"type": "module",
"packageManager": "pnpm@10.29.2+sha512.bef43fa759d91fd2da4b319a5a0d13ef7a45bb985a3d7342058470f9d2051a3ba8674e629672654686ef9443ad13a82da2beb9eeb3e0221c87b8154fff9d74b8",
"packageManager": "pnpm@10.30.2+sha512.36cdc707e7b7940a988c9c1ecf88d084f8514b5c3f085f53a2e244c2921d3b2545bc20dd4ebe1fc245feec463bb298aecea7a63ed1f7680b877dc6379d8d0cb4",
"repository": {
"type": "git",
"url": "https://github.com/amruthpillai/reactive-resume.git"
@@ -31,79 +31,78 @@
"typecheck": "tsgo --noEmit"
},
"dependencies": {
"@ai-sdk/anthropic": "^3.0.39",
"@ai-sdk/google": "^3.0.23",
"@ai-sdk/openai": "^3.0.26",
"@ai-sdk/react": "^3.0.80",
"@aws-sdk/client-s3": "^3.986.0",
"@better-auth/core": "1.5.0-beta.13",
"@ai-sdk/anthropic": "^3.0.46",
"@ai-sdk/google": "^3.0.30",
"@ai-sdk/openai": "^3.0.31",
"@ai-sdk/react": "^3.0.99",
"@aws-sdk/client-s3": "^3.996.0",
"@better-auth/core": "1.5.0-beta.18",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@fontsource-variable/ibm-plex-sans": "^5.2.8",
"@hookform/resolvers": "^5.2.2",
"@lingui/core": "^5.9.0",
"@lingui/react": "^5.9.0",
"@lingui/core": "^5.9.2",
"@lingui/react": "^5.9.2",
"@monaco-editor/react": "4.8.0-rc.3",
"@orpc/client": "^1.13.4",
"@orpc/json-schema": "^1.13.4",
"@orpc/openapi": "^1.13.4",
"@orpc/server": "^1.13.4",
"@orpc/tanstack-query": "^1.13.4",
"@orpc/zod": "^1.13.4",
"@orpc/client": "^1.13.5",
"@orpc/json-schema": "^1.13.5",
"@orpc/openapi": "^1.13.5",
"@orpc/server": "^1.13.5",
"@orpc/tanstack-query": "^1.13.5",
"@orpc/zod": "^1.13.5",
"@phosphor-icons/react": "^2.1.10",
"@phosphor-icons/web": "^2.1.2",
"@sindresorhus/slugify": "^3.0.0",
"@t3-oss/env-core": "^0.13.10",
"@tanstack/react-query": "5.90.20",
"@tanstack/react-router": "^1.159.5",
"@tanstack/react-router-ssr-query": "^1.159.5",
"@tanstack/react-start": "^1.159.5",
"@tanstack/zod-adapter": "^1.159.5",
"@tiptap/extension-highlight": "^3.19.0",
"@tiptap/extension-table": "^3.19.0",
"@tiptap/extension-text-align": "^3.19.0",
"@tiptap/pm": "^3.19.0",
"@tiptap/react": "^3.19.0",
"@tiptap/starter-kit": "^3.19.0",
"ai": "^6.0.78",
"ai-sdk-ollama": "^3.5.0",
"@tanstack/react-query": "5.90.21",
"@tanstack/react-router": "^1.162.8",
"@tanstack/react-router-ssr-query": "^1.162.8",
"@tanstack/react-start": "^1.162.8",
"@tanstack/zod-adapter": "^1.162.8",
"@tiptap/extension-highlight": "^3.20.0",
"@tiptap/extension-table": "^3.20.0",
"@tiptap/extension-text-align": "^3.20.0",
"@tiptap/pm": "^3.20.0",
"@tiptap/react": "^3.20.0",
"@tiptap/starter-kit": "^3.20.0",
"ai": "^6.0.97",
"ai-sdk-ollama": "^3.7.1",
"bcrypt": "^6.0.0",
"better-auth": "1.5.0-beta.13",
"better-auth": "1.5.0-beta.18",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"dompurify": "^3.3.1",
"drizzle-orm": "^1.0.0-beta.15-859cf75",
"drizzle-zod": "^0.8.3",
"@modelcontextprotocol/sdk": "^1.26.0",
"@modelcontextprotocol/sdk": "^1.27.0",
"es-toolkit": "^1.44.0",
"fast-deep-equal": "^3.1.3",
"fast-json-patch": "^3.1.1",
"fuse.js": "^7.1.0",
"immer": "^11.1.3",
"immer": "^11.1.4",
"input-otp": "^1.4.2",
"js-cookie": "^3.0.5",
"monaco-editor": "^0.55.1",
"motion": "^12.34.0",
"motion": "^12.34.3",
"nodemailer": "^8.0.1",
"pg": "^8.18.0",
"puppeteer-core": "^24.37.2",
"puppeteer-core": "^24.37.5",
"qrcode.react": "^4.2.0",
"radix-ui": "^1.4.3",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-hook-form": "^7.71.1",
"react-hook-form": "^7.71.2",
"react-hotkeys-hook": "^5.2.4",
"react-resizable-panels": "^4.6.2",
"react-window": "^2.2.6",
"react-resizable-panels": "^4.6.5",
"react-window": "^2.2.7",
"react-zoom-pan-pinch": "^3.7.0",
"shadcn": "^3.8.4",
"shadcn": "^3.8.5",
"sharp": "^0.34.5",
"sonner": "^2.0.7",
"tailwind-merge": "^3.4.0",
"tailwindcss": "^4.1.18",
"tiny-invariant": "^1.3.3",
"tailwind-merge": "^3.5.0",
"tailwindcss": "^4.2.1",
"ts-pattern": "^5.9.0",
"tw-animate-css": "^1.4.0",
"unique-names-generator": "^4.7.1",
@@ -115,34 +114,34 @@
"zustand": "^5.0.11"
},
"devDependencies": {
"@biomejs/biome": "^2.3.14",
"@lingui/babel-plugin-lingui-macro": "^5.9.0",
"@lingui/cli": "^5.9.0",
"@lingui/vite-plugin": "^5.9.0",
"@tailwindcss/vite": "^4.1.18",
"@biomejs/biome": "^2.4.4",
"@lingui/babel-plugin-lingui-macro": "^5.9.2",
"@lingui/cli": "^5.9.2",
"@lingui/vite-plugin": "^5.9.2",
"@tailwindcss/vite": "^4.2.1",
"@types/bcrypt": "^6.0.0",
"@types/js-cookie": "^3.0.6",
"@types/node": "^25.2.2",
"@types/nodemailer": "^7.0.9",
"@types/node": "^25.3.0",
"@types/nodemailer": "^7.0.11",
"@types/pg": "^8.16.0",
"@types/react": "^19.2.13",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@typescript/native-preview": "7.0.0-dev.20260209.1",
"@vitejs/plugin-react": "^5.1.3",
"@typescript/native-preview": "7.0.0-dev.20260224.1",
"@vitejs/plugin-react": "^5.1.4",
"babel-plugin-macros": "^3.1.0",
"drizzle-kit": "^1.0.0-beta.15-859cf75",
"knip": "^5.83.1",
"knip": "^5.85.0",
"nitro": "npm:nitro-nightly@latest",
"node-addon-api": "^8.5.0",
"node-gyp": "^12.2.0",
"npm-check-updates": "^19.3.2",
"npm-check-updates": "^19.4.1",
"tsx": "^4.21.0",
"vite": "^8.0.0-beta.13",
"vite": "^8.0.0-beta.15",
"vite-plugin-pwa": "^1.2.0"
},
"pnpm": {
"overrides": {
"vite": "^8.0.0-beta.13"
"vite": "^8.0.0-beta.15"
},
"onlyBuiltDependencies": [
"@prisma/engines",
+1789 -1982
View File
File diff suppressed because it is too large Load Diff
+8 -8
View File
@@ -33,10 +33,10 @@ export const Spotlight = ({
<motion.div
animate={{ x: [0, xOffset, 0] }}
transition={{ duration, repeat: Infinity, repeatType: "reverse", ease: "easeInOut" }}
className="pointer-events-none absolute start-0 top-0 z-40 h-svh w-svw"
className="pointer-events-none absolute inset-s-0 top-0 z-40 h-svh w-svw"
>
<div
className="absolute start-0 top-0"
className="absolute inset-s-0 top-0"
style={{
width: `${width}px`,
height: `${height}px`,
@@ -46,7 +46,7 @@ export const Spotlight = ({
/>
<div
className="absolute start-0 top-0 origin-top-left"
className="absolute inset-s-0 top-0 origin-top-left"
style={{
height: `${height}px`,
width: `${smallWidth}px`,
@@ -56,7 +56,7 @@ export const Spotlight = ({
/>
<div
className="absolute start-0 top-0 origin-top-left"
className="absolute inset-s-0 top-0 origin-top-left"
style={{
height: `${height}px`,
width: `${smallWidth}px`,
@@ -68,7 +68,7 @@ export const Spotlight = ({
<motion.div
animate={{ x: [0, -xOffset, 0] }}
className="pointer-events-none absolute end-0 top-0 z-40 h-svh w-svw"
className="pointer-events-none absolute inset-e-0 top-0 z-40 h-svh w-svw"
transition={{
duration,
repeat: Infinity,
@@ -77,7 +77,7 @@ export const Spotlight = ({
}}
>
<div
className="absolute end-0 top-0"
className="absolute inset-e-0 top-0"
style={{
width: `${width}px`,
height: `${height}px`,
@@ -87,7 +87,7 @@ export const Spotlight = ({
/>
<div
className="absolute end-0 top-0 origin-top-right"
className="absolute inset-e-0 top-0 origin-top-right"
style={{
height: `${height}px`,
width: `${smallWidth}px`,
@@ -97,7 +97,7 @@ export const Spotlight = ({
/>
<div
className="absolute end-0 top-0 origin-top-right"
className="absolute inset-e-0 top-0 origin-top-right"
style={{
height: `${height}px`,
width: `${smallWidth}px`,
+78 -39
View File
@@ -1,6 +1,22 @@
import {
closestCenter,
DndContext,
type DragEndEvent,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
} from "@dnd-kit/core";
import {
horizontalListSortingStrategy,
SortableContext,
sortableKeyboardCoordinates,
useSortable,
} from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { Trans } from "@lingui/react/macro";
import { PencilSimpleIcon, XIcon } from "@phosphor-icons/react";
import { motion, Reorder, useDragControls } from "motion/react";
import { motion } from "motion/react";
import * as React from "react";
import { Badge } from "@/components/ui/badge";
import { Input } from "@/components/ui/input";
@@ -12,6 +28,7 @@ const RETURN_KEY = "Enter";
const COMMA_KEY = ",";
type ChipItemProps = {
id: string;
chip: string;
index: number;
isEditing: boolean;
@@ -19,35 +36,37 @@ type ChipItemProps = {
onRemove: (index: number) => void;
};
function ChipItem({ chip, index, isEditing, onEdit, onRemove }: ChipItemProps) {
const controls = useDragControls();
function ChipItem({ id, chip, index, isEditing, onEdit, onRemove }: ChipItemProps) {
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id });
const [isHovered, setIsHovered] = React.useState(false);
const style = {
transform: CSS.Transform.toString(transform),
transition,
zIndex: isDragging ? 10 : undefined,
opacity: isDragging ? 0.6 : 1,
};
return (
<Reorder.Item
value={chip}
dragListener={false}
dragControls={controls}
initial={{ opacity: 1, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.9 }}
className="relative"
<div
style={style}
ref={setNodeRef}
className="relative touch-none"
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
{...attributes}
{...listeners}
>
<Badge
variant="outline"
className={cn(
"flex h-7 cursor-grab select-none items-center gap-0 overflow-hidden px-3 active:cursor-grabbing",
isEditing && "border-primary ring-1 ring-primary",
isDragging && "opacity-80",
)}
onPointerDown={(e) => {
e.preventDefault();
controls.start(e);
}}
>
<span className="max-w-32 truncate">{chip}</span>
<motion.div
initial={false}
animate={{ width: isHovered ? 40 : 0, marginInlineStart: isHovered ? 8 : 0, opacity: isHovered ? 1 : 0 }}
@@ -80,7 +99,7 @@ function ChipItem({ chip, index, isEditing, onEdit, onRemove }: ChipItemProps) {
</button>
</motion.div>
</Badge>
</Reorder.Item>
</div>
);
}
@@ -116,7 +135,6 @@ export function ChipInput({ value, defaultValue = [], onChange, className, ...pr
const trimmed = newValue.trim();
if (!trimmed || index < 0 || index >= chips.length) return;
// Check if the new value already exists at a different index
const existingIndex = chips.findIndex((c, i) => c === trimmed && i !== index);
if (existingIndex !== -1) return;
@@ -133,12 +151,10 @@ export function ChipInput({ value, defaultValue = [], onChange, className, ...pr
const newChips = chips.slice(0, index).concat(chips.slice(index + 1));
setChips(newChips);
// If we were editing this chip, clear edit mode
if (editingIndex === index) {
setEditingIndex(null);
setInput("");
} else if (editingIndex !== null && editingIndex > index) {
// Adjust editing index if we removed a chip before it
setEditingIndex(editingIndex - 1);
}
},
@@ -156,7 +172,6 @@ export function ChipInput({ value, defaultValue = [], onChange, className, ...pr
const handleReorder = React.useCallback(
(newOrder: string[]) => {
// Update editingIndex to follow the chip being edited
if (editingIndex !== null) {
const editingChip = chips[editingIndex];
const newIndex = newOrder.indexOf(editingChip);
@@ -169,14 +184,37 @@ export function ChipInput({ value, defaultValue = [], onChange, className, ...pr
[chips, editingIndex, setChips],
);
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: { distance: 3 },
}),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
}),
);
const handleDragEnd = React.useCallback(
(event: DragEndEvent) => {
const { active, over } = event;
if (!over || active.id === over.id) return;
const oldIndex = chips.indexOf(active.id as string);
const newIndex = chips.indexOf(over.id as string);
if (oldIndex !== -1 && newIndex !== -1 && oldIndex !== newIndex) {
const newOrder = Array.from(chips);
const [removed] = newOrder.splice(oldIndex, 1);
newOrder.splice(newIndex, 0, removed);
handleReorder(newOrder);
}
},
[chips, handleReorder],
);
const handleInputChange = React.useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const newValue = e.target.value;
// When editing, don't split on comma - allow full text editing
if (editingIndex !== null) {
if (newValue.includes(",")) {
// Save current edit
updateChip(editingIndex, newValue.replace(",", ""));
setEditingIndex(null);
setInput("");
@@ -186,7 +224,6 @@ export function ChipInput({ value, defaultValue = [], onChange, className, ...pr
return;
}
// When adding new chips, split on comma
if (newValue.includes(",")) {
const parts = newValue.split(",");
parts.slice(0, -1).forEach(addChip);
@@ -204,19 +241,16 @@ export function ChipInput({ value, defaultValue = [], onChange, className, ...pr
e.preventDefault();
if (editingIndex !== null) {
// Save edit
if (input.trim()) {
updateChip(editingIndex, input);
}
setEditingIndex(null);
setInput("");
} else if (input.trim()) {
// Add new chip
addChip(input);
setInput("");
}
} else if (e.key === "Escape" && editingIndex !== null) {
// Cancel edit
setEditingIndex(null);
setInput("");
}
@@ -227,18 +261,23 @@ export function ChipInput({ value, defaultValue = [], onChange, className, ...pr
return (
<div className={cn("flex flex-col gap-2", className)} {...props}>
{chips.length > 0 && (
<Reorder.Group axis="x" values={chips} onReorder={handleReorder} className="flex flex-wrap gap-1.5">
{chips.map((chip, idx) => (
<ChipItem
key={`${chip}-${idx}`}
chip={chip}
index={idx}
isEditing={editingIndex === idx}
onEdit={handleEdit}
onRemove={removeChip}
/>
))}
</Reorder.Group>
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
<SortableContext items={chips} strategy={horizontalListSortingStrategy}>
<div className="flex flex-wrap gap-1.5">
{chips.map((chip, idx) => (
<ChipItem
key={`${chip}-${idx}`}
id={chip}
chip={chip}
index={idx}
isEditing={editingIndex === idx}
onEdit={handleEdit}
onRemove={removeChip}
/>
))}
</div>
</SortableContext>
</DndContext>
)}
<div className="flex flex-col gap-2">
+2 -2
View File
@@ -167,11 +167,11 @@
}
strong {
font-weight: var(--page-body-font-weight-bold);
font-weight: var(--page-body-font-weight-bold, bold);
}
a {
font-weight: var(--page-body-font-weight-bold);
font-weight: var(--page-body-font-weight-bold, bold);
text-decoration: underline;
text-underline-offset: 0.15rem;
}
@@ -11,8 +11,8 @@ export function BreakpointIndicator({ position = "bottom-right" }: BreakpointInd
const isBottom = position.includes("bottom");
const top = isTop ? "top-0" : "bottom-0";
const right = isRight ? "end-0" : "start-0";
const left = isLeft ? "start-0" : "end-0";
const right = isRight ? "inset-e-0" : "inset-s-0";
const left = isLeft ? "inset-s-0" : "inset-e-0";
const bottom = isBottom ? "bottom-0" : "top-0";
return (
+2 -2
View File
@@ -147,7 +147,7 @@ function PageContainer({ pageIndex, pageLayout, pageClassName, showPageNumbers =
return (
<div data-page-index={pageIndex} className="relative">
{showPageNumbers && totalNumberOfPages > 1 && (
<div className="absolute start-0 -top-6 print:hidden">
<div className="absolute inset-s-0 -top-6 print:hidden">
<span className="font-medium text-foreground text-xs">
<Trans>
Page {pageNumber} of {totalNumberOfPages}
@@ -161,7 +161,7 @@ function PageContainer({ pageIndex, pageLayout, pageClassName, showPageNumbers =
</div>
{metadata.page.format !== "free-form" && pageHeight > maxPageHeight && (
<div className="absolute start-0 top-full mt-4 print:hidden">
<div className="absolute inset-s-0 top-full mt-4 print:hidden">
<a
rel="noopener"
target="_blank"
@@ -174,7 +174,7 @@ export function getSectionComponent(
return (
<section className={cn(`page-section page-section-custom page-section-${id}`, sectionClassName)}>
{customSection.type !== "summary" && customSection.type !== "cover-letter" && (
{customSection.type !== "cover-letter" && (
<h6 className="mb-1.5 text-(--page-primary-color)">{customSection.title}</h6>
)}
@@ -38,7 +38,7 @@ export function ChikoritaTemplate({ pageIndex, pageLayout }: TemplateProps) {
<div className="template-chikorita page-content">
{/* Sidebar Background */}
{!fullWidth && (
<div className="page-sidebar-background pointer-events-none absolute inset-y-0 z-0 w-(--page-sidebar-width) shrink-0 bg-(--page-primary-color) ltr:end-0 rtl:start-0" />
<div className="page-sidebar-background pointer-events-none absolute inset-y-0 z-0 w-(--page-sidebar-width) shrink-0 bg-(--page-primary-color) ltr:inset-e-0 rtl:inset-s-0" />
)}
<div className="flex">
+1 -1
View File
@@ -37,7 +37,7 @@ export function DitgarTemplate({ pageIndex, pageLayout }: TemplateProps) {
<div className="template-ditgar page-content">
{/* Sidebar Background */}
{(!fullWidth || isFirstPage) && (
<div className="page-sidebar-background pointer-events-none absolute inset-y-0 z-0 w-(--page-sidebar-width) shrink-0 bg-(--page-primary-color)/20 ltr:start-0 rtl:end-0" />
<div className="page-sidebar-background pointer-events-none absolute inset-y-0 z-0 w-(--page-sidebar-width) shrink-0 bg-(--page-primary-color)/20 ltr:inset-s-0 rtl:inset-e-0" />
)}
<div className="flex">
+1 -1
View File
@@ -27,7 +27,7 @@ export function GengarTemplate({ pageIndex, pageLayout }: TemplateProps) {
<div className="template-gengar page-content">
{/* Sidebar Background */}
{(!fullWidth || isFirstPage) && (
<div className="page-sidebar-background pointer-events-none absolute inset-y-0 z-0 w-(--page-sidebar-width) shrink-0 bg-(--page-primary-color)/20 ltr:start-0 rtl:end-0" />
<div className="page-sidebar-background pointer-events-none absolute inset-y-0 z-0 w-(--page-sidebar-width) shrink-0 bg-(--page-primary-color)/20 ltr:inset-s-0 rtl:inset-e-0" />
)}
<div className="flex">
+1 -1
View File
@@ -27,7 +27,7 @@ export function GlalieTemplate({ pageIndex, pageLayout }: TemplateProps) {
<div className="template-glalie page-content">
{/* Sidebar Background */}
{(!fullWidth || isFirstPage) && (
<div className="page-sidebar-background pointer-events-none absolute inset-y-0 z-0 w-(--page-sidebar-width) shrink-0 bg-(--page-primary-color)/20 ltr:start-0 rtl:end-0" />
<div className="page-sidebar-background pointer-events-none absolute inset-y-0 z-0 w-(--page-sidebar-width) shrink-0 bg-(--page-primary-color)/20 ltr:inset-s-0 rtl:inset-e-0" />
)}
<div className="flex">
@@ -14,6 +14,9 @@ const sectionClassName = cn(
// Section Heading
"[&>h6]:-mt-(--heading-negative-margin) [&>h6]:max-w-fit [&>h6]:bg-(--page-background-color) [&>h6]:px-4",
// Push the first section of a page down, to avoid clipping the header
"group-data-[layout=main]:first-of-type:mt-4",
);
/**
+1 -1
View File
@@ -49,7 +49,7 @@ function AlertDescription({ className, ...props }: React.ComponentProps<"div">)
}
function AlertAction({ className, ...props }: React.ComponentProps<"div">) {
return <div data-slot="alert-action" className={cn("absolute end-3 top-2.5", className)} {...props} />;
return <div data-slot="alert-action" className={cn("absolute inset-e-3 top-2.5", className)} {...props} />;
}
export { Alert, AlertTitle, AlertDescription, AlertAction };
+1 -1
View File
@@ -50,7 +50,7 @@ function AvatarBadge({ className, ...props }: React.ComponentProps<"span">) {
<span
data-slot="avatar-badge"
className={cn(
"absolute end-0 bottom-0 z-10 inline-flex select-none items-center justify-center rounded-full bg-primary text-primary-foreground bg-blend-color ring-2 ring-background",
"absolute inset-e-0 bottom-0 z-10 inline-flex select-none items-center justify-center rounded-full bg-primary text-primary-foreground bg-blend-color ring-2 ring-background",
"group-data-[size=sm]/avatar:size-2 group-data-[size=sm]/avatar:[&>svg]:hidden",
"group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2",
"group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2",
+2 -2
View File
@@ -129,7 +129,7 @@ function ContextMenuCheckboxItem({
checked={checked}
{...props}
>
<span className="pointer-events-none absolute end-2">
<span className="pointer-events-none absolute inset-e-2">
<ContextMenuPrimitive.ItemIndicator>
<CheckIcon />
</ContextMenuPrimitive.ItemIndicator>
@@ -153,7 +153,7 @@ function ContextMenuRadioItem({
)}
{...props}
>
<span className="pointer-events-none absolute end-2">
<span className="pointer-events-none absolute inset-e-2">
<ContextMenuPrimitive.ItemIndicator>
<CheckIcon />
</ContextMenuPrimitive.ItemIndicator>
+2 -2
View File
@@ -102,7 +102,7 @@ function DropdownMenuCheckboxItem({ className, children, checked, disabled, ...p
checked={checked}
{...props}
>
<span className="pointer-events-none absolute start-2 flex size-3.5 items-center justify-center">
<span className="pointer-events-none absolute inset-s-2 flex size-3.5 items-center justify-center">
<DropdownMenuItemIndicatorPrimitive initial={{ opacity: 0, scale: 0.5 }} animate={{ opacity: 1, scale: 1 }}>
<CheckIcon className="size-4" />
</DropdownMenuItemIndicatorPrimitive>
@@ -131,7 +131,7 @@ function DropdownMenuRadioItem({ className, children, disabled, ...props }: Drop
)}
{...props}
>
<span className="pointer-events-none absolute start-2 flex size-3.5 items-center justify-center">
<span className="pointer-events-none absolute inset-s-2 flex size-3.5 items-center justify-center">
<DropdownMenuItemIndicatorPrimitive layoutId="dropdown-menu-item-indicator-radio">
<CircleIcon className="size-2 fill-current" />
</DropdownMenuItemIndicatorPrimitive>
+1 -1
View File
@@ -27,7 +27,7 @@ function ResizableSeparator({
<ResizablePrimitive.Separator
data-slot="resizable-handle"
className={cn(
"relative flex w-px items-center justify-center bg-transparent transition-colors after:absolute after:inset-y-0 after:start-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-hidden data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[separator='active']:bg-sky-700 data-[separator='hover']:bg-sky-700 data-[panel-group-direction=vertical]:after:start-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:translate-x-0 data-[panel-group-direction=vertical]:after:-translate-y-1/2 [&[data-panel-group-direction=vertical]>div]:rotate-90",
"relative flex w-px items-center justify-center bg-transparent transition-colors after:absolute after:inset-s-1/2 after:inset-y-0 after:w-1 after:-translate-x-1/2 focus-visible:outline-hidden data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[separator='active']:bg-sky-700 data-[separator='hover']:bg-sky-700 data-[panel-group-direction=vertical]:after:inset-s-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:translate-x-0 data-[panel-group-direction=vertical]:after:-translate-y-1/2 [&[data-panel-group-direction=vertical]>div]:rotate-90",
className,
)}
{...props}
+2 -2
View File
@@ -50,7 +50,7 @@ function SheetContent({
data-slot="sheet-content"
data-side={side}
className={cn(
"data-[side=right]:data-closed:slide-out-to-end-10 data-[side=right]:data-open:slide-in-from-end-10 data-[side=left]:data-closed:slide-out-to-start-10 data-[side=left]:data-open:slide-in-from-start-10 data-[side=top]:data-closed:slide-out-to-top-10 data-[side=top]:data-open:slide-in-from-top-10 data-closed:fade-out-0 data-open:fade-in-0 data-[side=bottom]:data-closed:slide-out-to-bottom-10 data-[side=bottom]:data-open:slide-in-from-bottom-10 fixed z-50 flex flex-col gap-4 bg-background bg-clip-padding text-sm shadow-lg transition duration-200 ease-in-out data-[side=bottom]:inset-x-0 data-[side=top]:inset-x-0 data-[side=left]:inset-y-0 data-[side=right]:inset-y-0 data-[side=left]:start-0 data-[side=right]:end-0 data-[side=top]:top-0 data-[side=bottom]:bottom-0 data-[side=bottom]:h-auto data-[side=left]:h-full data-[side=right]:h-full data-[side=top]:h-auto data-[side=left]:w-3/4 data-[side=right]:w-3/4 data-closed:animate-out data-open:animate-in data-[side=bottom]:border-t data-[side=left]:border-r data-[side=top]:border-b data-[side=right]:border-l data-[side=left]:sm:max-w-sm data-[side=right]:sm:max-w-sm",
"data-[side=right]:data-closed:slide-out-to-end-10 data-[side=right]:data-open:slide-in-from-end-10 data-[side=left]:data-closed:slide-out-to-start-10 data-[side=left]:data-open:slide-in-from-start-10 data-[side=top]:data-closed:slide-out-to-top-10 data-[side=top]:data-open:slide-in-from-top-10 data-closed:fade-out-0 data-open:fade-in-0 data-[side=bottom]:data-closed:slide-out-to-bottom-10 data-[side=bottom]:data-open:slide-in-from-bottom-10 fixed z-50 flex flex-col gap-4 bg-background bg-clip-padding text-sm shadow-lg transition duration-200 ease-in-out data-[side=left]:inset-s-0 data-[side=right]:inset-e-0 data-[side=bottom]:inset-x-0 data-[side=top]:inset-x-0 data-[side=left]:inset-y-0 data-[side=right]:inset-y-0 data-[side=top]:top-0 data-[side=bottom]:bottom-0 data-[side=bottom]:h-auto data-[side=left]:h-full data-[side=right]:h-full data-[side=top]:h-auto data-[side=left]:w-3/4 data-[side=right]:w-3/4 data-closed:animate-out data-open:animate-in data-[side=bottom]:border-t data-[side=left]:border-r data-[side=top]:border-b data-[side=right]:border-l data-[side=left]:sm:max-w-sm data-[side=right]:sm:max-w-sm",
className,
)}
{...props}
@@ -58,7 +58,7 @@ function SheetContent({
{children}
{showCloseButton && (
<SheetPrimitive.Close data-slot="sheet-close" asChild>
<Button variant="ghost" className="absolute end-4 top-4" size="icon-sm">
<Button variant="ghost" className="absolute inset-e-4 top-4" size="icon-sm">
<XIcon />
<span className="sr-only">Close</span>
</Button>
+8 -8
View File
@@ -203,8 +203,8 @@ function Sidebar({
className={cn(
"fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] ease-in-out md:flex",
side === "left"
? "start-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
: "end-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
? "inset-s-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
: "inset-e-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
variant === "floating" || variant === "inset"
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]"
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l",
@@ -258,12 +258,12 @@ function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
onClick={toggleSidebar}
title="Toggle Sidebar"
className={cn(
"absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-in-out after:absolute after:inset-y-0 after:start-1/2 after:w-[2px] hover:after:bg-sidebar-border group-data-[side=right]:start-0 group-data-[side=left]:-end-4 sm:flex",
"absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-in-out after:absolute after:inset-s-1/2 after:inset-y-0 after:w-[2px] hover:after:bg-sidebar-border group-data-[side=left]:-inset-e-4 group-data-[side=right]:inset-s-0 sm:flex",
"in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize",
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
"group-data-[collapsible=offcanvas]:translate-x-0 hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:after:left-full",
"[[data-side=left][data-collapsible=offcanvas]_&]:-end-2",
"[[data-side=right][data-collapsible=offcanvas]_&]:-start-2",
"[[data-side=left][data-collapsible=offcanvas]_&]:-inset-e-2",
"[[data-side=right][data-collapsible=offcanvas]_&]:-inset-s-2",
className,
)}
{...props}
@@ -387,7 +387,7 @@ function SidebarGroupAction({
data-slot="sidebar-group-action"
data-sidebar="group-action"
className={cn(
"absolute end-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-hidden ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
"absolute inset-e-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-hidden ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
"after:absolute after:-inset-2 md:after:hidden",
"group-data-[collapsible=icon]:hidden",
className,
@@ -512,7 +512,7 @@ function SidebarMenuAction({
data-slot="sidebar-menu-action"
data-sidebar="menu-action"
className={cn(
"absolute end-1 top-1.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-hidden ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 peer-hover/menu-button:text-sidebar-accent-foreground [&>svg]:size-4 [&>svg]:shrink-0",
"absolute inset-e-1 top-1.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-hidden ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 peer-hover/menu-button:text-sidebar-accent-foreground [&>svg]:size-4 [&>svg]:shrink-0",
"after:absolute after:-inset-2 md:after:hidden",
"peer-data-[size=sm]/menu-button:top-1",
"peer-data-[size=default]/menu-button:top-1.5",
@@ -533,7 +533,7 @@ function SidebarMenuBadge({ className, ...props }: React.ComponentProps<"div">)
data-slot="sidebar-menu-badge"
data-sidebar="menu-badge"
className={cn(
"pointer-events-none absolute end-1 flex h-5 min-w-5 select-none items-center justify-center rounded-md px-1 font-medium text-sidebar-foreground text-xs tabular-nums",
"pointer-events-none absolute inset-e-1 flex h-5 min-w-5 select-none items-center justify-center rounded-md px-1 font-medium text-sidebar-foreground text-xs tabular-nums",
"peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground",
"peer-data-[size=sm]/menu-button:top-1",
"peer-data-[size=default]/menu-button:top-1.5",
+3 -3
View File
@@ -34,7 +34,7 @@ function Switch({ className, pressedWidth = 20, startIcon, endIcon, thumbIcon, .
{thumbIcon && (
<SwitchIconPrimitive
position="thumb"
className="-translate-1/2 absolute start-1/2 top-1/2 text-neutral-400 dark:text-neutral-500 [&_svg]:size-[9px]"
className="-translate-1/2 absolute inset-s-1/2 top-1/2 text-neutral-400 dark:text-neutral-500 [&_svg]:size-[9px]"
>
{thumbIcon}
</SwitchIconPrimitive>
@@ -44,7 +44,7 @@ function Switch({ className, pressedWidth = 20, startIcon, endIcon, thumbIcon, .
{startIcon && (
<SwitchIconPrimitive
position="left"
className="absolute start-0.5 top-1/2 -translate-y-1/2 text-neutral-400 dark:text-neutral-500 [&_svg]:size-[9px]"
className="absolute inset-s-0.5 top-1/2 -translate-y-1/2 text-neutral-400 dark:text-neutral-500 [&_svg]:size-[9px]"
>
{startIcon}
</SwitchIconPrimitive>
@@ -52,7 +52,7 @@ function Switch({ className, pressedWidth = 20, startIcon, endIcon, thumbIcon, .
{endIcon && (
<SwitchIconPrimitive
position="right"
className="absolute end-0.5 top-1/2 -translate-y-1/2 text-neutral-500 dark:text-neutral-400 [&_svg]:size-[9px]"
className="absolute inset-e-0.5 top-1/2 -translate-y-1/2 text-neutral-500 dark:text-neutral-400 [&_svg]:size-[9px]"
>
{endIcon}
</SwitchIconPrimitive>
+1 -1
View File
@@ -52,7 +52,7 @@ function TabsTrigger({ className, ...props }: React.ComponentProps<typeof TabsPr
"relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 whitespace-nowrap rounded-full border border-transparent px-3 py-1 font-medium text-foreground/60 text-sm transition-all hover:text-foreground focus-visible:border-ring focus-visible:outline-1 focus-visible:outline-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start group-data-[variant=default]/tabs-list:data-active:shadow-sm group-data-[variant=line]/tabs-list:data-active:shadow-none dark:text-muted-foreground dark:hover:text-foreground [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent",
"data-active:bg-background data-active:text-foreground dark:data-active:border-input dark:data-active:bg-input/30 dark:data-active:text-foreground",
"after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-end-1 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100",
"after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-[orientation=vertical]/tabs:after:-inset-e-1 group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100",
className,
)}
{...props}
+28 -24
View File
@@ -1,10 +1,14 @@
import { zodResolver } from "@hookform/resolvers/zod";
import type { MessageDescriptor } from "@lingui/core";
import { msg } from "@lingui/core/macro";
import { useLingui } from "@lingui/react";
import { Trans } from "@lingui/react/macro";
import { PencilSimpleLineIcon, PlusIcon } from "@phosphor-icons/react";
import { useForm, useFormContext } from "react-hook-form";
import type z from "zod";
import { useResumeStore } from "@/components/resume/store/resume";
import { Button } from "@/components/ui/button";
import { Combobox } from "@/components/ui/combobox";
import { DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
@@ -18,21 +22,21 @@ const formSchema = customSectionSchema;
type FormValues = z.infer<typeof formSchema>;
const SECTION_TYPE_OPTIONS: { value: CustomSectionType; label: string }[] = [
{ value: "summary", label: "Summary" },
{ value: "experience", label: "Experience" },
{ value: "education", label: "Education" },
{ value: "projects", label: "Projects" },
{ value: "profiles", label: "Profiles" },
{ value: "skills", label: "Skills" },
{ value: "languages", label: "Languages" },
{ value: "interests", label: "Interests" },
{ value: "awards", label: "Awards" },
{ value: "certifications", label: "Certifications" },
{ value: "publications", label: "Publications" },
{ value: "volunteer", label: "Volunteer" },
{ value: "references", label: "References" },
{ value: "cover-letter", label: "Cover Letter" },
const SECTION_TYPE_OPTIONS: { value: CustomSectionType; label: MessageDescriptor }[] = [
{ value: "summary", label: msg`Summary` },
{ value: "experience", label: msg`Experience` },
{ value: "education", label: msg`Education` },
{ value: "projects", label: msg`Projects` },
{ value: "profiles", label: msg`Profiles` },
{ value: "skills", label: msg`Skills` },
{ value: "languages", label: msg`Languages` },
{ value: "interests", label: msg`Interests` },
{ value: "awards", label: msg`Awards` },
{ value: "certifications", label: msg`Certifications` },
{ value: "publications", label: msg`Publications` },
{ value: "volunteer", label: msg`Volunteer` },
{ value: "references", label: msg`References` },
{ value: "cover-letter", label: msg`Cover Letter` },
];
export function CreateCustomSectionDialog({ data }: DialogProps<"resume.sections.custom.create">) {
@@ -150,6 +154,7 @@ export function UpdateCustomSectionDialog({ data }: DialogProps<"resume.sections
}
function CustomSectionForm({ isUpdate = false }: { isUpdate?: boolean }) {
const { i18n } = useLingui();
const form = useFormContext<FormValues>();
return (
@@ -179,17 +184,16 @@ function CustomSectionForm({ isUpdate = false }: { isUpdate?: boolean }) {
<Trans>Section Type</Trans>
</FormLabel>
<FormControl>
<select
<Combobox
{...field}
value={field.value}
disabled={isUpdate}
className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-xs transition-colors file:border-0 file:bg-transparent file:font-medium file:text-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
>
{SECTION_TYPE_OPTIONS.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
onValueChange={field.onChange}
options={SECTION_TYPE_OPTIONS.map((option) => ({
value: option.value,
label: i18n.t(option.label),
}))}
/>
</FormControl>
<FormMessage />
</FormItem>
+19 -3
View File
@@ -3,7 +3,6 @@ import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { betterAuth } from "better-auth/minimal";
import { apiKey, type GenericOAuthConfig, genericOAuth, openAPI, twoFactor } from "better-auth/plugins";
import { username } from "better-auth/plugins/username";
import { tanstackStartCookies } from "better-auth/tanstack-start";
import { and, eq, or } from "drizzle-orm";
import { db } from "@/integrations/drizzle/client";
import { env } from "@/utils/env";
@@ -20,6 +19,24 @@ function isCustomOAuthProviderEnabled() {
return Boolean(env.OAUTH_CLIENT_ID) && Boolean(env.OAUTH_CLIENT_SECRET) && (hasDiscovery || hasManual);
}
function getTrustedOrigins(): string[] {
const appUrl = new URL(env.APP_URL);
const trustedOrigins = new Set<string>([appUrl.origin.replace(/\/$/, "")]);
const LOCAL_ORIGINS = ["localhost", "127.0.0.1"];
if (LOCAL_ORIGINS.includes(appUrl.hostname)) {
for (const hostname of LOCAL_ORIGINS) {
if (hostname !== appUrl.hostname) {
const altUrl = new URL(env.APP_URL);
altUrl.hostname = hostname;
trustedOrigins.add(altUrl.origin.replace(/\/$/, ""));
}
}
}
return Array.from(trustedOrigins);
}
const getAuthConfig = () => {
const authConfigs: GenericOAuthConfig[] = [];
@@ -69,7 +86,7 @@ const getAuthConfig = () => {
database: drizzleAdapter(db, { schema, provider: "pg" }),
telemetry: { enabled: false },
trustedOrigins: [env.APP_URL],
trustedOrigins: getTrustedOrigins(),
advanced: {
database: { generateId },
useSecureCookies: env.APP_URL.startsWith("https://"),
@@ -229,7 +246,6 @@ const getAuthConfig = () => {
}),
twoFactor({ issuer: "Reactive Resume" }),
genericOAuth({ config: authConfigs }),
tanstackStartCookies(),
],
});
};
+40 -47
View File
@@ -24,7 +24,7 @@ export const user = pg.pgTable(
.defaultNow()
.$onUpdate(() => /* @__PURE__ */ new Date()),
},
(t) => [pg.index().on(t.email), pg.index().on(t.username)],
(t) => [pg.index().on(t.createdAt.asc())],
);
export const session = pg.pgTable(
@@ -84,26 +84,22 @@ export const account = pg.pgTable(
(t) => [pg.index().on(t.userId)],
);
export const verification = pg.pgTable(
"verification",
{
id: pg
.uuid("id")
.notNull()
.primaryKey()
.$defaultFn(() => generateId()),
identifier: pg.text("identifier").notNull().unique(),
value: pg.text("value").notNull(),
expiresAt: pg.timestamp("expires_at", { withTimezone: true }).notNull(),
createdAt: pg.timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: pg
.timestamp("updated_at", { withTimezone: true })
.notNull()
.defaultNow()
.$onUpdate(() => /* @__PURE__ */ new Date()),
},
(t) => [pg.index().on(t.identifier)],
);
export const verification = pg.pgTable("verification", {
id: pg
.uuid("id")
.notNull()
.primaryKey()
.$defaultFn(() => generateId()),
identifier: pg.text("identifier").notNull().unique(),
value: pg.text("value").notNull(),
expiresAt: pg.timestamp("expires_at", { withTimezone: true }).notNull(),
createdAt: pg.timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: pg
.timestamp("updated_at", { withTimezone: true })
.notNull()
.defaultNow()
.$onUpdate(() => /* @__PURE__ */ new Date()),
});
export const twoFactor = pg.pgTable(
"two_factor",
@@ -192,37 +188,34 @@ export const resume = pg.pgTable(
(t) => [
pg.unique().on(t.slug, t.userId),
pg.index().on(t.userId),
pg.index().on(t.createdAt.asc()),
pg.index().on(t.userId, t.updatedAt.desc()),
pg.index().on(t.isPublic, t.slug, t.userId),
],
);
export const resumeStatistics = pg.pgTable(
"resume_statistics",
{
id: pg
.uuid("id")
.notNull()
.primaryKey()
.$defaultFn(() => generateId()),
views: pg.integer("views").notNull().default(0),
downloads: pg.integer("downloads").notNull().default(0),
lastViewedAt: pg.timestamp("last_viewed_at", { withTimezone: true }),
lastDownloadedAt: pg.timestamp("last_downloaded_at", { withTimezone: true }),
resumeId: pg
.uuid("resume_id")
.unique()
.notNull()
.references(() => resume.id, { onDelete: "cascade" }),
createdAt: pg.timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: pg
.timestamp("updated_at", { withTimezone: true })
.notNull()
.defaultNow()
.$onUpdate(() => /* @__PURE__ */ new Date()),
},
(t) => [pg.index().on(t.resumeId)],
);
export const resumeStatistics = pg.pgTable("resume_statistics", {
id: pg
.uuid("id")
.notNull()
.primaryKey()
.$defaultFn(() => generateId()),
views: pg.integer("views").notNull().default(0),
downloads: pg.integer("downloads").notNull().default(0),
lastViewedAt: pg.timestamp("last_viewed_at", { withTimezone: true }),
lastDownloadedAt: pg.timestamp("last_downloaded_at", { withTimezone: true }),
resumeId: pg
.uuid("resume_id")
.unique()
.notNull()
.references(() => resume.id, { onDelete: "cascade" }),
createdAt: pg.timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: pg
.timestamp("updated_at", { withTimezone: true })
.notNull()
.defaultNow()
.$onUpdate(() => /* @__PURE__ */ new Date()),
});
export const apikey = pg.pgTable(
"apikey",
+1 -2
View File
@@ -1,6 +1,6 @@
import { createORPCClient, onError } from "@orpc/client";
import { RPCLink } from "@orpc/client/fetch";
import { BatchLinkPlugin, SimpleCsrfProtectionLinkPlugin } from "@orpc/client/plugins";
import { BatchLinkPlugin } from "@orpc/client/plugins";
import { createRouterClient, type InferRouterInputs, type InferRouterOutputs, type RouterClient } from "@orpc/server";
import { createTanstackQueryUtils } from "@orpc/tanstack-query";
import { createIsomorphicFn } from "@tanstack/react-start";
@@ -32,7 +32,6 @@ export const getORPCClient = createIsomorphicFn()
url: `${window.location.origin}/api/rpc`,
fetch: (request, init) => fetch(request, { ...init, credentials: "include" }),
plugins: [
new SimpleCsrfProtectionLinkPlugin(),
new BatchLinkPlugin({
mode: typeof window === "undefined" ? "buffered" : "streaming",
groups: [{ condition: () => true, context: {} }],
+1 -1
View File
@@ -78,7 +78,7 @@ export const storageRouter = {
},
})
.handler(async ({ context, input }): Promise<void> => {
// The filename is now the full path from the URL (e.g., "uploads/userId/pictures/timestamp.webp")
// The filename is now the full path from the URL (e.g., "uploads/userId/pictures/timestamp.ext")
// We need to extract just the path portion that matches the storage key
const key = input.filename.startsWith("uploads/")
? input.filename
+1 -1
View File
@@ -173,7 +173,7 @@ export const printerService = {
? getComputedStyle(container).getPropertyValue("--page-height").trim()
: null;
const currentHeight = containerHeight || rootHeight;
const heightValue = Math.max(Number.parseFloat(currentHeight), minPageHeight);
const heightValue = Math.min(Number.parseFloat(currentHeight), minPageHeight);
if (!Number.isNaN(heightValue)) {
// Subtract top + bottom margins from page height
+10 -1
View File
@@ -7,7 +7,6 @@ import {
PutObjectCommand,
S3Client,
} from "@aws-sdk/client-s3";
import sharp from "sharp";
import { env } from "@/utils/env";
interface StorageWriteInput {
@@ -90,6 +89,16 @@ interface ProcessedImage {
export async function processImageForUpload(file: File): Promise<ProcessedImage> {
const fileBuffer = await file.arrayBuffer();
console.log("FLAG_DISABLE_IMAGE_PROCESSING", env.FLAG_DISABLE_IMAGE_PROCESSING);
if (env.FLAG_DISABLE_IMAGE_PROCESSING) {
return {
data: new Uint8Array(fileBuffer),
contentType: file.type,
};
}
const sharp = (await import("sharp")).default;
const processedBuffer = await sharp(fileBuffer)
.resize(800, 800, { fit: "inside", withoutEnlargement: true })
.webp({ preset: "picture" })
+9 -13
View File
@@ -16,18 +16,14 @@ import { cn } from "@/utils/style";
export const Route = createFileRoute("/$username/$slug")({
component: RouteComponent,
loader: async ({ context, params: { username, slug } }) => {
try {
// Ignore .well-known requests
if (username === ".well-known") throw notFound();
// Ignore .well-known requests
if (username === ".well-known") throw notFound();
const resume = await context.queryClient.ensureQueryData(
orpc.resume.getBySlug.queryOptions({ input: { username, slug } }),
);
const resume = await context.queryClient.ensureQueryData(
orpc.resume.getBySlug.queryOptions({ input: { username, slug } }),
);
return { resume };
} catch {
throw notFound();
}
return { resume };
},
head: ({ loaderData }) => ({
meta: [{ title: loaderData ? `${loaderData.resume.name} - Reactive Resume` : "Reactive Resume" }],
@@ -69,7 +65,7 @@ function RouteComponent() {
const handleDownload = useCallback(async () => {
if (!resume) return;
const { url } = await printResumeAsPDF({ id: resume.id });
downloadFromUrl(url, `resume-${resume.name}.pdf`);
downloadFromUrl(url, `${resume.name}.pdf`);
}, [resume, printResumeAsPDF]);
if (!isReady) return <LoadingScreen />;
@@ -79,14 +75,14 @@ function RouteComponent() {
<div
className={cn("mx-auto max-w-[210mm]", "print:m-0 print:block print:max-w-full print:px-0", "md:my-4 md:px-4")}
>
<ResumePreview pageClassName="print:w-full! w-full max-w-full" />
<ResumePreview className="space-y-4" pageClassName="print:w-full! w-full max-w-full" />
</div>
<Button
size="lg"
variant="secondary"
disabled={isPrinting}
className="fixed end-4 bottom-4 z-50 hidden rounded-full px-4 md:inline-flex print:hidden"
className="fixed inset-e-4 bottom-4 z-50 hidden rounded-full px-4 md:inline-flex print:hidden"
onClick={handleDownload}
>
{isPrinting ? <Spinner /> : <DownloadSimpleIcon />}
+4 -4
View File
@@ -131,8 +131,8 @@ export const DonationBanner = () => (
<FloatingIcon icon={HeartIcon} className="bottom-[20%] left-[20%]" delay={2.5} />
{/* Gradient Orbs */}
<div className="absolute -start-32 top-1/4 size-64 rounded-full bg-primary/5 blur-3xl" />
<div className="absolute -end-32 bottom-1/4 size-64 rounded-full bg-rose-500/5 blur-3xl" />
<div className="absolute -inset-s-32 top-1/4 size-64 rounded-full bg-primary/5 blur-3xl" />
<div className="absolute -inset-e-32 bottom-1/4 size-64 rounded-full bg-rose-500/5 blur-3xl" />
</div>
<div className="container relative px-8">
@@ -146,8 +146,8 @@ export const DonationBanner = () => (
>
<div aria-hidden="true" className="relative mb-6">
<PulsingHeart />
<SparkleEffect className="-end-4 -top-2" />
<SparkleEffect className="-start-3 bottom-0" />
<SparkleEffect className="-inset-e-4 -top-2" />
<SparkleEffect className="-inset-s-3 bottom-0" />
</div>
<motion.h2
+2 -2
View File
@@ -35,7 +35,7 @@ const getCommunityLinks = (): FooterLinkItem[] => [
{ url: "https://github.com/amruthpillai/reactive-resume/issues", label: t`Report an issue` },
{ url: "https://crowdin.com/project/reactive-resume", label: t`Translations` },
{ url: "https://reddit.com/r/reactiveresume", label: t`Subreddit` },
{ url: "https://discord.gg/EE8yFqW4", label: t`Discord` },
{ url: "https://discord.gg/aSyA5ZSxpb", label: t`Discord` },
];
const socialLinks: SocialLink[] = [
@@ -134,7 +134,7 @@ function FooterLink({ url, label }: FooterLinkItem) {
initial={{ width: 0, opacity: 0 }}
animate={isHovered ? { width: "100%", opacity: 1 } : { width: 0, opacity: 0 }}
transition={{ duration: 0.25, ease: "easeOut" }}
className="pointer-events-none absolute start-0 -bottom-0.5 h-px rounded bg-primary"
className="pointer-events-none absolute inset-s-0 -bottom-0.5 h-px rounded bg-primary"
/>
</a>
</li>
+1 -1
View File
@@ -119,7 +119,7 @@ export function Hero() {
<motion.div
aria-hidden="true"
role="presentation"
className="absolute start-1/2 bottom-8 -translate-x-1/2"
className="absolute inset-s-1/2 bottom-8 -translate-x-1/2"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 2, duration: 1 }}
+2 -2
View File
@@ -7,8 +7,8 @@ export function Prefooter() {
<section id="prefooter" className="relative overflow-hidden py-16 md:py-24">
{/* Background decoration */}
<div aria-hidden="true" className="pointer-events-none absolute inset-0">
<div className="absolute start-1/4 top-0 size-96 rounded-full bg-primary/5 blur-3xl" />
<div className="absolute end-1/4 bottom-0 size-96 rounded-full bg-primary/5 blur-3xl" />
<div className="absolute inset-s-1/4 top-0 size-96 rounded-full bg-primary/5 blur-3xl" />
<div className="absolute inset-e-1/4 bottom-0 size-96 rounded-full bg-primary/5 blur-3xl" />
</div>
<div className="relative space-y-8">
+1 -1
View File
@@ -47,7 +47,7 @@ function StatisticCard({ statistic, index }: StatisticCardProps) {
{/* Background decoration */}
<div aria-hidden="true" className="pointer-events-none absolute inset-0 overflow-hidden">
<motion.div
className="absolute start-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 text-primary/2"
className="absolute inset-s-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 text-primary/2"
initial={{ scale: 0.8, opacity: 0 }}
whileInView={{ scale: 1, opacity: 1 }}
viewport={{ once: true }}
+1 -1
View File
@@ -23,7 +23,7 @@ function TemplateItem({ metadata }: TemplateItemProps) {
<div className="absolute inset-0 bg-linear-to-t from-black/70 via-black/20 to-transparent opacity-0 transition-opacity duration-300 group-hover:opacity-100" />
{/* Template name on hover */}
<div className="absolute start-0 end-0 bottom-0 translate-y-full p-4 transition-transform duration-300 group-hover:translate-y-0">
<div className="absolute inset-x-0 bottom-0 translate-y-full p-4 transition-transform duration-300 group-hover:translate-y-0">
<p className="font-semibold text-white drop-shadow-lg">{metadata.name}</p>
</div>

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