Files
krishna-portfolio/src/components/SelectedWorks.tsx
2026-05-30 14:54:58 -05:00

220 lines
8.5 KiB
TypeScript

import React, { useState } from "react";
import { motion, AnimatePresence } from "motion/react";
import {
LineChart, HardDrive, Terminal, Brain, Copy, Check,
FileCode, Server, Cpu, GitBranch
} from "lucide-react";
import { ScramblerText } from "./DynamicTextEffects";
import { PROJECTS } from "../data";
import type { ProjectItem } from "../types";
const ProjectIconMap: Record<string, React.ComponentType<{ className?: string }>> = {
brain: Brain,
"line-chart": LineChart,
"hard-drive": HardDrive,
terminal: Terminal,
"file-code": FileCode,
server: Server,
cpu: Cpu,
"git-branch": GitBranch,
};
function resolveIcon(iconName?: string): React.ComponentType<{ className?: string }> {
if (iconName && iconName in ProjectIconMap) {
return ProjectIconMap[iconName];
}
return FileCode;
}
function HeroCard({ project }: { project: ProjectItem }) {
return (
<div className="md:col-span-8 group relative overflow-hidden bg-zinc-950/25 border border-zinc-900 rounded-lg p-6 md:p-8 flex flex-col justify-end min-h-[400px] hover:border-zinc-700/80 hover:bg-zinc-950/40 transition-all duration-300">
{project.image && (
<>
<div
className="absolute inset-0 bg-cover bg-center transition-transform duration-700 group-hover:scale-[1.025] opacity-25 group-hover:opacity-45 grayscale group-hover:grayscale-0 select-none pointer-events-none"
style={{ backgroundImage: `url('${project.image}')` }}
/>
<div className="absolute inset-0 bg-gradient-to-t from-zinc-950 via-zinc-950/70 to-zinc-950/20" />
</>
)}
<div className="relative z-10 space-y-3">
{project.category && (
<span className="font-mono text-[9px] font-bold text-blue-200 bg-blue-950/80 border border-blue-900 px-2.5 py-1 rounded inline-block uppercase tracking-wider">
{project.category}
</span>
)}
<h3 className="font-sans text-2xl md:text-3xl font-extrabold text-white tracking-tight">
{project.title}
</h3>
<p className="font-sans text-zinc-400 text-sm md:text-base max-w-md leading-relaxed">
<ScramblerText text={project.description} delay={200} />
</p>
</div>
</div>
);
}
function IconCard({ project }: { project: ProjectItem }) {
const Icon = resolveIcon(project.icon);
return (
<div className="md:col-span-4 group relative overflow-hidden bg-zinc-950/25 border border-zinc-900 rounded-lg p-6 md:p-8 flex flex-col justify-between min-h-[400px] hover:border-zinc-700/80 hover:bg-zinc-950/40 transition-all duration-300">
<div className="absolute inset-0 bg-gradient-to-br from-white/[0.01] to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
<div className="space-y-6 relative z-10">
<div className="w-12 h-12 rounded bg-zinc-900/60 border border-zinc-800 flex items-center justify-center text-zinc-400 group-hover:text-white group-hover:scale-105 group-hover:border-zinc-700 transition-all duration-300">
<Icon className="w-5 h-5" />
</div>
<div className="space-y-2">
<h3 className="font-sans text-xl md:text-2xl font-bold text-white tracking-tight">
{project.title}
</h3>
<p className="font-sans text-zinc-400 text-sm leading-relaxed">
<ScramblerText text={project.description} delay={250} />
</p>
</div>
</div>
{project.tags && project.tags.length > 0 && (
<div className="flex flex-wrap gap-1.5 pt-4 relative z-10">
{project.tags.map((tag) => (
<span
key={tag}
className="font-mono text-[9px] text-zinc-500 border border-zinc-800 px-2 py-0.5 bg-zinc-900/10 rounded"
>
{tag}
</span>
))}
</div>
)}
</div>
);
}
function TerminalCard({ project }: { project: ProjectItem }) {
const [copied, setCopied] = useState(false);
const copyCommand = () => {
if (project.terminalCommand) {
navigator.clipboard.writeText(project.terminalCommand);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
};
return (
<div className="md:col-span-8 group relative overflow-hidden bg-zinc-950/40 border border-zinc-900 rounded-lg p-6 md:p-8 flex flex-col sm:flex-row items-center gap-8 min-h-[300px] hover:border-zinc-700 hover:bg-zinc-950/40 transition-all duration-300">
<div className="flex-1 space-y-4">
<div>
{project.category && (
<span className="font-mono text-[9px] text-zinc-500 font-bold mb-1.5 block tracking-widest uppercase">
<ScramblerText text={project.category} delay={150} />
</span>
)}
<h3 className="font-sans text-xl md:text-2xl font-extrabold text-white tracking-tight leading-snug">
{project.title}
</h3>
</div>
<p className="font-sans text-zinc-400 text-sm leading-relaxed">
<ScramblerText text={project.description} delay={350} />
</p>
<div
onClick={copyCommand}
className="group/term relative flex items-center justify-between bg-zinc-900/60 border border-zinc-800 rounded px-4 py-3 cursor-pointer hover:border-white transition-colors duration-300"
>
<div className="flex items-center gap-2.5 overflow-hidden">
<Terminal className="w-4 h-4 text-zinc-500 shrink-0 group-hover/term:text-white" />
<code className="font-mono text-xs text-zinc-300 group-hover/term:text-white truncate">
{project.terminalCommand}
</code>
</div>
<div className="shrink-0 pl-2">
<AnimatePresence mode="wait">
{copied ? (
<motion.div
key="check"
initial={{ scale: 0.5, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.5, opacity: 0 }}
transition={{ duration: 0.15 }}
>
<Check className="w-3.5 h-3.5 text-green-400" />
</motion.div>
) : (
<motion.div
key="copy"
initial={{ scale: 0.5, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.5, opacity: 0 }}
transition={{ duration: 0.15 }}
>
<Copy className="w-3.5 h-3.5 text-zinc-500 group-hover/term:text-white" />
</motion.div>
)}
</AnimatePresence>
</div>
</div>
</div>
{project.image && (
<div className="hidden sm:block w-1/3 aspect-square bg-zinc-950 border border-zinc-800 rounded flex items-center justify-center overflow-hidden">
<img
className="w-full h-full object-cover grayscale opacity-55 group-hover:grayscale-0 group-hover:opacity-100 transition-all duration-700"
src={project.image}
alt={`${project.title} preview`}
referrerPolicy="no-referrer"
/>
</div>
)}
</div>
);
}
export default function SelectedWorks() {
return (
<section id="projects" className="relative py-28 max-w-[1200px] mx-auto px-6 overflow-hidden select-none">
<div className="absolute top-1/4 -left-10 w-[300px] h-[300px] bg-white/[0.01] rounded-full blur-[90px] pointer-events-none" />
<div className="mb-14">
<motion.div
initial={{ opacity: 0, x: -10 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5 }}
className="space-y-4"
>
<h2 className="font-sans text-4xl md:text-[50px] font-extrabold text-white tracking-tight">
Selected Works
</h2>
<div className="w-20 h-1 bg-white" />
</motion.div>
</div>
<div className="grid grid-cols-1 md:grid-cols-12 gap-6 h-auto">
{PROJECTS.map((project, index) => (
<motion.div
key={project.id}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: index * 0.1 }}
className="contents"
>
{project.terminalCommand ? (
<TerminalCard project={project} />
) : project.image ? (
<HeroCard project={project} />
) : (
<IconCard project={project} />
)}
</motion.div>
))}
</div>
</section>
);
}