diff --git a/jack-portfolio/index.html b/jack-portfolio/index.html deleted file mode 100644 index 1056123..0000000 --- a/jack-portfolio/index.html +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - Jack -- 3D Creator - - - - - -
- - - diff --git a/jack-portfolio/scripts/build-preview.mjs b/jack-portfolio/scripts/build-preview.mjs deleted file mode 100644 index 8f13570..0000000 --- a/jack-portfolio/scripts/build-preview.mjs +++ /dev/null @@ -1,68 +0,0 @@ -// 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/LiveProjectButton.tsx b/jack-portfolio/src/components/LiveProjectButton.tsx deleted file mode 100644 index d2c4094..0000000 --- a/jack-portfolio/src/components/LiveProjectButton.tsx +++ /dev/null @@ -1,10 +0,0 @@ -export default function LiveProjectButton() { - return ( - - ); -} diff --git a/jack-portfolio/src/data/content.ts b/jack-portfolio/src/data/content.ts deleted file mode 100644 index 55720e5..0000000 --- a/jack-portfolio/src/data/content.ts +++ /dev/null @@ -1,76 +0,0 @@ -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 = A(`${FIGMA}/d24c01ad3a56fc65e942a1f501eb73db42d7cf9a/Rectangle_40443.81459862.png`); - -export const aboutIcons = { - 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 = - "With more than five years of experience in design, i focus on branding, web design, and user experience, i truly enjoy working with businesses that aim to stand out and present their best image. Let's build something incredible together!"; - -export const marqueeImages = [ - 'hero-space-voyage-preview-eECLH3Yc', - 'hero-codenest-preview-Cgppc2qV', - 'hero-vex-ventures-preview-BczMFIiw', - 'hero-stellar-ai-v2-preview-DjvxjG3C', - 'hero-asme-preview-B_nGDnTP', - 'hero-transform-data-preview-Cx5OU29N', - 'hero-vitara-preview-Cjz2QYyU', - 'hero-terra-preview-BFjrCr7T', - 'hero-skyelite-preview-DHaZIgUv', - 'hero-aethera-preview-DknSlcTa', - 'hero-designpro-preview-D8c5_een', - 'hero-stellar-ai-preview-D3HL6bw1', - 'hero-xportfolio-preview-D4A8maiC', - 'hero-orbit-web3-preview-BXt4OttD', - 'hero-nexora-preview-cx5HmUgo', - 'hero-evr-ventures-preview-DZxeVFEX', - 'hero-planet-orbit-preview-DWAP8Z1P', - 'hero-new-era-preview-CocuDUm9', - 'hero-wealth-preview-B70idl_u', - 'hero-luminex-preview-CxOP7ce6', - 'hero-celestia-preview-0yO3jXO8', -].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.' }, - { name: 'Rendering', description: 'High-quality, photorealistic renders that showcase designs with custom lighting, textures, and materials to bring concepts to life.' }, - { name: 'Motion Design', description: 'Dynamic animations and motion graphics that add energy and storytelling to brands, products, and digital experiences.' }, - { name: 'Branding', description: 'Crafting cohesive visual identities -- from logos to full brand systems -- that communicate a clear and memorable presence.' }, - { name: 'Web Design', description: 'Designing clean, modern, and conversion-focused websites with attention to layout, typography, and user experience.' }, -]; - -const cf = (file: string) => - A(`https://images.higgs.ai/?default=1&output=webp&url=${encodeURIComponent( - `https://d8j0ntlcm91z4.cloudfront.net/user_38xzZboKViGWJOttwIXH07lWA1P/${file}` - )}&w=1280&q=85`); - -export const projects = [ - { - category: 'Client', - name: 'Nextlevel Studio', - col1: [cf('hf_20260412_055344_5eff02e0-87a5-41ce-b64f-eb08da8f33db.png'), cf('hf_20260412_055431_11d841fd-8b41-46a5-82e4-b04f2407a7d8.png')], - col2: cf('hf_20260412_055451_e317bf2d-28d4-48cc-86b0-6f72f25b6327.png'), - }, - { - category: 'Personal', - name: 'Aura Brand Identity', - col1: [cf('hf_20260412_055654_911201c5-36d9-4bc6-bac7-331adfce159f.png'), cf('hf_20260412_055723_5ceda0b8-d9c2-4665-b2e3-83ba19ba76d1.png')], - col2: cf('hf_20260412_055753_adc5dcbd-a8e6-49c0-b43a-9b030d835cea.png'), - }, - { - category: 'Client', - name: 'Solaris Digital', - col1: [cf('hf_20260412_055759_963cfb0b-4bd1-4b0f-9d0a-09bd6cf95b2f.png'), cf('hf_20260412_060108_438f781a-9846-4dcc-89ab-c4e6cb830f5b.png')], - col2: cf('hf_20260412_055818_9d062121-ad7e-46b9-999a-1a6a692ef1ee.png'), - }, -]; diff --git a/jack-portfolio/src/data/embedded.ts b/jack-portfolio/src/data/embedded.ts deleted file mode 100644 index 7b4a692..0000000 --- a/jack-portfolio/src/data/embedded.ts +++ /dev/null @@ -1,3 +0,0 @@ -// 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 = {}; diff --git a/jack-portfolio/src/sections/MarqueeSection.tsx b/jack-portfolio/src/sections/MarqueeSection.tsx deleted file mode 100644 index d8dac90..0000000 --- a/jack-portfolio/src/sections/MarqueeSection.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { useEffect, useRef, useState } from 'react'; -import { marqueeImages } from '../data/content'; - -const row1 = marqueeImages.slice(0, 11); -const row2 = marqueeImages.slice(11); - -function Row({ images, transform }: { images: string[]; transform: string }) { - const tripled = [...images, ...images, ...images]; - return ( -
- {tripled.map((src, i) => ( - - ))} -
- ); -} - -export default function MarqueeSection() { - const ref = useRef(null); - const [offset, setOffset] = useState(0); - - useEffect(() => { - const onScroll = () => { - if (!ref.current) return; - const top = ref.current.getBoundingClientRect().top + window.scrollY; - setOffset((window.scrollY - top + window.innerHeight) * 0.3); - }; - onScroll(); - window.addEventListener('scroll', onScroll, { passive: true }); - return () => window.removeEventListener('scroll', onScroll); - }, []); - - return ( -
- - -
- ); -} diff --git a/jack-portfolio/vite.config.ts b/jack-portfolio/vite.config.ts deleted file mode 100644 index 01fbbfd..0000000 --- a/jack-portfolio/vite.config.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { defineConfig } from 'vite'; -import react from '@vitejs/plugin-react'; - -export default defineConfig({ - plugins: [react()], - base: './', -}); diff --git a/jack-portfolio/.gitignore b/keith-portfolio/.gitignore similarity index 100% rename from jack-portfolio/.gitignore rename to keith-portfolio/.gitignore diff --git a/jack-portfolio/PRD.md b/keith-portfolio/PRD.md similarity index 65% rename from jack-portfolio/PRD.md rename to keith-portfolio/PRD.md index 917a20f..fe5cba5 100644 --- a/jack-portfolio/PRD.md +++ b/keith-portfolio/PRD.md @@ -1,9 +1,9 @@ -# PRD: "Jack — 3D Creator" Portfolio Template +# PRD: Keith Rana Portfolio (built from the "Jack 3D Creator" template) ## The restaurant analogy | Stage | Restaurant | This project | |---|---|---| -| Phase 1 (today) | Food stand: one great dish, open today | The full single-page site, looks right, animations work | +| Phase 1 (done) | Food stand: one great dish, open today | The full single-page site with Keith's real content, photos and animations | | Phase 2 | Small restaurant: a few tables, a real menu | Make it easy to edit (content in one file), better mobile + accessibility | | Phase 3 | Full restaurant: front of house + back of house | Real contact form, email, project pages, CMS, analytics | @@ -33,3 +33,13 @@ Done when: builds with no errors; page loads with no console errors; every butto ## Security notes Phase 1 has no forms, no logins, no secrets, so risk is low. External links use rel="noopener noreferrer". When Phase 3 adds a form, validation and spam protection must live on the server, and API keys must never be in front-end code. + +## Phase 1 status (done) +- Content comes from Keith's existing portfolio: profile, 8 capabilities, 3 roles + 5 earlier roles, 4 projects, 10 certifications, contact details (all in `src/data/content.ts`). +- Sections: Hero (portrait), scrolling photo/skills rows, About, Capability, Experience, Projects (stacking cards), Credentials, Contact. +- Photos and the Kanit font ship inside the site (no outside image hosts needed). +- Tested at 360, 390, 768, 1024, 1440 and 1920 px wide: no sideways scroll, no console errors, all images load. +- Cloud preview: `node scripts/build-preview.mjs out.html` builds one self-contained page. + +## Not used yet +- `it_helpdesk.db` (87 IT help-desk FAQs) was supplied but is not portfolio content. Candidate for a Phase 2 FAQ section if wanted. diff --git a/keith-portfolio/index.html b/keith-portfolio/index.html new file mode 100644 index 0000000..79c9b37 --- /dev/null +++ b/keith-portfolio/index.html @@ -0,0 +1,13 @@ + + + + + + + Keith Rana -- IT Service Delivery + + +
+ + + diff --git a/jack-portfolio/package-lock.json b/keith-portfolio/package-lock.json similarity index 99% rename from jack-portfolio/package-lock.json rename to keith-portfolio/package-lock.json index 65c7931..90e5b60 100644 --- a/jack-portfolio/package-lock.json +++ b/keith-portfolio/package-lock.json @@ -1,13 +1,14 @@ { - "name": "jack-portfolio", + "name": "keith-portfolio", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "jack-portfolio", + "name": "keith-portfolio", "version": "1.0.0", "dependencies": { + "@fontsource/kanit": "^5.3.0", "framer-motion": "^12.38.0", "lucide-react": "^0.344.0", "react": "^18.3.1", @@ -710,6 +711,15 @@ "node": ">=12" } }, + "node_modules/@fontsource/kanit": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@fontsource/kanit/-/kanit-5.3.0.tgz", + "integrity": "sha512-XrVxf5NWb8QLAEbQS2GYir3AY0WbOLr5Q80fWurnoFWXyc7CWvioGNPC2dxRGZKocMUXbwGhuK+sJqWkRLAWNA==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", diff --git a/jack-portfolio/package.json b/keith-portfolio/package.json similarity index 90% rename from jack-portfolio/package.json rename to keith-portfolio/package.json index 72593a0..b0cc478 100644 --- a/jack-portfolio/package.json +++ b/keith-portfolio/package.json @@ -1,5 +1,5 @@ { - "name": "jack-portfolio", + "name": "keith-portfolio", "private": true, "version": "1.0.0", "type": "module", @@ -9,6 +9,7 @@ "preview": "vite preview" }, "dependencies": { + "@fontsource/kanit": "^5.3.0", "framer-motion": "^12.38.0", "lucide-react": "^0.344.0", "react": "^18.3.1", diff --git a/jack-portfolio/postcss.config.js b/keith-portfolio/postcss.config.js similarity index 100% rename from jack-portfolio/postcss.config.js rename to keith-portfolio/postcss.config.js diff --git a/keith-portfolio/scripts/build-preview.mjs b/keith-portfolio/scripts/build-preview.mjs new file mode 100644 index 0000000..7420e09 --- /dev/null +++ b/keith-portfolio/scripts/build-preview.mjs @@ -0,0 +1,24 @@ +// Builds ONE self-contained HTML page for the Claude cloud preview. +// Claude's cloud pages block outside images and files, so photos and fonts are baked in. +// Usage: node scripts/build-preview.mjs +import { execFileSync } from 'node:child_process'; +import { readFileSync, readdirSync, writeFileSync } from 'node:fs'; + +const out = process.argv[2] ?? 'preview.html'; +execFileSync('npm', ['run', 'build'], { stdio: 'inherit', env: { ...process.env, INLINE: '1' } }); + +const dir = 'dist/assets/'; +const files = readdirSync(dir); +const js = readFileSync(dir + files.find((f) => f.endsWith('.js')), 'utf8').replace(/<\/script/g, '<\\/script'); +const css = readFileSync(dir + files.find((f) => f.endsWith('.css')), 'utf8'); + +const html = ` +Keith Rana Portfolio + +
+ +`; +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/App.tsx b/keith-portfolio/src/App.tsx similarity index 66% rename from jack-portfolio/src/App.tsx rename to keith-portfolio/src/App.tsx index 1f24042..ae1398c 100644 --- a/jack-portfolio/src/App.tsx +++ b/keith-portfolio/src/App.tsx @@ -2,7 +2,10 @@ import HeroSection from './sections/HeroSection'; import MarqueeSection from './sections/MarqueeSection'; import AboutSection from './sections/AboutSection'; import ServicesSection from './sections/ServicesSection'; +import ExperienceSection from './sections/ExperienceSection'; import ProjectsSection from './sections/ProjectsSection'; +import CredentialsSection from './sections/CredentialsSection'; +import ContactSection from './sections/ContactSection'; export default function App() { return ( @@ -11,7 +14,10 @@ export default function App() { + + + ); } diff --git a/keith-portfolio/src/assets/leadership.webp b/keith-portfolio/src/assets/leadership.webp new file mode 100644 index 0000000..e1c4f44 Binary files /dev/null and b/keith-portfolio/src/assets/leadership.webp differ diff --git a/keith-portfolio/src/assets/portrait.jpg b/keith-portfolio/src/assets/portrait.jpg new file mode 100644 index 0000000..145b9e2 Binary files /dev/null and b/keith-portfolio/src/assets/portrait.jpg differ diff --git a/keith-portfolio/src/assets/signal-mark.webp b/keith-portfolio/src/assets/signal-mark.webp new file mode 100644 index 0000000..2713513 Binary files /dev/null and b/keith-portfolio/src/assets/signal-mark.webp differ diff --git a/keith-portfolio/src/assets/systems.webp b/keith-portfolio/src/assets/systems.webp new file mode 100644 index 0000000..52b0cd7 Binary files /dev/null and b/keith-portfolio/src/assets/systems.webp differ diff --git a/jack-portfolio/src/components/AnimatedText.tsx b/keith-portfolio/src/components/AnimatedText.tsx similarity index 100% rename from jack-portfolio/src/components/AnimatedText.tsx rename to keith-portfolio/src/components/AnimatedText.tsx diff --git a/jack-portfolio/src/components/ContactButton.tsx b/keith-portfolio/src/components/ContactButton.tsx similarity index 87% rename from jack-portfolio/src/components/ContactButton.tsx rename to keith-portfolio/src/components/ContactButton.tsx index 13fd75b..b36d598 100644 --- a/jack-portfolio/src/components/ContactButton.tsx +++ b/keith-portfolio/src/components/ContactButton.tsx @@ -1,7 +1,9 @@ +import { profile } from "../data/content"; + export default function ContactButton() { return ( - - + + - - + + - - + + - - + +
+ {stats.map((s) => ( +
+
{s.value}
+
{s.label}
+
+ ))} +
@@ -31,7 +38,7 @@ export default function AboutSection() { style={{ fontSize: 'clamp(1rem, 2vw, 1.35rem)' }} />
-
+
diff --git a/keith-portfolio/src/sections/ContactSection.tsx b/keith-portfolio/src/sections/ContactSection.tsx new file mode 100644 index 0000000..bfc7a91 --- /dev/null +++ b/keith-portfolio/src/sections/ContactSection.tsx @@ -0,0 +1,47 @@ +import { ArrowUpRight, Phone } from 'lucide-react'; +import FadeIn from '../components/FadeIn'; +import ContactButton from '../components/ContactButton'; +import { profile } from '../data/content'; + +export default function ContactSection() { + return ( +
+ +

+ Let’s talk +

+
+ +

+ Based in {profile.location}. Available for the right conversation. +

+
+ + + + +
+ +
+ {profile.name} + {profile.role} + {profile.location} +
+
+ ); +} diff --git a/keith-portfolio/src/sections/CredentialsSection.tsx b/keith-portfolio/src/sections/CredentialsSection.tsx new file mode 100644 index 0000000..78ce32d --- /dev/null +++ b/keith-portfolio/src/sections/CredentialsSection.tsx @@ -0,0 +1,31 @@ +import FadeIn from '../components/FadeIn'; +import { certifications, education } from '../data/content'; + +export default function CredentialsSection() { + return ( +
+ +

+ Credentials +

+
+ +
+ Education + {education.degree} + {education.school}, {education.year} +
+
+
+ {certifications.map((c, i) => ( + +
+ {String(i + 1).padStart(2, '0')} +

{c}

+
+
+ ))} +
+
+ ); +} diff --git a/keith-portfolio/src/sections/ExperienceSection.tsx b/keith-portfolio/src/sections/ExperienceSection.tsx new file mode 100644 index 0000000..16dd492 --- /dev/null +++ b/keith-portfolio/src/sections/ExperienceSection.tsx @@ -0,0 +1,55 @@ +import { Check } from 'lucide-react'; +import FadeIn from '../components/FadeIn'; +import { foundations, roles } from '../data/content'; + +export default function ExperienceSection() { + return ( +
+ +

+ Experience +

+
+
+ {roles.map((r, i) => ( + +
+
+ {r.dates} + {r.organisation} + {String(i + 1).padStart(2, '0')} +
+
+

{r.role}

+

{r.summary}

+
    + {r.points.map((p) => ( +
  • + + {p} +
  • + ))} +
+
+
+
+ ))} + + +
+

Earlier foundations

+
+ {foundations.map((f) => ( +
+ {f.dates} + {f.role} + {f.organisation} +
+ ))} +
+
+
+
+
+ ); +} diff --git a/jack-portfolio/src/sections/HeroSection.tsx b/keith-portfolio/src/sections/HeroSection.tsx similarity index 58% rename from jack-portfolio/src/sections/HeroSection.tsx rename to keith-portfolio/src/sections/HeroSection.tsx index 1456e6f..634d293 100644 --- a/jack-portfolio/src/sections/HeroSection.tsx +++ b/keith-portfolio/src/sections/HeroSection.tsx @@ -1,20 +1,20 @@ import FadeIn from '../components/FadeIn'; import Magnet from '../components/Magnet'; import ContactButton from '../components/ContactButton'; -import { portrait } from '../data/content'; +import { images, profile } from '../data/content'; const links = [ { label: 'About', href: '#about' }, - { label: 'Price', href: '#services' }, + { label: 'Skills', href: '#skills' }, { label: 'Projects', href: '#projects' }, - { label: 'Contact', href: 'mailto:hello@example.com' }, + { label: 'Contact', href: '#contact' }, ]; export default function HeroSection() { return ( -
+
-
-
+
- Portrait of Jack + Keith Rana at his work desk
-
+

- a 3d creator driven by crafting striking and unforgettable projects + {profile.tagline}

diff --git a/keith-portfolio/src/sections/MarqueeSection.tsx b/keith-portfolio/src/sections/MarqueeSection.tsx new file mode 100644 index 0000000..a40511b --- /dev/null +++ b/keith-portfolio/src/sections/MarqueeSection.tsx @@ -0,0 +1,75 @@ +import { useEffect, useRef, useState } from 'react'; +import type { ReactNode } from 'react'; +import { expertise, images } from '../data/content'; + +const TILE_W = 420; +const GAP = 12; + +const photoTiles: { src: string; alt: string; position: string }[] = [ + { src: images.portrait, alt: 'Keith Rana at his work desk', position: 'center 20%' }, + { src: images.leadership, alt: 'Hands organising a service-delivery briefing', position: 'center' }, + { src: images.systems, alt: 'Abstract composition of connected service pathways', position: 'center' }, +]; + +const photos = photoTiles.map((t) => ( + {t.alt} +)); + +const skills = expertise.map((e, i) => ( +
+ {String(i + 1).padStart(2, '0')} + {e.name} +
+)); + +function Row({ tiles, setWidth, transform }: { tiles: ReactNode[]; setWidth: number; transform: string }) { + return ( +
+ {[0, 1, 2].map((copy) => ( +
+ {tiles} +
+ ))} +
+ ); +} + +export default function MarqueeSection() { + const ref = useRef(null); + const [offset, setOffset] = useState(0); + + useEffect(() => { + const onScroll = () => { + if (!ref.current) return; + const top = ref.current.getBoundingClientRect().top + window.scrollY; + setOffset((window.scrollY - top + window.innerHeight) * 0.3); + }; + onScroll(); + window.addEventListener('scroll', onScroll, { passive: true }); + window.addEventListener('resize', onScroll); + return () => { + window.removeEventListener('scroll', onScroll); + window.removeEventListener('resize', onScroll); + }; + }, []); + + const width = (n: number) => n * (TILE_W + GAP) + GAP; + + return ( +
+ + +
+ ); +} diff --git a/jack-portfolio/src/sections/ProjectsSection.tsx b/keith-portfolio/src/sections/ProjectsSection.tsx similarity index 58% rename from jack-portfolio/src/sections/ProjectsSection.tsx rename to keith-portfolio/src/sections/ProjectsSection.tsx index 4f47419..6d4e798 100644 --- a/jack-portfolio/src/sections/ProjectsSection.tsx +++ b/keith-portfolio/src/sections/ProjectsSection.tsx @@ -1,7 +1,7 @@ import { useRef } from 'react'; import { motion, useScroll, useTransform } from 'framer-motion'; +import { Check } from 'lucide-react'; import FadeIn from '../components/FadeIn'; -import LiveProjectButton from '../components/LiveProjectButton'; import { projects } from '../data/content'; type Project = (typeof projects)[number]; @@ -15,30 +15,40 @@ function ProjectCard({ project, index, total }: { project: Project; index: numbe const scale = useTransform(scrollYProgress, [0, 1], [1, targetScale]); return ( -
+
-
+
{String(index + 1).padStart(2, '0')} -
+

{project.category}

{project.name}

- +

+ {project.body} +

- - + +
-
- +
+ Delivered +
    + {project.delivered.map((d) => ( +
  • + + {d} +
  • + ))} +
@@ -51,7 +61,7 @@ export default function ProjectsSection() {

- Project + Projects

{projects.map((p, i) => ( diff --git a/jack-portfolio/src/sections/ServicesSection.tsx b/keith-portfolio/src/sections/ServicesSection.tsx similarity index 66% rename from jack-portfolio/src/sections/ServicesSection.tsx rename to keith-portfolio/src/sections/ServicesSection.tsx index 1328108..5a7227b 100644 --- a/jack-portfolio/src/sections/ServicesSection.tsx +++ b/keith-portfolio/src/sections/ServicesSection.tsx @@ -1,23 +1,23 @@ import FadeIn from '../components/FadeIn'; -import { services } from '../data/content'; +import { expertise } from '../data/content'; export default function ServicesSection() { return ( -
+

- Services + Capability

- {services.map((s, i) => ( - + {expertise.map((s, i) => ( +
- + {String(i + 1).padStart(2, '0')} -
+

{s.name}

{s.description} diff --git a/keith-portfolio/src/vite-env.d.ts b/keith-portfolio/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/keith-portfolio/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/jack-portfolio/tailwind.config.js b/keith-portfolio/tailwind.config.js similarity index 100% rename from jack-portfolio/tailwind.config.js rename to keith-portfolio/tailwind.config.js diff --git a/jack-portfolio/tsconfig.json b/keith-portfolio/tsconfig.json similarity index 100% rename from jack-portfolio/tsconfig.json rename to keith-portfolio/tsconfig.json diff --git a/keith-portfolio/vite.config.ts b/keith-portfolio/vite.config.ts new file mode 100644 index 0000000..0e8be07 --- /dev/null +++ b/keith-portfolio/vite.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +// INLINE=1 bakes every image and font into the JS/CSS (used for the one-file cloud preview). +export default defineConfig({ + plugins: [react()], + base: './', + build: { assetsInlineLimit: process.env.INLINE ? 100_000_000 : 4096 }, +});