mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-07-12 22:14:54 +10:00
fixes #2727
This commit is contained in:
+1
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
const nextPackages = ["@monaco-editor/react"];
|
||||
|
||||
const betaPackages = ["vite", "drizzle-orm", "drizzle-kit", "@better-auth/core", "@better-auth/passkey", "better-auth"];
|
||||
const betaPackages = ["vite", "drizzle-orm", "drizzle-kit"];
|
||||
|
||||
/** @type {import('npm-check-updates').RunOptions} */
|
||||
module.exports = {
|
||||
|
||||
Vendored
+8
-5
@@ -1,4 +1,7 @@
|
||||
{
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
},
|
||||
"biome.enabled": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.biome": "explicit",
|
||||
@@ -15,16 +18,16 @@
|
||||
},
|
||||
"files.watcherExclude": {
|
||||
"**/routeTree.gen.ts": true,
|
||||
"pnpm-lock.yaml": true,
|
||||
"locales/**.po": true
|
||||
"locales/**.po": true,
|
||||
"pnpm-lock.yaml": true
|
||||
},
|
||||
"i18n-ally.enabledParsers": ["po"],
|
||||
"i18n-ally.localesPaths": ["locales"],
|
||||
"i18n-ally.sourceLanguage": "en-US",
|
||||
"search.exclude": {
|
||||
"**/routeTree.gen.ts": true,
|
||||
"pnpm-lock.yaml": true,
|
||||
"locales/**.po": true
|
||||
"locales/**.po": true,
|
||||
"pnpm-lock.yaml": true
|
||||
},
|
||||
"tailwindCSS.classFunctions": ["cn", "cva"],
|
||||
"tailwindCSS.experimental.classRegex": [
|
||||
@@ -33,5 +36,5 @@
|
||||
["cn\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"]
|
||||
],
|
||||
"tailwindCSS.experimental.configFile": "src/styles/globals.css",
|
||||
"typescript.experimental.useTsgo": true
|
||||
"typescript.tsdk": "node_modules/typescript/lib"
|
||||
}
|
||||
|
||||
@@ -1,189 +0,0 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
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
|
||||
|
||||
```bash
|
||||
# Start development server (runs on port 3000)
|
||||
pnpm dev
|
||||
|
||||
# Build for production
|
||||
pnpm build
|
||||
|
||||
# Start production server
|
||||
pnpm start
|
||||
|
||||
# Linting and formatting (uses Biome)
|
||||
pnpm lint
|
||||
|
||||
# Type checking (uses tsgo)
|
||||
pnpm typecheck
|
||||
|
||||
# Database operations
|
||||
pnpm db:generate # Generate migration files
|
||||
pnpm db:migrate # Run migrations
|
||||
pnpm db:push # Push schema changes directly
|
||||
pnpm db:studio # Open Drizzle Studio
|
||||
|
||||
# Extract i18n strings for translation
|
||||
pnpm lingui:extract
|
||||
|
||||
# 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
|
||||
2. Start required services: `docker compose -f compose.dev.yml up -d`
|
||||
- PostgreSQL (port 5432)
|
||||
- Browserless/Chromium for PDF generation (port 4000)
|
||||
- SeaweedFS for S3-compatible storage (port 8333)
|
||||
- Mailpit for email testing (ports 1025, 8025)
|
||||
- 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, 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
|
||||
- `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 (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
|
||||
|
||||
The resume schema is defined in `src/schema/resume/data.ts`. Key concepts:
|
||||
- **ResumeData** - Complete resume data including basics, sections, customSections, metadata
|
||||
- **Sections** - Built-in sections (profiles, experience, education, skills, etc.)
|
||||
- **CustomSections** - User-created sections that follow one of the built-in section types
|
||||
- **Metadata** - Template, layout, typography, design settings, custom CSS
|
||||
|
||||
### Resume Templates
|
||||
|
||||
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 tracking
|
||||
|
||||
### Routing
|
||||
|
||||
Uses TanStack Router with file-based routing. Key routes:
|
||||
- `/_home/` - Public landing pages
|
||||
- `/auth/` - Authentication flows
|
||||
- `/dashboard/` - User dashboard and resume management
|
||||
- `/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 (`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'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
|
||||
|
||||
Key variables (see `.env.example` for full list):
|
||||
- `APP_URL` - Application URL
|
||||
- `DATABASE_URL` - PostgreSQL connection string
|
||||
- `AUTH_SECRET` - Secret for authentication
|
||||
- `PRINTER_ENDPOINT` - WebSocket endpoint for PDF printer service
|
||||
- `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
|
||||
+19
-20
@@ -4,7 +4,7 @@
|
||||
"version": "5.0.10",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"packageManager": "pnpm@10.30.2+sha512.36cdc707e7b7940a988c9c1ecf88d084f8514b5c3f085f53a2e244c2921d3b2545bc20dd4ebe1fc245feec463bb298aecea7a63ed1f7680b877dc6379d8d0cb4",
|
||||
"packageManager": "pnpm@10.30.3",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/amruthpillai/reactive-resume.git"
|
||||
@@ -28,15 +28,14 @@
|
||||
"lingui:extract": "lingui extract --clean --overwrite",
|
||||
"lint": "biome check --write",
|
||||
"start": "node .output/server/index.mjs",
|
||||
"typecheck": "tsgo --noEmit"
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@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",
|
||||
"@ai-sdk/anthropic": "^3.0.47",
|
||||
"@ai-sdk/google": "^3.0.33",
|
||||
"@ai-sdk/openai": "^3.0.36",
|
||||
"@ai-sdk/react": "^3.0.105",
|
||||
"@aws-sdk/client-s3": "^3.999.0",
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
@@ -44,6 +43,7 @@
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"@lingui/core": "^5.9.2",
|
||||
"@lingui/react": "^5.9.2",
|
||||
"@modelcontextprotocol/sdk": "^1.27.1",
|
||||
"@monaco-editor/react": "4.8.0-rc.3",
|
||||
"@orpc/client": "^1.13.5",
|
||||
"@orpc/json-schema": "^1.13.5",
|
||||
@@ -56,27 +56,26 @@
|
||||
"@sindresorhus/slugify": "^3.0.0",
|
||||
"@t3-oss/env-core": "^0.13.10",
|
||||
"@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",
|
||||
"@tanstack/react-router": "^1.163.2",
|
||||
"@tanstack/react-router-ssr-query": "^1.163.2",
|
||||
"@tanstack/react-start": "^1.163.2",
|
||||
"@tanstack/zod-adapter": "^1.163.2",
|
||||
"@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",
|
||||
"ai": "^6.0.103",
|
||||
"ai-sdk-ollama": "^3.8.0",
|
||||
"bcrypt": "^6.0.0",
|
||||
"better-auth": "1.5.0-beta.18",
|
||||
"better-auth": "v1.4.19",
|
||||
"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.27.0",
|
||||
"es-toolkit": "^1.44.0",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fast-json-patch": "^3.1.1",
|
||||
@@ -87,7 +86,7 @@
|
||||
"monaco-editor": "^0.55.1",
|
||||
"motion": "^12.34.3",
|
||||
"nodemailer": "^8.0.1",
|
||||
"pg": "^8.18.0",
|
||||
"pg": "^8.19.0",
|
||||
"puppeteer-core": "^24.37.5",
|
||||
"qrcode.react": "^4.2.0",
|
||||
"radix-ui": "^1.4.3",
|
||||
@@ -121,12 +120,11 @@
|
||||
"@tailwindcss/vite": "^4.2.1",
|
||||
"@types/bcrypt": "^6.0.0",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/node": "^25.3.0",
|
||||
"@types/node": "^25.3.2",
|
||||
"@types/nodemailer": "^7.0.11",
|
||||
"@types/pg": "^8.16.0",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.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",
|
||||
@@ -134,8 +132,9 @@
|
||||
"nitro": "npm:nitro-nightly@latest",
|
||||
"node-addon-api": "^8.5.0",
|
||||
"node-gyp": "^12.2.0",
|
||||
"npm-check-updates": "^19.4.1",
|
||||
"npm-check-updates": "^19.6.2",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^8.0.0-beta.15",
|
||||
"vite-plugin-pwa": "^1.2.0"
|
||||
},
|
||||
|
||||
Generated
+991
-1094
File diff suppressed because it is too large
Load Diff
@@ -55,114 +55,129 @@ function renderItemByType(type: CustomSectionType, item: CustomSectionItem, item
|
||||
.exhaustive();
|
||||
}
|
||||
|
||||
type SectionProps = { id: string };
|
||||
|
||||
export function getSectionComponent(
|
||||
section: "summary" | SectionType | (string & {}),
|
||||
{ sectionClassName, itemClassName }: SectionComponentProps = {},
|
||||
) {
|
||||
return match(section)
|
||||
.with("summary", () => {
|
||||
const SummarySection = ({ id: _id }: { id: string }) => <PageSummary className={sectionClassName} />;
|
||||
const SummarySection = (_: SectionProps) => <PageSummary className={sectionClassName} />;
|
||||
|
||||
return SummarySection;
|
||||
})
|
||||
.with("profiles", () => {
|
||||
const ProfilesSection = ({ id: _id }: { id: string }) => (
|
||||
const ProfilesSection = (_: SectionProps) => (
|
||||
<PageSection type="profiles" className={sectionClassName}>
|
||||
{(item) => <ProfilesItem {...item} className={itemClassName} />}
|
||||
</PageSection>
|
||||
);
|
||||
|
||||
return ProfilesSection;
|
||||
})
|
||||
.with("experience", () => {
|
||||
const ExperienceSection = ({ id: _id }: { id: string }) => (
|
||||
const ExperienceSection = (_: SectionProps) => (
|
||||
<PageSection type="experience" className={sectionClassName}>
|
||||
{(item) => <ExperienceItem {...item} className={itemClassName} />}
|
||||
</PageSection>
|
||||
);
|
||||
|
||||
return ExperienceSection;
|
||||
})
|
||||
.with("education", () => {
|
||||
const EducationSection = ({ id: _id }: { id: string }) => (
|
||||
const EducationSection = (_: SectionProps) => (
|
||||
<PageSection type="education" className={sectionClassName}>
|
||||
{(item) => <EducationItem {...item} className={itemClassName} />}
|
||||
</PageSection>
|
||||
);
|
||||
|
||||
return EducationSection;
|
||||
})
|
||||
.with("projects", () => {
|
||||
const ProjectsSection = ({ id: _id }: { id: string }) => (
|
||||
const ProjectsSection = (_: SectionProps) => (
|
||||
<PageSection type="projects" className={sectionClassName}>
|
||||
{(item) => <ProjectsItem {...item} className={itemClassName} />}
|
||||
</PageSection>
|
||||
);
|
||||
|
||||
return ProjectsSection;
|
||||
})
|
||||
.with("skills", () => {
|
||||
const SkillsSection = ({ id: _id }: { id: string }) => (
|
||||
const SkillsSection = (_: SectionProps) => (
|
||||
<PageSection type="skills" className={sectionClassName}>
|
||||
{(item) => <SkillsItem {...item} className={itemClassName} />}
|
||||
</PageSection>
|
||||
);
|
||||
|
||||
return SkillsSection;
|
||||
})
|
||||
.with("languages", () => {
|
||||
const LanguagesSection = ({ id: _id }: { id: string }) => (
|
||||
const LanguagesSection = (_: SectionProps) => (
|
||||
<PageSection type="languages" className={sectionClassName}>
|
||||
{(item) => <LanguagesItem {...item} className={itemClassName} />}
|
||||
</PageSection>
|
||||
);
|
||||
|
||||
return LanguagesSection;
|
||||
})
|
||||
.with("interests", () => {
|
||||
const InterestsSection = ({ id: _id }: { id: string }) => (
|
||||
const InterestsSection = (_: SectionProps) => (
|
||||
<PageSection type="interests" className={sectionClassName}>
|
||||
{(item) => <InterestsItem {...item} className={itemClassName} />}
|
||||
</PageSection>
|
||||
);
|
||||
|
||||
return InterestsSection;
|
||||
})
|
||||
.with("awards", () => {
|
||||
const AwardsSection = ({ id: _id }: { id: string }) => (
|
||||
const AwardsSection = (_: SectionProps) => (
|
||||
<PageSection type="awards" className={sectionClassName}>
|
||||
{(item) => <AwardsItem {...item} className={itemClassName} />}
|
||||
</PageSection>
|
||||
);
|
||||
|
||||
return AwardsSection;
|
||||
})
|
||||
.with("certifications", () => {
|
||||
const CertificationsSection = ({ id: _id }: { id: string }) => (
|
||||
const CertificationsSection = (_: SectionProps) => (
|
||||
<PageSection type="certifications" className={sectionClassName}>
|
||||
{(item) => <CertificationsItem {...item} className={itemClassName} />}
|
||||
</PageSection>
|
||||
);
|
||||
|
||||
return CertificationsSection;
|
||||
})
|
||||
.with("publications", () => {
|
||||
const PublicationsSection = ({ id: _id }: { id: string }) => (
|
||||
const PublicationsSection = (_: SectionProps) => (
|
||||
<PageSection type="publications" className={sectionClassName}>
|
||||
{(item) => <PublicationsItem {...item} className={itemClassName} />}
|
||||
</PageSection>
|
||||
);
|
||||
|
||||
return PublicationsSection;
|
||||
})
|
||||
.with("volunteer", () => {
|
||||
const VolunteerSection = ({ id: _id }: { id: string }) => (
|
||||
const VolunteerSection = (_: SectionProps) => (
|
||||
<PageSection type="volunteer" className={sectionClassName}>
|
||||
{(item) => <VolunteerItem {...item} className={itemClassName} />}
|
||||
</PageSection>
|
||||
);
|
||||
|
||||
return VolunteerSection;
|
||||
})
|
||||
.with("references", () => {
|
||||
const ReferencesSection = ({ id: _id }: { id: string }) => (
|
||||
const ReferencesSection = (_: SectionProps) => (
|
||||
<PageSection type="references" className={sectionClassName}>
|
||||
{(item) => <ReferencesItem {...item} className={itemClassName} />}
|
||||
</PageSection>
|
||||
);
|
||||
|
||||
return ReferencesSection;
|
||||
})
|
||||
.otherwise(() => {
|
||||
// Custom section - render based on its type
|
||||
const CustomSectionComponent = ({ id }: { id: string }) => {
|
||||
const CustomSectionComponent = ({ id }: SectionProps) => {
|
||||
const customSection = useResumeStore((state) => state.resume.data.customSections.find((s) => s.id === id));
|
||||
|
||||
if (!customSection) return null;
|
||||
|
||||
@@ -11,15 +11,10 @@ type PageSummaryProps = {
|
||||
export function PageSummary({ className }: PageSummaryProps) {
|
||||
const section = useResumeStore((state) => state.resume.data.summary);
|
||||
|
||||
if (section.hidden || !stripHtml(section.content)) return null;
|
||||
|
||||
return (
|
||||
<section
|
||||
className={cn(
|
||||
"page-section page-section-summary",
|
||||
section.hidden && "hidden",
|
||||
!stripHtml(section.content) && "hidden",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<section className={cn("page-section page-section-summary", className)}>
|
||||
<h6 className="mb-1.5 text-(--page-primary-color)">{section.title || getSectionTitle("summary")}</h6>
|
||||
|
||||
<div className="section-content">
|
||||
|
||||
@@ -29,10 +29,6 @@ export function DitgarTemplate({ pageIndex, pageLayout }: TemplateProps) {
|
||||
const isFirstPage = pageIndex === 0;
|
||||
const { main, sidebar, fullWidth } = pageLayout;
|
||||
|
||||
const SummaryComponent = getSectionComponent("summary", {
|
||||
sectionClassName: cn(sectionClassName, "px-(--page-margin-x) pt-(--page-margin-y)"),
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="template-ditgar page-content">
|
||||
{/* Sidebar Background */}
|
||||
@@ -55,15 +51,11 @@ export function DitgarTemplate({ pageIndex, pageLayout }: TemplateProps) {
|
||||
)}
|
||||
|
||||
<main data-layout="main" className={cn("main group z-10", !fullWidth ? "col-span-2" : "col-span-3")}>
|
||||
{isFirstPage && <SummaryComponent id="summary" />}
|
||||
|
||||
<div className="space-y-4 px-(--page-margin-x) pt-(--page-margin-y)">
|
||||
{main
|
||||
.filter((section) => section !== "summary")
|
||||
.map((section) => {
|
||||
const Component = getSectionComponent(section, { sectionClassName });
|
||||
return <Component key={section} id={section} />;
|
||||
})}
|
||||
{main.map((section) => {
|
||||
const Component = getSectionComponent(section, { sectionClassName });
|
||||
return <Component key={section} id={section} />;
|
||||
})}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BetterAuthError } from "@better-auth/core/error";
|
||||
import { BetterAuthError } from "better-auth";
|
||||
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
||||
import { betterAuth } from "better-auth/minimal";
|
||||
import { apiKey, type GenericOAuthConfig, genericOAuth, openAPI, twoFactor } from "better-auth/plugins";
|
||||
|
||||
@@ -153,6 +153,7 @@ export const printerService = {
|
||||
|
||||
// Now measure the total height (margins are now part of the DOM)
|
||||
let totalHeight = 0;
|
||||
|
||||
for (const el of pageElements) {
|
||||
const pageEl = el as HTMLElement;
|
||||
// offsetHeight includes padding and border, but not margin
|
||||
@@ -164,23 +165,13 @@ export const printerService = {
|
||||
return Math.max(totalHeight, minPageHeight);
|
||||
}
|
||||
|
||||
// For A4/Letter: existing behavior
|
||||
// The --page-height CSS variable controls the height of each resume page.
|
||||
// We need to reduce it by the PDF margins so content fits within the printable area.
|
||||
// Without this, content would overflow and create empty pages.
|
||||
const rootHeight = getComputedStyle(root).getPropertyValue("--page-height").trim();
|
||||
const containerHeight = container
|
||||
? getComputedStyle(container).getPropertyValue("--page-height").trim()
|
||||
: null;
|
||||
const currentHeight = containerHeight || rootHeight;
|
||||
const heightValue = Math.min(Number.parseFloat(currentHeight), minPageHeight);
|
||||
// For A4/Letter
|
||||
const heightValue = minPageHeight;
|
||||
|
||||
if (!Number.isNaN(heightValue)) {
|
||||
// Subtract top + bottom margins from page height
|
||||
const newHeight = `${heightValue - marginY}px`;
|
||||
if (container) container.style.setProperty("--page-height", newHeight);
|
||||
root.style.setProperty("--page-height", newHeight);
|
||||
}
|
||||
// Subtract top + bottom margins from page height
|
||||
const newHeight = `${heightValue - marginY}px`;
|
||||
if (container) container.style.setProperty("--page-height", newHeight);
|
||||
root.style.setProperty("--page-height", newHeight);
|
||||
|
||||
// Add page break CSS to each resume page element (identified by data-page-index attribute)
|
||||
// This ensures each visual resume page starts a new PDF page
|
||||
|
||||
@@ -25,9 +25,9 @@ function RouteComponent() {
|
||||
queryKey: ["auth", "api-keys"],
|
||||
queryFn: () => authClient.apiKey.list(),
|
||||
select: ({ data }) => {
|
||||
if (!data?.apiKeys) return [];
|
||||
if (!data) return [];
|
||||
|
||||
return data.apiKeys
|
||||
return data
|
||||
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())
|
||||
.filter((key) => !!key.expiresAt && key.expiresAt.getTime() > Date.now());
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user