fix:projects section

This commit is contained in:
Ashwani Senapati
2025-11-03 16:09:36 +05:30
parent 1ea24e850c
commit 6474ac4004
2 changed files with 243 additions and 119 deletions

View File

@@ -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>
);
}

View File

@@ -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>
);
}
}