fix:projects section
This commit is contained in:
@@ -1,79 +1,114 @@
|
|||||||
|
// components/DescriptionModal.tsx
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React from "react";
|
import { motion } from "framer-motion";
|
||||||
import { Orbitron, JetBrains_Mono } from "next/font/google";
|
import { FiX, FiExternalLink, FiGithub } from "react-icons/fi";
|
||||||
|
|
||||||
interface Project {
|
type Project = {
|
||||||
_id: string;
|
_id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
imageUrl?: string;
|
||||||
|
techStack?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
}
|
deployedLink?: string;
|
||||||
const orbitron = Orbitron({
|
githubLink?: string;
|
||||||
subsets: ["latin"],
|
};
|
||||||
weight: ["400", "700"],
|
|
||||||
});
|
|
||||||
|
|
||||||
const jetbrainsMono = JetBrains_Mono({
|
|
||||||
subsets: ["latin"],
|
|
||||||
weight: ["400", "700"],
|
|
||||||
});
|
|
||||||
|
|
||||||
interface DescriptionModalProps {
|
interface DescriptionModalProps {
|
||||||
project: Project;
|
project: Project;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DescriptionModal: React.FC<DescriptionModalProps> = ({
|
const parseTechStack = (techStack?: string): string[] => {
|
||||||
project,
|
if (!techStack) return [];
|
||||||
onClose,
|
return techStack.split(/[,|]/).map(tech => tech.trim()).filter(Boolean);
|
||||||
}) => {
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
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 { FiArrowUpRight } from "react-icons/fi";
|
||||||
import NeuralBackground from "@/app/components/NeuralBackground";
|
import NeuralBackground from "@/app/components/NeuralBackground";
|
||||||
import { TypeAnimation } from "react-type-animation";
|
import { TypeAnimation } from "react-type-animation";
|
||||||
import { motion } from "framer-motion";
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
import DescriptionModal from "@/app/components/DescriptionModal";
|
import DescriptionModal from "@/app/components/DescriptionModal";
|
||||||
import Navbar from "../components/Navbar";
|
import Navbar from "../components/Navbar";
|
||||||
|
|
||||||
|
|
||||||
// === FONT CONFIGURATIONS ===
|
// === FONT CONFIGURATIONS ===
|
||||||
const orbitron = Orbitron({
|
const orbitron = Orbitron({
|
||||||
subsets: ["latin"],
|
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 ===
|
// === MAIN COMPONENT ===
|
||||||
export default function ViewProjectsPage() {
|
export default function ViewProjectsPage() {
|
||||||
const [projects, setProjects] = useState<Project[]>([]);
|
const [projects, setProjects] = useState<Project[]>([]);
|
||||||
@@ -91,6 +122,7 @@ export default function ViewProjectsPage() {
|
|||||||
const [usingFallback, setUsingFallback] = useState(false);
|
const [usingFallback, setUsingFallback] = useState(false);
|
||||||
const [activeIndex, setActiveIndex] = useState(0);
|
const [activeIndex, setActiveIndex] = useState(0);
|
||||||
const [selectedProject, setSelectedProject] = useState<Project | null>(null);
|
const [selectedProject, setSelectedProject] = useState<Project | null>(null);
|
||||||
|
const [expandedDescriptions, setExpandedDescriptions] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
// === FETCH PROJECTS FROM BACKEND ===
|
// === FETCH PROJECTS FROM BACKEND ===
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -137,44 +169,48 @@ export default function ViewProjectsPage() {
|
|||||||
// === MAIN RENDER ===
|
// === MAIN RENDER ===
|
||||||
return (
|
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">
|
<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');
|
@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>
|
`}</style>
|
||||||
<NeuralBackground />
|
<NeuralBackground />
|
||||||
<Navbar />
|
<Navbar />
|
||||||
<div className="relative z-10">
|
<div className="relative z-10">
|
||||||
<h1 className="text-4xl md:text-6xl font-bold mt-5 text-center tracking-widest relative font-[Orbitron]">
|
<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)]">
|
<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
|
PROJECTS
|
||||||
</span>
|
</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" />
|
<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>
|
||||||
|
|
||||||
{/* === PROJECTS GRID === */}
|
{/* === PROJECTS GRID === */}
|
||||||
<div className="flex flex-col gap-y-16 lg:gap-y-24 max-w-7xl mx-auto">
|
<div className="flex flex-col gap-y-16 lg:gap-y-24 max-w-7xl mx-auto">
|
||||||
{projects.map((project, index) => {
|
{projects.map((project, index) => {
|
||||||
const theme = cardThemes[index % cardThemes.length];
|
const theme = cardThemes[index % cardThemes.length];
|
||||||
const isReversed = index % 2 !== 0;
|
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 (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={project._id || index}
|
key={project._id || index}
|
||||||
className="grid grid-cols-1 lg:grid-cols-5 gap-8 lg:gap-12 items-center"
|
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 }}
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
transition={{ duration: 0.8, ease: "easeOut" }}
|
transition={{ duration: 0.8, ease: "easeOut" }}
|
||||||
viewport={{ once: true }}
|
viewport={{ once: true }}
|
||||||
>
|
>
|
||||||
{/* === IMAGE SECTION === */}
|
{/* === IMAGE SECTION - Appears First === */}
|
||||||
<motion.div
|
<motion.div
|
||||||
className={`
|
className={`
|
||||||
lg:col-span-3 w-full aspect-video rounded-lg overflow-hidden shadow-2xl
|
lg:col-span-3 w-full aspect-video rounded-lg overflow-hidden shadow-2xl
|
||||||
transition-all duration-300 hover:shadow-cyan-500/30
|
transition-all duration-300 hover:shadow-cyan-500/30
|
||||||
${isReversed ? "lg:order-last" : ""}
|
${isReversed ? "lg:order-last" : ""}
|
||||||
`}
|
`}
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0, scale: 0.9 }}
|
||||||
whileInView={{ opacity: 1 }}
|
whileInView={{ opacity: 1, scale: 1 }}
|
||||||
transition={{ duration: 1.0, delay: 0.2 }}
|
transition={{ duration: 0.7, delay: 0.1 }}
|
||||||
viewport={{ once: true }}
|
viewport={{ once: true }}
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
@@ -189,39 +225,50 @@ export default function ViewProjectsPage() {
|
|||||||
alt={project.name}
|
alt={project.name}
|
||||||
width={1200}
|
width={1200}
|
||||||
height={700}
|
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}
|
priority={index === 0}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</a>
|
</a>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* === TEXT / DETAILS SECTION === */}
|
{/* === TEXT / DETAILS SECTION - Appears After Image === */}
|
||||||
<motion.div
|
<motion.div
|
||||||
className={`
|
className={`
|
||||||
lg:col-span-2 flex flex-col gap-4
|
lg:col-span-2 flex flex-col gap-4
|
||||||
${isReversed ? "lg:items-start" : "lg:items-end"}
|
${isReversed ? "lg:items-start" : "lg:items-end"}
|
||||||
`}
|
`}
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, x: isReversed ? 20 : -20 }}
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
whileInView={{ opacity: 1, x: 0 }}
|
||||||
transition={{ duration: 0.8, delay: 0.3 }}
|
transition={{ duration: 0.8, delay: 0.4 }}
|
||||||
viewport={{ once: true }}
|
viewport={{ once: true }}
|
||||||
>
|
>
|
||||||
{/* === PROJECT TITLE === */}
|
{/* === PROJECT TITLE - Typing Effect === */}
|
||||||
<h2
|
<motion.div
|
||||||
className={`
|
className="w-full text-center"
|
||||||
text-3xl font-bold text-white ${orbitron.className}
|
initial={{ opacity: 0 }}
|
||||||
text-center w-full
|
whileInView={{ opacity: 1 }}
|
||||||
`}
|
transition={{ duration: 0.6, delay: 0.5 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
>
|
>
|
||||||
{project.name}
|
<TypeAnimation
|
||||||
</h2>
|
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 && (
|
{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"
|
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)}
|
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 */}
|
{/* Window Header Style Dots */}
|
||||||
<div className="flex items-center gap-2 p-3 bg-slate-900/50 border-b border-slate-700">
|
<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">
|
<div className="p-6">
|
||||||
<TypeAnimation
|
<TypeAnimation
|
||||||
sequence={[
|
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"
|
wrapper="p"
|
||||||
speed={{ type: "keyStrokeDelayInMs", value: 30 }}
|
speed={30}
|
||||||
className={`
|
className={`text-gray-300 text-base ${jetbrainsMono.className}`}
|
||||||
text-gray-300 text-base
|
|
||||||
${isReversed ? "lg:text-left" : "lg:text-right"}
|
|
||||||
${jetbrainsMono.className}
|
|
||||||
`}
|
|
||||||
style={{ whiteSpace: "pre-line" }}
|
style={{ whiteSpace: "pre-line" }}
|
||||||
repeat={0}
|
repeat={0}
|
||||||
cursor={false}
|
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>
|
||||||
</div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* === TECH STACK TEXT === */}
|
{/* === TECH STACK - Fade In Animation === */}
|
||||||
<p
|
{techStackArray.length > 0 && (
|
||||||
className={`text-gray-400 ${isReversed ? "lg:text-left" : "lg:text-right"}`}
|
<motion.div
|
||||||
>
|
className={`w-full ${isReversed ? "lg:text-left" : "lg:text-right"}`}
|
||||||
<strong>Tech Stack:</strong> {project.techStack}
|
initial={{ opacity: 0 }}
|
||||||
</p>
|
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 === */}
|
{/* === LINKS - Fade In Animation === */}
|
||||||
<div
|
<motion.div
|
||||||
className={`flex items-center gap-6 mt-2 ${
|
className={`flex items-center gap-6 mt-2 ${
|
||||||
isReversed ? "lg:justify-start" : "lg:justify-end"
|
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 && (
|
{project.deployedLink && (
|
||||||
<a
|
<a
|
||||||
@@ -287,7 +374,7 @@ export default function ViewProjectsPage() {
|
|||||||
<FaGithub />
|
<FaGithub />
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
</div>
|
</motion.div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
@@ -295,13 +382,15 @@ export default function ViewProjectsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* === DESCRIPTION MODAL === */}
|
{/* === DESCRIPTION MODAL - Updated to include Tech Stack === */}
|
||||||
{selectedProject && (
|
<AnimatePresence>
|
||||||
<DescriptionModal
|
{selectedProject && (
|
||||||
project={selectedProject}
|
<DescriptionModal
|
||||||
onClose={() => setSelectedProject(null)}
|
project={selectedProject}
|
||||||
/>
|
onClose={() => setSelectedProject(null)}
|
||||||
)}
|
/>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user