diff --git a/public/career/projects/aeroplan-estore/thumb-card.webp b/public/career/projects/aeroplan-estore/thumb-card.webp new file mode 100644 index 0000000..2025626 Binary files /dev/null and b/public/career/projects/aeroplan-estore/thumb-card.webp differ diff --git a/public/career/projects/aeroplan-estore/thumb-half.webp b/public/career/projects/aeroplan-estore/thumb-half.webp new file mode 100644 index 0000000..ccbafb1 Binary files /dev/null and b/public/career/projects/aeroplan-estore/thumb-half.webp differ diff --git a/public/career/projects/aeroplan-shopping-button/thumb-card.webp b/public/career/projects/aeroplan-shopping-button/thumb-card.webp new file mode 100644 index 0000000..add3a0e Binary files /dev/null and b/public/career/projects/aeroplan-shopping-button/thumb-card.webp differ diff --git a/public/career/projects/aeroplan-shopping-button/thumb-half.webp b/public/career/projects/aeroplan-shopping-button/thumb-half.webp new file mode 100644 index 0000000..6b7bbdc Binary files /dev/null and b/public/career/projects/aeroplan-shopping-button/thumb-half.webp differ diff --git a/public/career/projects/air-miles-rewards-portal/thumb-card.webp b/public/career/projects/air-miles-rewards-portal/thumb-card.webp new file mode 100644 index 0000000..639d518 Binary files /dev/null and b/public/career/projects/air-miles-rewards-portal/thumb-card.webp differ diff --git a/public/career/projects/air-miles-rewards-portal/thumb-half.webp b/public/career/projects/air-miles-rewards-portal/thumb-half.webp new file mode 100644 index 0000000..f09f757 Binary files /dev/null and b/public/career/projects/air-miles-rewards-portal/thumb-half.webp differ diff --git a/public/career/projects/approved-by-acca/thumb-card.webp b/public/career/projects/approved-by-acca/thumb-card.webp new file mode 100644 index 0000000..ffcb1d9 Binary files /dev/null and b/public/career/projects/approved-by-acca/thumb-card.webp differ diff --git a/public/career/projects/approved-by-acca/thumb-half.webp b/public/career/projects/approved-by-acca/thumb-half.webp new file mode 100644 index 0000000..297715c Binary files /dev/null and b/public/career/projects/approved-by-acca/thumb-half.webp differ diff --git a/public/career/projects/baby-jogger/thumb-card.webp b/public/career/projects/baby-jogger/thumb-card.webp new file mode 100644 index 0000000..c5d2c60 Binary files /dev/null and b/public/career/projects/baby-jogger/thumb-card.webp differ diff --git a/public/career/projects/baby-jogger/thumb-half.webp b/public/career/projects/baby-jogger/thumb-half.webp new file mode 100644 index 0000000..046a7bc Binary files /dev/null and b/public/career/projects/baby-jogger/thumb-half.webp differ diff --git a/public/career/projects/beyond-mpd-olympus/thumb-card.webp b/public/career/projects/beyond-mpd-olympus/thumb-card.webp new file mode 100644 index 0000000..b9219a5 Binary files /dev/null and b/public/career/projects/beyond-mpd-olympus/thumb-card.webp differ diff --git a/public/career/projects/beyond-mpd-olympus/thumb-half.webp b/public/career/projects/beyond-mpd-olympus/thumb-half.webp new file mode 100644 index 0000000..52ff720 Binary files /dev/null and b/public/career/projects/beyond-mpd-olympus/thumb-half.webp differ diff --git a/public/career/projects/bring-your-toyota-home/thumb-card.webp b/public/career/projects/bring-your-toyota-home/thumb-card.webp new file mode 100644 index 0000000..a72a94d Binary files /dev/null and b/public/career/projects/bring-your-toyota-home/thumb-card.webp differ diff --git a/public/career/projects/bring-your-toyota-home/thumb-half.webp b/public/career/projects/bring-your-toyota-home/thumb-half.webp new file mode 100644 index 0000000..481d7d1 Binary files /dev/null and b/public/career/projects/bring-your-toyota-home/thumb-half.webp differ diff --git a/public/career/projects/canada-dry-enter-pin/thumb-card.webp b/public/career/projects/canada-dry-enter-pin/thumb-card.webp new file mode 100644 index 0000000..7773b69 Binary files /dev/null and b/public/career/projects/canada-dry-enter-pin/thumb-card.webp differ diff --git a/public/career/projects/canada-dry-enter-pin/thumb-half.webp b/public/career/projects/canada-dry-enter-pin/thumb-half.webp new file mode 100644 index 0000000..b4d9591 Binary files /dev/null and b/public/career/projects/canada-dry-enter-pin/thumb-half.webp differ diff --git a/public/career/projects/canada-dry/thumb-card.webp b/public/career/projects/canada-dry/thumb-card.webp new file mode 100644 index 0000000..7773b69 Binary files /dev/null and b/public/career/projects/canada-dry/thumb-card.webp differ diff --git a/public/career/projects/canada-dry/thumb-half.webp b/public/career/projects/canada-dry/thumb-half.webp new file mode 100644 index 0000000..b4d9591 Binary files /dev/null and b/public/career/projects/canada-dry/thumb-half.webp differ diff --git a/public/career/projects/cardinal-meats/thumb-card.webp b/public/career/projects/cardinal-meats/thumb-card.webp new file mode 100644 index 0000000..033dd17 Binary files /dev/null and b/public/career/projects/cardinal-meats/thumb-card.webp differ diff --git a/public/career/projects/cardinal-meats/thumb-half.webp b/public/career/projects/cardinal-meats/thumb-half.webp new file mode 100644 index 0000000..864a734 Binary files /dev/null and b/public/career/projects/cardinal-meats/thumb-half.webp differ diff --git a/public/career/projects/ccpa/thumb-card.webp b/public/career/projects/ccpa/thumb-card.webp new file mode 100644 index 0000000..35708a8 Binary files /dev/null and b/public/career/projects/ccpa/thumb-card.webp differ diff --git a/public/career/projects/ccpa/thumb-half.webp b/public/career/projects/ccpa/thumb-half.webp new file mode 100644 index 0000000..4b3466e Binary files /dev/null and b/public/career/projects/ccpa/thumb-half.webp differ diff --git a/public/career/projects/early-software-engineering/thumb-card.webp b/public/career/projects/early-software-engineering/thumb-card.webp new file mode 100644 index 0000000..ee46d91 Binary files /dev/null and b/public/career/projects/early-software-engineering/thumb-card.webp differ diff --git a/public/career/projects/early-software-engineering/thumb-half.webp b/public/career/projects/early-software-engineering/thumb-half.webp new file mode 100644 index 0000000..ba64de0 Binary files /dev/null and b/public/career/projects/early-software-engineering/thumb-half.webp differ diff --git a/public/career/projects/gridflow-app/thumb-card.webp b/public/career/projects/gridflow-app/thumb-card.webp new file mode 100644 index 0000000..76ec00c Binary files /dev/null and b/public/career/projects/gridflow-app/thumb-card.webp differ diff --git a/public/career/projects/gridflow-app/thumb-half.webp b/public/career/projects/gridflow-app/thumb-half.webp new file mode 100644 index 0000000..c07f286 Binary files /dev/null and b/public/career/projects/gridflow-app/thumb-half.webp differ diff --git a/public/career/projects/industrial-design/thumb-card.webp b/public/career/projects/industrial-design/thumb-card.webp new file mode 100644 index 0000000..cd9beb7 Binary files /dev/null and b/public/career/projects/industrial-design/thumb-card.webp differ diff --git a/public/career/projects/industrial-design/thumb-half.webp b/public/career/projects/industrial-design/thumb-half.webp new file mode 100644 index 0000000..7d49603 Binary files /dev/null and b/public/career/projects/industrial-design/thumb-half.webp differ diff --git a/public/career/projects/interiors-architecture/thumb-card.webp b/public/career/projects/interiors-architecture/thumb-card.webp new file mode 100644 index 0000000..e5fc606 Binary files /dev/null and b/public/career/projects/interiors-architecture/thumb-card.webp differ diff --git a/public/career/projects/interiors-architecture/thumb-half.webp b/public/career/projects/interiors-architecture/thumb-half.webp new file mode 100644 index 0000000..a648a28 Binary files /dev/null and b/public/career/projects/interiors-architecture/thumb-half.webp differ diff --git a/public/career/projects/loblaw-perfect-experience/thumb-card.webp b/public/career/projects/loblaw-perfect-experience/thumb-card.webp new file mode 100644 index 0000000..c369e1a Binary files /dev/null and b/public/career/projects/loblaw-perfect-experience/thumb-card.webp differ diff --git a/public/career/projects/loblaw-perfect-experience/thumb-half.webp b/public/career/projects/loblaw-perfect-experience/thumb-half.webp new file mode 100644 index 0000000..e82ec89 Binary files /dev/null and b/public/career/projects/loblaw-perfect-experience/thumb-half.webp differ diff --git a/public/career/projects/moda-match/thumb-card.webp b/public/career/projects/moda-match/thumb-card.webp new file mode 100644 index 0000000..550f59b Binary files /dev/null and b/public/career/projects/moda-match/thumb-card.webp differ diff --git a/public/career/projects/moda-match/thumb-half.webp b/public/career/projects/moda-match/thumb-half.webp new file mode 100644 index 0000000..f80f85d Binary files /dev/null and b/public/career/projects/moda-match/thumb-half.webp differ diff --git a/public/career/projects/monogram/thumb-card.webp b/public/career/projects/monogram/thumb-card.webp new file mode 100644 index 0000000..0f7b2cd Binary files /dev/null and b/public/career/projects/monogram/thumb-card.webp differ diff --git a/public/career/projects/monogram/thumb-half.webp b/public/career/projects/monogram/thumb-half.webp new file mode 100644 index 0000000..fe66209 Binary files /dev/null and b/public/career/projects/monogram/thumb-half.webp differ diff --git a/public/career/projects/parents-canada/thumb-card.webp b/public/career/projects/parents-canada/thumb-card.webp new file mode 100644 index 0000000..624d753 Binary files /dev/null and b/public/career/projects/parents-canada/thumb-card.webp differ diff --git a/public/career/projects/parents-canada/thumb-half.webp b/public/career/projects/parents-canada/thumb-half.webp new file mode 100644 index 0000000..4fdd511 Binary files /dev/null and b/public/career/projects/parents-canada/thumb-half.webp differ diff --git a/public/career/projects/paypower-prepaid-mastercard/thumb-card.webp b/public/career/projects/paypower-prepaid-mastercard/thumb-card.webp new file mode 100644 index 0000000..a5d7745 Binary files /dev/null and b/public/career/projects/paypower-prepaid-mastercard/thumb-card.webp differ diff --git a/public/career/projects/paypower-prepaid-mastercard/thumb-half.webp b/public/career/projects/paypower-prepaid-mastercard/thumb-half.webp new file mode 100644 index 0000000..a2f5d8b Binary files /dev/null and b/public/career/projects/paypower-prepaid-mastercard/thumb-half.webp differ diff --git a/public/career/projects/rocmar-dashboard/thumb-card.webp b/public/career/projects/rocmar-dashboard/thumb-card.webp new file mode 100644 index 0000000..0be26e6 Binary files /dev/null and b/public/career/projects/rocmar-dashboard/thumb-card.webp differ diff --git a/public/career/projects/rocmar-dashboard/thumb-half.webp b/public/career/projects/rocmar-dashboard/thumb-half.webp new file mode 100644 index 0000000..b2f7c12 Binary files /dev/null and b/public/career/projects/rocmar-dashboard/thumb-half.webp differ diff --git a/public/career/projects/techtank-next/thumb-card.webp b/public/career/projects/techtank-next/thumb-card.webp new file mode 100644 index 0000000..534dcb0 Binary files /dev/null and b/public/career/projects/techtank-next/thumb-card.webp differ diff --git a/public/career/projects/techtank-next/thumb-half.webp b/public/career/projects/techtank-next/thumb-half.webp new file mode 100644 index 0000000..818d71c Binary files /dev/null and b/public/career/projects/techtank-next/thumb-half.webp differ diff --git a/public/career/projects/tkodev-web/thumb-card.webp b/public/career/projects/tkodev-web/thumb-card.webp new file mode 100644 index 0000000..60d9202 Binary files /dev/null and b/public/career/projects/tkodev-web/thumb-card.webp differ diff --git a/public/career/projects/tkodev-web/thumb-half.webp b/public/career/projects/tkodev-web/thumb-half.webp new file mode 100644 index 0000000..eb16a30 Binary files /dev/null and b/public/career/projects/tkodev-web/thumb-half.webp differ diff --git a/public/career/projects/toffifee-spin-to-win/thumb-card.webp b/public/career/projects/toffifee-spin-to-win/thumb-card.webp new file mode 100644 index 0000000..0875bd0 Binary files /dev/null and b/public/career/projects/toffifee-spin-to-win/thumb-card.webp differ diff --git a/public/career/projects/toffifee-spin-to-win/thumb-half.webp b/public/career/projects/toffifee-spin-to-win/thumb-half.webp new file mode 100644 index 0000000..68a1808 Binary files /dev/null and b/public/career/projects/toffifee-spin-to-win/thumb-half.webp differ diff --git a/public/career/projects/toyota-make-a-date/thumb-card.webp b/public/career/projects/toyota-make-a-date/thumb-card.webp new file mode 100644 index 0000000..ac9a7e5 Binary files /dev/null and b/public/career/projects/toyota-make-a-date/thumb-card.webp differ diff --git a/public/career/projects/toyota-make-a-date/thumb-half.webp b/public/career/projects/toyota-make-a-date/thumb-half.webp new file mode 100644 index 0000000..fe4d3e9 Binary files /dev/null and b/public/career/projects/toyota-make-a-date/thumb-half.webp differ diff --git a/public/career/projects/web10-era/thumb-card.webp b/public/career/projects/web10-era/thumb-card.webp new file mode 100644 index 0000000..129207b Binary files /dev/null and b/public/career/projects/web10-era/thumb-card.webp differ diff --git a/public/career/projects/web10-era/thumb-half.webp b/public/career/projects/web10-era/thumb-half.webp new file mode 100644 index 0000000..98f796f Binary files /dev/null and b/public/career/projects/web10-era/thumb-half.webp differ diff --git a/public/career/projects/weiser/thumb-card.webp b/public/career/projects/weiser/thumb-card.webp new file mode 100644 index 0000000..bbc6cc2 Binary files /dev/null and b/public/career/projects/weiser/thumb-card.webp differ diff --git a/public/career/projects/weiser/thumb-half.webp b/public/career/projects/weiser/thumb-half.webp new file mode 100644 index 0000000..4dfa31a Binary files /dev/null and b/public/career/projects/weiser/thumb-half.webp differ diff --git a/src/app/design/page.tsx b/src/app/design/page.tsx index 299023b..856ebce 100644 --- a/src/app/design/page.tsx +++ b/src/app/design/page.tsx @@ -69,7 +69,12 @@ import { projectEntries, projectEntriesByCategory } from '@/constants/projects' import { appdata } from '@/constants/system' import { techtalkEntries } from '@/constants/techtalks' import { testimonialEntries } from '@/constants/testimonials' -import { getCareerYears, getJobGroups, getProjectMainAsset } from '@/utils/career' +import { + getCareerYears, + getJobGroups, + getProjectMainAsset, + getProjectMainThumb +} from '@/utils/career' import { createMetadata } from '@/utils/system' const metadata = createMetadata({ @@ -153,8 +158,9 @@ const DesignPage = () => { const specimenStory = specimenProject.media?.stories?.[0] const specimenVisual = specimenProject.media?.visuals?.[0] const mainAsset = getProjectMainAsset(specimenProject) + const mainThumb = getProjectMainThumb(specimenProject) const tileAssets = projectEntries - .map((projectEntry) => getProjectMainAsset(projectEntry)) + .map((projectEntry) => getProjectMainThumb(projectEntry)) .filter((asset) => asset.src !== placeholderAsset.src) .slice(0, 10) @@ -551,7 +557,7 @@ const DesignPage = () => {

Tile

- +

diff --git a/src/app/works/page.tsx b/src/app/works/page.tsx index 441f455..595034a 100644 --- a/src/app/works/page.tsx +++ b/src/app/works/page.tsx @@ -10,7 +10,7 @@ import { personEntryById } from '@/constants/profile' import { projectEntries, projectEntriesByCategory } from '@/constants/projects' import { appdata } from '@/constants/system' import { shuffle } from '@/utils/array' -import { getProjectMainAsset } from '@/utils/career' +import { getProjectMainThumb } from '@/utils/career' import { createMetadata } from '@/utils/system' const metadata = createMetadata({ @@ -29,7 +29,7 @@ const WorksPage = () => { ) const tileAssets = shuffle( projectEntries - .map((projectEntry) => getProjectMainAsset(projectEntry)) + .map((projectEntry) => getProjectMainThumb(projectEntry)) .filter((asset) => asset.src !== placeholderAsset.src) ).slice(0, 21) diff --git a/src/components/atoms/contour.tsx b/src/components/atoms/contour.tsx index 29ee415..c57dec3 100644 --- a/src/components/atoms/contour.tsx +++ b/src/components/atoms/contour.tsx @@ -17,6 +17,8 @@ const styles = { // Tuning const baseAlpha = 0.1 // faint lines const indexAlpha = 0.15 // index lines +const fillAlphaMin = 0 // elevation tint at the lowest tracked terrain value +const fillAlphaMax = 0.025 // elevation tint at the highest tracked terrain value; stays faint so lines still read as the focal layer const fps = 12 // render cap; GPU-side now, so a smooth rate is cheap const timeScale = 0.025 // how fast the terrain evolves (per second) const noiseFreq = 0.005 // spatial scale: higher = zoomed further out = far more lines on screen @@ -127,7 +129,12 @@ void main() { float alpha = mix(${f(baseAlpha)}, ${f(indexAlpha)}, isIndex); float line = (1.0 - smoothstep(0.0, halfWidth * g + 1e-6, di)) * alpha * inRange; - outColor = vec4(uStroke, line); + // elevation tint: a faint fill under the lines, rising with terrain height so higher ground reads + // slightly denser, same monochrome stroke color as the lines. + float fill = mix(${f(fillAlphaMin)}, ${f(fillAlphaMax)}, clamp(t / float(${levels - 1}), 0.0, 1.0)) * inRange; + float shade = line + fill * (1.0 - line); + + outColor = vec4(uStroke, shade); }` type ContourRef = HTMLCanvasElement diff --git a/src/components/molecules/project.tsx b/src/components/molecules/project.tsx index a9f5339..e3c428a 100644 --- a/src/components/molecules/project.tsx +++ b/src/components/molecules/project.tsx @@ -7,7 +7,7 @@ import { Ticker } from '@/components/atoms/ticker' import { clientById } from '@/constants/client' import { appTimeZone } from '@/constants/date' import { ProjectEntry } from '@/types/career' -import { getProjectMainAsset } from '@/utils/career' +import { getProjectMainThumb } from '@/utils/career' import { formatAttribution } from '@/utils/string' import { cn, cva, type VariantProps } from '@/utils/theme' import { Badge } from '../atoms/badge' @@ -62,7 +62,7 @@ const Project = forwardRef((props, ref) => { const { basic, extended, parents } = project // render vars - const asset = getProjectMainAsset(project) + const asset = getProjectMainThumb(project) const client = clientById[parents.clientId] ?? undefined const clientLogo = client?.media?.dark const agency = parents?.agencyId ? clientById[parents?.agencyId] : undefined diff --git a/src/constants/projects.ts b/src/constants/projects.ts index 59d4edf..4c936bc 100644 --- a/src/constants/projects.ts +++ b/src/constants/projects.ts @@ -122,6 +122,22 @@ const projectEntries: ProjectEntry[] = [ title: 'Case Study System and Authored Data', body: 'Modeled the portfolio around structured career constants rather than page-specific copy: each project carries metadata, stats, story assets, visuals, collaborators, and client relationships. The project dossier view proves that architecture by turning the same data into cinematic hero tiles, story sections, evidence plates, next-project navigation, metadata, and SEO.' } + ], + thumbs: [ + { + type: 'image', + src: '/career/projects/tkodev-web/thumb-card.webp', + width: 960, + height: 540, + alt: 'tko.dev — Cinematic Portfolio Homepage' + }, + { + type: 'image', + src: '/career/projects/tkodev-web/thumb-half.webp', + width: 480, + height: 270, + alt: 'tko.dev — Cinematic Portfolio Homepage' + } ] }, parents: { @@ -210,6 +226,22 @@ const projectEntries: ProjectEntry[] = [ height: 1282, alt: 'Gridflow Library — saved-imagery moodboard collections and tag sets' } + ], + thumbs: [ + { + type: 'image', + src: '/career/projects/gridflow-app/thumb-card.webp', + width: 960, + height: 540, + alt: 'Gridflow marketing landing page — "Plan your grid as a whole"' + }, + { + type: 'image', + src: '/career/projects/gridflow-app/thumb-half.webp', + width: 480, + height: 270, + alt: 'Gridflow marketing landing page — "Plan your grid as a whole"' + } ] }, parents: { @@ -306,6 +338,22 @@ const projectEntries: ProjectEntry[] = [ title: 'Role-Based Onboarding Funnels', body: 'Mapped community growth into role-based funnels for speakers, hosts, sponsors, and volunteers. Each path translates a vague "I want to help" into expectations, benefits, and a next step, reflecting the product work behind the site: clarify the audience, reduce ambiguity, and convert goodwill into active participation.' } + ], + thumbs: [ + { + type: 'image', + src: '/career/projects/techtank-next/thumb-card.webp', + width: 960, + height: 540, + alt: 'TechTank Toronto — Community Homepage' + }, + { + type: 'image', + src: '/career/projects/techtank-next/thumb-half.webp', + width: 480, + height: 270, + alt: 'TechTank Toronto — Community Homepage' + } ] }, parents: { @@ -347,6 +395,22 @@ const projectEntries: ProjectEntry[] = [ height: 2160, alt: 'Approved by Acca — Mortgage Agent Landing Page' } + ], + thumbs: [ + { + type: 'image', + src: '/career/projects/approved-by-acca/thumb-card.webp', + width: 960, + height: 540, + alt: 'Approved by Acca — Mortgage Agent Landing Page' + }, + { + type: 'image', + src: '/career/projects/approved-by-acca/thumb-half.webp', + width: 480, + height: 270, + alt: 'Approved by Acca — Mortgage Agent Landing Page' + } ] }, parents: { @@ -433,6 +497,22 @@ const projectEntries: ProjectEntry[] = [ title: 'Storefront Instrumentation at Scale', body: 'Shaped the SDK around real commerce surfaces instead of a clean-room demo: divergent Next.js versions, browser and server runtimes, and line-of-business requirements all had to fit one instrumentation model. The Shoppers storefront anchors the story because it proves the abstraction on a production-scale experience with frontend performance, traces, errors, and SLO signals flowing into VictoriaMetrics, Tempo, and Grafana.' } + ], + thumbs: [ + { + type: 'image', + src: '/career/projects/loblaw-perfect-experience/thumb-card.webp', + width: 960, + height: 540, + alt: 'Loblaw Perfect Experience — Grafana Dashboard' + }, + { + type: 'image', + src: '/career/projects/loblaw-perfect-experience/thumb-half.webp', + width: 480, + height: 270, + alt: 'Loblaw Perfect Experience — Grafana Dashboard' + } ] }, parents: { @@ -542,6 +622,22 @@ const projectEntries: ProjectEntry[] = [ title: 'Basic Tripping Controls', body: 'Translated the same safety-critical model into a basic mode for operators who need fast control over static pressure, dynamic pressure, and hookload thresholds. This is where product judgment and engineering constraints meet: preserve the rig visualization and live gauges, reduce the input surface, and keep the workflow dependable under field pressure.' } + ], + thumbs: [ + { + type: 'image', + src: '/career/projects/beyond-mpd-olympus/thumb-card.webp', + width: 960, + height: 540, + alt: 'Beyond MPD — Ramp Control Interface' + }, + { + type: 'image', + src: '/career/projects/beyond-mpd-olympus/thumb-half.webp', + width: 480, + height: 270, + alt: 'Beyond MPD — Ramp Control Interface' + } ] }, parents: { @@ -636,6 +732,22 @@ const projectEntries: ProjectEntry[] = [ title: 'Account Onboarding and Card Dashboard', body: 'Built the everyday account surfaces customers rely on after launch: onboarding, balance review, pending and posted transactions, bill payment, reloads, and card actions. These screens are user-facing proof of the platform work beneath them, where shared flows, configuration points, CI/CD, Sentry, and Mixpanel made releases reliable across iOS, Android, and web.' } + ], + thumbs: [ + { + type: 'image', + src: '/career/projects/paypower-prepaid-mastercard/thumb-card.webp', + width: 960, + height: 540, + alt: 'PayPower Mastercard App — Product Overview' + }, + { + type: 'image', + src: '/career/projects/paypower-prepaid-mastercard/thumb-half.webp', + width: 480, + height: 270, + alt: 'PayPower Mastercard App — Product Overview' + } ] }, parents: { @@ -739,6 +851,22 @@ const projectEntries: ProjectEntry[] = [ title: 'Multi-Level Caching and Performance', body: 'Owned the delivery shape alongside the product owner: PRDs, estimates, client expectations, and the caching strategy needed to support hundreds of retail sites. The multi-level cache reduced backend load while keeping activation and offer states responsive, turning a first-of-its-kind browser extension into a shippable product for a national loyalty platform.' } + ], + thumbs: [ + { + type: 'image', + src: '/career/projects/aeroplan-shopping-button/thumb-card.webp', + width: 960, + height: 540, + alt: 'Aeroplan Shopping Button — Cross-Browser Architecture' + }, + { + type: 'image', + src: '/career/projects/aeroplan-shopping-button/thumb-half.webp', + width: 480, + height: 270, + alt: 'Aeroplan Shopping Button — Cross-Browser Architecture' + } ] }, parents: { @@ -857,6 +985,22 @@ const projectEntries: ProjectEntry[] = [ title: 'Accessibility Compliance — AODA AA / WCAG 2.0', body: 'Led five engineers through a full AODA/WCAG 2.0 AA accessibility overhaul across the React/Redux application. The engagement covered structural semantics, focus management, ARIA roles, keyboard navigation, colour contrast, and regression protection, with 80%+ test coverage across critical components using React Testing Library and Jest.' } + ], + thumbs: [ + { + type: 'image', + src: '/career/projects/aeroplan-estore/thumb-card.webp', + width: 960, + height: 540, + alt: 'Aeroplan eStore — Initial Architecture and MVP' + }, + { + type: 'image', + src: '/career/projects/aeroplan-estore/thumb-half.webp', + width: 480, + height: 270, + alt: 'Aeroplan eStore — Initial Architecture and MVP' + } ] }, parents: { @@ -943,6 +1087,22 @@ const projectEntries: ProjectEntry[] = [ title: 'Figma-to-Production Dashboard System', body: 'Designed the full interface and design system in Figma, then carried it into production as the sole owner of the dashboard experience. The UI work was not surface polish: reusable components, typography, color semantics, and interaction patterns made a dense operational tool feel coherent for an architectural engineering firm.' } + ], + thumbs: [ + { + type: 'image', + src: '/career/projects/rocmar-dashboard/thumb-card.webp', + width: 960, + height: 540, + alt: 'RocMar Employee Dashboard — Data Integration Overview' + }, + { + type: 'image', + src: '/career/projects/rocmar-dashboard/thumb-half.webp', + width: 480, + height: 270, + alt: 'RocMar Employee Dashboard — Data Integration Overview' + } ] }, parents: { @@ -1081,6 +1241,22 @@ const projectEntries: ProjectEntry[] = [ title: 'Accessible Education for Reward Mechanics', body: 'Turned reward mechanics into clear, accessible interface sections that could explain earning paths without overwhelming the user. The How to Earn screen supports the story because it shows the balance the work required: brand expression, AODA/WCAG 2.0 AA compliance, responsive component behavior, and Jest/Nock-backed confidence in a shared codebase.' } + ], + thumbs: [ + { + type: 'image', + src: '/career/projects/air-miles-rewards-portal/thumb-card.webp', + width: 960, + height: 540, + alt: 'Air Miles Landing Portal' + }, + { + type: 'image', + src: '/career/projects/air-miles-rewards-portal/thumb-half.webp', + width: 480, + height: 270, + alt: 'Air Miles Landing Portal' + } ] }, parents: { @@ -1123,6 +1299,22 @@ const projectEntries: ProjectEntry[] = [ height: 1080, alt: 'Moda Match — Virtual Fitting Room' } + ], + thumbs: [ + { + type: 'image', + src: '/career/projects/moda-match/thumb-card.webp', + width: 960, + height: 540, + alt: 'Moda Match — Virtual Fitting Room' + }, + { + type: 'image', + src: '/career/projects/moda-match/thumb-half.webp', + width: 480, + height: 270, + alt: 'Moda Match — Virtual Fitting Room' + } ] }, parents: { @@ -1180,6 +1372,22 @@ const projectEntries: ProjectEntry[] = [ height: 1080, alt: 'Cardinal Meats & Food Service — Website & CMS' } + ], + thumbs: [ + { + type: 'image', + src: '/career/projects/cardinal-meats/thumb-card.webp', + width: 960, + height: 540, + alt: 'Cardinal Meats & Food Service — Website & CMS' + }, + { + type: 'image', + src: '/career/projects/cardinal-meats/thumb-half.webp', + width: 480, + height: 270, + alt: 'Cardinal Meats & Food Service — Website & CMS' + } ] }, parents: { @@ -1237,6 +1445,22 @@ const projectEntries: ProjectEntry[] = [ height: 1080, alt: 'Weiser — Canadian Catalog Website' } + ], + thumbs: [ + { + type: 'image', + src: '/career/projects/weiser/thumb-card.webp', + width: 960, + height: 540, + alt: 'Weiser — Canadian Catalog Website' + }, + { + type: 'image', + src: '/career/projects/weiser/thumb-half.webp', + width: 480, + height: 270, + alt: 'Weiser — Canadian Catalog Website' + } ] }, parents: { @@ -1284,6 +1508,22 @@ const projectEntries: ProjectEntry[] = [ height: 1748, alt: 'Canada Chiropractic Protective Association — Infographic' } + ], + thumbs: [ + { + type: 'image', + src: '/career/projects/ccpa/thumb-card.webp', + width: 960, + height: 540, + alt: 'Canada Chiropractic Protective Association — Website & CMS' + }, + { + type: 'image', + src: '/career/projects/ccpa/thumb-half.webp', + width: 480, + height: 270, + alt: 'Canada Chiropractic Protective Association — Website & CMS' + } ] }, parents: { @@ -1317,6 +1557,22 @@ const projectEntries: ProjectEntry[] = [ height: 1080, alt: 'Canada Dry — Enter PIN Contest' } + ], + thumbs: [ + { + type: 'image', + src: '/career/projects/canada-dry-enter-pin/thumb-card.webp', + width: 960, + height: 540, + alt: 'Canada Dry — Enter PIN Contest' + }, + { + type: 'image', + src: '/career/projects/canada-dry-enter-pin/thumb-half.webp', + width: 480, + height: 270, + alt: 'Canada Dry — Enter PIN Contest' + } ] }, parents: { @@ -1357,6 +1613,22 @@ const projectEntries: ProjectEntry[] = [ height: 1080, alt: 'Canada Dry — Website' } + ], + thumbs: [ + { + type: 'image', + src: '/career/projects/canada-dry/thumb-card.webp', + width: 960, + height: 540, + alt: 'Canada Dry — Website' + }, + { + type: 'image', + src: '/career/projects/canada-dry/thumb-half.webp', + width: 480, + height: 270, + alt: 'Canada Dry — Website' + } ] }, parents: { @@ -1398,6 +1670,22 @@ const projectEntries: ProjectEntry[] = [ height: 1080, alt: 'Monogram — Canadian Catalog Website' } + ], + thumbs: [ + { + type: 'image', + src: '/career/projects/monogram/thumb-card.webp', + width: 960, + height: 540, + alt: 'Monogram — Canadian Catalog Website' + }, + { + type: 'image', + src: '/career/projects/monogram/thumb-half.webp', + width: 480, + height: 270, + alt: 'Monogram — Canadian Catalog Website' + } ] }, parents: { @@ -1432,6 +1720,22 @@ const projectEntries: ProjectEntry[] = [ height: 1080, alt: 'Parents Canada — Magazine Website' } + ], + thumbs: [ + { + type: 'image', + src: '/career/projects/parents-canada/thumb-card.webp', + width: 960, + height: 540, + alt: 'Parents Canada — Magazine Website' + }, + { + type: 'image', + src: '/career/projects/parents-canada/thumb-half.webp', + width: 480, + height: 270, + alt: 'Parents Canada — Magazine Website' + } ] }, parents: { @@ -1479,6 +1783,22 @@ const projectEntries: ProjectEntry[] = [ height: 1652, alt: 'Toffifee — Spin Animation' } + ], + thumbs: [ + { + type: 'image', + src: '/career/projects/toffifee-spin-to-win/thumb-card.webp', + width: 960, + height: 540, + alt: 'Toffifee — Spin to Win Contest' + }, + { + type: 'image', + src: '/career/projects/toffifee-spin-to-win/thumb-half.webp', + width: 480, + height: 270, + alt: 'Toffifee — Spin to Win Contest' + } ] }, parents: { @@ -1526,6 +1846,22 @@ const projectEntries: ProjectEntry[] = [ height: 796, alt: 'Toyota — Bring Your Toyota Home Contest (Full)' } + ], + thumbs: [ + { + type: 'image', + src: '/career/projects/bring-your-toyota-home/thumb-card.webp', + width: 960, + height: 540, + alt: 'Toyota — Bring Your Toyota Home Contest' + }, + { + type: 'image', + src: '/career/projects/bring-your-toyota-home/thumb-half.webp', + width: 480, + height: 270, + alt: 'Toyota — Bring Your Toyota Home Contest' + } ] }, parents: { @@ -1565,6 +1901,22 @@ const projectEntries: ProjectEntry[] = [ height: 574, alt: 'Toyota — Make a Date Contest' } + ], + thumbs: [ + { + type: 'image', + src: '/career/projects/toyota-make-a-date/thumb-card.webp', + width: 960, + height: 540, + alt: 'Toyota — Make a Date Contest' + }, + { + type: 'image', + src: '/career/projects/toyota-make-a-date/thumb-half.webp', + width: 480, + height: 270, + alt: 'Toyota — Make a Date Contest' + } ] }, parents: { @@ -1605,6 +1957,22 @@ const projectEntries: ProjectEntry[] = [ height: 1080, alt: 'BabyJogger — Canadian Catalog Website' } + ], + thumbs: [ + { + type: 'image', + src: '/career/projects/baby-jogger/thumb-card.webp', + width: 960, + height: 540, + alt: 'BabyJogger — Canadian Catalog Website' + }, + { + type: 'image', + src: '/career/projects/baby-jogger/thumb-half.webp', + width: 480, + height: 270, + alt: 'BabyJogger — Canadian Catalog Website' + } ] }, parents: { @@ -1702,6 +2070,22 @@ const projectEntries: ProjectEntry[] = [ title: 'Game Logic, Timers, and Everyday Tools', body: 'Built small games and utility apps to practice the product behavior that still matters in larger systems: state, timing, input handling, conditional logic, feedback loops, and error recovery. Tic Tac Toe, Simon Says, the Pomodoro timer, calculator, and weather widget became a compact proving ground for the same muscle used later on production teams: translate an expected experience into dependable interface logic.' } + ], + thumbs: [ + { + type: 'image', + src: '/career/projects/early-software-engineering/thumb-card.webp', + width: 960, + height: 540, + alt: 'Early Software Engineering — Full-Stack Foundation' + }, + { + type: 'image', + src: '/career/projects/early-software-engineering/thumb-half.webp', + width: 480, + height: 270, + alt: 'Early Software Engineering — Full-Stack Foundation' + } ] }, parents: { @@ -1840,6 +2224,22 @@ const projectEntries: ProjectEntry[] = [ title: 'Identity Systems for Social Platforms', body: 'Built full visual identities for early social spaces like Xanga, where a profile was less a feed and more a self-authored website. The variant supports the story because it shows more than decoration: color, layout, typography, illustration, and mood all working together inside platform constraints. Long before formal design systems, I was already shaping reusable visual language around identity, audience, and atmosphere.' } + ], + thumbs: [ + { + type: 'image', + src: '/career/projects/web10-era/thumb-card.webp', + width: 960, + height: 540, + alt: 'Web 1.0 Era — PixelInfinity.ca Theme' + }, + { + type: 'image', + src: '/career/projects/web10-era/thumb-half.webp', + width: 480, + height: 270, + alt: 'Web 1.0 Era — PixelInfinity.ca Theme' + } ] }, parents: { @@ -2020,6 +2420,22 @@ const projectEntries: ProjectEntry[] = [ title: 'Material, Volume, and Atmosphere', body: 'Refined the interior as a relationship between mass, void, texture, and daylight rather than a collection of decorated rooms. The second cutaway shows that design judgment at a quieter level: how surfaces meet, how volumes stack, and how a space can feel calm while still carrying architectural tension. It is portfolio evidence of taste — proportion, restraint, and spatial composition under real constraints.' } + ], + thumbs: [ + { + type: 'image', + src: '/career/projects/interiors-architecture/thumb-card.webp', + width: 960, + height: 540, + alt: "Interior & Architecture Design — 1800's Queen St. Modernization" + }, + { + type: 'image', + src: '/career/projects/interiors-architecture/thumb-half.webp', + width: 480, + height: 270, + alt: "Interior & Architecture Design — 1800's Queen St. Modernization" + } ] }, parents: { @@ -2222,6 +2638,22 @@ const projectEntries: ProjectEntry[] = [ title: 'Fitment, Fabrication, and Iteration', body: 'Used Fusion 360, Blender, laser-cutting constraints, and 3D printing to turn hardware problems into manufacturable parts: compact PC cases, water-cooling blocks, adapter mounts, fittings, desk accessories, trophies, keyfobs, and everyday objects. The screenshots show the range, but the portfolio signal is taste under constraint: measure carefully, prototype honestly, refine the geometry, and keep every visual decision accountable to how the object is held, installed, seen, and used.' } + ], + thumbs: [ + { + type: 'image', + src: '/career/projects/industrial-design/thumb-card.webp', + width: 960, + height: 540, + alt: '3D Modeling — Bekant Server Rack for NAS and SFF Servers' + }, + { + type: 'image', + src: '/career/projects/industrial-design/thumb-half.webp', + width: 480, + height: 270, + alt: '3D Modeling — Bekant Server Rack for NAS and SFF Servers' + } ] }, parents: { diff --git a/src/types/career.ts b/src/types/career.ts index 0d04020..2451db0 100644 --- a/src/types/career.ts +++ b/src/types/career.ts @@ -4,7 +4,6 @@ import { type AssetEntry, type StoryEntry } from '@/types/layout' type CompetencyEntry = { id: string title: string - /** Optional expanded copy revealed when the row opens. */ desc?: string } @@ -21,7 +20,6 @@ type PersonEntry = { email?: string phone?: string resume?: string - /** The person's competencies, indexed on the home overview. */ competencies?: CompetencyEntry[] } media?: { @@ -33,18 +31,14 @@ type PersonEntry = { /** An organization in the record: an employer, school, or Tony's own ventures. */ type ClientEntry = { id: string - /** How the organization relates to the career: hiring client, school, or self-owned. */ category?: 'client' | 'education' | 'self' basic: { name: string href?: string } media?: { - /** Theme-agnostic logo. */ base?: AssetEntry - /** Logo for light backgrounds. */ light?: AssetEntry - /** Logo for dark backgrounds. */ dark?: AssetEntry } } @@ -55,11 +49,9 @@ type JobEntry = { basic: { title: string desc: string - /** The engagement type shown in the employment ledger. */ type: 'contract' | 'full-time' | 'volunteer' | 'break' location: string startDate: Date - /** Absent when the role is current. */ endDate?: Date } extended?: { @@ -79,7 +71,6 @@ type TestimonialEntry = { date: Date } parents: { - /** How the author knows the recipient (e.g. "managed", "worked with"). */ relation: string authorId: PersonEntry['id'] recipientId: PersonEntry['id'] @@ -114,7 +105,6 @@ type EducationEntry = { subtitle: string desc?: string startDate: Date - /** Absent when the program is ongoing. */ endDate?: Date } parents?: { @@ -137,33 +127,26 @@ type ProjectEntry = { subtitle: string desc: string role: string - /** Where the project sits: the featured trio, active, or inactive. */ category: 'featured' | 'inactive' | 'active' startDate: Date - /** Absent when the project is ongoing. */ endDate?: Date } extended?: { duties?: string[] - /** The headline outcome the project won. */ win?: string - /** The measurable effect the project had. */ impact?: string skills?: string[] tools?: string[] stats?: StatEntry[] } media?: { - /** Supporting plates that widen the record without carrying the narrative. */ visuals?: AssetEntry[] - /** Annotated figures that carry the dossier's argument. */ stories?: StoryEntry[] + thumbs?: AssetEntry[] } parents: { clientId: ClientEntry['id'] - /** The agency Tony worked through, when the project was not direct. */ agencyId?: ClientEntry['id'] - /** Collaborators credited on the project. */ staffIds?: PersonEntry['id'][] } } diff --git a/src/utils/career.ts b/src/utils/career.ts index e57cc7e..aeb08cc 100644 --- a/src/utils/career.ts +++ b/src/utils/career.ts @@ -14,6 +14,15 @@ const getProjectMainAsset = (project: ProjectEntry): AssetEntry => { return project.media?.stories?.[0]?.asset ?? project.media?.visuals?.[0] ?? placeholderAsset } +/** + * A project's card thumbnail: the card-sized thumb, falling back to the site placeholder. + * + * @param project - The project to read media from. + */ +const getProjectMainThumb = (project: ProjectEntry): AssetEntry => { + return project.media?.thumbs?.[0] ?? placeholderAsset +} + /** * The ids of projects that own a detail page: only featured entries link to `/works/[workId]`. * @@ -152,6 +161,7 @@ export { getFeaturedProjectIds, getJobGroups, getProjectClientIds, - getProjectMainAsset + getProjectMainAsset, + getProjectMainThumb } export type { JobGroup }