diff --git a/images/at-the-desk.jpg b/images/at-the-desk.jpg new file mode 100644 index 0000000..f12e4e5 Binary files /dev/null and b/images/at-the-desk.jpg differ diff --git a/images/editorial-blazer.jpg b/images/editorial-blazer.jpg new file mode 100644 index 0000000..e676ed5 Binary files /dev/null and b/images/editorial-blazer.jpg differ diff --git a/images/gpt-image-1.5_authoritative_thought_leader_portrait_man_with_bald_head_and_brown-ginger_full_b-0.jpg b/images/gpt-image-1.5_authoritative_thought_leader_portrait_man_with_bald_head_and_brown-ginger_full_b-0.jpg deleted file mode 100644 index d86bfff..0000000 Binary files a/images/gpt-image-1.5_authoritative_thought_leader_portrait_man_with_bald_head_and_brown-ginger_full_b-0.jpg and /dev/null differ diff --git a/images/illustrated-portrait.jpg b/images/illustrated-portrait.jpg new file mode 100644 index 0000000..b6c659f Binary files /dev/null and b/images/illustrated-portrait.jpg differ diff --git a/images/Jason_fullsize-scaled.jpg b/images/jason-portrait.jpg similarity index 100% rename from images/Jason_fullsize-scaled.jpg rename to images/jason-portrait.jpg diff --git a/images/gpt-image-1.5_bold_social_media_profile_portrait_man_with_bald_head_and_brown-ginger_full_bear-0.jpg b/images/social-neon.jpg similarity index 100% rename from images/gpt-image-1.5_bold_social_media_profile_portrait_man_with_bald_head_and_brown-ginger_full_bear-0.jpg rename to images/social-neon.jpg diff --git a/images/tech-founder-dark.jpg b/images/tech-founder-dark.jpg new file mode 100644 index 0000000..01b1745 Binary files /dev/null and b/images/tech-founder-dark.jpg differ diff --git a/images/tech-founder-warm.jpg b/images/tech-founder-warm.jpg new file mode 100644 index 0000000..40e8021 Binary files /dev/null and b/images/tech-founder-warm.jpg differ diff --git a/images/thought-leader-city.jpg b/images/thought-leader-city.jpg new file mode 100644 index 0000000..604fd69 Binary files /dev/null and b/images/thought-leader-city.jpg differ diff --git a/images/thought-leader-office.jpg b/images/thought-leader-office.jpg new file mode 100644 index 0000000..958900b Binary files /dev/null and b/images/thought-leader-office.jpg differ diff --git a/scripts/rename-media.ts b/scripts/rename-media.ts new file mode 100644 index 0000000..d245a25 --- /dev/null +++ b/scripts/rename-media.ts @@ -0,0 +1,74 @@ +/** + * Rename media — re-uploads each file under its new filename. + * Payload replaces the old file + regenerates thumbnails while keeping the same ID. + */ + +import { getPayload } from 'payload' +import config from '@payload-config' +import path from 'node:path' +import fs from 'node:fs' + +const imagesDir = path.resolve(process.cwd(), 'images') + +const renames: Array<{ alt: string; newFile: string }> = [ + { alt: 'Jason Woltje portrait', newFile: 'jason-portrait.jpg' }, + { alt: 'Stylized portrait — thought leader', newFile: 'thought-leader-city.jpg' }, + { alt: 'Stylized portrait — social', newFile: 'social-neon.jpg' }, + { alt: 'Jason Woltje — tech founder portrait, dark background', newFile: 'tech-founder-dark.jpg' }, + { alt: 'Jason Woltje — social media profile, neon gradient', newFile: 'social-neon.jpg' }, + { alt: 'Jason Woltje — at the desk, documentary style', newFile: 'at-the-desk.jpg' }, + { alt: 'Jason Woltje — illustrated portrait', newFile: 'illustrated-portrait.jpg' }, + { alt: 'Jason Woltje — thought leader portrait, city backdrop', newFile: 'thought-leader-city.jpg' }, + { alt: 'Jason Woltje — fashion editorial portrait', newFile: 'editorial-blazer.jpg' }, +] + +async function main() { + const payload = await getPayload({ config }) + + const seen = new Set() + + for (const r of renames) { + const { docs } = await payload.find({ + collection: 'media', + where: { alt: { equals: r.alt } }, + limit: 1, + depth: 0, + }) + if (docs.length === 0) { + console.log(` skip — no media with alt "${r.alt}"`) + continue + } + + const doc = docs[0]! + if (seen.has(doc.id as number)) continue + seen.add(doc.id as number) + + const currentFilename = (doc as any).filename as string + if (currentFilename === r.newFile) { + console.log(` ↷ id=${doc.id} already named ${r.newFile}`) + continue + } + + const filePath = path.join(imagesDir, r.newFile) + if (!fs.existsSync(filePath)) { + console.log(` ⚠ file missing: ${r.newFile}`) + continue + } + + await payload.update({ + collection: 'media', + id: doc.id, + filePath, + data: {}, + }) + console.log(` ✓ id=${doc.id}: ${currentFilename} → ${r.newFile}`) + } + + console.log('\nDone.') + process.exit(0) +} + +main().catch((err) => { + console.error('Fatal:', err) + process.exit(1) +}) diff --git a/scripts/update-images.ts b/scripts/update-images.ts new file mode 100644 index 0000000..1e803eb --- /dev/null +++ b/scripts/update-images.ts @@ -0,0 +1,179 @@ +/** + * Update images — jasonwoltje.com + * Uploads new photos and assigns them to globals/posts. + * Idempotent by alt-text match. + */ + +import { getPayload } from 'payload' +import config from '@payload-config' +import fs from 'node:fs' +import path from 'node:path' + +async function main() { + const payload = await getPayload({ config }) + const imagesDir = path.resolve(process.cwd(), 'images') + + const uploads: Array<{ file: string; alt: string; key: string }> = [ + { + file: 'gpt-image-1.5_creative_tech_founder_portrait_man_with_bald_head_and_brown-ginger_full_beard_bl-0.jpg', + alt: 'Jason Woltje — tech founder portrait, dark background', + key: 'hero', + }, + { + file: 'gpt-image-1.5_bold_social_media_profile_portrait_man_with_bald_head_and_brown-ginger_full_bear-0.jpg', + alt: 'Jason Woltje — social media profile, neon gradient', + key: 'og', + }, + { + file: 'gpt-image-1.5_documentary_style_environmental_portrait_man_with_bald_head_and_brown-ginger_ful-0.jpg', + alt: 'Jason Woltje — at the desk, documentary style', + key: 'desk', + }, + { + file: 'gpt-image-1.5_illustrated_portrait_stylized_modern_digital_art_style_man_with_bald_head_and_br-0.jpg', + alt: 'Jason Woltje — illustrated portrait', + key: 'illustration', + }, + { + file: 'gpt-image-1.5_authoritative_thought_leader_portrait_man_with_bald_head_and_brown-ginger_full_b-0.jpg', + alt: 'Jason Woltje — thought leader portrait, city backdrop', + key: 'thought-leader', + }, + { + file: 'gpt-image-1.5_high-end_fashion_forward_portrait_man_with_bald_head_and_brown-ginger_full_beard-0.jpg', + alt: 'Jason Woltje — fashion editorial portrait', + key: 'editorial', + }, + ] + + const ids: Record = {} + + console.log('\n── Uploading new images ──────────────────────────────────') + for (const u of uploads) { + const filePath = path.join(imagesDir, u.file) + if (!fs.existsSync(filePath)) { + console.log(` ⚠ Missing: ${u.file}`) + continue + } + + const existing = await payload.find({ + collection: 'media', + where: { alt: { equals: u.alt } }, + limit: 1, + }) + + if (existing.totalDocs > 0) { + ids[u.key] = existing.docs[0]!.id as number + console.log(` ↷ Already exists: ${u.alt} (id=${ids[u.key]})`) + continue + } + + // Update alt of old upload if it used a generic name (from initial seed) + const doc = await payload.create({ + collection: 'media', + filePath, + data: { alt: u.alt }, + }) + ids[u.key] = doc.id as number + console.log(` ✓ Uploaded: ${u.alt} (id=${doc.id})`) + } + + // ── Update Home hero ────────────────────────────────────────────────── + if (ids['hero']) { + console.log('\n── Updating Home hero image ─────────────────────────────') + const home = await payload.findGlobal({ slug: 'home', depth: 0 }) + const heroData = (home as Record).hero as Record | undefined + await payload.updateGlobal({ + slug: 'home', + data: { + hero: { + ...(heroData ?? {}), + heroImage: ids['hero'], + }, + }, + }) + console.log(` ✓ Home heroImage → id=${ids['hero']}`) + } + + // ── Update SEO OG image ─────────────────────────────────────────────── + if (ids['og']) { + console.log('\n── Updating SEO defaultOgImage ──────────────────────────') + await payload.updateGlobal({ + slug: 'seo', + data: { defaultOgImage: ids['og'] }, + }) + console.log(` ✓ SEO defaultOgImage → id=${ids['og']}`) + } + + // ── Update post cover images ────────────────────────────────────────── + console.log('\n── Updating post cover images ───────────────────────────') + const postCovers: Array<{ slugContains: string; imageKey: string }> = [ + { slugContains: 'cascading-traefik', imageKey: 'desk' }, + { slugContains: 'migrating-from-it', imageKey: 'thought-leader' }, + { slugContains: 'payload-3', imageKey: 'illustration' }, + ] + + for (const pc of postCovers) { + const imageId = ids[pc.imageKey] + if (!imageId) continue + + const { docs } = await payload.find({ + collection: 'posts', + where: { slug: { contains: pc.slugContains } }, + limit: 1, + depth: 0, + }) + if (docs.length === 0) { + console.log(` ⚠ Post not found: slug contains "${pc.slugContains}"`) + continue + } + const post = docs[0]! + await payload.update({ + collection: 'posts', + id: post.id, + data: { coverImage: imageId }, + }) + console.log(` ✓ Post "${post.slug}" coverImage → id=${imageId}`) + } + + // ── Assign project hero images ──────────────────────────────────────── + console.log('\n── Updating project hero images ─────────────────────────') + const projectHeroes: Array<{ slugContains: string; imageKey: string }> = [ + { slugContains: 'mosaic-stack', imageKey: 'hero' }, + { slugContains: 'jasonwoltje', imageKey: 'editorial' }, + ] + + for (const ph of projectHeroes) { + const imageId = ids[ph.imageKey] + if (!imageId) continue + + const { docs } = await payload.find({ + collection: 'projects', + where: { slug: { contains: ph.slugContains } }, + limit: 1, + depth: 0, + }) + if (docs.length === 0) { + console.log(` ⚠ Project not found: slug contains "${ph.slugContains}"`) + continue + } + const project = docs[0]! + await payload.update({ + collection: 'projects', + id: project.id, + data: { heroImage: imageId }, + }) + console.log(` ✓ Project "${project.slug}" heroImage → id=${imageId}`) + } + + console.log('\n════════════════════════════════════════════════════════') + console.log(' IMAGE UPDATE COMPLETE') + console.log('════════════════════════════════════════════════════════\n') + + process.exit(0) +} + +main().catch((err) => { + console.error('Fatal:', err) + process.exit(1) +}) diff --git a/src/globals/About.ts b/src/globals/About.ts index 906a763..3a15ff9 100644 --- a/src/globals/About.ts +++ b/src/globals/About.ts @@ -16,7 +16,14 @@ export const About: GlobalConfig = { { name: "portrait", type: "upload", relationTo: "media" }, ], }, - { name: "bio", type: "richText", editor: lexicalEditor({}) }, + { + type: "collapsible", + label: "Bio", + admin: { initCollapsed: false }, + fields: [ + { name: "bio", type: "richText", editor: lexicalEditor({}) }, + ], + }, { name: "timeline", type: "array",