Skip to content

Commit 324917b

Browse files
committed
refactor: major refactor for projects page.
1 parent 5977157 commit 324917b

File tree

8 files changed

+444
-153
lines changed

8 files changed

+444
-153
lines changed

src/components/GenerativeArt.js

Lines changed: 106 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,93 +1,121 @@
11
import React, { useMemo } from 'react';
2-
import ReactDOMServer from 'react-dom/server';
3-
import { DownloadSimple } from '@phosphor-icons/react';
42

5-
const GenerativeArt = () => {
6-
const art = useMemo(() => {
7-
const SVG_SIZE = 500;
8-
const NUM_SHAPES = 25;
3+
const GenerativeArt = ({ seed = 'fezcodex', className }) => {
4+
// Sanitize seed for use in SVG IDs
5+
const safeId = useMemo(() => seed.replace(/[^a-z0-9]/gi, '-').toLowerCase(), [seed]);
6+
7+
const shapes = useMemo(() => {
8+
// Seeded RNG
9+
let h = 0xdeadbeef;
10+
const safeSeed = seed || 'fezcodex';
11+
for (let i = 0; i < safeSeed.length; i++) {
12+
h = Math.imul(h ^ safeSeed.charCodeAt(i), 2654435761);
13+
}
14+
const rng = () => {
15+
h = Math.imul(h ^ (h >>> 16), 2246822507);
16+
h = Math.imul(h ^ (h >>> 13), 3266489909);
17+
return ((h ^= h >>> 16) >>> 0) / 4294967296;
18+
};
19+
20+
const hue = Math.floor(rng() * 360);
21+
const primaryColor = `hsl(${hue}, 70%, 60%)`;
22+
const secondaryColor = `hsl(${(hue + 180) % 360}, 60%, 50%)`;
23+
const accentColor = `hsl(${(hue + 90) % 360}, 80%, 60%)`;
24+
25+
const type = Math.floor(rng() * 3);
926
const shapes = [];
1027

11-
const colors = ['#FBBF24', '#A7F3D0', '#F87171', '#60A5FA', '#A78BFA'];
28+
if (type === 0) {
29+
// Bauhaus Grid
30+
const gridSize = 5;
31+
const cellSize = 100 / gridSize;
32+
let count = 0;
1233

13-
for (let i = 0; i < NUM_SHAPES; i++) {
14-
const x = Math.random() * SVG_SIZE;
15-
const y = Math.random() * SVG_SIZE;
16-
const width = 10 + Math.random() * 150;
17-
const height = 10 + Math.random() * 150;
18-
const fill = colors[Math.floor(Math.random() * colors.length)];
19-
const rotation = Math.random() * 360;
20-
const opacity = 0.5 + Math.random() * 0.5;
34+
const addShape = (x, y) => {
35+
const shapeType = Math.floor(rng() * 4);
36+
const rotation = Math.floor(rng() * 4) * 90;
37+
const colorRoll = rng();
38+
let color = primaryColor;
39+
if (colorRoll > 0.6) color = secondaryColor;
40+
if (colorRoll > 0.9) color = accentColor;
41+
if (colorRoll < 0.1) color = '#ffffff';
42+
shapes.push({ mode: 'grid', x: x * cellSize, y: y * cellSize, size: cellSize, shapeType, rotation, color, isOutline: rng() > 0.6 });
43+
count++;
44+
};
2145

22-
shapes.push(
23-
<rect
24-
key={i}
25-
x={x}
26-
y={y}
27-
width={width}
28-
height={height}
29-
fill={fill}
30-
opacity={opacity}
31-
transform={`rotate(${rotation} ${x + width / 2} ${y + height / 2})`}
32-
/>,
33-
);
46+
for (let x = 0; x < gridSize; x++) {
47+
for (let y = 0; y < gridSize; y++) {
48+
if (rng() > 0.5) addShape(x, y);
49+
}
50+
}
51+
// Guarantee at least 5 shapes
52+
if (count < 5) {
53+
for(let i=0; i<5; i++) addShape(Math.floor(rng()*gridSize), Math.floor(rng()*gridSize));
54+
}
55+
} else if (type === 1) {
56+
// Tech Circuit
57+
const count = 15;
58+
for (let i = 0; i < count; i++) {
59+
const isVertical = rng() > 0.5;
60+
const x = Math.floor(rng() * 10) * 10;
61+
const y = Math.floor(rng() * 10) * 10;
62+
const thickness = 0.5 + rng() * 1.5;
63+
shapes.push({
64+
mode: 'tech',
65+
x, y, isVertical, length: 20 + rng() * 60, thickness,
66+
color: rng() > 0.8 ? '#ffffff' : primaryColor,
67+
opacity: 0.4 + rng() * 0.6
68+
});
69+
if(rng() > 0.4) shapes.push({ mode: 'node', cx: x, cy: y, r: thickness * 2, color: accentColor });
70+
}
71+
} else {
72+
// Geometric Flow
73+
for (let i = 0; i < 8; i++) {
74+
shapes.push({
75+
mode: 'flow',
76+
cx: rng() * 100, cy: rng() * 100,
77+
r: 10 + rng() * 40,
78+
color: i % 2 === 0 ? primaryColor : secondaryColor,
79+
opacity: 0.3 + rng() * 0.3
80+
});
81+
}
3482
}
3583

36-
return (
37-
<svg
38-
width="100%"
39-
height="100%"
40-
viewBox={`0 0 ${SVG_SIZE} ${SVG_SIZE}`}
41-
xmlns="http://www.w3.org/2000/svg"
42-
>
84+
return shapes;
85+
}, [seed]);
86+
87+
return (
88+
<div className={`w-full h-full bg-neutral-950 overflow-hidden relative ${className}`}>
89+
<svg viewBox="0 0 100 100" preserveAspectRatio="xMidYMid slice" className="w-full h-full">
4390
<defs>
44-
<clipPath id="art-board">
45-
<rect width={SVG_SIZE} height={SVG_SIZE} />
46-
</clipPath>
91+
<pattern id={`bg-grid-${safeId}`} width="20" height="20" patternUnits="userSpaceOnUse">
92+
<circle cx="1" cy="1" r="0.5" fill="white" opacity="0.05"/>
93+
</pattern>
4794
</defs>
48-
<rect width={SVG_SIZE} height={SVG_SIZE} fill="#1F2937" />
49-
<g clipPath="url(#art-board)">{shapes}</g>
95+
<rect width="100" height="100" fill={`url(#bg-grid-${safeId})`}/>
96+
{shapes.map((s, i) => {
97+
if (s.mode === 'grid') {
98+
const center = s.size / 2;
99+
const p = s.size * 0.1;
100+
const is = s.size - (p * 2);
101+
return (
102+
<g key={i} transform={`translate(${s.x}, ${s.y}) rotate(${s.rotation}, ${center}, ${center})`}>
103+
{s.shapeType === 0 && <rect x={p} y={p} width={is} height={is} fill={s.isOutline ? 'none' : s.color} stroke={s.color} strokeWidth={s.isOutline ? 1.5 : 0} opacity="0.9" rx="1" />}
104+
{s.shapeType === 1 && <circle cx={center} cy={center} r={is / 2} fill={s.isOutline ? 'none' : s.color} stroke={s.color} strokeWidth={s.isOutline ? 1.5 : 0} opacity="0.9" />}
105+
{s.shapeType === 2 && <path d={`M ${p} ${p} L ${s.size-p} ${p} A ${is} ${is} 0 0 1 ${p} ${s.size-p} Z`} fill={s.color} opacity="0.9" />}
106+
{s.shapeType === 3 && <polygon points={`${p},${s.size-p} ${s.size/2},${p} ${s.size-p},${s.size-p}`} fill={s.isOutline ? 'none' : s.color} stroke={s.color} strokeWidth={s.isOutline ? 1.5 : 0} opacity="0.9" />}
107+
</g>
108+
);
109+
}
110+
if (s.mode === 'tech') return <rect key={i} x={s.x} y={s.y} width={s.isVertical ? s.thickness : s.length} height={s.isVertical ? s.length : s.thickness} fill={s.color} opacity={s.opacity} />;
111+
if (s.mode === 'node') return <circle key={i} cx={s.cx} cy={s.cy} r={s.r} fill={s.color} opacity="0.8" />;
112+
if (s.mode === 'flow') return <circle key={i} cx={s.cx} cy={s.cy} r={s.r} fill={s.color} opacity={s.opacity} style={{mixBlendMode: 'screen'}} />;
113+
return null;
114+
})}
50115
</svg>
51-
);
52-
}, []);
53-
54-
const handleDownload = () => {
55-
const svgString = ReactDOMServer.renderToString(art);
56-
const blob = new Blob([svgString], { type: 'image/svg+xml' });
57-
const url = URL.createObjectURL(blob);
58-
const link = document.createElement('a');
59-
link.href = url;
60-
link.download = 'generative-art.svg';
61-
document.body.appendChild(link);
62-
link.click();
63-
document.body.removeChild(link);
64-
URL.revokeObjectURL(url);
65-
};
66-
67-
return (
68-
<div>
69-
<div
70-
style={{
71-
width: '100%',
72-
height: '60vh',
73-
border: '1px solid #374151',
74-
borderRadius: '8px',
75-
overflow: 'hidden',
76-
}}
77-
>
78-
{art}
79-
</div>
80-
<div className="flex justify-center mt-4">
81-
<button
82-
onClick={handleDownload}
83-
className="flex items-center gap-2 text-lg font-arvo font-normal px-6 py-2 rounded-md border transition-colors duration-300 ease-in-out border-green-700 bg-green-800/50 text-white hover:bg-green-700/50"
84-
>
85-
<DownloadSimple size={24} />
86-
Download SVG
87-
</button>
88-
</div>
116+
<div className="absolute inset-0 opacity-[0.15] pointer-events-none mix-blend-overlay" style={{backgroundImage: `url("data:image/svg+xml,%3Csvg viewBox='0 0 512 512' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='3'/%3E%3C/filter%3E%3Crect width='512' height='512' filter='url(%23n)'/%3E%3C/svg%3E")`}} />
89117
</div>
90118
);
91119
};
92120

93-
export default GenerativeArt;
121+
export default GenerativeArt;

src/components/Layout.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ const Layout = ({
9292
/>
9393
{isSearchVisible && <Search isVisible={isSearchVisible} />}
9494
<main className="flex-grow">{children}</main>
95-
<Footer />
95+
{location.pathname !== '/projects' && <Footer />}
9696
</div>
9797
</div>
9898
</>

src/components/ProjectTile.js

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import React from 'react';
2+
import {Link} from 'react-router-dom';
3+
import {ArrowUpRight} from '@phosphor-icons/react';
4+
import {motion} from 'framer-motion';
5+
import GenerativeArt from './GenerativeArt';
6+
7+
const ProjectTile = ({project}) => {
8+
return (<motion.div
9+
whileHover={{y: -5}}
10+
className="group relative flex flex-col overflow-hidden rounded-sm bg-zinc-900 border border-white/10"
11+
>
12+
<Link to={`/projects/${project.slug}`} className="flex flex-col h-full">
13+
{/* Image Container */}
14+
<div className="relative aspect-[4/3] w-full overflow-hidden">
15+
<GenerativeArt
16+
seed={project.title}
17+
className="w-full h-full transition-transform duration-700 ease-out group-hover:scale-105"
18+
/>
19+
20+
{/* Overlay Tag */}
21+
<div className="absolute top-3 left-3">
22+
<span
23+
className="px-2 py-1 text-[10px] font-mono font-bold uppercase tracking-widest text-white bg-black/50 backdrop-blur-md rounded border border-white/10">
24+
{project.slug.split('-')[0]}
25+
</span>
26+
</div>
27+
28+
{/* Hover Button */}
29+
<div
30+
className="absolute top-3 right-3 opacity-0 translate-y-2 transition-all duration-300 group-hover:opacity-100 group-hover:translate-y-0">
31+
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-white text-black shadow-lg">
32+
<ArrowUpRight weight="bold" size={16}/>
33+
</div>
34+
</div>
35+
</div>
36+
37+
{/* Content */}
38+
<div className="flex flex-col flex-grow p-5">
39+
<h3
40+
className="text-xl font-medium font-sans uppercase text-white mb-2 group-hover:text-cyan-400 transition-colors line-clamp-1">
41+
{project.title}
42+
</h3>
43+
<p className="text-sm text-gray-400 line-clamp-2 leading-relaxed mb-4 flex-grow"> {project.shortDescription}
44+
</p>
45+
46+
{/* Tech Stack */}
47+
<div className="flex flex-wrap gap-2 mt-auto">
48+
{project.technologies && project.technologies.slice(0, 3).map((tech) => (<span key={tech}
49+
className="text-[10px] font-mono text-gray-500 uppercase tracking-wider border border-white/10 px-1.5 py-0.5 rounded">
50+
{tech}
51+
</span>))}
52+
</div>
53+
</div>
54+
</Link>
55+
</motion.div>);
56+
};
57+
58+
export default ProjectTile;

src/pages/AboutPage.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@ import {
77
Graph,
88
Terminal,
99
Article,
10+
Bug,
1011
} from '@phosphor-icons/react';
1112
import CommandPalette from '../components/CommandPalette';
1213
import { useCommandPalette } from '../context/CommandPaletteContext';
1314
import NeuromancerHUD from './about-views/NeuromancerHUD';
1415
import SystemArchitecture from './about-views/SystemArchitecture';
1516
import MindMapConstellation from './about-views/MindMapConstellation';
1617
import ClassifiedDossier from './about-views/ClassifiedDossier';
18+
import Brutalist from './about-views/Brutalist';
1719
import { useAchievements } from '../context/AchievementContext';
1820
import useSeo from "../hooks/useSeo";
1921

@@ -23,6 +25,7 @@ const ViewSwitcher = ({ currentView, setView }) => {
2325
{ id: 'hud', icon: Terminal, label: 'Terminal' },
2426
{ id: 'blueprint', icon: TreeStructure, label: 'Blueprint' },
2527
{ id: 'map', icon: Graph, label: 'Mind Map' },
28+
{ id: 'brutalist', icon: Bug, label: 'Brutalist' },
2629
];
2730

2831
return (
@@ -75,6 +78,8 @@ const AboutPage = () => {
7578
switch (currentView) {
7679
case 'dossier':
7780
return 'bg-white text-black border-black border-2 font-mono uppercase tracking-widest text-xs hover:bg-[#4a0404] hover:text-white hover:border-[#4a0404] rounded-none shadow-none';
81+
case 'brutalist':
82+
return 'bg-black text-white border-white border-2 font-mono uppercase tracking-widest text-xs hover:bg-white hover:text-black rounded-none';
7883
case 'hud':
7984
return 'bg-black text-green-500 border-green-500 border font-mono tracking-wider hover:bg-green-500 hover:text-black shadow-[0_0_10px_rgba(0,255,0,0.3)] rounded-sm';
8085
case 'blueprint':
@@ -117,6 +122,7 @@ const AboutPage = () => {
117122
{view === 'blueprint' && <SystemArchitecture />}
118123
{view === 'map' && <MindMapConstellation />}
119124
{view === 'dossier' && <ClassifiedDossier />}
125+
{view === 'brutalist' && <Brutalist />}
120126
</motion.div>
121127
</AnimatePresence>
122128

src/pages/HomePage.js

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
AppWindow // For "Explore Apps"
1313
} from '@phosphor-icons/react';
1414
import PostItem from '../components/PostItem';
15-
import ProjectCard from '../components/ProjectCard';
15+
import ProjectTile from '../components/ProjectTile';
1616
import { useProjects } from '../utils/projectParser';
1717
import useSeo from '../hooks/useSeo';
1818
import usePersistentState from '../hooks/usePersistentState';
@@ -191,15 +191,17 @@ const HomePage = () => {
191191
return (
192192
<section className="mb-24 mt-8">
193193
<SectionHeader icon={Cpu} title="Pinned Projects" link="/projects" linkText="View all projects" />
194-
<div className="flex flex-col border-t border-white/10">
194+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
195195
{pinnedProjects.map((project, index) => (
196-
<ProjectCard
196+
<motion.div
197197
key={project.slug}
198-
project={project}
199-
index={index}
200-
isActive={activeProject?.slug === project.slug}
201-
onHover={setActiveProject}
202-
/>
198+
initial={{ opacity: 0, y: 20 }}
199+
whileInView={{ opacity: 1, y: 0 }}
200+
transition={{ delay: index * 0.1 }}
201+
viewport={{ once: true }}
202+
>
203+
<ProjectTile project={project} />
204+
</motion.div>
203205
))}
204206
</div>
205207
</section>

0 commit comments

Comments
 (0)