diff --git a/.gitignore b/.gitignore
index 6b1038f..4c3b555 100644
--- a/.gitignore
+++ b/.gitignore
@@ -105,4 +105,5 @@ dist
.dynamodb/
# TernJS port file
-.tern-port
\ No newline at end of file
+.tern-port
+.vercel
diff --git a/bun.lock b/bun.lock
index 49f4c61..c47d6b0 100644
--- a/bun.lock
+++ b/bun.lock
@@ -27,7 +27,7 @@
"fs": "0.0.1-security",
"geist": "^1.4.2",
"lucide-react": "^0.454.0",
- "next": "^15.4.5",
+ "next": "^15.5.2",
"pdfjs-dist": "^4.10.38",
"radix-ui": "^1.4.2",
"react": "^18.3.1",
@@ -181,25 +181,25 @@
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.7", "", { "dependencies": { "@emnapi/core": "^1.3.1", "@emnapi/runtime": "^1.3.1", "@tybys/wasm-util": "^0.9.0" } }, "sha512-5yximcFK5FNompXfJFoWanu5l8v1hNGqNHh9du1xETp9HWk/B/PzvchX55WYOPaIeNglG8++68AAiauBAtbnzw=="],
- "@next/env": ["@next/env@15.4.5", "", {}, "sha512-ruM+q2SCOVCepUiERoxOmZY9ZVoecR3gcXNwCYZRvQQWRjhOiPJGmQ2fAiLR6YKWXcSAh7G79KEFxN3rwhs4LQ=="],
+ "@next/env": ["@next/env@15.5.2", "", {}, "sha512-Qe06ew4zt12LeO6N7j8/nULSOe3fMXE4dM6xgpBQNvdzyK1sv5y4oAP3bq4LamrvGCZtmRYnW8URFCeX5nFgGg=="],
"@next/eslint-plugin-next": ["@next/eslint-plugin-next@15.4.5", "", { "dependencies": { "fast-glob": "3.3.1" } }, "sha512-YhbrlbEt0m4jJnXHMY/cCUDBAWgd5SaTa5mJjzOt82QwflAFfW/h3+COp2TfVSzhmscIZ5sg2WXt3MLziqCSCw=="],
- "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.4.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-84dAN4fkfdC7nX6udDLz9GzQlMUwEMKD7zsseXrl7FTeIItF8vpk1lhLEnsotiiDt+QFu3O1FVWnqwcRD2U3KA=="],
+ "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.5.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-8bGt577BXGSd4iqFygmzIfTYizHb0LGWqH+qgIF/2EDxS5JsSdERJKA8WgwDyNBZgTIIA4D8qUtoQHmxIIquoQ=="],
- "@next/swc-darwin-x64": ["@next/swc-darwin-x64@15.4.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-CL6mfGsKuFSyQjx36p2ftwMNSb8PQog8y0HO/ONLdQqDql7x3aJb/wB+LA651r4we2pp/Ck+qoRVUeZZEvSurA=="],
+ "@next/swc-darwin-x64": ["@next/swc-darwin-x64@15.5.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-2DjnmR6JHK4X+dgTXt5/sOCu/7yPtqpYt8s8hLkHFK3MGkka2snTv3yRMdHvuRtJVkPwCGsvBSwmoQCHatauFQ=="],
- "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@15.4.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-1hTVd9n6jpM/thnDc5kYHD1OjjWYpUJrJxY4DlEacT7L5SEOXIifIdTye6SQNNn8JDZrcN+n8AWOmeJ8u3KlvQ=="],
+ "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@15.5.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-3j7SWDBS2Wov/L9q0mFJtEvQ5miIqfO4l7d2m9Mo06ddsgUK8gWfHGgbjdFlCp2Ek7MmMQZSxpGFqcC8zGh2AA=="],
- "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@15.4.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-4W+D/nw3RpIwGrqpFi7greZ0hjrCaioGErI7XHgkcTeWdZd146NNu1s4HnaHonLeNTguKnL2Urqvj28UJj6Gqw=="],
+ "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@15.5.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-s6N8k8dF9YGc5T01UPQ08yxsK6fUow5gG1/axWc1HVVBYQBgOjca4oUZF7s4p+kwhkB1bDSGR8QznWrFZ/Rt5g=="],
- "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@15.4.5", "", { "os": "linux", "cpu": "x64" }, "sha512-N6Mgdxe/Cn2K1yMHge6pclffkxzbSGOydXVKYOjYqQXZYjLCfN/CuFkaYDeDHY2VBwSHyM2fUjYBiQCIlxIKDA=="],
+ "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@15.5.2", "", { "os": "linux", "cpu": "x64" }, "sha512-o1RV/KOODQh6dM6ZRJGZbc+MOAHww33Vbs5JC9Mp1gDk8cpEO+cYC/l7rweiEalkSm5/1WGa4zY7xrNwObN4+Q=="],
- "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@15.4.5", "", { "os": "linux", "cpu": "x64" }, "sha512-YZ3bNDrS8v5KiqgWE0xZQgtXgCTUacgFtnEgI4ccotAASwSvcMPDLua7BWLuTfucoRv6mPidXkITJLd8IdJplQ=="],
+ "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@15.5.2", "", { "os": "linux", "cpu": "x64" }, "sha512-/VUnh7w8RElYZ0IV83nUcP/J4KJ6LLYliiBIri3p3aW2giF+PAVgZb6mk8jbQSB3WlTai8gEmCAr7kptFa1H6g=="],
- "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.4.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-9Wr4t9GkZmMNcTVvSloFtjzbH4vtT4a8+UHqDoVnxA5QyfWe6c5flTH1BIWPGNWSUlofc8dVJAE7j84FQgskvQ=="],
+ "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.5.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-sMPyTvRcNKXseNQ/7qRfVRLa0VhR0esmQ29DD6pqvG71+JdVnESJaHPA8t7bc67KD5spP3+DOCNLhqlEI2ZgQg=="],
- "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.4.5", "", { "os": "win32", "cpu": "x64" }, "sha512-voWk7XtGvlsP+w8VBz7lqp8Y+dYw/MTI4KeS0gTVtfdhdJ5QwhXLmNrndFOin/MDoCvUaLWMkYKATaCoUkt2/A=="],
+ "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.5.2", "", { "os": "win32", "cpu": "x64" }, "sha512-W5VvyZHnxG/2ukhZF/9Ikdra5fdNftxI6ybeVKYvBPDtyx7x4jPPSNduUkfH5fo3zG0JQ0bPxgy41af2JX5D4Q=="],
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
@@ -935,7 +935,7 @@
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
- "next": ["next@15.4.5", "", { "dependencies": { "@next/env": "15.4.5", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.4.5", "@next/swc-darwin-x64": "15.4.5", "@next/swc-linux-arm64-gnu": "15.4.5", "@next/swc-linux-arm64-musl": "15.4.5", "@next/swc-linux-x64-gnu": "15.4.5", "@next/swc-linux-x64-musl": "15.4.5", "@next/swc-win32-arm64-msvc": "15.4.5", "@next/swc-win32-x64-msvc": "15.4.5", "sharp": "^0.34.3" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-nJ4v+IO9CPmbmcvsPebIoX3Q+S7f6Fu08/dEWu0Ttfa+wVwQRh9epcmsyCPjmL2b8MxC+CkBR97jgDhUUztI3g=="],
+ "next": ["next@15.5.2", "", { "dependencies": { "@next/env": "15.5.2", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.5.2", "@next/swc-darwin-x64": "15.5.2", "@next/swc-linux-arm64-gnu": "15.5.2", "@next/swc-linux-arm64-musl": "15.5.2", "@next/swc-linux-x64-gnu": "15.5.2", "@next/swc-linux-x64-musl": "15.5.2", "@next/swc-win32-arm64-msvc": "15.5.2", "@next/swc-win32-x64-msvc": "15.5.2", "sharp": "^0.34.3" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-H8Otr7abj1glFhbGnvUt3gz++0AF1+QoCXEBmd/6aKbfdFwrn0LpA836Ed5+00va/7HQSDD+mOoVhn3tNy3e/Q=="],
"node-abi": ["node-abi@3.74.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-c5XK0MjkGBrQPGYG24GBADZud0NCbznxNx0ZkS+ebUTrmV1qTDxPxSL8zEAPURXSbLRWVexxmP4986BziahL5w=="],
diff --git a/next-env.d.ts b/next-env.d.ts
index 1b3be08..830fb59 100644
--- a/next-env.d.ts
+++ b/next-env.d.ts
@@ -1,5 +1,6 @@
///
///
+///
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
diff --git a/package.json b/package.json
index 2cd03a8..74d75b2 100644
--- a/package.json
+++ b/package.json
@@ -4,7 +4,7 @@
"private": true,
"type": "module",
"scripts": {
- "build": "next build",
+ "build": "bun run update-pdfs && next build",
"dev": "next dev",
"lint": "next lint",
"lint:fix": "next lint --fix",
@@ -12,7 +12,8 @@
"start": "next start",
"typecheck": "tsc --noEmit",
"format:write": "prettier --write \"**/*.{ts,tsx,js,jsx,mdx}\" --cache",
- "format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,mdx}\" --cache"
+ "format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,mdx}\" --cache",
+ "update-pdfs": "bun scripts/update-pdfs.js"
},
"dependencies": {
"@radix-ui/react-avatar": "^1.1.10",
@@ -38,7 +39,7 @@
"fs": "0.0.1-security",
"geist": "^1.4.2",
"lucide-react": "^0.454.0",
- "next": "^15.4.5",
+ "next": "^15.5.2",
"pdfjs-dist": "^4.10.38",
"radix-ui": "^1.4.2",
"react": "^18.3.1",
diff --git a/public/publications/cv.pdf b/public/publications/cv.pdf
new file mode 100644
index 0000000..5d0146c
Binary files /dev/null and b/public/publications/cv.pdf differ
diff --git a/public/publications/resume.pdf b/public/publications/resume.pdf
new file mode 100644
index 0000000..127153e
Binary files /dev/null and b/public/publications/resume.pdf differ
diff --git a/scripts/update-pdfs.js b/scripts/update-pdfs.js
new file mode 100644
index 0000000..f9cd996
--- /dev/null
+++ b/scripts/update-pdfs.js
@@ -0,0 +1,139 @@
+#!/usr/bin/env node
+
+import fs from "fs";
+import path from "path";
+import https from "https";
+import { fileURLToPath } from "url";
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+const GITHUB_REPO = "soconnor0919/resume-cv";
+const PUBLIC_DIR = path.join(__dirname, "..", "public");
+const PUBLICATIONS_DIR = path.join(PUBLIC_DIR, "publications");
+
+// Ensure publications directory exists
+if (!fs.existsSync(PUBLICATIONS_DIR)) {
+ fs.mkdirSync(PUBLICATIONS_DIR, { recursive: true });
+}
+
+// PDF files to download
+const PDF_FILES = [
+ {
+ name: "cv.pdf",
+ url: `https://github.com/${GITHUB_REPO}/releases/download/latest/cv.pdf`,
+ description: "Academic CV",
+ },
+ {
+ name: "resume.pdf",
+ url: `https://github.com/${GITHUB_REPO}/releases/download/latest/resume.pdf`,
+ description: "Professional Resume",
+ },
+];
+
+/**
+ * Download a file from URL to local path
+ */
+function downloadFile(url, outputPath) {
+ return new Promise((resolve, reject) => {
+ console.log(`Downloading ${url}...`);
+
+ const file = fs.createWriteStream(outputPath);
+
+ https
+ .get(url, (response) => {
+ // Handle redirects
+ if (response.statusCode === 301 || response.statusCode === 302) {
+ file.close();
+ fs.unlinkSync(outputPath);
+ return downloadFile(response.headers.location, outputPath)
+ .then(resolve)
+ .catch(reject);
+ }
+
+ if (response.statusCode !== 200) {
+ file.close();
+ fs.unlinkSync(outputPath);
+ return reject(
+ new Error(`HTTP ${response.statusCode}: ${response.statusMessage}`),
+ );
+ }
+
+ // Check content type (GitHub serves PDFs as application/octet-stream)
+ const contentType = response.headers["content-type"];
+ if (
+ contentType &&
+ !contentType.includes("application/pdf") &&
+ !contentType.includes("application/octet-stream")
+ ) {
+ file.close();
+ fs.unlinkSync(outputPath);
+ return reject(
+ new Error(`Expected PDF but got content-type: ${contentType}`),
+ );
+ }
+
+ response.pipe(file);
+
+ file.on("finish", () => {
+ file.close();
+ console.log(`✓ Downloaded ${path.basename(outputPath)}`);
+ resolve();
+ });
+
+ file.on("error", (err) => {
+ file.close();
+ fs.unlinkSync(outputPath);
+ reject(err);
+ });
+ })
+ .on("error", (err) => {
+ file.close();
+ fs.unlinkSync(outputPath);
+ reject(err);
+ });
+ });
+}
+
+/**
+ * Main function to download all PDFs
+ */
+async function updatePDFs() {
+ console.log("Updating PDFs from GitHub releases...\n");
+
+ try {
+ for (const pdf of PDF_FILES) {
+ const outputPath = path.join(PUBLICATIONS_DIR, pdf.name);
+
+ try {
+ await downloadFile(pdf.url, outputPath);
+
+ // Verify the file was downloaded and has content
+ const stats = fs.statSync(outputPath);
+ if (stats.size === 0) {
+ throw new Error("Downloaded file is empty");
+ }
+
+ console.log(` Size: ${(stats.size / 1024).toFixed(1)} KB`);
+ console.log(` Path: ${path.relative(process.cwd(), outputPath)}\n`);
+ } catch (error) {
+ console.error(`✗ Failed to download ${pdf.name}: ${error.message}\n`);
+ process.exitCode = 1;
+ }
+ }
+
+ if (process.exitCode !== 1) {
+ console.log("✓ All PDFs updated successfully!");
+ }
+ } catch (error) {
+ console.error("Failed to update PDFs:", error.message);
+ process.exit(1);
+ }
+}
+
+// Run the script if called directly
+if (import.meta.url === `file://${process.argv[1]}`) {
+ updatePDFs();
+}
+
+export { updatePDFs };
diff --git a/src/app/cv/page.tsx b/src/app/cv/page.tsx
index 22c2f12..19e2073 100644
--- a/src/app/cv/page.tsx
+++ b/src/app/cv/page.tsx
@@ -29,11 +29,9 @@ import {
import Link from "next/link";
import type { PDFDocumentProxy, PDFPageProxy } from "pdfjs-dist";
-// GitHub release URLs for PDFs
-const CV_URL =
- "https://github.com/soconnor0919/resume-cv/releases/download/latest/cv.pdf";
-const RESUME_URL =
- "https://github.com/soconnor0919/resume-cv/releases/download/latest/resume.pdf";
+// Local PDF file URLs
+const CV_URL = "/publications/cv.pdf";
+const RESUME_URL = "/publications/resume.pdf";
interface PDFViewerProps {
url: string;
@@ -76,18 +74,8 @@ function PDFViewer({ url, title, type }: PDFViewerProps) {
const arrayBuffer = await response.arrayBuffer();
return new Uint8Array(arrayBuffer);
} catch (error) {
- // If direct fetch fails (e.g., CORS), try proxy
- console.warn("Direct fetch failed, trying proxy:", error);
-
- const proxyUrl = `/api/pdf-proxy?url=${encodeURIComponent(pdfUrl)}`;
- const response = await fetch(proxyUrl);
-
- if (!response.ok) {
- throw new Error(`Failed to download PDF via proxy: ${response.status}`);
- }
-
- const arrayBuffer = await response.arrayBuffer();
- return new Uint8Array(arrayBuffer);
+ console.error("Failed to download PDF:", error);
+ throw error;
}
};
@@ -298,11 +286,7 @@ function PDFViewer({ url, title, type }: PDFViewerProps) {
asChild
className="button-hover gap-2"
>
-
+
View PDF
@@ -336,11 +320,7 @@ function PDFViewer({ url, title, type }: PDFViewerProps) {
asChild
className="button-hover gap-2"
>
-
+
View PDF in New Tab
@@ -383,11 +363,7 @@ function PDFViewer({ url, title, type }: PDFViewerProps) {
asChild
className="button-hover gap-2"
>
-
+
View PDF
diff --git a/tsconfig.json b/tsconfig.json
index 0794dae..e450ba3 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -39,5 +39,5 @@
"**/*.js",
".next/types/**/*.ts"
],
- "exclude": ["node_modules", "drizzle.config.ts"]
+ "exclude": ["node_modules", "drizzle.config.ts", "scripts/**/*"]
}
diff --git a/vercel.json b/vercel.json
new file mode 100644
index 0000000..a283794
--- /dev/null
+++ b/vercel.json
@@ -0,0 +1,36 @@
+{
+ "buildCommand": "bun run build",
+ "installCommand": "bun install",
+ "functions": {
+ "src/app/api/**/*.ts": {
+ "maxDuration": 30
+ }
+ },
+ "headers": [
+ {
+ "source": "/publications/(.*)\\.pdf",
+ "headers": [
+ {
+ "key": "Cache-Control",
+ "value": "public, max-age=86400, s-maxage=86400"
+ },
+ {
+ "key": "Content-Type",
+ "value": "application/pdf"
+ },
+ {
+ "key": "Content-Disposition",
+ "value": "inline"
+ }
+ ]
+ }
+ ],
+ "rewrites": [
+ {
+ "source": "/api/publications/:filename",
+ "destination": "/publications/:filename"
+ }
+ ],
+
+ "framework": "nextjs"
+}