diff --git a/jack-portfolio/scripts/build-preview.mjs b/jack-portfolio/scripts/build-preview.mjs new file mode 100644 index 0000000..8f13570 --- /dev/null +++ b/jack-portfolio/scripts/build-preview.mjs @@ -0,0 +1,68 @@ +// Builds ONE self-contained HTML file for the Claude cloud preview. +// Claude's cloud pages block outside images, so every image is downloaded +// here and baked into the page as a data: URI. +// Usage: node scripts/build-preview.mjs +import { execFileSync } from 'node:child_process'; +import { readFileSync, readdirSync, writeFileSync } from 'node:fs'; + +const out = process.argv[2] ?? "jack-preview.html"; +import { buildSync } from 'esbuild'; +import { rmSync, writeFileSync as write } from 'node:fs'; + +// 1. Read the real list of image URLs from src/data/content.ts +buildSync({ entryPoints: ['src/data/content.ts'], bundle: true, format: 'esm', outfile: 'dist-tmp/content.mjs', logLevel: 'error' }); +const content = await import(new URL('../dist-tmp/content.mjs', import.meta.url).href); +rmSync('dist-tmp', { recursive: true, force: true }); +const urls = new Set(); +const walk = (v) => { + if (typeof v === 'string' && v.startsWith('https://')) urls.add(v); + else if (v && typeof v === 'object') Object.values(v).forEach(walk); +}; +walk(content); + +// 2. Download each one and turn it into a data: URI +const mime = { png: 'image/png', gif: 'image/gif', webp: 'image/webp', jpg: 'image/jpeg' }; +const map = {}; +let total = 0; +for (const url of urls) { + try { + const buf = execFileSync('curl', ['-sSfL', '-m', '60', url], { maxBuffer: 64 * 1024 * 1024, stdio: ['ignore', 'pipe', 'ignore'] }); + const head = buf.subarray(0, 12).toString('latin1'); + const type = head.startsWith('GIF') ? 'gif' : head.includes('WEBP') ? 'webp' : head.startsWith('\x89PNG') ? 'png' : 'jpg'; + map[url] = `data:${mime[type]};base64,${buf.toString('base64')}`; + total += buf.length; + } catch { + console.warn('could not download:', url.slice(0, 90)); + } +} +console.log(`images embedded: ${Object.keys(map).length}/${urls.size} (${(total / 1048576).toFixed(1)} MB)`); + +// 3. Build with the pictures baked in, then put the empty lookup file back +const stub = 'src/data/embedded.ts'; +const original = readFileSync(stub, 'utf8'); +write(stub, `export const embedded: Record = ${JSON.stringify(map)};\n`); +try { + execFileSync('npm', ['run', 'build'], { stdio: 'inherit' }); +} finally { + write(stub, original); +} +const dir = 'dist/assets/'; +const files = readdirSync(dir); +let js = readFileSync(dir + files.find((f) => f.endsWith('.js')), 'utf8'); +const css = readFileSync(dir + files.find((f) => f.endsWith('.css')), 'utf8'); + +js = js.replace(/<\/script/g, '<\\/script'); +const html = `Jack 3D Creator + + + + +
+ + +`; +writeFileSync(out, html); +const mb = Buffer.byteLength(html) / 1048576; +console.log(`wrote ${out} (${mb.toFixed(1)} MB)${mb > 16 ? ' WARNING: over the 16 MB page limit' : ''}`); diff --git a/jack-portfolio/src/components/ContactButton.tsx b/jack-portfolio/src/components/ContactButton.tsx index 0fb1c71..13fd75b 100644 --- a/jack-portfolio/src/components/ContactButton.tsx +++ b/jack-portfolio/src/components/ContactButton.tsx @@ -2,7 +2,7 @@ export default function ContactButton() { return ( Live Project diff --git a/jack-portfolio/src/data/content.ts b/jack-portfolio/src/data/content.ts index 7c8661f..55720e5 100644 --- a/jack-portfolio/src/data/content.ts +++ b/jack-portfolio/src/data/content.ts @@ -1,13 +1,17 @@ +import { embedded } from './embedded'; + +const A = (url: string) => embedded[url] ?? url; + const FIGMA = 'https://shrug-person-78902957.figma.site/_components/v2'; const ABOUT = `${FIGMA}/ebb2b8f25d8e24d5f0a5ca8af4c950de81aa2fd7`; -export const portrait = `${FIGMA}/d24c01ad3a56fc65e942a1f501eb73db42d7cf9a/Rectangle_40443.81459862.png`; +export const portrait = A(`${FIGMA}/d24c01ad3a56fc65e942a1f501eb73db42d7cf9a/Rectangle_40443.81459862.png`); export const aboutIcons = { - moon: `${ABOUT}/moon_icon.11395d36.png`, - object: `${ABOUT}/p59_1.4659672e.png`, - lego: `${ABOUT}/lego_icon-1.703bb594.png`, - group: `${ABOUT}/Group_134-1.2e04f3ce.png`, + moon: A(`${ABOUT}/moon_icon.11395d36.png`), + object: A(`${ABOUT}/p59_1.4659672e.png`), + lego: A(`${ABOUT}/lego_icon-1.703bb594.png`), + group: A(`${ABOUT}/Group_134-1.2e04f3ce.png`), }; export const aboutText = @@ -35,7 +39,7 @@ export const marqueeImages = [ 'hero-wealth-preview-B70idl_u', 'hero-luminex-preview-CxOP7ce6', 'hero-celestia-preview-0yO3jXO8', -].map((n) => `https://motionsites.ai/assets/${n}.gif`); +].map((n) => A(`https://motionsites.ai/assets/${n}.gif`)); export const services = [ { name: '3D Modeling', description: 'Creation of detailed objects, characters, or environments tailored to specific client needs, ideal for games, products, and visualizations.' }, @@ -46,9 +50,9 @@ export const services = [ ]; const cf = (file: string) => - `https://images.higgs.ai/?default=1&output=webp&url=${encodeURIComponent( + A(`https://images.higgs.ai/?default=1&output=webp&url=${encodeURIComponent( `https://d8j0ntlcm91z4.cloudfront.net/user_38xzZboKViGWJOttwIXH07lWA1P/${file}` - )}&w=1280&q=85`; + )}&w=1280&q=85`); export const projects = [ { diff --git a/jack-portfolio/src/data/embedded.ts b/jack-portfolio/src/data/embedded.ts new file mode 100644 index 0000000..7b4a692 --- /dev/null +++ b/jack-portfolio/src/data/embedded.ts @@ -0,0 +1,3 @@ +// Filled in automatically by scripts/build-preview.mjs (image URL -> data: URI). +// Empty in normal use, so the site loads images from their original URLs. +export const embedded: Record = {};