Add image-embedding preview builder, fix button wrapping on phones
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Msa87P1aZQSV9MWhEu3VfJ
This commit is contained in:
5 files changed
+85
-10
No files matched your search
@@ -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 <output.html>
|
||||
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<string, string> = ${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 = `<title>Jack 3D Creator</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Kanit:wght@300;400;500;600;700;800;900&display=swap" rel="stylesheet">
|
||||
<style>${css}
|
||||
html,body{background:#0C0C0C}
|
||||
img{background:linear-gradient(135deg,#17171c,#24242c)}</style>
|
||||
<div id="root"></div>
|
||||
<script>document.addEventListener("error",function(e){var t=e.target;if(t&&t.tagName==="IMG"){t.removeAttribute("src");t.alt="";}},true);</script>
|
||||
<script type="module">${js}</script>
|
||||
`;
|
||||
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' : ''}`);
|
||||
@@ -2,7 +2,7 @@ export default function ContactButton() {
|
||||
return (
|
||||
<a
|
||||
href="mailto:hello@example.com"
|
||||
className="inline-block rounded-full px-8 py-3 text-xs font-medium uppercase tracking-widest text-white sm:px-10 sm:py-3.5 sm:text-sm md:px-12 md:py-4 md:text-base"
|
||||
className="inline-block whitespace-nowrap rounded-full px-8 py-3 text-xs font-medium uppercase tracking-widest text-white sm:px-10 sm:py-3.5 sm:text-sm md:px-12 md:py-4 md:text-base"
|
||||
style={{
|
||||
background: 'linear-gradient(123deg, #18011F 7%, #B600A8 37%, #7621B0 72%, #BE4C00 100%)',
|
||||
boxShadow: '0px 4px 4px rgba(181, 1, 167, 0.25), 4px 4px 12px #7721B1 inset',
|
||||
|
||||
@@ -2,7 +2,7 @@ export default function LiveProjectButton() {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-full border-2 border-[#D7E2EA] px-8 py-3 text-sm font-medium uppercase tracking-widest text-[#D7E2EA] transition-colors hover:bg-[#D7E2EA]/10 sm:px-10 sm:py-3.5 sm:text-base"
|
||||
className="whitespace-nowrap rounded-full border-2 border-[#D7E2EA] px-8 py-3 text-sm font-medium uppercase tracking-widest text-[#D7E2EA] transition-colors hover:bg-[#D7E2EA]/10 sm:px-10 sm:py-3.5 sm:text-base"
|
||||
>
|
||||
Live Project
|
||||
</button>
|
||||
|
||||
@@ -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 = [
|
||||
{
|
||||
|
||||
@@ -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<string, string> = {};
|
||||
Reference in new issue
Block a user