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" +}