refactor:redesign some components
This commit is contained in:
@@ -1,73 +1,3 @@
|
|||||||
# Welcome to your Lovable project
|
techzaa.alpha@gmail.com,
|
||||||
|
</br>
|
||||||
## Project info
|
TechZaa@@##123
|
||||||
|
|
||||||
**URL**: https://lovable.dev/projects/REPLACE_WITH_PROJECT_ID
|
|
||||||
|
|
||||||
## How can I edit this code?
|
|
||||||
|
|
||||||
There are several ways of editing your application.
|
|
||||||
|
|
||||||
**Use Lovable**
|
|
||||||
|
|
||||||
Simply visit the [Lovable Project](https://lovable.dev/projects/REPLACE_WITH_PROJECT_ID) and start prompting.
|
|
||||||
|
|
||||||
Changes made via Lovable will be committed automatically to this repo.
|
|
||||||
|
|
||||||
**Use your preferred IDE**
|
|
||||||
|
|
||||||
If you want to work locally using your own IDE, you can clone this repo and push changes. Pushed changes will also be reflected in Lovable.
|
|
||||||
|
|
||||||
The only requirement is having Node.js & npm installed - [install with nvm](https://github.com/nvm-sh/nvm#installing-and-updating)
|
|
||||||
|
|
||||||
Follow these steps:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
# Step 1: Clone the repository using the project's Git URL.
|
|
||||||
git clone <YOUR_GIT_URL>
|
|
||||||
|
|
||||||
# Step 2: Navigate to the project directory.
|
|
||||||
cd <YOUR_PROJECT_NAME>
|
|
||||||
|
|
||||||
# Step 3: Install the necessary dependencies.
|
|
||||||
npm i
|
|
||||||
|
|
||||||
# Step 4: Start the development server with auto-reloading and an instant preview.
|
|
||||||
npm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
**Edit a file directly in GitHub**
|
|
||||||
|
|
||||||
- Navigate to the desired file(s).
|
|
||||||
- Click the "Edit" button (pencil icon) at the top right of the file view.
|
|
||||||
- Make your changes and commit the changes.
|
|
||||||
|
|
||||||
**Use GitHub Codespaces**
|
|
||||||
|
|
||||||
- Navigate to the main page of your repository.
|
|
||||||
- Click on the "Code" button (green button) near the top right.
|
|
||||||
- Select the "Codespaces" tab.
|
|
||||||
- Click on "New codespace" to launch a new Codespace environment.
|
|
||||||
- Edit files directly within the Codespace and commit and push your changes once you're done.
|
|
||||||
|
|
||||||
## What technologies are used for this project?
|
|
||||||
|
|
||||||
This project is built with:
|
|
||||||
|
|
||||||
- Vite
|
|
||||||
- TypeScript
|
|
||||||
- React
|
|
||||||
- shadcn-ui
|
|
||||||
- Tailwind CSS
|
|
||||||
|
|
||||||
## How can I deploy this project?
|
|
||||||
|
|
||||||
Simply open [Lovable](https://lovable.dev/projects/REPLACE_WITH_PROJECT_ID) and click on Share -> Publish.
|
|
||||||
|
|
||||||
## Can I connect a custom domain to my Lovable project?
|
|
||||||
|
|
||||||
Yes, you can!
|
|
||||||
|
|
||||||
To connect a domain, navigate to Project > Settings > Domains and click Connect Domain.
|
|
||||||
|
|
||||||
Read more here: [Setting up a custom domain](https://docs.lovable.dev/features/custom-domain#custom-domain)
|
|
||||||
@@ -1,102 +1,173 @@
|
|||||||
import { motion } from "framer-motion";
|
import { motion, useReducedMotion } from "framer-motion";
|
||||||
import { ExternalLink } from "lucide-react";
|
import { ArrowUpRight, Layers } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useId, useState } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
export function ProjectCard({ project, color, index, isInView }) {
|
interface ProjectPayload {
|
||||||
|
_id: string;
|
||||||
|
title: string;
|
||||||
|
category: string[];
|
||||||
|
isFeatured: boolean;
|
||||||
|
technologies: string[];
|
||||||
|
publishedYear: string | number;
|
||||||
|
previewUrl: string;
|
||||||
|
image?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProjectCardProps {
|
||||||
|
project: ProjectPayload;
|
||||||
|
index: number;
|
||||||
|
isInView: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ProjectCard({ project, index, isInView }: ProjectCardProps) {
|
||||||
const [mousePosition, setMousePosition] = useState({ x: 50, y: 50 });
|
const [mousePosition, setMousePosition] = useState({ x: 50, y: 50 });
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
|
const shouldReduceMotion = useReducedMotion();
|
||||||
|
const titleId = useId();
|
||||||
|
|
||||||
const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
|
const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
if (shouldReduceMotion) return;
|
||||||
const rect = e.currentTarget.getBoundingClientRect();
|
const rect = e.currentTarget.getBoundingClientRect();
|
||||||
const x = ((e.clientX - rect.left) / rect.width) * 100;
|
const x = ((e.clientX - rect.left) / rect.width) * 100;
|
||||||
const y = ((e.clientY - rect.top) / rect.height) * 100;
|
const y = ((e.clientY - rect.top) / rect.height) * 100;
|
||||||
setMousePosition({ x, y });
|
setMousePosition({ x, y });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Safe Fallback Asset Frame
|
||||||
|
const projectImage =
|
||||||
|
project.image ||
|
||||||
|
"https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?q=80&w=800&auto=format&fit=crop";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link to={`/projects/${project._id}`}>
|
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 60, rotateX: 15 }}
|
initial={{ opacity: 0, y: shouldReduceMotion ? 0 : 40 }}
|
||||||
animate={isInView ? { opacity: 1, y: 0, rotateX: 0 } : {}}
|
animate={isInView ? { opacity: 1, y: 0 } : {}}
|
||||||
transition={{ duration: 0.6, delay: index * 0.08 }}
|
transition={{
|
||||||
whileHover={{ scale: 1.02 }}
|
duration: 0.5,
|
||||||
className="group relative rounded-3xl overflow-hidden cursor-pointer h-full"
|
delay: index * 0.05,
|
||||||
|
ease: [0.215, 0.61, 0.355, 1],
|
||||||
|
}}
|
||||||
|
className="group relative bg-card border border-border/60 hover:border-primary/30 rounded-2xl overflow-hidden shadow-md hover:shadow-xl transition-all duration-300 flex flex-col h-full transform-gpu"
|
||||||
onMouseMove={handleMouseMove}
|
onMouseMove={handleMouseMove}
|
||||||
onMouseEnter={() => setIsHovered(true)}
|
onMouseEnter={() => setIsHovered(true)}
|
||||||
onMouseLeave={() => {
|
onMouseLeave={() => {
|
||||||
setIsHovered(false);
|
setIsHovered(false);
|
||||||
setMousePosition({ x: 50, y: 50 });
|
setMousePosition({ x: 50, y: 50 });
|
||||||
}}
|
}}
|
||||||
|
aria-labelledby={titleId}
|
||||||
>
|
>
|
||||||
{/* Main Card Container with 3D Tilt */}
|
{/* Interactive Visual Window */}
|
||||||
<div
|
<div
|
||||||
className="relative h-full rounded-3xl overflow-hidden shadow-2xl transition-transform duration-300"
|
className="aspect-[16/10] overflow-hidden relative bg-muted w-full border-b border-border/40"
|
||||||
style={{
|
style={{
|
||||||
transform: isHovered
|
transform:
|
||||||
? `perspective(1000px) rotateX(${(50 - mousePosition.y) * 0.12}deg) rotateY(${(mousePosition.x - 50) * 0.15}deg)`
|
isHovered && !shouldReduceMotion
|
||||||
: "perspective(1000px) rotateX(0deg) rotateY(0deg)",
|
? `perspective(1000px) rotateX(${(50 - mousePosition.y) * 0.06}deg) rotateY(${(mousePosition.x - 50) * 0.06}deg)`
|
||||||
|
: "perspective(1000px) rotateX(0deg) rotateY(0deg)",
|
||||||
transition: isHovered
|
transition: isHovered
|
||||||
? "transform 0.1s ease-out"
|
? "transform 0.05s ease-out"
|
||||||
: "transform 0.4s ease-out",
|
: "transform 0.3s ease-out",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Image Container */}
|
<img
|
||||||
<div className="aspect-[16/13] overflow-hidden relative">
|
src={projectImage}
|
||||||
<img
|
alt={`Interface screenshot overview for ${project.title}`}
|
||||||
src={project.image}
|
className="w-full h-full object-cover transition-transform duration-700 ease-out group-hover:scale-[1.04]"
|
||||||
alt={project.title}
|
loading="lazy"
|
||||||
className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-110"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Dynamic Shine Overlay */}
|
|
||||||
<div
|
|
||||||
className="absolute inset-0 opacity-0 group-hover:opacity-30 transition-opacity duration-300 pointer-events-none"
|
|
||||||
style={{
|
|
||||||
background: `radial-gradient(circle at ${mousePosition.x}% ${mousePosition.y}%, rgba(255,255,255,0.8) 0%, transparent 60%)`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Gradient Overlay */}
|
|
||||||
<div
|
|
||||||
className={`absolute inset-0 bg-gradient-to-t ${color} via-black/40 to-transparent opacity-60 group-hover:opacity-90 transition-all duration-500`}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Content */}
|
{/* Techzaa Radial Spotlight Overlay */}
|
||||||
<div className="absolute inset-0 flex flex-col justify-end p-8">
|
{!shouldReduceMotion && (
|
||||||
<div className="space-y-4 transform transition-all duration-500 group-hover:translate-y-0">
|
<div
|
||||||
{/* Category */}
|
className="absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity duration-500 pointer-events-none mix-blend-screen"
|
||||||
<motion.span
|
style={{
|
||||||
initial={{ opacity: 0, y: 20 }}
|
background: `radial-gradient(circle 180px at ${mousePosition.x}% ${mousePosition.y}%, rgba(var(--primary-rgb, 99, 102, 241), 0.15), transparent 80%)`,
|
||||||
animate={
|
}}
|
||||||
isHovered ? { opacity: 1, y: 0 } : { opacity: 0.7, y: 10 }
|
/>
|
||||||
}
|
)}
|
||||||
className="inline-block px-4 py-1.5 rounded-full bg-white/10 backdrop-blur-md text-white text-xs font-medium border border-white/20"
|
|
||||||
>
|
|
||||||
{project.category}
|
|
||||||
</motion.span>
|
|
||||||
|
|
||||||
{/* Title */}
|
{/* Absolute Year Floating Tag */}
|
||||||
<h3 className="text-2xl md:text-3xl font-bold text-white leading-tight tracking-tight">
|
<span className="absolute top-3 right-3 text-[10px] font-bold px-2 py-0.5 rounded bg-background/80 backdrop-blur-md border border-border/40 text-muted-foreground tracking-wider">
|
||||||
{project.title}
|
{project.publishedYear}
|
||||||
</h3>
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* View Project Link */}
|
{/* Structured Informational Body Content */}
|
||||||
<Link
|
<div className="p-6 flex flex-col flex-grow justify-between space-y-6">
|
||||||
to={project.liveUrl}
|
<div className="space-y-3">
|
||||||
className="flex items-center gap-3 text-white/80 group-hover:text-white transition-colors"
|
{/* Category Chip List Mapping handles strings or arrays cleanly */}
|
||||||
>
|
<div
|
||||||
<span className="font-medium text-sm tracking-wider">live</span>
|
className="flex flex-wrap gap-1"
|
||||||
<ExternalLink className="w-5 h-5 transition-transform group-hover:rotate-45" />
|
aria-label="Project classifications"
|
||||||
</Link>
|
>
|
||||||
|
{Array.isArray(project.category) ? (
|
||||||
|
project.category.slice(0, 3).map((cat) => (
|
||||||
|
<span
|
||||||
|
key={cat}
|
||||||
|
className="inline-flex items-center text-[10px] font-bold uppercase tracking-wider text-primary bg-primary/5 border border-primary/10 px-2 py-0.5 rounded"
|
||||||
|
>
|
||||||
|
{cat}
|
||||||
|
</span>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<span className="inline-flex items-center text-[10px] font-bold uppercase tracking-wider text-primary bg-primary/5 border border-primary/10 px-2 py-0.5 rounded">
|
||||||
|
{project.category}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Title Identifier linked to parent card components via standard a11y specs */}
|
||||||
|
<h3
|
||||||
|
id={titleId}
|
||||||
|
className="text-xl font-bold tracking-tight text-foreground leading-snug group-hover:text-primary transition-colors duration-200"
|
||||||
|
>
|
||||||
|
{project.title}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{/* Previewing active Framework stacks cleanly underneath */}
|
||||||
|
{project.technologies && project.technologies.length > 0 && (
|
||||||
|
<div className="flex flex-wrap gap-1 pt-1">
|
||||||
|
{project.technologies.slice(0, 4).map((tech) => (
|
||||||
|
<span
|
||||||
|
key={tech}
|
||||||
|
className="text-[11px] font-medium text-muted-foreground bg-secondary/40 px-2 py-0.5 rounded border border-border/30"
|
||||||
|
>
|
||||||
|
{tech}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
{project.technologies.length > 4 && (
|
||||||
|
<span className="text-[10px] font-semibold text-muted-foreground/60 px-1.5 py-0.5">
|
||||||
|
+{project.technologies.length - 4} more
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Neon Border Glow */}
|
{/* Action Controls Matrix Bar */}
|
||||||
<div className="absolute inset-0 rounded-3xl border-2 border-transparent group-hover:border-primary/60 transition-all duration-500 pointer-events-none neon-glow" />
|
<div className="flex items-center justify-between pt-4 border-t border-border/40 w-full text-xs font-semibold tracking-wide">
|
||||||
|
<Link
|
||||||
|
to={`/projects/${project._id}`}
|
||||||
|
className="inline-flex items-center text-muted-foreground hover:text-foreground transition-colors py-1 focus-visible:outline-none focus-visible:underline"
|
||||||
|
aria-label={`Read technical case documentation for ${project.title}`}
|
||||||
|
>
|
||||||
|
<Layers className="w-3.5 h-3.5 mr-1.5 text-primary/70" />
|
||||||
|
Case Study
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href={project.previewUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="inline-flex items-center text-primary hover:opacity-80 transition-opacity py-1 focus-visible:outline-none focus-visible:underline"
|
||||||
|
aria-label={`Launch deployment window for ${project.title}`}
|
||||||
|
>
|
||||||
|
Live Platform
|
||||||
|
<ArrowUpRight className="w-3.5 h-3.5 ml-1 transition-transform group-hover:translate-x-0.5 group-hover:-translate-y-0.5" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</Link>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import { motion, useInView } from "framer-motion";
|
|||||||
import { ArrowRight } from "lucide-react";
|
import { ArrowRight } from "lucide-react";
|
||||||
import { useRef } from "react";
|
import { useRef } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { ProjectCard } from "./ProjectCard";
|
|
||||||
import PremiumBadge from "../shared/PremiumBadge";
|
import PremiumBadge from "../shared/PremiumBadge";
|
||||||
|
import { ProjectCard } from "./ProjectCard";
|
||||||
|
|
||||||
const gradientColors = [
|
const gradientColors = [
|
||||||
"from-neon-blue/90",
|
"from-neon-blue/90",
|
||||||
@@ -19,9 +19,9 @@ export default function ProjectsSection() {
|
|||||||
fields: "category, title, image, liveUrl",
|
fields: "category, title, image, liveUrl",
|
||||||
limit: 6,
|
limit: 6,
|
||||||
});
|
});
|
||||||
|
console.log(projectsData?.data.data);
|
||||||
const projects = projectsData?.data.data.result || [];
|
const projects = projectsData?.data.data || [];
|
||||||
|
console.log("project from homepage", projects);
|
||||||
const ref = useRef<HTMLElement>(null);
|
const ref = useRef<HTMLElement>(null);
|
||||||
const isInView = useInView(ref, { once: true, margin: "-100px" });
|
const isInView = useInView(ref, { once: true, margin: "-100px" });
|
||||||
|
|
||||||
@@ -50,9 +50,12 @@ export default function ProjectsSection() {
|
|||||||
transition={{ duration: 0.7 }}
|
transition={{ duration: 0.7 }}
|
||||||
className="text-center mb-20"
|
className="text-center mb-20"
|
||||||
>
|
>
|
||||||
<PremiumBadge text="our projects"/>
|
<PremiumBadge text="our projects" />
|
||||||
<h2 className="text-4xl md:text-5xl lg:text-6xl font-bold mb-6">
|
<h2 className="text-4xl md:text-5xl lg:text-6xl font-bold mb-6">
|
||||||
Featured <span className="text-transparent bg-clip-text bg-gradient-to-r from-primary to-accent-foreground">Projects</span>
|
Featured{" "}
|
||||||
|
<span className="text-transparent bg-clip-text bg-gradient-to-r from-primary to-accent-foreground">
|
||||||
|
Projects
|
||||||
|
</span>
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-muted-foreground max-w-2xl mx-auto text-lg">
|
<p className="text-muted-foreground max-w-2xl mx-auto text-lg">
|
||||||
Crafted with passion. Delivered with excellence.
|
Crafted with passion. Delivered with excellence.
|
||||||
@@ -62,13 +65,10 @@ export default function ProjectsSection() {
|
|||||||
{/* Projects Grid */}
|
{/* Projects Grid */}
|
||||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8 mb-16">
|
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8 mb-16">
|
||||||
{projects.map((project, index) => {
|
{projects.map((project, index) => {
|
||||||
const color = gradientColors[index % gradientColors.length];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ProjectCard
|
<ProjectCard
|
||||||
key={project._id}
|
key={project._id}
|
||||||
project={project}
|
project={project}
|
||||||
color={color}
|
|
||||||
index={index}
|
index={index}
|
||||||
isInView={isInView}
|
isInView={isInView}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ export default function TeamSection() {
|
|||||||
}
|
}
|
||||||
pagination={{ clickable: true }}
|
pagination={{ clickable: true }}
|
||||||
modules={[Autoplay, Pagination]}
|
modules={[Autoplay, Pagination]}
|
||||||
className="pb-12" // Space for pagination dots
|
className="pb-12"
|
||||||
breakpoints={{
|
breakpoints={{
|
||||||
640: { slidesPerView: 2 },
|
640: { slidesPerView: 2 },
|
||||||
1024: { slidesPerView: 3 },
|
1024: { slidesPerView: 3 },
|
||||||
@@ -84,18 +84,7 @@ export default function TeamSection() {
|
|||||||
className="absolute inset-0 w-full h-full object-cover object-top transition-transform duration-500 group-hover:scale-110"
|
className="absolute inset-0 w-full h-full object-cover object-top transition-transform duration-500 group-hover:scale-110"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute inset-0 rounded-full bg-background/80 backdrop-blur-sm flex items-center justify-center gap-3 opacity-0 group-hover:opacity-100 transition-opacity duration-300">
|
|
||||||
<Link to={member.linkedin} target="_blank">
|
|
||||||
<Linkedin className="w-5 h-5 cursor-pointer hover:text-primary transition-colors" />
|
|
||||||
</Link>
|
|
||||||
<Link to={member.twitter} target="_blank">
|
|
||||||
<Twitter className="w-5 h-5 cursor-pointer hover:text-primary transition-colors" />
|
|
||||||
</Link>
|
|
||||||
<Link to={member.github} target="_blank">
|
|
||||||
{" "}
|
|
||||||
<Github className="w-5 h-5 cursor-pointer hover:text-primary transition-colors" />
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 className="text-xl font-bold mb-1 group-hover:text-primary transition-colors">
|
<h3 className="text-xl font-bold mb-1 group-hover:text-primary transition-colors">
|
||||||
|
|||||||
+1
-1
@@ -17,7 +17,7 @@ const Index = () => {
|
|||||||
<HeroSection />
|
<HeroSection />
|
||||||
<ServicesSection />
|
<ServicesSection />
|
||||||
<ProjectsSection />
|
<ProjectsSection />
|
||||||
<TeamSection />
|
{/* <TeamSection /> */}
|
||||||
<AboutSection />
|
<AboutSection />
|
||||||
<TestimonialsSection />
|
<TestimonialsSection />
|
||||||
{/* <BlogSection /> */}
|
{/* <BlogSection /> */}
|
||||||
|
|||||||
+273
-307
@@ -1,67 +1,77 @@
|
|||||||
import PageTransition from "@/components/home/PageTransition";
|
import PageTransition from "@/components/home/PageTransition";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { useProjectById } from "@/hooks/queires/useProjects";
|
import { useProjectById } from "@/hooks/queires/useProjects";
|
||||||
import { motion } from "framer-motion";
|
import { motion, useReducedMotion } from "framer-motion";
|
||||||
import {
|
import {
|
||||||
|
AlertCircle,
|
||||||
Calendar,
|
Calendar,
|
||||||
|
CheckCircle,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
Clock,
|
Clock,
|
||||||
ExternalLink,
|
ExternalLink,
|
||||||
Github,
|
HelpCircle,
|
||||||
Users,
|
Sliders,
|
||||||
|
Sparkles,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import ReactMarkdown from "react-markdown";
|
|
||||||
import { Link, useNavigate, useParams } from "react-router-dom";
|
import { Link, useNavigate, useParams } from "react-router-dom";
|
||||||
import remarkGfm from "remark-gfm";
|
|
||||||
|
|
||||||
interface Result {
|
// Standardizing backend data matching layer safely
|
||||||
label: string;
|
interface BackendProjectPayload {
|
||||||
value: string | number;
|
_id: string;
|
||||||
}
|
|
||||||
|
|
||||||
interface ProjectData {
|
|
||||||
id: string;
|
|
||||||
category: string;
|
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
year: string | number;
|
category: string[];
|
||||||
duration: string;
|
status: string;
|
||||||
team: string | number;
|
isFeatured: boolean;
|
||||||
client: string;
|
|
||||||
liveUrl: string;
|
|
||||||
codeUrl?: string;
|
|
||||||
image: string;
|
image: string;
|
||||||
results?: Result[];
|
imageGallery: string[];
|
||||||
fullDescription: string;
|
technologies: string[];
|
||||||
features?: string[];
|
publishedYear: string | number;
|
||||||
technologies?: string[];
|
problem: string;
|
||||||
challenges?: string[];
|
solutions: string;
|
||||||
gallery?: string[];
|
challenges: string;
|
||||||
|
workingDuration: string;
|
||||||
|
previewUrl: string;
|
||||||
|
features: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const CATEGORY_LABELS: Record<string, string> = {
|
|
||||||
web: "Web Development",
|
|
||||||
mobile: "Mobile App",
|
|
||||||
ai: "AI & Machine Learning",
|
|
||||||
cloud: "Cloud Solutions",
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function ProjectDetails() {
|
export default function ProjectDetails() {
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const shouldReduceMotion = useReducedMotion();
|
||||||
const { data, isLoading, isError } = useProjectById(id || "");
|
const { data, isLoading, isError } = useProjectById(id || "");
|
||||||
|
|
||||||
const project: ProjectData | null =
|
const project: BackendProjectPayload | null =
|
||||||
data?.data?.data || data?.data || data || null;
|
data?.data?.data || data?.data || data || null;
|
||||||
|
|
||||||
|
const faderUpVariants = {
|
||||||
|
hidden: { opacity: 0, y: shouldReduceMotion ? 0 : 20 },
|
||||||
|
visible: (custom: number) => ({
|
||||||
|
opacity: 1,
|
||||||
|
y: 0,
|
||||||
|
transition: {
|
||||||
|
duration: 0.5,
|
||||||
|
delay: custom * 0.08,
|
||||||
|
ease: [0.215, 0.61, 0.355, 1],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<PageTransition>
|
<PageTransition>
|
||||||
<div className="min-h-screen bg-background flex items-center justify-center">
|
<div
|
||||||
|
className="min-h-screen bg-background flex items-center justify-center"
|
||||||
|
role="status"
|
||||||
|
aria-live="polite"
|
||||||
|
>
|
||||||
<div className="flex flex-col items-center gap-4">
|
<div className="flex flex-col items-center gap-4">
|
||||||
<div className="w-10 h-10 border-4 border-primary border-t-transparent rounded-full animate-spin" />
|
<div className="relative w-10 h-10">
|
||||||
<span className="text-sm font-medium text-muted-foreground animate-pulse">
|
<div className="absolute inset-0 border-4 border-primary/10 rounded-full" />
|
||||||
Loading project details...
|
<div className="absolute inset-0 border-4 border-primary border-t-transparent rounded-full animate-spin" />
|
||||||
|
</div>
|
||||||
|
<span className="text-xs font-semibold tracking-widest text-muted-foreground uppercase animate-pulse">
|
||||||
|
Parsing Techzaa Case Asset...
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -72,20 +82,25 @@ export default function ProjectDetails() {
|
|||||||
if (isError || !project) {
|
if (isError || !project) {
|
||||||
return (
|
return (
|
||||||
<PageTransition>
|
<PageTransition>
|
||||||
<div className="min-h-screen bg-background flex items-center justify-center p-4">
|
<div className="min-h-screen bg-background flex items-center justify-center p-6">
|
||||||
<div className="text-center max-w-md border border-primary/20 bg-secondary/30 backdrop-blur-xl p-8 rounded-3xl shadow-2xl">
|
<div className="text-center max-w-sm border border-destructive/20 bg-card p-8 rounded-2xl shadow-xl space-y-6">
|
||||||
<h1 className="text-3xl font-bold tracking-tight mb-3">
|
<div className="inline-flex p-3 bg-destructive/10 rounded-full text-destructive">
|
||||||
Project Not Found
|
<AlertCircle className="w-5 h-5" />
|
||||||
</h1>
|
</div>
|
||||||
<p className="text-muted-foreground mb-6">
|
<div className="space-y-2">
|
||||||
The project you're looking for does not exist or could not be
|
<h1 className="text-xl font-bold tracking-tight">
|
||||||
loaded.
|
Deployment Not Located
|
||||||
</p>
|
</h1>
|
||||||
|
<p className="text-xs text-muted-foreground leading-relaxed">
|
||||||
|
The targeted project record configuration cannot be pulled from
|
||||||
|
the API ecosystem.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => navigate("/projects")}
|
onClick={() => navigate("/projects")}
|
||||||
className="rounded-full w-full bg-primary text-primary-foreground hover:opacity-90 transition-all shadow-lg"
|
className="w-full rounded-xl text-xs font-semibold uppercase tracking-wider"
|
||||||
>
|
>
|
||||||
Back to Projects
|
Return to home
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -95,272 +110,222 @@ export default function ProjectDetails() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PageTransition>
|
<PageTransition>
|
||||||
<div className="min-h-screen bg-background text-foreground overflow-x-hidden selection:bg-primary/30 selection:text-primary">
|
<div className="min-h-screen bg-background text-foreground selection:bg-primary/20 selection:text-primary antialiased">
|
||||||
|
{/* HERO SECTION - Architectural Dynamic Header */}
|
||||||
|
<section className="pt-36 pb-16 relative overflow-hidden">
|
||||||
|
<div className="container mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
||||||
|
{/* Dynamic Breadcrumbs */}
|
||||||
|
<nav aria-label="Breadcrumb" className="mb-10">
|
||||||
|
<ol className="flex items-center space-x-2 text-xs font-medium text-muted-foreground">
|
||||||
|
<li>
|
||||||
|
<Link
|
||||||
|
to="/"
|
||||||
|
className="hover:text-primary transition-colors focus-visible:outline-none focus-visible:underline"
|
||||||
|
>
|
||||||
|
Home
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-center space-x-2">
|
||||||
|
<ChevronRight className="w-3 h-3 opacity-60" />
|
||||||
|
<Link
|
||||||
|
to="/projects"
|
||||||
|
className="hover:text-primary transition-colors focus-visible:outline-none focus-visible:underline"
|
||||||
|
>
|
||||||
|
Projects
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-center space-x-2" aria-current="page">
|
||||||
|
<ChevronRight className="w-3 h-3 opacity-60" />
|
||||||
|
<span className="text-foreground font-semibold truncate max-w-[200px]">
|
||||||
|
{project.title}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{/* Asymmetric Split Layout */}
|
||||||
{/* Hero Section */}
|
<div className="grid grid-cols-1 lg:grid-cols-12 gap-12 items-start">
|
||||||
<section className="pt-36 pb-20 relative overflow-hidden">
|
{/* Primary Metas Block */}
|
||||||
{/* Ambient Background Glows */}
|
<div className="lg:col-span-7 space-y-6">
|
||||||
<div className="absolute inset-0 pointer-events-none z-0">
|
<motion.div
|
||||||
<div className="absolute top-1/4 left-[10%] w-[500px] h-[500px] bg-primary/15 rounded-full mix-blend-screen filter blur-[120px] animate-pulse duration-[6000ms]" />
|
initial="hidden"
|
||||||
<div className="absolute bottom-0 right-[10%] w-[400px] h-[400px] bg-purple-500/10 rounded-full mix-blend-screen filter blur-[100px] delay-1000" />
|
animate="visible"
|
||||||
</div>
|
custom={0}
|
||||||
|
className="flex flex-wrap gap-1.5"
|
||||||
<div className="container mx-auto px-4 md:px-6 relative z-10">
|
|
||||||
{/* Breadcrumb Navigation */}
|
|
||||||
<motion.nav
|
|
||||||
initial={{ opacity: 0, y: -10 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.4 }}
|
|
||||||
className="flex items-center gap-2 text-xs font-medium text-muted-foreground/80 mb-10"
|
|
||||||
aria-label="Breadcrumb"
|
|
||||||
>
|
|
||||||
<Link to="/" className="hover:text-primary transition-colors">
|
|
||||||
Home
|
|
||||||
</Link>
|
|
||||||
<ChevronRight className="w-3.5 h-3.5 opacity-50" />
|
|
||||||
<Link
|
|
||||||
to="/projects"
|
|
||||||
className="hover:text-primary transition-colors"
|
|
||||||
>
|
|
||||||
Projects
|
|
||||||
</Link>
|
|
||||||
<ChevronRight className="w-3.5 h-3.5 opacity-50" />
|
|
||||||
<span className="text-foreground font-semibold truncate">
|
|
||||||
{project.title}
|
|
||||||
</span>
|
|
||||||
</motion.nav>
|
|
||||||
|
|
||||||
<div className="grid lg:grid-cols-2 gap-12 items-center">
|
|
||||||
{/* Content Block */}
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<motion.span
|
|
||||||
initial={{ opacity: 0, y: 15 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.4 }}
|
|
||||||
className="self-start px-3.5 py-1 rounded-full bg-primary/15 text-primary border border-primary/20 text-xs font-semibold tracking-wider uppercase mb-6"
|
|
||||||
>
|
>
|
||||||
{CATEGORY_LABELS[project.category] || project.category}
|
{Array.isArray(project.category) ? (
|
||||||
</motion.span>
|
project.category.map((cat) => (
|
||||||
|
<span
|
||||||
|
key={cat}
|
||||||
|
className="inline-flex items-center gap-1 px-2.5 py-0.5 rounded bg-secondary text-secondary-foreground border border-border/80 text-[11px] font-semibold tracking-wide"
|
||||||
|
>
|
||||||
|
{cat}
|
||||||
|
</span>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<span className="inline-flex items-center gap-1 px-2.5 py-0.5 rounded bg-secondary text-secondary-foreground border border-border/80 text-[11px] font-semibold tracking-wide">
|
||||||
|
{project.category}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
<motion.h1
|
<motion.h1
|
||||||
initial={{ opacity: 0, y: 15 }}
|
animate="visible"
|
||||||
animate={{ opacity: 1, y: 0 }}
|
custom={1}
|
||||||
transition={{ duration: 0.5, delay: 0.1 }}
|
className="text-3xl sm:text-4xl lg:text-5xl font-black tracking-tight text-foreground leading-[1.1]"
|
||||||
className="text-4xl md:text-5xl lg:text-6xl font-black tracking-tight leading-none mb-6"
|
|
||||||
>
|
>
|
||||||
{project.title}
|
{project.title}
|
||||||
</motion.h1>
|
</motion.h1>
|
||||||
|
|
||||||
<motion.p
|
<motion.p
|
||||||
initial={{ opacity: 0, y: 15 }}
|
initial="hidden"
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate="visible"
|
||||||
transition={{ duration: 0.5, delay: 0.2 }}
|
custom={2}
|
||||||
className="text-base md:text-lg text-muted-foreground leading-relaxed max-w-2xl mb-10"
|
className="text-base sm:text-lg text-muted-foreground leading-relaxed max-w-2xl"
|
||||||
>
|
>
|
||||||
{project.description}
|
{project.description}
|
||||||
</motion.p>
|
</motion.p>
|
||||||
|
|
||||||
{/* Project Metadata */}
|
{/* Primary Interaction Arrays */}
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 15 }}
|
initial="hidden"
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate="visible"
|
||||||
transition={{ duration: 0.5, delay: 0.3 }}
|
custom={3}
|
||||||
className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-10"
|
className="flex flex-col sm:flex-row gap-3 pt-2"
|
||||||
>
|
>
|
||||||
{[
|
<Button
|
||||||
{ label: "Year", value: project.year, icon: Calendar },
|
asChild
|
||||||
{ label: "Duration", value: project.duration, icon: Clock },
|
size="lg"
|
||||||
{ label: "Team Size", value: project.team, icon: Users },
|
className="rounded-xl font-semibold tracking-wide shadow-lg shadow-primary/5 transition-transform active:scale-[0.98]"
|
||||||
{ label: "Client", value: project.client },
|
|
||||||
].map((item, idx) => (
|
|
||||||
<div
|
|
||||||
key={item.label}
|
|
||||||
className="group flex flex-col items-center justify-center p-4 bg-secondary/40 backdrop-blur-md border border-border/50 rounded-2xl transition-all duration-300 hover:border-primary/30 hover:shadow-lg"
|
|
||||||
>
|
|
||||||
{item.icon ? (
|
|
||||||
<item.icon className="w-5 h-5 text-primary mb-2.5 transition-transform group-hover:scale-105" />
|
|
||||||
) : (
|
|
||||||
<div className="w-5 h-5 mb-2.5" />
|
|
||||||
)}
|
|
||||||
<span className="text-[10px] font-medium tracking-wider uppercase text-muted-foreground mb-1">
|
|
||||||
{item.label}
|
|
||||||
</span>
|
|
||||||
<span className="text-sm font-bold truncate max-w-full text-center">
|
|
||||||
{item.value}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
{/* Call to Actions */}
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 15 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.5, delay: 0.4 }}
|
|
||||||
className="flex flex-wrap gap-4"
|
|
||||||
>
|
|
||||||
<Link
|
|
||||||
to={project.liveUrl}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="w-full sm:w-auto"
|
|
||||||
>
|
>
|
||||||
<Button className="w-full sm:w-auto px-6 h-11 rounded-full bg-primary text-primary-foreground font-medium gap-2 shadow-xl shadow-primary/10 hover:opacity-90 transition-all duration-300">
|
<a
|
||||||
<ExternalLink className="w-4 h-4" />
|
href={project.previewUrl}
|
||||||
View Live
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
{project.codeUrl && (
|
|
||||||
<Link
|
|
||||||
to={project.codeUrl}
|
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="w-full sm:w-auto"
|
|
||||||
>
|
>
|
||||||
<Button
|
<ExternalLink className="w-4 h-4 mr-2" />
|
||||||
variant="outline"
|
Live Preview Link
|
||||||
className="w-full sm:w-auto px-6 h-11 rounded-full border-border/70 bg-secondary/30 backdrop-blur-sm font-medium gap-2 hover:bg-secondary transition-all duration-300"
|
</a>
|
||||||
>
|
</Button>
|
||||||
<Github className="w-4 h-4 text-muted-foreground" />
|
|
||||||
View Code
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Project Cover Visual */}
|
{/* Cover Interactive Card Frame */}
|
||||||
<motion.div
|
<div className="lg:col-span-5 w-full">
|
||||||
initial={{ opacity: 0, scale: 0.95 }}
|
<motion.div
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
initial={{ opacity: 0, scale: shouldReduceMotion ? 1 : 0.97 }}
|
||||||
transition={{ duration: 0.6, delay: 0.2 }}
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
className="relative group rounded-3xl overflow-hidden border border-border/60 shadow-2xl bg-secondary/30 aspect-[4/3] lg:aspect-auto h-[450px]"
|
transition={{ duration: 0.5 }}
|
||||||
>
|
className="relative rounded-2xl overflow-hidden border border-border bg-card aspect-[4/3] shadow-lg transform-gpu"
|
||||||
<img
|
>
|
||||||
src={project.image}
|
<img
|
||||||
alt={project.title}
|
src={project.image}
|
||||||
className="w-full h-full object-cover object-center transition-transform duration-700 ease-out group-hover:scale-[1.02]"
|
alt={`Case Application mockup frame for ${project.title}`}
|
||||||
loading="lazy"
|
className="w-full h-full object-cover object-center"
|
||||||
/>
|
loading="eager"
|
||||||
<div className="absolute inset-0 bg-gradient-to-t from-background/80 via-transparent to-transparent opacity-80" />
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Key Results Section */}
|
{/* METADATA BAR SECTION */}
|
||||||
{project.results && project.results.length > 0 && (
|
<section
|
||||||
<section className="py-20 border-t border-b border-border/50 bg-secondary/20">
|
className="py-8 bg-secondary/20 border-y border-border/40"
|
||||||
<div className="container mx-auto px-4 md:px-6">
|
aria-label="System parameters"
|
||||||
<motion.div
|
>
|
||||||
initial={{ opacity: 0, y: 15 }}
|
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
|
||||||
viewport={{ once: true }}
|
{[
|
||||||
className="text-center max-w-2xl mx-auto mb-14"
|
{
|
||||||
>
|
label: "Deployment Year",
|
||||||
<h2 className="text-3xl font-bold tracking-tight mb-3">
|
value: project.publishedYear,
|
||||||
Key Performance Outcomes
|
icon: Calendar,
|
||||||
</h2>
|
},
|
||||||
<p className="text-sm text-muted-foreground">
|
{
|
||||||
Measurable impact and business value generated by this
|
label: "Production Period",
|
||||||
project.
|
value: project.workingDuration,
|
||||||
</p>
|
icon: Clock,
|
||||||
</motion.div>
|
},
|
||||||
|
{
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-6 max-w-5xl mx-auto">
|
label: "Project Status",
|
||||||
{project.results.map((result, index) => (
|
value: project.status,
|
||||||
<motion.div
|
icon: Sliders,
|
||||||
key={result.label}
|
},
|
||||||
initial={{ opacity: 0, y: 25 }}
|
].map((item) => (
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
<div
|
||||||
viewport={{ once: true }}
|
key={item.label}
|
||||||
transition={{ delay: index * 0.08, duration: 0.4 }}
|
className="p-4 bg-card border border-border/60 rounded-xl flex items-center justify-between shadow-sm"
|
||||||
className="flex flex-col items-center bg-background/60 backdrop-blur-md border border-border/40 rounded-3xl p-8 transition-all duration-300 hover:border-primary/30 hover:shadow-xl"
|
>
|
||||||
>
|
<div className="space-y-0.5 truncate mr-2">
|
||||||
<span className="text-3xl md:text-4xl font-black text-primary tracking-tight mb-2">
|
<span className="block text-[10px] font-bold tracking-wider uppercase text-muted-foreground">
|
||||||
{result.value}
|
{item.label}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xs font-medium text-center text-muted-foreground">
|
<span className="block text-sm font-bold text-foreground truncate">
|
||||||
{result.label}
|
{item.value}
|
||||||
</span>
|
</span>
|
||||||
</motion.div>
|
</div>
|
||||||
))}
|
<item.icon className="w-4 h-4 text-primary opacity-70 flex-shrink-0" />
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Main Details Section */}
|
|
||||||
<section className="py-28">
|
|
||||||
<div className="container mx-auto px-4 md:px-6">
|
|
||||||
<div className="grid lg:grid-cols-12 gap-16 max-w-7xl mx-auto">
|
|
||||||
{/* Main Text Content */}
|
|
||||||
<div className="lg:col-span-7 flex flex-col justify-start">
|
|
||||||
<h3 className="text-3xl font-bold tracking-tight mb-8">
|
|
||||||
About the Project
|
|
||||||
</h3>
|
|
||||||
<div className="prose prose-neutral dark:prose-invert max-w-none text-muted-foreground leading-relaxed">
|
|
||||||
<ReactMarkdown
|
|
||||||
remarkPlugins={[remarkGfm]}
|
|
||||||
components={{
|
|
||||||
p: ({ children }) => (
|
|
||||||
<p className="text-base md:text-lg text-muted-foreground leading-relaxed mb-6">
|
|
||||||
{children}
|
|
||||||
</p>
|
|
||||||
),
|
|
||||||
strong: ({ children }) => (
|
|
||||||
<strong className="font-semibold text-foreground">
|
|
||||||
{children}
|
|
||||||
</strong>
|
|
||||||
),
|
|
||||||
ul: ({ children }) => (
|
|
||||||
<ul className="list-disc list-inside space-y-2 mb-6 ml-2 text-muted-foreground">
|
|
||||||
{children}
|
|
||||||
</ul>
|
|
||||||
),
|
|
||||||
li: ({ children }) => (
|
|
||||||
<li className="text-muted-foreground">{children}</li>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{project.fullDescription}
|
|
||||||
</ReactMarkdown>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
{/* Sidebar Capabilities */}
|
{/* COMPREHENSIVE CASE LOGIC SECTION */}
|
||||||
<div className="lg:col-span-5 flex flex-col gap-12">
|
<section className="py-20 bg-background">
|
||||||
{/* Features */}
|
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
{project.features && project.features.length > 0 && (
|
<div className="grid grid-cols-1 lg:grid-cols-12 gap-12 lg:gap-16 items-start">
|
||||||
<div>
|
{/* Engineering Metrics Column (Left side) */}
|
||||||
<h3 className="text-xl font-bold tracking-tight mb-6">
|
<div className="lg:col-span-6 space-y-10">
|
||||||
Key Capabilities
|
{project.problem && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<h3 className="text-lg font-bold tracking-tight text-foreground flex items-center gap-2">
|
||||||
|
<HelpCircle className="w-4 h-4 text-primary" />
|
||||||
|
The Structural Problem
|
||||||
</h3>
|
</h3>
|
||||||
<ul className="space-y-4">
|
<p className="text-sm text-muted-foreground leading-relaxed bg-secondary/10 border border-border/40 p-4 rounded-xl">
|
||||||
{project.features.map((feature, index) => (
|
{project.problem}
|
||||||
<li key={index} className="flex items-start gap-4">
|
</p>
|
||||||
<span className="flex-shrink-0 w-1.5 h-1.5 rounded-full bg-primary mt-2.5" />
|
|
||||||
<span className="text-sm md:text-base text-muted-foreground leading-snug">
|
|
||||||
{feature}
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Technologies Used */}
|
{project.solutions && (
|
||||||
{project.technologies && project.technologies.length > 0 && (
|
<div className="space-y-3">
|
||||||
<div className="border-t border-border/40 pt-8">
|
<h3 className="text-lg font-bold tracking-tight text-foreground flex items-center gap-2">
|
||||||
<h3 className="text-xl font-bold tracking-tight mb-6">
|
<CheckCircle className="w-4 h-4 text-primary" />
|
||||||
Technologies Used
|
Our Applied Strategy
|
||||||
</h3>
|
</h3>
|
||||||
<div className="flex flex-wrap gap-2.5">
|
<p className="text-sm text-muted-foreground leading-relaxed bg-primary/5 border border-primary/10 p-4 rounded-xl">
|
||||||
|
{project.solutions}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{project.challenges && (
|
||||||
|
<div className="space-y-3 border-t border-border/40 pt-6">
|
||||||
|
<h3 className="text-lg font-bold tracking-tight text-foreground">
|
||||||
|
Critical Bottlenecks Resolved
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||||
|
{project.challenges}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="lg:col-span-6 space-y-10">
|
||||||
|
{project.technologies && project.technologies.length > 0 && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-sm font-bold tracking-wider uppercase">
|
||||||
|
Tech Stack
|
||||||
|
</h3>
|
||||||
|
<div className="flex flex-wrap gap-1.5">
|
||||||
{project.technologies.map((tech) => (
|
{project.technologies.map((tech) => (
|
||||||
<span
|
<span
|
||||||
key={tech}
|
key={tech}
|
||||||
className="px-3.5 py-1.5 bg-secondary text-xs font-medium rounded-full border border-border/60 text-muted-foreground"
|
className="px-3 py-1 bg-card text-xs font-semibold rounded border border-border/80 text-foreground transition-colors cursor-default hover:bg-secondary/40"
|
||||||
>
|
>
|
||||||
{tech}
|
{tech}
|
||||||
</span>
|
</span>
|
||||||
@@ -369,19 +334,20 @@ export default function ProjectDetails() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Challenges Solved */}
|
{project.features && project.features.length > 0 && (
|
||||||
{project.challenges && project.challenges.length > 0 && (
|
<div className="space-y-4 border-t border-border/40 pt-6">
|
||||||
<div className="border-t border-border/40 pt-8">
|
<h3 className="text-lg font-bold tracking-tight text-foreground flex items-center gap-2">
|
||||||
<h3 className="text-xl font-bold tracking-tight mb-6">
|
<Sparkles className="w-4 h-4 text-primary" />
|
||||||
Challenges Solved
|
Delivered Platform Capabilities
|
||||||
</h3>
|
</h3>
|
||||||
<ul className="space-y-4">
|
<ul className="grid grid-cols-1 sm:grid-cols-2 gap-2.5">
|
||||||
{project.challenges.map((challenge, index) => (
|
{project.features.map((feature, idx) => (
|
||||||
<li key={index} className="flex items-start gap-4">
|
<li
|
||||||
<span className="flex-shrink-0 w-1.5 h-1.5 rounded-full bg-purple-500 mt-2.5" />
|
key={idx}
|
||||||
<span className="text-sm md:text-base text-muted-foreground leading-snug">
|
className="flex items-center gap-2 text-xs text-muted-foreground bg-card border border-border/40 p-2.5 rounded-lg"
|
||||||
{challenge}
|
>
|
||||||
</span>
|
<div className="w-1.5 h-1.5 rounded-full bg-primary flex-shrink-0" />
|
||||||
|
<span className="truncate">{feature}</span>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
@@ -392,36 +358,36 @@ export default function ProjectDetails() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Gallery Section */}
|
{/* PLATFORM PRESENTATION SHOWCASE GALLERY */}
|
||||||
{project.gallery && project.gallery.length > 0 && (
|
{project.imageGallery && project.imageGallery.length > 0 && (
|
||||||
<section className="py-20 border-t border-border/50 bg-secondary/30">
|
<section className="py-20 border-t border-border/40 bg-secondary/10">
|
||||||
<div className="container mx-auto px-4 md:px-6">
|
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<div className="text-center max-w-2xl mx-auto mb-14">
|
<div className="max-w-xl mx-auto text-center mb-12 space-y-1">
|
||||||
<h2 className="text-3xl font-bold tracking-tight mb-3">
|
<h2 className="text-xl sm:text-2xl font-bold tracking-tight">
|
||||||
Project Gallery
|
Platform Interface Metrics
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
A visual overview of the implementation and user interface.
|
Operational state validation screens captured during
|
||||||
|
deployment execution.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid md:grid-cols-3 gap-6 max-w-6xl mx-auto">
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
{project.gallery.map((image, index) => (
|
{project.imageGallery.map((image, index) => (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={index}
|
key={index}
|
||||||
initial={{ opacity: 0, scale: 0.97 }}
|
initial={{ opacity: 0, y: shouldReduceMotion ? 0 : 15 }}
|
||||||
whileInView={{ opacity: 1, scale: 1 }}
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
viewport={{ once: true }}
|
viewport={{ once: true, margin: "-20px" }}
|
||||||
transition={{ delay: index * 0.06, duration: 0.4 }}
|
transition={{ delay: index * 0.05, duration: 0.4 }}
|
||||||
className="group relative aspect-[4/3] rounded-3xl overflow-hidden border border-border/60 bg-background/40"
|
className="group relative aspect-[4/3] rounded-xl overflow-hidden border border-border bg-card shadow-sm transform-gpu"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={image}
|
src={image}
|
||||||
alt={`${project.title} visual display ${index + 1}`}
|
alt={`Platform runtime verification graphic state capture ${index + 1}`}
|
||||||
className="w-full h-full object-cover object-center transition-all duration-500 group-hover:scale-105"
|
className="w-full h-full object-cover object-center transition-transform duration-500 group-hover:scale-[1.02]"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
<div className="absolute inset-0 bg-gradient-to-t from-background/50 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+199
-243
@@ -1,315 +1,271 @@
|
|||||||
import PageTransition from "@/components/home/PageTransition";
|
import PageTransition from "@/components/home/PageTransition";
|
||||||
|
import { ProjectCard } from "@/components/home/ProjectCard";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { useProjects } from "@/hooks/queires/useProjects";
|
import { useProjects } from "@/hooks/queires/useProjects";
|
||||||
import { AnimatePresence, motion } from "framer-motion";
|
import { AnimatePresence, motion, useReducedMotion } from "framer-motion";
|
||||||
import {
|
import {
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
Brain,
|
Brain,
|
||||||
Cloud,
|
Cloud,
|
||||||
ExternalLink,
|
|
||||||
Github,
|
|
||||||
Globe,
|
Globe,
|
||||||
Loader2,
|
Layers,
|
||||||
Search,
|
Search,
|
||||||
Smartphone,
|
Smartphone,
|
||||||
|
Sparkles,
|
||||||
Workflow,
|
Workflow,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useMemo, useState } from "react";
|
import { useDeferredValue, useMemo, useState } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
const categories = [
|
const CATEGORIES = [
|
||||||
{ id: "all", name: "All Projects", icon: null },
|
{ id: "all", name: "All Work", icon: Layers },
|
||||||
{ id: "web", name: "Web", icon: Globe },
|
{ id: "web", name: "Web Systems", icon: Globe },
|
||||||
{ id: "mobile", name: "Mobile", icon: Smartphone },
|
{ id: "mobile", name: "Mobile", icon: Smartphone },
|
||||||
{ id: "ai", name: "AI", icon: Brain },
|
{ id: "ai", name: "AI & ML", icon: Brain },
|
||||||
{ id: "cloud", name: "Cloud", icon: Cloud },
|
{ id: "cloud", name: "Cloud Platforms", icon: Cloud },
|
||||||
{ id: "devops", name: "DEVOPS", icon: Workflow },
|
{ id: "devops", name: "DevOps", icon: Workflow },
|
||||||
];
|
];
|
||||||
|
|
||||||
const containerVariants = {
|
const containerVariants = {
|
||||||
hidden: { opacity: 0 },
|
hidden: { opacity: 0 },
|
||||||
visible: {
|
visible: {
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
transition: {
|
transition: { staggerChildren: 0.04 },
|
||||||
staggerChildren: 0.08,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Projects() {
|
export default function Projects() {
|
||||||
const { data: projectsData, isLoading, isError } = useProjects();
|
const { data: projectsData, isLoading, isError } = useProjects();
|
||||||
|
const shouldReduceMotion = useReducedMotion();
|
||||||
|
|
||||||
const [activeCategory, setActiveCategory] = useState("all");
|
const [activeCategory, setActiveCategory] = useState("all");
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
|
||||||
|
const deferredSearchQuery = useDeferredValue(searchQuery);
|
||||||
|
|
||||||
const filteredProjects = useMemo(() => {
|
const filteredProjects = useMemo(() => {
|
||||||
const projects = projectsData?.data?.data?.result || [];
|
const projects = projectsData?.data?.data || projectsData?.data || [];
|
||||||
if (!projects) return [];
|
if (!Array.isArray(projects)) return [];
|
||||||
|
|
||||||
return projects.filter((project) => {
|
return projects.filter((project) => {
|
||||||
const matchesCategory =
|
const matchesCategory =
|
||||||
activeCategory === "all" || project.category === activeCategory;
|
activeCategory === "all" ||
|
||||||
|
(Array.isArray(project.category)
|
||||||
|
? project.category.some(
|
||||||
|
(cat: string) =>
|
||||||
|
cat.toLowerCase() === activeCategory.toLowerCase() ||
|
||||||
|
cat.toLowerCase().includes(activeCategory.toLowerCase()),
|
||||||
|
)
|
||||||
|
: project.category?.toLowerCase() === activeCategory.toLowerCase());
|
||||||
|
|
||||||
|
const normalizedSearch = deferredSearchQuery.trim().toLowerCase();
|
||||||
const matchesSearch =
|
const matchesSearch =
|
||||||
searchQuery.trim() === "" ||
|
normalizedSearch === "" ||
|
||||||
project.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
project.title?.toLowerCase().includes(normalizedSearch) ||
|
||||||
project.description.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
project.description?.toLowerCase().includes(normalizedSearch) ||
|
||||||
project.technologies.some((tech: string) =>
|
(Array.isArray(project.technologies) &&
|
||||||
tech.toLowerCase().includes(searchQuery.toLowerCase()),
|
project.technologies.some((tech: string) =>
|
||||||
);
|
tech.toLowerCase().includes(normalizedSearch),
|
||||||
|
));
|
||||||
|
|
||||||
return matchesCategory && matchesSearch;
|
return matchesCategory && matchesSearch;
|
||||||
});
|
});
|
||||||
}, [projectsData, activeCategory, searchQuery]);
|
}, [projectsData, activeCategory, deferredSearchQuery]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageTransition>
|
<PageTransition>
|
||||||
<div className="min-h-screen bg-background overflow-x-hidden text-foreground antialiased">
|
<div className="min-h-screen bg-background text-foreground antialiased selection:bg-primary/20">
|
||||||
{/* Hero Section */}
|
<section className="pt-32 pb-12 relative overflow-hidden border-b border-border/40">
|
||||||
<section className="pt-32 pb-16 relative overflow-hidden border-b border-primary/10">
|
<div className="container mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
||||||
<div className="absolute inset-0 pointer-events-none select-none">
|
|
||||||
<div className="absolute top-1/4 left-1/4 w-96 h-96 bg-primary/10 rounded-full blur-3xl" />
|
{/* Back Arrow Wrapper - Kept clean on left side */}
|
||||||
<div className="absolute bottom-0 right-1/4 w-64 h-64 bg-accent/20 rounded-full blur-3xl" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="container mx-auto px-4 relative z-10">
|
|
||||||
{/* Back Button */}
|
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, x: -20 }}
|
initial={{ opacity: 0, x: shouldReduceMotion ? 0 : -10 }}
|
||||||
animate={{ opacity: 1, x: 0 }}
|
animate={{ opacity: 1, x: 0 }}
|
||||||
transition={{ duration: 0.4 }}
|
transition={{ duration: 0.3 }}
|
||||||
|
className="w-full flex justify-start"
|
||||||
>
|
>
|
||||||
<Link to="/">
|
<Button
|
||||||
<Button variant="ghost" className="mb-8 group h-10 px-4">
|
asChild
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="mb-6 group text-muted-foreground hover:text-foreground"
|
||||||
|
>
|
||||||
|
<Link to="/">
|
||||||
<ArrowLeft className="w-4 h-4 mr-2 transition-transform group-hover:-translate-x-1" />
|
<ArrowLeft className="w-4 h-4 mr-2 transition-transform group-hover:-translate-x-1" />
|
||||||
Back to Home
|
Back to Home
|
||||||
</Button>
|
</Link>
|
||||||
</Link>
|
</Button>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* Header */}
|
{/* Core Header Content Module - Flex-centered alignment */}
|
||||||
<motion.div
|
<div className="w-full flex flex-col items-center justify-center text-center">
|
||||||
initial={{ opacity: 0, y: 15 }}
|
<div className="max-w-3xl mb-12">
|
||||||
animate={{ opacity: 1, y: 0 }}
|
<motion.h1
|
||||||
transition={{ duration: 0.5 }}
|
initial={{ opacity: 0, y: shouldReduceMotion ? 0 : 15 }}
|
||||||
className="text-center mb-10 max-w-3xl mx-auto"
|
animate={{ opacity: 1, y: 0 }}
|
||||||
>
|
className="text-4xl sm:text-5xl lg:text-6xl font-black tracking-tight leading-none mb-4"
|
||||||
<h1 className="text-4xl md:text-6xl font-extrabold tracking-tight mb-4 bg-gradient-to-r from-primary via-primary/80 to-accent bg-clip-text text-transparent">
|
>
|
||||||
Our Projects
|
All Projects
|
||||||
</h1>
|
</motion.h1>
|
||||||
<p className="text-muted-foreground text-lg md:text-xl leading-relaxed max-w-2xl mx-auto">
|
<motion.p
|
||||||
Explore our portfolio of innovative solutions designed to scale
|
initial={{ opacity: 0, y: shouldReduceMotion ? 0 : 15 }}
|
||||||
and transform businesses across industries.
|
animate={{ opacity: 1, y: 0 }}
|
||||||
</p>
|
transition={{ delay: 0.05 }}
|
||||||
</motion.div>
|
className="text-base sm:text-lg text-muted-foreground leading-relaxed max-w-2xl mx-auto"
|
||||||
|
>
|
||||||
{/* Interactive Search and Filter Section */}
|
A high-fidelity showroom tracking production ecosystems, AI
|
||||||
<motion.div
|
operations, and cloud architectures compiled by Techzaa.
|
||||||
initial={{ opacity: 0, y: 15 }}
|
</motion.p>
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.5, delay: 0.1 }}
|
|
||||||
className="flex flex-col items-center gap-6 mb-12"
|
|
||||||
>
|
|
||||||
{/* Search Bar */}
|
|
||||||
<div className="relative w-full max-w-md">
|
|
||||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-muted-foreground" />
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Search projects, tech, or keywords..."
|
|
||||||
value={searchQuery}
|
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
|
||||||
className="w-full pl-10 pr-4 py-2.5 rounded-full border border-border/50 bg-background/50 backdrop-blur-sm focus:outline-none focus:ring-2 focus:ring-primary/40 transition-all text-sm shadow-sm"
|
|
||||||
aria-label="Search projects"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Filter Tabs */}
|
{/* Interface Control Bar Matrix - Fully Centered Substructures */}
|
||||||
<div className="flex flex-wrap justify-center gap-2 max-w-4xl">
|
<motion.div
|
||||||
{categories.map((category) => {
|
initial={{ opacity: 0, y: shouldReduceMotion ? 0 : 15 }}
|
||||||
const Icon = category.icon;
|
animate={{ opacity: 1, y: 0 }}
|
||||||
const isActive = activeCategory === category.id;
|
transition={{ delay: 0.1 }}
|
||||||
|
className="space-y-6 w-full pt-6 border-t border-border/40 flex flex-col items-center justify-center"
|
||||||
|
>
|
||||||
|
{/* Search input container centralized down the layout grid */}
|
||||||
|
<div className="relative max-w-md w-full group mx-auto">
|
||||||
|
<Search className="absolute left-3.5 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground group-focus-within:text-primary transition-colors" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Query by parameter, core feature, or framework stack..."
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
className="w-full pl-10 pr-4 py-2 text-sm rounded-xl border border-border bg-card/60 placeholder:text-muted-foreground/60 focus:outline-none focus:ring-2 focus:ring-primary/30 transition-all shadow-sm"
|
||||||
|
aria-label="Search deployment records"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Categories Tab Matrix - Wrapped with center axis parameters */}
|
||||||
|
<div
|
||||||
|
className="flex flex-wrap items-center justify-center gap-2 max-w-2xl w-full"
|
||||||
|
role="tablist"
|
||||||
|
aria-label="Project Filtering Options"
|
||||||
|
>
|
||||||
|
{CATEGORIES.map((cat) => {
|
||||||
|
const Icon = cat.icon;
|
||||||
|
const isActive = activeCategory === cat.id;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
key={cat.id}
|
||||||
|
variant={isActive ? "default" : "outline"}
|
||||||
|
onClick={() => setActiveCategory(cat.id)}
|
||||||
|
role="tab"
|
||||||
|
aria-selected={isActive}
|
||||||
|
className="rounded-xl text-xs font-semibold tracking-wide h-9 px-4 transition-all duration-200"
|
||||||
|
>
|
||||||
|
<Icon className="w-3.5 h-3.5 mr-2 opacity-80" />
|
||||||
|
{cat.name}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
key={category.id}
|
|
||||||
variant={isActive ? "default" : "outline"}
|
|
||||||
onClick={() => setActiveCategory(category.id)}
|
|
||||||
aria-pressed={isActive}
|
|
||||||
className={`rounded-full px-5 py-2 text-sm font-medium transition-all duration-300 ${
|
|
||||||
isActive
|
|
||||||
? "bg-primary text-primary-foreground shadow-sm shadow-primary/20"
|
|
||||||
: "border-border/60 hover:bg-muted/50"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{Icon && <Icon className="w-4 h-4 mr-2" />}
|
|
||||||
{category.name}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Projects Grid */}
|
{/* Projects View Output Section */}
|
||||||
<section className="py-20 bg-background/50">
|
<section className="py-16 bg-background">
|
||||||
<div className="container mx-auto px-4">
|
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
{/* Loading / Error State */}
|
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<div className="flex flex-col items-center justify-center py-24 gap-4 text-muted-foreground animate-pulse">
|
<div
|
||||||
<Loader2 className="w-8 h-8 animate-spin text-primary" />
|
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8"
|
||||||
<span className="text-sm font-medium">Loading projects...</span>
|
role="status"
|
||||||
|
aria-label="Loading project payload"
|
||||||
|
>
|
||||||
|
{[...Array(6)].map((_, idx) => (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
className="bg-card border border-border/40 rounded-2xl p-4 space-y-6 animate-pulse"
|
||||||
|
>
|
||||||
|
<div className="aspect-[16/10] bg-muted rounded-xl w-full" />
|
||||||
|
<div className="space-y-3 px-2">
|
||||||
|
<div className="h-4 bg-muted rounded-md w-1/3" />
|
||||||
|
<div className="h-6 bg-muted rounded-md w-3/4" />
|
||||||
|
<div className="h-4 bg-muted rounded-md w-full" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isError && (
|
{isError && (
|
||||||
<div className="text-center py-20 text-destructive">
|
<div className="text-center py-16 border border-destructive/20 bg-destructive/5 rounded-2xl max-w-lg mx-auto p-6 space-y-3">
|
||||||
<p className="text-lg font-semibold mb-2">
|
<p className="text-base font-bold text-foreground">
|
||||||
Failed to load projects.
|
API Synchronization Failure
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-xs text-muted-foreground leading-normal">
|
||||||
Please check your network connection or try again later.
|
Could not safely sync target records from Techzaa's data layer
|
||||||
|
cluster. Confirm connection status parameters.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!isLoading && !isError && (
|
{!isLoading && !isError && (
|
||||||
<AnimatePresence mode="wait">
|
<AnimatePresence mode="popLayout">
|
||||||
<motion.div
|
{filteredProjects.length > 0 ? (
|
||||||
key={`${activeCategory}-${searchQuery}`}
|
<motion.div
|
||||||
variants={containerVariants}
|
key={`${activeCategory}-${deferredSearchQuery}`}
|
||||||
initial="hidden"
|
variants={containerVariants}
|
||||||
animate="visible"
|
initial="hidden"
|
||||||
exit="hidden"
|
animate="visible"
|
||||||
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8"
|
exit="hidden"
|
||||||
>
|
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 items-stretch"
|
||||||
{filteredProjects.map((project) => (
|
>
|
||||||
<motion.article
|
{filteredProjects.map((project, idx) => (
|
||||||
key={project._id}
|
<motion.div
|
||||||
layout
|
layout={!shouldReduceMotion}
|
||||||
className="group flex flex-col justify-between bg-card/40 border border-border/50 backdrop-blur-sm rounded-3xl overflow-hidden hover:border-primary/50 transition-all duration-500 hover:shadow-xl hover:shadow-accent/5 p-4"
|
key={project._id}
|
||||||
>
|
className="h-full"
|
||||||
<div>
|
>
|
||||||
{/* Project Image */}
|
<ProjectCard
|
||||||
<div className="relative h-56 rounded-2xl overflow-hidden mb-6">
|
project={{
|
||||||
<img
|
_id: project._id,
|
||||||
src={project.image}
|
title: project.title,
|
||||||
alt={project.title}
|
category: Array.isArray(project.category)
|
||||||
loading="lazy"
|
? project.category
|
||||||
className="w-full h-full object-cover transition-transform duration-700 ease-out group-hover:scale-105"
|
: [project.category],
|
||||||
/>
|
isFeatured: project.isFeatured || false,
|
||||||
<div className="absolute inset-0 bg-gradient-to-t from-background/90 via-background/20 to-transparent opacity-80" />
|
technologies: project.technologies || [],
|
||||||
|
publishedYear:
|
||||||
{/* Category Badge */}
|
project.publishedYear || project.year || "2026",
|
||||||
<div className="absolute top-4 left-4">
|
previewUrl:
|
||||||
<span className="px-3 py-1 rounded-full bg-primary/20 text-primary text-xs font-semibold backdrop-blur-md border border-primary/20">
|
project.previewUrl || project.liveUrl || "#",
|
||||||
{
|
image: project.image,
|
||||||
categories.find(
|
}}
|
||||||
(c) => c.id === project.category,
|
index={idx}
|
||||||
)?.name
|
isInView={true}
|
||||||
}
|
/>
|
||||||
</span>
|
</motion.div>
|
||||||
</div>
|
))}
|
||||||
|
</motion.div>
|
||||||
{/* Quick External Links */}
|
) : (
|
||||||
<div className="absolute top-4 right-4 flex gap-2 opacity-0 group-hover:opacity-100 transition-opacity duration-300">
|
<motion.div
|
||||||
{project.liveUrl && (
|
initial={{ opacity: 0 }}
|
||||||
<a
|
animate={{ opacity: 1 }}
|
||||||
href={project.liveUrl}
|
className="text-center py-24 border border-dashed border-border rounded-2xl max-w-md mx-auto p-8 space-y-2"
|
||||||
target="_blank"
|
>
|
||||||
rel="noopener noreferrer"
|
<Sparkles className="w-5 h-5 text-muted-foreground/60 mx-auto" />
|
||||||
className="p-2 rounded-full bg-background/80 backdrop-blur-sm border border-border/40 hover:bg-primary/20 transition-colors"
|
<h3 className="text-sm font-bold text-foreground">
|
||||||
aria-label={`View live project: ${project.title}`}
|
No Projects Located
|
||||||
>
|
</h3>
|
||||||
<ExternalLink className="w-4 h-4" />
|
<p className="text-xs text-muted-foreground leading-relaxed">
|
||||||
</a>
|
Your lookup parameter variations produced zero operational
|
||||||
)}
|
matches in our architecture records.
|
||||||
{project.githubUrl && (
|
</p>
|
||||||
<a
|
</motion.div>
|
||||||
href={project.githubUrl}
|
)}
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="p-2 rounded-full bg-background/80 backdrop-blur-sm border border-border/40 hover:bg-primary/20 transition-colors"
|
|
||||||
aria-label={`View GitHub repository for: ${project.title}`}
|
|
||||||
>
|
|
||||||
<Github className="w-4 h-4" />
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Title & Description */}
|
|
||||||
<div className="px-2">
|
|
||||||
<div className="flex items-center gap-3 text-xs font-medium text-muted-foreground mb-3">
|
|
||||||
<span>{project.client}</span>
|
|
||||||
<span className="w-1.5 h-1.5 rounded-full bg-muted-foreground/40" />
|
|
||||||
<span>{project.year}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3 className="text-xl font-bold tracking-tight mb-3 group-hover:text-primary transition-colors line-clamp-1">
|
|
||||||
<Link to={`/projects/${project._id}`}>
|
|
||||||
{project.title}
|
|
||||||
</Link>
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<p className="text-muted-foreground text-sm leading-relaxed mb-6 line-clamp-3">
|
|
||||||
{project.description}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Technologies and Detail Navigation */}
|
|
||||||
<div className="px-2">
|
|
||||||
<div className="flex flex-wrap gap-1.5 max-h-16 overflow-hidden mb-4">
|
|
||||||
{project.technologies
|
|
||||||
.slice(0, 5)
|
|
||||||
.map((tech: string) => (
|
|
||||||
<span
|
|
||||||
key={tech}
|
|
||||||
className="px-2.5 py-0.5 rounded-md bg-primary text-primary-foreground text-xs font-semibold tracking-wide"
|
|
||||||
>
|
|
||||||
{tech}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
{project.technologies.length > 5 && (
|
|
||||||
<span className="px-2.5 py-0.5 rounded-md bg-secondary text-secondary-foreground text-xs font-semibold tracking-wide">
|
|
||||||
+{project.technologies.length - 5}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-end pt-2 border-t border-border/30">
|
|
||||||
<Link to={`/projects/${project._id}`}>
|
|
||||||
<Button
|
|
||||||
variant="link"
|
|
||||||
size="sm"
|
|
||||||
className="text-xs text-primary p-0 h-auto font-semibold group/btn"
|
|
||||||
>
|
|
||||||
View Details
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</motion.article>
|
|
||||||
))}
|
|
||||||
</motion.div>
|
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Empty State */}
|
|
||||||
{!isLoading && !isError && filteredProjects.length === 0 && (
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 10 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
className="text-center py-20"
|
|
||||||
>
|
|
||||||
<p className="text-muted-foreground text-lg">
|
|
||||||
No projects match your current search or category.
|
|
||||||
</p>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</PageTransition>
|
</PageTransition>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user