fix:projects section
This commit is contained in:
@@ -1,79 +1,114 @@
|
||||
// components/DescriptionModal.tsx
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { Orbitron, JetBrains_Mono } from "next/font/google";
|
||||
import { motion } from "framer-motion";
|
||||
import { FiX, FiExternalLink, FiGithub } from "react-icons/fi";
|
||||
|
||||
interface Project {
|
||||
type Project = {
|
||||
_id: string;
|
||||
name: string;
|
||||
imageUrl?: string;
|
||||
techStack?: string;
|
||||
description?: string;
|
||||
}
|
||||
const orbitron = Orbitron({
|
||||
subsets: ["latin"],
|
||||
weight: ["400", "700"],
|
||||
});
|
||||
|
||||
const jetbrainsMono = JetBrains_Mono({
|
||||
subsets: ["latin"],
|
||||
weight: ["400", "700"],
|
||||
});
|
||||
deployedLink?: string;
|
||||
githubLink?: string;
|
||||
};
|
||||
|
||||
interface DescriptionModalProps {
|
||||
project: Project;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const DescriptionModal: React.FC<DescriptionModalProps> = ({
|
||||
project,
|
||||
onClose,
|
||||
}) => {
|
||||
if (!project) return null;
|
||||
|
||||
const handleBackdropClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
id={`modal-${project._id}`}
|
||||
tabIndex={-1}
|
||||
aria-hidden="true"
|
||||
onClick={handleBackdropClick}
|
||||
className="fixed inset-0 z-50 flex justify-center items-center w-full h-full bg-black/70 backdrop-blur-sm p-4 overflow-y-auto"
|
||||
>
|
||||
<div className="relative w-full max-w-2xl max-h-[90vh]">
|
||||
<div className="relative bg-gradient-to-br from-[#0A0F1E] via-[#0F172A] to-[#1E293B] rounded-lg shadow-xl border border-cyan-500/30 overflow-hidden">
|
||||
|
||||
<div className="flex items-center gap-2 p-3 bg-slate-900/50 border-b border-slate-700">
|
||||
<span className="block w-3 h-3 rounded-full bg-red-500"></span>
|
||||
<span className="block w-3 h-3 rounded-full bg-yellow-500"></span>
|
||||
<span className="block w-3 h-3 rounded-full bg-green-500"></span>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="relative flex items-center p-4 md:p-5 border-b border-slate-700">
|
||||
<h3
|
||||
className={`text-xl font-semibold text-cyan-400 text-center w-full ${orbitron.className}`}
|
||||
>
|
||||
{project.name}
|
||||
</h3>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div className="p-4 md:p-5 space-y-4 max-h-[60vh] overflow-y-auto pr-5">
|
||||
<p
|
||||
className={`text-base leading-relaxed text-gray-300 whitespace-pre-line ${jetbrainsMono.className}`}
|
||||
>
|
||||
{project.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
const parseTechStack = (techStack?: string): string[] => {
|
||||
if (!techStack) return [];
|
||||
return techStack.split(/[,|]/).map(tech => tech.trim()).filter(Boolean);
|
||||
};
|
||||
|
||||
export default DescriptionModal;
|
||||
export default function DescriptionModal({ project, onClose }: DescriptionModalProps) {
|
||||
const techStackArray = parseTechStack(project.techStack);
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm p-4"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
onClick={onClose}
|
||||
>
|
||||
<motion.div
|
||||
className="bg-slate-900/95 backdrop-blur-xl rounded-2xl border border-slate-700 max-w-2xl w-full max-h-[90vh] overflow-hidden"
|
||||
initial={{ scale: 0.9, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
exit={{ scale: 0.9, opacity: 0 }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-6 border-b border-slate-700">
|
||||
<h2 className="text-2xl font-bold text-white">{project.name}</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-2 hover:bg-slate-800 rounded-lg transition-colors text-slate-400 hover:text-white"
|
||||
>
|
||||
<FiX size={24} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-6 overflow-y-auto max-h-[calc(90vh-120px)]">
|
||||
{/* Description Section */}
|
||||
{project.description && (
|
||||
<div className="mb-6">
|
||||
<h3 className="text-lg font-semibold text-cyan-400 mb-3">Description</h3>
|
||||
<p className="text-gray-300 leading-relaxed whitespace-pre-line">
|
||||
{project.description}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tech Stack Section */}
|
||||
{techStackArray.length > 0 && (
|
||||
<div className="mb-6">
|
||||
<h3 className="text-lg font-semibold text-cyan-400 mb-3">Tech Stack</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{techStackArray.map((tech, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="px-3 py-2 bg-slate-800/60 rounded-lg text-cyan-200 border border-cyan-500/30"
|
||||
>
|
||||
{tech}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Links */}
|
||||
<div className="flex gap-4 pt-4 border-t border-slate-700">
|
||||
{project.deployedLink && (
|
||||
<a
|
||||
href={project.deployedLink}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-2 px-4 py-2 bg-cyan-600 hover:bg-cyan-700 rounded-lg transition-colors text-white"
|
||||
>
|
||||
<FiExternalLink />
|
||||
Live Demo
|
||||
</a>
|
||||
)}
|
||||
{project.githubLink && (
|
||||
<a
|
||||
href={project.githubLink}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-2 px-4 py-2 bg-slate-700 hover:bg-slate-600 rounded-lg transition-colors text-white"
|
||||
>
|
||||
<FiGithub />
|
||||
GitHub
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
@@ -8,11 +8,10 @@ import { FaGithub } from "react-icons/fa";
|
||||
import { FiArrowUpRight } from "react-icons/fi";
|
||||
import NeuralBackground from "@/app/components/NeuralBackground";
|
||||
import { TypeAnimation } from "react-type-animation";
|
||||
import { motion } from "framer-motion";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import DescriptionModal from "@/app/components/DescriptionModal";
|
||||
import Navbar from "../components/Navbar";
|
||||
|
||||
|
||||
// === FONT CONFIGURATIONS ===
|
||||
const orbitron = Orbitron({
|
||||
subsets: ["latin"],
|
||||
@@ -84,6 +83,38 @@ function LoadingScreen() {
|
||||
);
|
||||
}
|
||||
|
||||
// === TECH STACK UTILITIES ===
|
||||
const parseTechStack = (techStack?: string): string[] => {
|
||||
if (!techStack) return [];
|
||||
|
||||
// Handle comma, pipe, or space separated tech stacks
|
||||
return techStack.split(/[,|]/).map(tech => tech.trim()).filter(Boolean);
|
||||
};
|
||||
|
||||
const truncateTechStack = (techStack: string[], maxItems: number = 4): { displayed: string[], remaining: number } => {
|
||||
if (techStack.length <= maxItems) {
|
||||
return { displayed: techStack, remaining: 0 };
|
||||
}
|
||||
return {
|
||||
displayed: techStack.slice(0, maxItems),
|
||||
remaining: techStack.length - maxItems
|
||||
};
|
||||
};
|
||||
|
||||
const truncateDescription = (description?: string, wordLimit: number = 40): { truncated: string, isTruncated: boolean } => {
|
||||
if (!description) return { truncated: "", isTruncated: false };
|
||||
|
||||
const words = description.split(' ');
|
||||
if (words.length <= wordLimit) {
|
||||
return { truncated: description, isTruncated: false };
|
||||
}
|
||||
|
||||
return {
|
||||
truncated: words.slice(0, wordLimit).join(' ') + '...',
|
||||
isTruncated: true
|
||||
};
|
||||
};
|
||||
|
||||
// === MAIN COMPONENT ===
|
||||
export default function ViewProjectsPage() {
|
||||
const [projects, setProjects] = useState<Project[]>([]);
|
||||
@@ -91,6 +122,7 @@ export default function ViewProjectsPage() {
|
||||
const [usingFallback, setUsingFallback] = useState(false);
|
||||
const [activeIndex, setActiveIndex] = useState(0);
|
||||
const [selectedProject, setSelectedProject] = useState<Project | null>(null);
|
||||
const [expandedDescriptions, setExpandedDescriptions] = useState<Set<string>>(new Set());
|
||||
|
||||
// === FETCH PROJECTS FROM BACKEND ===
|
||||
useEffect(() => {
|
||||
@@ -137,44 +169,48 @@ export default function ViewProjectsPage() {
|
||||
// === MAIN RENDER ===
|
||||
return (
|
||||
<main className="relative min-h-screen w-full bg-gradient-to-b from-[#050816] via-[#0A0F1E] to-[#0F172A] py-16 px-4 sm:px-8">
|
||||
<style jsx global>{`
|
||||
<style jsx global>{`
|
||||
@import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;500;600;700;900&family=Roboto:wght@300;400;500;700&display=swap');
|
||||
`}</style>
|
||||
<NeuralBackground />
|
||||
<Navbar />
|
||||
<div className="relative z-10">
|
||||
<h1 className="text-4xl md:text-6xl font-bold mt-5 text-center tracking-widest relative font-[Orbitron]">
|
||||
<span className="relative text-transparent bg-clip-text bg-gradient-to-r from-blue-400 via-cyan-300 to-blue-600 drop-shadow-[0_0_25px_rgba(0,200,255,0.6)]">
|
||||
PROJECTS
|
||||
</span>
|
||||
<div className="absolute -bottom-2 left-1/2 transform -translate-x-1/2 w-24 h-[3px] bg-gradient-to-r from-blue-500 to-cyan-400 rounded-full" />
|
||||
</h1>
|
||||
<h1 className="text-4xl md:text-6xl font-bold mb-20 text-center tracking-widest relative font-[Orbitron]">
|
||||
<span className="relative text-transparent bg-clip-text bg-gradient-to-r from-blue-400 via-cyan-300 to-blue-600 drop-shadow-[0_0_25px_rgba(0,200,255,0.6)]">
|
||||
PROJECTS
|
||||
</span>
|
||||
<div className="absolute -bottom-2 left-1/2 transform -translate-x-1/2 w-24 h-[3px] bg-gradient-to-r from-blue-500 to-cyan-400 rounded-full" />
|
||||
</h1>
|
||||
|
||||
{/* === PROJECTS GRID === */}
|
||||
<div className="flex flex-col gap-y-16 lg:gap-y-24 max-w-7xl mx-auto">
|
||||
{projects.map((project, index) => {
|
||||
const theme = cardThemes[index % cardThemes.length];
|
||||
const isReversed = index % 2 !== 0;
|
||||
const techStackArray = parseTechStack(project.techStack);
|
||||
const { displayed: displayedTech, remaining: remainingTech } = truncateTechStack(techStackArray, 4);
|
||||
const { truncated: truncatedDesc, isTruncated: isDescTruncated } = truncateDescription(project.description, 40);
|
||||
const isDescriptionExpanded = expandedDescriptions.has(project._id);
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
key={project._id || index}
|
||||
className="grid grid-cols-1 lg:grid-cols-5 gap-8 lg:gap-12 items-center"
|
||||
initial={{ opacity: 0, y: 40 }} // 👇 fade & slide in
|
||||
initial={{ opacity: 0, y: 40 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, ease: "easeOut" }}
|
||||
viewport={{ once: true }}
|
||||
>
|
||||
{/* === IMAGE SECTION === */}
|
||||
{/* === IMAGE SECTION - Appears First === */}
|
||||
<motion.div
|
||||
className={`
|
||||
lg:col-span-3 w-full aspect-video rounded-lg overflow-hidden shadow-2xl
|
||||
transition-all duration-300 hover:shadow-cyan-500/30
|
||||
${isReversed ? "lg:order-last" : ""}
|
||||
`}
|
||||
initial={{ opacity: 0 }}
|
||||
whileInView={{ opacity: 1 }}
|
||||
transition={{ duration: 1.0, delay: 0.2 }}
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
whileInView={{ opacity: 1, scale: 1 }}
|
||||
transition={{ duration: 0.7, delay: 0.1 }}
|
||||
viewport={{ once: true }}
|
||||
>
|
||||
<a
|
||||
@@ -189,39 +225,50 @@ export default function ViewProjectsPage() {
|
||||
alt={project.name}
|
||||
width={1200}
|
||||
height={700}
|
||||
className="object-cover w-full h-full"
|
||||
className="object-cover w-full h-full hover:scale-105 transition-transform duration-500"
|
||||
priority={index === 0}
|
||||
/>
|
||||
)}
|
||||
</a>
|
||||
</motion.div>
|
||||
|
||||
{/* === TEXT / DETAILS SECTION === */}
|
||||
{/* === TEXT / DETAILS SECTION - Appears After Image === */}
|
||||
<motion.div
|
||||
className={`
|
||||
lg:col-span-2 flex flex-col gap-4
|
||||
${isReversed ? "lg:items-start" : "lg:items-end"}
|
||||
`}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, delay: 0.3 }}
|
||||
initial={{ opacity: 0, x: isReversed ? 20 : -20 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
transition={{ duration: 0.8, delay: 0.4 }}
|
||||
viewport={{ once: true }}
|
||||
>
|
||||
{/* === PROJECT TITLE === */}
|
||||
<h2
|
||||
className={`
|
||||
text-3xl font-bold text-white ${orbitron.className}
|
||||
text-center w-full
|
||||
`}
|
||||
{/* === PROJECT TITLE - Typing Effect === */}
|
||||
<motion.div
|
||||
className="w-full text-center"
|
||||
initial={{ opacity: 0 }}
|
||||
whileInView={{ opacity: 1 }}
|
||||
transition={{ duration: 0.6, delay: 0.5 }}
|
||||
viewport={{ once: true }}
|
||||
>
|
||||
{project.name}
|
||||
</h2>
|
||||
<TypeAnimation
|
||||
sequence={[project.name || "Untitled Project"]}
|
||||
wrapper="h2"
|
||||
speed={50}
|
||||
className={`text-3xl font-bold text-white ${orbitron.className}`}
|
||||
cursor={false}
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
{/* === DESCRIPTION BOX === */}
|
||||
{/* === DESCRIPTION BOX - Typing Effect === */}
|
||||
{project.description && (
|
||||
<div
|
||||
<motion.div
|
||||
className="bg-slate-800/60 backdrop-blur-md rounded-lg border border-slate-700 w-full cursor-pointer hover:border-cyan-600/50 transition-all overflow-hidden"
|
||||
onClick={() => setSelectedProject(project)}
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.6 }}
|
||||
viewport={{ once: true }}
|
||||
>
|
||||
{/* Window Header Style Dots */}
|
||||
<div className="flex items-center gap-2 p-3 bg-slate-900/50 border-b border-slate-700">
|
||||
@@ -234,35 +281,75 @@ export default function ViewProjectsPage() {
|
||||
<div className="p-6">
|
||||
<TypeAnimation
|
||||
sequence={[
|
||||
"hello world , this is a dummy description for the project page. Replace this with actual project descriptions . This project is very cool.",
|
||||
isDescriptionExpanded ? project.description : truncatedDesc,
|
||||
500
|
||||
]}
|
||||
wrapper="p"
|
||||
speed={{ type: "keyStrokeDelayInMs", value: 30 }}
|
||||
className={`
|
||||
text-gray-300 text-base
|
||||
${isReversed ? "lg:text-left" : "lg:text-right"}
|
||||
${jetbrainsMono.className}
|
||||
`}
|
||||
speed={30}
|
||||
className={`text-gray-300 text-base ${jetbrainsMono.className}`}
|
||||
style={{ whiteSpace: "pre-line" }}
|
||||
repeat={0}
|
||||
cursor={false}
|
||||
/>
|
||||
|
||||
{/* View More for Description */}
|
||||
{isDescTruncated && !isDescriptionExpanded && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setExpandedDescriptions(prev => new Set(prev).add(project._id));
|
||||
}}
|
||||
className="mt-3 text-cyan-400 hover:text-cyan-300 text-sm font-medium transition-colors"
|
||||
>
|
||||
View more...
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{/* === TECH STACK TEXT === */}
|
||||
<p
|
||||
className={`text-gray-400 ${isReversed ? "lg:text-left" : "lg:text-right"}`}
|
||||
>
|
||||
<strong>Tech Stack:</strong> {project.techStack}
|
||||
</p>
|
||||
{/* === TECH STACK - Fade In Animation === */}
|
||||
{techStackArray.length > 0 && (
|
||||
<motion.div
|
||||
className={`w-full ${isReversed ? "lg:text-left" : "lg:text-right"}`}
|
||||
initial={{ opacity: 0 }}
|
||||
whileInView={{ opacity: 1 }}
|
||||
transition={{ duration: 0.6, delay: 0.7 }}
|
||||
viewport={{ once: true }}
|
||||
>
|
||||
<p className="text-gray-400 mb-2">
|
||||
<strong>Tech Stack:</strong>
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2 justify-start">
|
||||
{displayedTech.map((tech, techIndex) => (
|
||||
<span
|
||||
key={techIndex}
|
||||
className="px-3 py-1 bg-slate-700/50 rounded-full text-sm text-cyan-200 border border-cyan-500/30"
|
||||
>
|
||||
{tech}
|
||||
</span>
|
||||
))}
|
||||
{remainingTech > 0 && (
|
||||
<button
|
||||
onClick={() => setSelectedProject(project)}
|
||||
className="px-3 py-1 bg-cyan-600/20 rounded-full text-sm text-cyan-300 border border-cyan-400/30 hover:bg-cyan-600/30 transition-colors"
|
||||
>
|
||||
+{remainingTech} more
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{/* === LINKS === */}
|
||||
<div
|
||||
{/* === LINKS - Fade In Animation === */}
|
||||
<motion.div
|
||||
className={`flex items-center gap-6 mt-2 ${
|
||||
isReversed ? "lg:justify-start" : "lg:justify-end"
|
||||
}`}
|
||||
initial={{ opacity: 0 }}
|
||||
whileInView={{ opacity: 1 }}
|
||||
transition={{ duration: 0.6, delay: 0.8 }}
|
||||
viewport={{ once: true }}
|
||||
>
|
||||
{project.deployedLink && (
|
||||
<a
|
||||
@@ -287,7 +374,7 @@ export default function ViewProjectsPage() {
|
||||
<FaGithub />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
);
|
||||
@@ -295,13 +382,15 @@ export default function ViewProjectsPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* === DESCRIPTION MODAL === */}
|
||||
{selectedProject && (
|
||||
<DescriptionModal
|
||||
project={selectedProject}
|
||||
onClose={() => setSelectedProject(null)}
|
||||
/>
|
||||
)}
|
||||
{/* === DESCRIPTION MODAL - Updated to include Tech Stack === */}
|
||||
<AnimatePresence>
|
||||
{selectedProject && (
|
||||
<DescriptionModal
|
||||
project={selectedProject}
|
||||
onClose={() => setSelectedProject(null)}
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user