Skip to content

Commit 93a4e44

Browse files
committed
add old version
1 parent 2dc7217 commit 93a4e44

File tree

1 file changed

+328
-0
lines changed

1 file changed

+328
-0
lines changed

src/app/PuzzleClient copy.tsx

Lines changed: 328 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,328 @@
1+
"use client";
2+
3+
import {
4+
useEffect,
5+
useMemo,
6+
useRef,
7+
useState,
8+
} from 'react';
9+
10+
import clsx from 'clsx';
11+
import Image from 'next/image';
12+
13+
function useGridSize() {
14+
const [gridSize, setGridSize] = useState(3);
15+
16+
useEffect(() => {
17+
function handleResize() {
18+
const width = window.innerWidth;
19+
if (width < 640) {
20+
setGridSize(3);
21+
} else if (width < 1024) {
22+
setGridSize(4);
23+
} else {
24+
setGridSize(5);
25+
}
26+
}
27+
handleResize();
28+
window.addEventListener("resize", handleResize);
29+
return () => window.removeEventListener("resize", handleResize);
30+
}, []);
31+
32+
return gridSize;
33+
}
34+
35+
const ALL_FACTS = [
36+
// 1
37+
`Zak King <a href="mailto:[email protected]">[email protected]</a>`,
38+
// 2
39+
`Reach out on <a href="https://www.linkedin.com/in/zakandrewking/" target="_blank">LinkedIn</a>`,
40+
// 3
41+
`VP of Eng @ <a href="https://www.delfina.com" target="_blank">Delfina</a> (2022–present): maternal health, AI, EHR integration`,
42+
// 4
43+
`Associate Dir DevOps (2019–2022) @ <a href="https://www.amyris.com" target="_blank">Amyris</a>`,
44+
// 5
45+
`Project Scientist, Principle Investigator (2017–2019) @ <a href="https://ucsd.edu" target="_blank">UC San Diego</a>`,
46+
// 6
47+
`PhD in Bioengineering, UC San Diego, <a href="http://systemsbiology.ucsd.edu" target="_blank">SBRG Lab</a>`,
48+
// 7
49+
`<a href="https://scholar.google.com/citations?user=ESLgsdUAAAAJ" target="_blank">30+ publications, 5000+ citations</a>`,
50+
// 8
51+
`BSE (Biomedical Eng) @ <a href="https://umich.edu" target="_blank">UMich</a> (2011)`,
52+
// 9
53+
`Primary dev of <a href="https://escher.github.io" target="_blank">Escher</a> & <a href="http://bigg.ucsd.edu" target="_blank">BiGG Models</a>`,
54+
// 10
55+
`Scrum (CSPO 2017) & <a href="https://www.amanet.org/5-day-mba-certificate-program/" target="_blank">AMA 5-Day MBA (2021)</a>`,
56+
// 11
57+
`Expert in Docker, Terraform, HPC, GraphQL, Observability (<a href="https://datadog.com" target="_blank">Datadog</a>)`,
58+
// 12
59+
`Focus on user-centered products, team autonomy & growth: <a href="https://www.delfina.com/resource/finding-your-users-in-digital-health" target="_blank">Learn more</a>`,
60+
// 13
61+
`Synthetic biology & metabolic engineering: <a href="https://doi.org/10.1016/j.copbio.2014.12.016" target="_blank">Research highlights</a>`,
62+
// 14
63+
`Expertise in compliance: <a href="https://www.delfina.com/security" target="_blank">SOC2 & HIPAA at Delfina</a>`,
64+
// 15
65+
// 16
66+
`<a href="https://www.nsfgrfp.org/" target="_blank">NSF GRFP</a> & <a href="https://jacobsschool.ucsd.edu/idea/admitted-undergraduates/jacobs-scholars" target="_blank">Jacobs Fellowship</a> recipient`,
67+
// 17
68+
// 18
69+
`I love coding: Dart, Python, Terraform, TS: <a href="https://github.com/zakandrewking" target="_blank">My GitHub</a>`,
70+
// 19
71+
`App on iOS: <a href="https://apps.apple.com/us/app/delfina-pregnancy-tracker/id6478985864" target="_blank">Delfina iOS link</a> | Android: <a href="https://play.google.com/store/apps/details?id=com.delfina.gaia" target="_blank">Delfina Android link</a>`,
72+
// 20
73+
`Driving maternal health crisis solutions w/ AI: <a href="https://delfina.com" target="_blank">Learn about mission</a>`,
74+
// 21
75+
`Strain engineering: <a href="https://www.biorxiv.org/content/10.1101/2023.01.03.521657v1" target="_blank">DARPA & 400+ production strains</a>`,
76+
// 22
77+
// 23
78+
// 24
79+
`Dissertation: <a href="https://escholarship.org/content/qt83d340c7/qt83d340c7.pdf" target="_blank">"Optimization of microbial cell factories..."</a>`,
80+
// 25
81+
`<a href="/resume.pdf" target="_blank">Resume PDF</a>`,
82+
`Last updated Feb 20, 2025`,
83+
];
84+
85+
function getFactsForGridSize(gridSize: number) {
86+
const needed = gridSize * gridSize;
87+
return ALL_FACTS.slice(0, needed);
88+
}
89+
90+
function getPuzzleImages(gridSize: number) {
91+
const basePath = `/puzzle-${gridSize}x${gridSize}`;
92+
const paths: string[] = [];
93+
for (let row = 0; row < gridSize; row++) {
94+
for (let col = 0; col < gridSize; col++) {
95+
const x = col + 1;
96+
const y = gridSize - row;
97+
paths.push(`${basePath}/image${x}x${y}.jpeg`);
98+
}
99+
}
100+
return paths;
101+
}
102+
103+
function shuffleArray<T>(arr: T[]): T[] {
104+
for (let i = arr.length - 1; i > 0; i--) {
105+
const j = Math.floor(Math.random() * (i + 1));
106+
[arr[i], arr[j]] = [arr[j], arr[i]];
107+
}
108+
return arr;
109+
}
110+
111+
export default function PuzzleClient() {
112+
const gridSize = useGridSize();
113+
const resumeFacts = useMemo(() => getFactsForGridSize(gridSize), [gridSize]);
114+
const puzzleImages = useMemo(() => getPuzzleImages(gridSize), [gridSize]);
115+
const [tiles, setTiles] = useState<(number | null)[]>([]);
116+
const [containerSize, setContainerSize] = useState(0);
117+
const touchStartRef = useRef<{ x: number; y: number; index: number } | null>(
118+
null
119+
);
120+
121+
useEffect(() => {
122+
const total = puzzleImages.length;
123+
const arr: (number | null)[] = Array.from(
124+
{ length: total - 1 },
125+
(_, i) => i
126+
);
127+
arr.push(null);
128+
shuffleArray(arr);
129+
setTiles(arr);
130+
}, [puzzleImages, gridSize]);
131+
132+
useEffect(() => {
133+
function updateSize() {
134+
const maxWidth = window.innerWidth - 8;
135+
const maxHeight = window.innerHeight - 8;
136+
setContainerSize(Math.min(maxWidth, maxHeight));
137+
}
138+
updateSize();
139+
window.addEventListener("resize", updateSize);
140+
return () => window.removeEventListener("resize", updateSize);
141+
}, []);
142+
143+
function getRowCol(idx: number) {
144+
return [Math.floor(idx / gridSize), idx % gridSize];
145+
}
146+
147+
function isAdjacent(idx1: number, idx2: number) {
148+
const [r1, c1] = getRowCol(idx1);
149+
const [r2, c2] = getRowCol(idx2);
150+
return (
151+
(r1 === r2 && Math.abs(c1 - c2) === 1) ||
152+
(c1 === c2 && Math.abs(r1 - r2) === 1)
153+
);
154+
}
155+
156+
function moveTile(from: number, to: number) {
157+
setTiles((prev) => {
158+
const newTiles = [...prev];
159+
[newTiles[from], newTiles[to]] = [newTiles[to], newTiles[from]];
160+
return newTiles;
161+
});
162+
}
163+
164+
function handleTileClick(idx: number) {
165+
const blankIndex = tiles.indexOf(null);
166+
if (isAdjacent(idx, blankIndex)) {
167+
moveTile(idx, blankIndex);
168+
}
169+
}
170+
171+
const handleTouchStart = (e: React.TouchEvent, index: number) => {
172+
e.preventDefault();
173+
const touch = e.touches[0];
174+
touchStartRef.current = { x: touch.clientX, y: touch.clientY, index };
175+
};
176+
177+
const handleTouchEnd = (e: React.TouchEvent) => {
178+
const touchStart = touchStartRef.current;
179+
if (!touchStart) return;
180+
181+
const touch = e.changedTouches[0];
182+
const deltaX = touch.clientX - touchStart.x;
183+
const deltaY = touch.clientY - touchStart.y;
184+
const absDeltaX = Math.abs(deltaX);
185+
const absDeltaY = Math.abs(deltaY);
186+
187+
if (Math.max(absDeltaX, absDeltaY) < 10) {
188+
handleTileClick(touchStart.index);
189+
touchStartRef.current = null;
190+
return;
191+
}
192+
193+
let direction: "left" | "right" | "up" | "down" | null = null;
194+
if (absDeltaX > absDeltaY) {
195+
direction = deltaX > 0 ? "right" : "left";
196+
} else {
197+
direction = deltaY > 0 ? "down" : "up";
198+
}
199+
200+
const currentIndex = touchStart.index;
201+
const [row, col] = getRowCol(currentIndex);
202+
let adjacentIndex: number | null = null;
203+
204+
switch (direction) {
205+
case "left":
206+
adjacentIndex = col > 0 ? currentIndex - 1 : null;
207+
break;
208+
case "right":
209+
adjacentIndex = col < gridSize - 1 ? currentIndex + 1 : null;
210+
break;
211+
case "up":
212+
adjacentIndex = row > 0 ? currentIndex - gridSize : null;
213+
break;
214+
case "down":
215+
adjacentIndex = row < gridSize - 1 ? currentIndex + gridSize : null;
216+
break;
217+
}
218+
219+
if (adjacentIndex !== null && tiles[adjacentIndex] === null) {
220+
moveTile(currentIndex, adjacentIndex);
221+
}
222+
223+
touchStartRef.current = null;
224+
};
225+
226+
const tileSize = containerSize / gridSize;
227+
228+
return (
229+
<div className="min-h-screen w-screen flex items-center justify-center">
230+
<div
231+
className="relative mx-1 overflow-hidden"
232+
style={{ width: containerSize, height: containerSize }}
233+
>
234+
{/* Bottom layer: NxN facts */}
235+
<div
236+
className="absolute inset-0 grid"
237+
style={{
238+
gridTemplateRows: `repeat(${gridSize}, 1fr)`,
239+
gridTemplateColumns: `repeat(${gridSize}, 1fr)`,
240+
}}
241+
>
242+
{resumeFacts.map((fact, i) => (
243+
<div
244+
key={i}
245+
className="relative flex items-center justify-center border overflow-hidden"
246+
>
247+
<div
248+
className="text-sm text-center [&_a]:cursor-pointer [&_a]:text-primary [&_a]:underline [&_a]:pointer-events-auto hover:[&_a]:opacity-70 px-1"
249+
style={{
250+
display: "-webkit-box",
251+
WebkitLineClamp: 4,
252+
WebkitBoxOrient: "vertical",
253+
overflow: "hidden",
254+
}}
255+
dangerouslySetInnerHTML={{ __html: fact }}
256+
/>
257+
</div>
258+
))}
259+
</div>
260+
261+
{/* Top layer: NxN puzzle */}
262+
<div
263+
className="absolute inset-0 pointer-events-none"
264+
style={{ zIndex: 1 }}
265+
>
266+
{tiles.map((tile, index) => {
267+
if (tile === null) return null;
268+
269+
const [row, col] = getRowCol(index);
270+
const x = col * tileSize;
271+
const y = row * tileSize;
272+
const blankIndex = tiles.indexOf(null);
273+
const canMove = isAdjacent(index, blankIndex);
274+
275+
return (
276+
<div
277+
key={tile}
278+
className={clsx(
279+
"absolute flex items-center justify-center border pointer-events-auto",
280+
canMove
281+
? "cursor-pointer hover:opacity-90"
282+
: "cursor-default",
283+
"transition-transform duration-300 ease-in-out"
284+
)}
285+
style={{
286+
width: tileSize,
287+
height: tileSize,
288+
transform: `translate(${x}px, ${y}px)`,
289+
}}
290+
onClick={() => canMove && handleTileClick(index)}
291+
onTouchStart={(e) => canMove && handleTouchStart(e, index)}
292+
onTouchEnd={(e) => canMove && handleTouchEnd(e)}
293+
>
294+
<Image
295+
src={puzzleImages[tile]}
296+
alt={`Tile ${tile}`}
297+
fill
298+
className="object-cover"
299+
/>
300+
</div>
301+
);
302+
})}
303+
</div>
304+
305+
<style jsx global>{`
306+
* {
307+
box-sizing: border-box;
308+
}
309+
html {
310+
color-scheme: light dark;
311+
}
312+
body {
313+
margin: 0;
314+
background-color: #fdfdfd;
315+
color: #333;
316+
padding: 0;
317+
}
318+
@media (prefers-color-scheme: dark) {
319+
body {
320+
background-color: #111;
321+
color: #eee;
322+
}
323+
}
324+
`}</style>
325+
</div>
326+
</div>
327+
);
328+
}

0 commit comments

Comments
 (0)