Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion .husky/pre-commit
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1 +1,24 @@
npx lint-staged
#!/usr/bin/env sh

# Source nvm if available (WSL / Linux with nvm-managed node)
[ -f "$HOME/.nvm/nvm.sh" ] && . "$HOME/.nvm/nvm.sh"

# Run lint-staged directly if npx is on PATH
if command -v npx >/dev/null 2>&1; then
npx lint-staged
exit $?
fi

# Fallback: Windows Git client (GitHub Desktop, VS Code, etc.)
# npx isn't available in MINGW bash — delegate to WSL where node lives
if command -v wsl.exe >/dev/null 2>&1; then
# MINGW pwd returns //wsl.localhost/Ubuntu/home/... — strip the UNC prefix
WSL_DIR="$(pwd | sed 's|^//wsl\.localhost/[^/]*||')"
if [ -n "$WSL_DIR" ]; then
wsl.exe bash -lc ". \$HOME/.nvm/nvm.sh 2>/dev/null; cd \"${WSL_DIR}\" && npx lint-staged"
exit $?
fi
fi

echo "husky: npx not found — skipping pre-commit hook"
exit 0
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
24
7 changes: 6 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,12 @@ Generated files that should be committed: `data/generated/career-data.json`, `da

## Code Quality

A **pre-commit hook** runs automatically on every `git commit`. It formats staged files with Prettier before the commit is recorded, so formatting issues never reach CI. It is installed by `npm install` via the `prepare` lifecycle hook — no extra setup needed.
A **pre-commit hook** runs automatically on every `git commit`. It formats staged files with Prettier via lint-staged before the commit is recorded, so formatting issues never reach CI.

The hook is installed by `npm install` via the `prepare` lifecycle hook — no extra setup needed. It works across all Git environments:

- **WSL / Linux / macOS terminal:** Uses `npx` directly (sources nvm if present)
- **Windows Git clients (GitHub Desktop, VS Code, etc.):** Automatically delegates to WSL via `wsl.exe` when `npx` isn't available in the Windows shell, converting UNC paths to WSL-native paths

Run the release checklist before pushing:

Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,9 @@ npm run format # Prettier format all files
npm run format:check # Prettier check (CI-friendly)
```

A **pre-commit hook** runs automatically on every `git commit` (installed via `npm install`). It runs Prettier on staged files only, so formatting issues are fixed before they reach CI. No extra setup needed — husky wires it in via the `prepare` npm lifecycle hook.
A **pre-commit hook** runs automatically on every `git commit` (installed via `npm install`). It runs Prettier on staged files via lint-staged, so formatting issues are fixed before they reach CI. No extra setup needed — husky wires it in via the `prepare` npm lifecycle hook.

The hook works across all Git environments: WSL/Linux/macOS terminals use `npx` directly, while Windows Git clients (GitHub Desktop, VS Code) automatically delegate to WSL when `npx` isn't available in the Windows shell.

### Pre-Push Checklist

Expand Down
2 changes: 1 addition & 1 deletion app/components/BackToTop.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export default function BackToTop() {
type="button"
onClick={scrollToTop}
aria-label="Back to top"
className={`no-print fixed bottom-6 right-6 z-50 rounded-full bg-slate-900 p-2.5 text-white shadow-lg transition-all hover:bg-slate-700 focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:outline-none dark:bg-slate-200 dark:text-slate-900 dark:hover:bg-slate-300 ${
className={`no-print fixed bottom-6 right-6 z-50 rounded-full bg-slate-900 p-2.5 text-white shadow-lg transition-[opacity,transform,background-color] duration-200 hover:bg-slate-700 focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:outline-none dark:bg-slate-200 dark:text-slate-900 dark:hover:bg-slate-300 ${
visible ? "translate-y-0 opacity-100" : "translate-y-4 opacity-0 pointer-events-none"
}`}
>
Expand Down
15 changes: 8 additions & 7 deletions app/components/SectionNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,18 +45,19 @@ export default function SectionNav({ sections }: { sections: Section[] }) {
}, []);

// Track which section is active based on scroll position.
// Reads --sticky-offset (header + nav) from CSS so JS and CSS stay in sync.
// Reads --header-height and --nav-height from CSS so JS and CSS stay in sync.
// (--sticky-offset is a calc() expression that parseFloat cannot resolve.)
useEffect(() => {
const onScroll = () => {
if (window.scrollY < 100) {
setActiveId("");
return;
}

const stickyOffset = parseFloat(
getComputedStyle(document.documentElement).getPropertyValue("--sticky-offset") || "0",
);
const threshold = stickyOffset + 16;
const style = getComputedStyle(document.documentElement);
const headerH = parseFloat(style.getPropertyValue("--header-height") || "0");
const navH = parseFloat(style.getPropertyValue("--nav-height") || "0");
const threshold = headerH + navH + 16;

let current = "";
for (const section of sections) {
Expand Down Expand Up @@ -90,9 +91,9 @@ export default function SectionNav({ sections }: { sections: Section[] }) {
<a
key={s.id}
href={`#${s.id}`}
className={`inline-flex min-h-[44px] shrink-0 items-center rounded-md px-3 transition-colors focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-1 focus-visible:outline-none ${
className={`inline-flex min-h-[44px] shrink-0 items-center rounded-md px-2.5 transition-colors focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-1 focus-visible:outline-none ${
activeId === s.id
? "bg-slate-900 text-white dark:bg-slate-200 dark:text-slate-900"
? "bg-slate-200 font-medium text-slate-900 dark:bg-slate-700 dark:text-slate-100"
: "text-slate-500 hover:bg-slate-100 hover:text-slate-700 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-200"
}`}
>
Expand Down
28 changes: 23 additions & 5 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -28,24 +28,24 @@ html {
}

.resume-prose h2 {
@apply text-lg font-semibold uppercase tracking-wider text-slate-700 border-b border-slate-300 pb-1 mt-8 mb-4;
@apply text-xl font-semibold uppercase tracking-wider text-slate-700 border-b border-slate-300 pb-1 mt-8 mb-4;
scroll-margin-top: calc(var(--sticky-offset) + 16px);
}

.resume-prose h3 {
@apply text-base font-semibold text-slate-900 mb-0;
@apply text-lg font-semibold text-slate-900 mb-0;
}

.resume-prose h3 + p {
@apply mt-0;
}

.resume-prose p {
@apply text-sm leading-relaxed text-slate-700;
@apply text-base leading-relaxed text-slate-700;
}

.resume-prose ul {
@apply text-sm leading-relaxed text-slate-700 pl-5 my-2;
@apply text-base leading-relaxed text-slate-700 pl-5 my-2;
}

.resume-prose li {
Expand Down Expand Up @@ -112,10 +112,28 @@ html {
display: none;
}

/* ─── Reduced Motion ──────────────────────────────────────────────────────── */
/* Respect user preferences for reduced motion (WCAG 2.2) */

@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}

/* ─── Print Styles ───────────────────────────────────────────────────────── */
/* Clean PDF output when printing from browser (Cmd+P / Ctrl+P) */

@media print {
@page {
margin: 0.5in;
}

/* Force light colors for print — override any dark mode preferences */
body {
@apply bg-white text-black;
Expand Down Expand Up @@ -146,7 +164,7 @@ html {
}

.resume-prose h3 {
font-size: 11pt;
font-size: 10.5pt;
page-break-after: avoid;
color: #0f172a;
}
Expand Down
16 changes: 8 additions & 8 deletions app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -168,12 +168,12 @@ export default function Home() {
</p>
</div>
{/* Row 2: Contact + Downloads — unified text-style links */}
<div className="mt-1 flex flex-wrap items-center gap-x-0.5 gap-y-0.5">
<div className="mt-1 flex flex-wrap items-center gap-x-1 gap-y-1">
{profile.email && (
<a
href={`mailto:${profile.email}`}
aria-label="Send email to Paul Prae"
className="inline-flex min-h-[44px] items-center rounded-md px-2 text-xs text-slate-500 transition-colors hover:text-slate-900 focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-1 focus-visible:outline-none dark:text-slate-400 dark:hover:text-slate-100"
className="inline-flex min-h-[44px] items-center rounded-md px-2.5 text-xs text-slate-500 transition-colors hover:text-slate-900 focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-1 focus-visible:outline-none dark:text-slate-400 dark:hover:text-slate-100"
>
Email
</a>
Expand All @@ -184,7 +184,7 @@ export default function Home() {
target="_blank"
rel="noopener noreferrer"
aria-label="View Paul Prae on LinkedIn"
className="inline-flex min-h-[44px] items-center rounded-md px-2 text-xs text-slate-500 transition-colors hover:text-slate-900 focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-1 focus-visible:outline-none dark:text-slate-400 dark:hover:text-slate-100"
className="inline-flex min-h-[44px] items-center rounded-md px-2.5 text-xs text-slate-500 transition-colors hover:text-slate-900 focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-1 focus-visible:outline-none dark:text-slate-400 dark:hover:text-slate-100"
>
LinkedIn
</a>
Expand All @@ -195,23 +195,23 @@ export default function Home() {
target="_blank"
rel="noopener noreferrer"
aria-label="View Paul Prae on GitHub"
className="inline-flex min-h-[44px] items-center rounded-md px-2 text-xs text-slate-500 transition-colors hover:text-slate-900 focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-1 focus-visible:outline-none dark:text-slate-400 dark:hover:text-slate-100"
className="inline-flex min-h-[44px] items-center rounded-md px-2.5 text-xs text-slate-500 transition-colors hover:text-slate-900 focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-1 focus-visible:outline-none dark:text-slate-400 dark:hover:text-slate-100"
>
GitHub
</a>
)}
{/* Separator between contact and download groups */}
<span
aria-hidden="true"
className="select-none px-1 text-xs text-slate-300 dark:text-slate-600"
className="select-none px-1.5 text-xs text-slate-300 dark:text-slate-600"
>
·
</span>
<a
href={pdfPath}
download
aria-label="Download resume as PDF"
className="inline-flex min-h-[44px] items-center gap-1 rounded-md px-2 text-xs text-slate-500 transition-colors hover:text-slate-900 focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-1 focus-visible:outline-none dark:text-slate-400 dark:hover:text-slate-100"
className="inline-flex min-h-[44px] items-center gap-1 rounded-md px-2.5 text-xs text-slate-500 transition-colors hover:text-slate-900 focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-1 focus-visible:outline-none dark:text-slate-400 dark:hover:text-slate-100"
>
<DownloadIcon />
PDF{pdfSize && <span className="opacity-60">({pdfSize})</span>}
Expand All @@ -220,7 +220,7 @@ export default function Home() {
href={docxPath}
download
aria-label="Download resume as DOCX"
className="inline-flex min-h-[44px] items-center gap-1 rounded-md px-2 text-xs text-slate-500 transition-colors hover:text-slate-900 focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-1 focus-visible:outline-none dark:text-slate-400 dark:hover:text-slate-100"
className="inline-flex min-h-[44px] items-center gap-1 rounded-md px-2.5 text-xs text-slate-500 transition-colors hover:text-slate-900 focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-1 focus-visible:outline-none dark:text-slate-400 dark:hover:text-slate-100"
>
<DownloadIcon />
DOCX{docxSize && <span className="opacity-60">({docxSize})</span>}
Expand All @@ -229,7 +229,7 @@ export default function Home() {
href={mdPath}
download
aria-label="Download resume as Markdown"
className="inline-flex min-h-[44px] items-center gap-1 rounded-md px-2 text-xs text-slate-500 transition-colors hover:text-slate-900 focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-1 focus-visible:outline-none dark:text-slate-400 dark:hover:text-slate-100"
className="inline-flex min-h-[44px] items-center gap-1 rounded-md px-2.5 text-xs text-slate-500 transition-colors hover:text-slate-900 focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-1 focus-visible:outline-none dark:text-slate-400 dark:hover:text-slate-100"
>
<DownloadIcon />
MD{mdSize && <span className="opacity-60">({mdSize})</span>}
Expand Down
6 changes: 4 additions & 2 deletions docs/linux-dev-environment-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,12 @@ This script installs:
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
nvm install --lts
nvm alias default lts/*
nvm install # reads version from .nvmrc (Node 24)
nvm alias default 24
```

The project includes a `.nvmrc` file pinning the Node.js major version. Running `nvm install` or `nvm use` in the repo root picks up this version automatically.

### 3.2 Export dependencies

```bash
Expand Down
2 changes: 1 addition & 1 deletion docs/technical-design-document.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ Supporting commands:

### 6.1 Automated checks

- Pre-commit: `lint-staged` runs Prettier on staged files (via husky `prepare` hook — installs on `npm install`)
- Pre-commit: `lint-staged` runs Prettier on staged files (via husky `prepare` hook — installs on `npm install`). Works cross-platform: WSL/Linux/macOS use `npx` directly; Windows Git clients (GitHub Desktop, VS Code) auto-delegate to WSL
- `npm run lint`
- `npm run format:check`
- `npm test`
Expand Down
36 changes: 31 additions & 5 deletions docs/windows-dev-environment-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,25 @@ git config --global pull.rebase false
git config --global push.autoSetupRemote true
```

## 7. Optional WSL Setup
## 7. Git GUI Tools

### GitHub Desktop

Install from [desktop.github.com](https://desktop.github.com/) or via winget:

```powershell
winget install --id GitHub.GitHubDesktop --exact
```

GitHub Desktop uses its own bundled Git (MINGW bash) which does **not** have access to WSL-installed Node.js or `npx`. The project's husky pre-commit hook handles this automatically — when `npx` is not available, the hook delegates to WSL via `wsl.exe`, converting the UNC path to a WSL-native path.

No additional configuration is needed for GitHub Desktop to work with pre-commit hooks on this project.

### VS Code Source Control

VS Code's built-in Git also uses the Windows Git installation. The same WSL delegation in the pre-commit hook applies here. Ensure Git for Windows is installed (§5) and the repo is opened via the WSL filesystem path (`\\wsl.localhost\Ubuntu\...`).

## 8. Optional WSL Setup

If using Claude Code sandboxing or Linux-native workflows:

Expand All @@ -123,7 +141,7 @@ mkdir -p ~/dev

Continue in `docs/linux-dev-environment-setup.md`.

## 8. Package Cache Optimization
## 9. Package Cache Optimization

Set npm and pip caches to Dev Drive:

Expand All @@ -134,7 +152,7 @@ setx PIP_CACHE_DIR D:\packages\pip

Restart terminal sessions after `setx`.

## 9. Project Bootstrap
## 10. Project Bootstrap

```powershell
git clone https://github.com/praeducer/paulprae-com.git D:\dev\paulprae-com
Expand All @@ -144,7 +162,7 @@ npm install

Then follow `README.md` for `.env.local`, data inputs, and pipeline commands.

## 10. Verification Checklist
## 11. Verification Checklist

Run and confirm:

Expand All @@ -165,7 +183,7 @@ npm run format:check
npm test
```

## 11. Troubleshooting
## 12. Troubleshooting

### Long-path or EPERM errors

Expand All @@ -188,6 +206,14 @@ fsutil devdrv query D:
- Ensure expected binaries resolve first in PATH.
- If also using WSL, keep Windows and WSL installs separate and explicit.

### Pre-commit hook fails in GitHub Desktop or VS Code

The husky pre-commit hook automatically delegates to WSL when `npx` is not found. If it still fails:

1. Verify WSL is running: `wsl --status`
2. Verify Node.js in WSL: `wsl bash -lc "source ~/.nvm/nvm.sh && node --version"`
3. Ensure the repo was cloned on the WSL filesystem (`\\wsl.localhost\Ubuntu\...`), not `C:\`

### Cross-filesystem performance is slow

Keep active project on its native filesystem (Windows tools on `D:\dev`, Linux tools in `~/dev`).
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
"version": "0.1.0",
"private": true,
"description": "AI-powered career platform — paulprae.com",
"engines": {
"node": ">=24"
},
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
Expand Down