merge to rimi
This commit is contained in:
+1
-1
@@ -23,7 +23,7 @@ function AnimatedRoutes() {
|
||||
<Routes location={location} key={location.pathname}>
|
||||
<Route path="/" element={<Index />} />
|
||||
<Route path="/projects" element={<Projects />} />
|
||||
<Route path="/projects/:slug" element={<ProjectDetails />} />
|
||||
<Route path="/projects/:id" element={<ProjectDetails />} />
|
||||
{/* <Route path="/blog" element={<Blog />} />
|
||||
<Route path="/blog/:id" element={<BlogArticle />} /> */}
|
||||
{/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
|
||||
|
||||
+106
-93
@@ -4,135 +4,148 @@ import {
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion";
|
||||
import { motion, useInView } from "framer-motion";
|
||||
import { ArrowRight, Mail } from "lucide-react";
|
||||
import { useRef } from "react";
|
||||
import { motion, useInView, useScroll, useSpring } from "framer-motion";
|
||||
import { ArrowUpRight, ShieldQuestion, Sparkles } from "lucide-react";
|
||||
import { useRef, useState } from "react";
|
||||
|
||||
const faqs = [
|
||||
{
|
||||
question: "What services does TechZaa offer?",
|
||||
answer:
|
||||
"We provide comprehensive technology solutions including web development, mobile app development, AI/ML solutions, and cloud infrastructure services. Our team specializes in creating custom software tailored to your business needs.",
|
||||
"We provide comprehensive technology solutions including web development, mobile app development, AI/ML solutions, and cloud infrastructure services.",
|
||||
category: "Services",
|
||||
},
|
||||
{
|
||||
question: "How long does a typical project take?",
|
||||
answer:
|
||||
"Project timelines vary based on complexity and scope. A simple website might take 4-6 weeks, while complex enterprise applications can take 3-6 months. We provide detailed timelines during our initial consultation and keep you updated throughout the development process.",
|
||||
"Timelines vary: simple sites take 4-6 weeks, while enterprise apps take 3-6 months. We prioritize agile delivery and constant feedback.",
|
||||
category: "Process",
|
||||
},
|
||||
{
|
||||
question: "What is your development process?",
|
||||
answer:
|
||||
"We follow an agile methodology with iterative development cycles. Our process includes: Discovery & Planning, UI/UX Design, Development Sprints, Quality Assurance, Deployment, and Ongoing Support. We maintain transparent communication with regular updates and demos.",
|
||||
"We follow an agile methodology: Discovery, UI/UX Design, Development Sprints, QA, Deployment, and 24/7 Ongoing Support.",
|
||||
category: "Workflow",
|
||||
},
|
||||
{
|
||||
question: "Do you provide ongoing maintenance and support?",
|
||||
question: "Do you provide maintenance?",
|
||||
answer:
|
||||
"Yes! We offer comprehensive maintenance packages that include bug fixes, security updates, performance optimization, and feature enhancements. Our support team is available to ensure your application runs smoothly 24/7.",
|
||||
"Yes. Our maintenance packages include security updates, performance optimization, and feature enhancements to keep you ahead.",
|
||||
category: "Support",
|
||||
},
|
||||
{
|
||||
question: "How do you handle project pricing?",
|
||||
answer:
|
||||
"We offer flexible pricing models including fixed-price projects, time & materials, and dedicated team arrangements. After understanding your requirements, we provide transparent quotes with no hidden costs. Contact us for a free consultation and estimate.",
|
||||
},
|
||||
{
|
||||
question: "What industries do you serve?",
|
||||
answer:
|
||||
"We've delivered successful projects across fintech, healthcare, e-commerce, education, real estate, and SaaS industries. Our diverse experience allows us to bring cross-industry insights and best practices to every project.",
|
||||
"Flexible models: fixed-price, time & materials, or dedicated teams. We ensure 100% transparency with no hidden costs.",
|
||||
category: "Billing",
|
||||
},
|
||||
];
|
||||
|
||||
export default function FAQSection() {
|
||||
const ref = useRef<HTMLElement>(null);
|
||||
const isInView = useInView(ref, { once: true, margin: "-100px" });
|
||||
const [activeItem, setActiveItem] = useState<string | undefined>(undefined);
|
||||
const containerRef = useRef(null);
|
||||
const isInView = useInView(containerRef, { once: true, amount: 0.2 });
|
||||
|
||||
// Progress bar for reading engagement
|
||||
const { scrollYProgress } = useScroll({
|
||||
target: containerRef,
|
||||
offset: ["start end", "end start"],
|
||||
});
|
||||
const scaleX = useSpring(scrollYProgress, { stiffness: 100, damping: 30 });
|
||||
|
||||
return (
|
||||
<section
|
||||
id="faq"
|
||||
ref={ref}
|
||||
className="pb-20 relative overflow-hidden bg-background"
|
||||
ref={containerRef}
|
||||
className="pb-20 overflow-hidden transition-colors duration-500"
|
||||
>
|
||||
{/* Decorative gradients */}
|
||||
<div className="absolute top-0 right-0 w-[500px] h-[500px] bg-primary/5 rounded-full blur-3xl -z-10" />
|
||||
<div className="absolute bottom-0 left-0 w-[500px] h-[500px] bg-secondary/5 rounded-full blur-3xl -z-10" />
|
||||
|
||||
<div className="container mx-auto px-4 relative z-10">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-[1fr,1.5fr] gap-12 lg:gap-20 items-start">
|
||||
{/* Left Column - Header and CTA */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -30 }}
|
||||
animate={isInView ? { opacity: 1, x: 0 } : {}}
|
||||
transition={{ duration: 0.6 }}
|
||||
className="flex flex-col gap-8 lg:sticky lg:top-24"
|
||||
>
|
||||
<div>
|
||||
<span className="inline-flex px-4 py-1.5 rounded-full text-xs font-semibold tracking-wider uppercase bg-primary/10 text-primary mb-4">
|
||||
<div className="mx-auto px-6 max-w-6xl">
|
||||
<div className="flex flex-col gap-10">
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="flex items-center gap-2 mb-6">
|
||||
<div className="p-2 bg-primary rounded-lg text-primary-foreground">
|
||||
<ShieldQuestion size={20} />
|
||||
</div>
|
||||
<span className="text-sm font-bold uppercase text-primary/80">
|
||||
Got Questions?
|
||||
</span>
|
||||
<h2 className="text-4xl md:text-5xl font-bold tracking-tight text-foreground mb-4 leading-[1.15]">
|
||||
Frequently Asked <br />
|
||||
<span className="text-primary">Questions</span>
|
||||
</h2>
|
||||
<p className="text-muted-foreground text-lg leading-relaxed max-w-md">
|
||||
Everything you need to know about working with TechZaa. Can't
|
||||
find what you're looking for? Reach out to our team.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Support CTA Card */}
|
||||
<div className="p-6 rounded-2xl border border-border/50 bg-card/40 backdrop-blur-sm flex flex-col gap-5 max-w-md shadow-sm">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center text-primary">
|
||||
<Mail className="w-5 h-5" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-semibold text-foreground text-sm">
|
||||
Still have questions?
|
||||
</h4>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
We're here to help you out with any queries.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<a
|
||||
href="mailto:support@techzaa.com"
|
||||
className="inline-flex items-center justify-center gap-2 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground shadow hover:bg-primary/90 h-10 px-4 py-2 rounded-xl w-full group"
|
||||
>
|
||||
Contact Support
|
||||
<ArrowRight className="w-4 h-4 transition-transform group-hover:translate-x-1" />
|
||||
</a>
|
||||
</div>
|
||||
</motion.div>
|
||||
<h2 className="text-4xl md:text-5xl font-bold">
|
||||
Clear Answers {" "}
|
||||
<span className="text-transparent bg-clip-text bg-gradient-to-r from-primary to-primary-foreground">
|
||||
For Big Ideas.
|
||||
</span>
|
||||
</h2>
|
||||
|
||||
{/* Right Column - Accordion */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={isInView ? { opacity: 1, y: 0 } : {}}
|
||||
transition={{ duration: 0.6, delay: 0.2 }}
|
||||
className="w-full"
|
||||
<p className="text-muted-foreground max-w-lg text-center mt-5">
|
||||
Everything you need to know about working with TechZaa. Can't find
|
||||
what you're looking for? Reach out to our team.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Bottom Section: Interactive Accordion */}
|
||||
<Accordion
|
||||
type="single"
|
||||
collapsible
|
||||
onValueChange={setActiveItem}
|
||||
className="w-full space-y-4"
|
||||
>
|
||||
<Accordion type="single" collapsible className="space-y-4">
|
||||
{faqs.map((faq, index) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={isInView ? { opacity: 1, y: 0 } : {}}
|
||||
transition={{ duration: 0.4, delay: 0.06 * index }}
|
||||
{faqs.map((faq, index) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={isInView ? { opacity: 1, y: 0 } : {}}
|
||||
transition={{ delay: index * 0.1, duration: 0.5 }}
|
||||
>
|
||||
<AccordionItem
|
||||
value={`item-${index}`}
|
||||
className={`
|
||||
group border-none rounded-3xl transition-all duration-500 px-2
|
||||
${
|
||||
activeItem === `item-${index}`
|
||||
? "bg-primary/20 shadow-[0_20px_50px_rgba(0,0,0,0.05)] scale-[1.01]"
|
||||
: "bg-accent"
|
||||
}
|
||||
`}
|
||||
>
|
||||
<AccordionItem
|
||||
value={`item-${index}`}
|
||||
className="bg-card/40 border border-border/40 rounded-2xl px-6 py-1 overflow-hidden data-[state=open]:border-primary/50 data-[state=open]:shadow-md transition-all duration-300"
|
||||
>
|
||||
<AccordionTrigger className="text-left hover:no-underline py-5 text-base md:text-lg font-medium text-foreground hover:text-primary transition-colors">
|
||||
{faq.question}
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="text-muted-foreground pt-1 pb-5 text-sm md:text-base leading-relaxed">
|
||||
<AccordionTrigger className="flex items-center justify-between p-6 hover:no-underline gap-4">
|
||||
<div className="flex items-center gap-6 text-left">
|
||||
<span
|
||||
className={`
|
||||
hidden sm:flex items-center justify-center w-10 h-10 rounded-full border text-xs font-bold transition-all duration-500
|
||||
${
|
||||
activeItem === `item-${index}`
|
||||
? "bg-primary border-primary text-primary-foreground rotate-[360deg]"
|
||||
: "border-border text-muted-foreground"
|
||||
}
|
||||
`}
|
||||
>
|
||||
{activeItem === `item-${index}` ? (
|
||||
<Sparkles size={14} />
|
||||
) : (
|
||||
`0${index + 1}`
|
||||
)}
|
||||
</span>
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-[10px] uppercase tracking-widest font-bold text-primary opacity-0 group-data-[state=open]:opacity-100 transition-opacity">
|
||||
{faq.category}
|
||||
</span>
|
||||
<span className="text-lg md:text-xl font-semibold tracking-tight">
|
||||
{faq.question}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-6 pb-8 md:pl-[104px]">
|
||||
<div className="text-muted-foreground text-base md:text-lg leading-relaxed max-w-2xl border-l-2 border-primary/20 pl-6">
|
||||
{faq.answer}
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</motion.div>
|
||||
))}
|
||||
</Accordion>
|
||||
</motion.div>
|
||||
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</motion.div>
|
||||
))}
|
||||
</Accordion>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import { motion } from "framer-motion";
|
||||
import { ExternalLink } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
export function ProjectCard({ project, color, index, isInView }) {
|
||||
const [mousePosition, setMousePosition] = useState({ x: 50, y: 50 });
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
const rect = e.currentTarget.getBoundingClientRect();
|
||||
const x = ((e.clientX - rect.left) / rect.width) * 100;
|
||||
const y = ((e.clientY - rect.top) / rect.height) * 100;
|
||||
setMousePosition({ x, y });
|
||||
};
|
||||
|
||||
return (
|
||||
<Link to={`/projects/${project._id}`}>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 60, rotateX: 15 }}
|
||||
animate={isInView ? { opacity: 1, y: 0, rotateX: 0 } : {}}
|
||||
transition={{ duration: 0.6, delay: index * 0.08 }}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
className="group relative rounded-3xl overflow-hidden cursor-pointer h-full"
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => {
|
||||
setIsHovered(false);
|
||||
setMousePosition({ x: 50, y: 50 });
|
||||
}}
|
||||
>
|
||||
{/* Main Card Container with 3D Tilt */}
|
||||
<div
|
||||
className="relative h-full rounded-3xl overflow-hidden shadow-2xl transition-transform duration-300"
|
||||
style={{
|
||||
transform: isHovered
|
||||
? `perspective(1000px) rotateX(${(50 - mousePosition.y) * 0.12}deg) rotateY(${(mousePosition.x - 50) * 0.15}deg)`
|
||||
: "perspective(1000px) rotateX(0deg) rotateY(0deg)",
|
||||
transition: isHovered
|
||||
? "transform 0.1s ease-out"
|
||||
: "transform 0.4s ease-out",
|
||||
}}
|
||||
>
|
||||
{/* Image Container */}
|
||||
<div className="aspect-[16/13] overflow-hidden relative">
|
||||
<img
|
||||
src={project.image}
|
||||
alt={project.title}
|
||||
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 */}
|
||||
<div className="absolute inset-0 flex flex-col justify-end p-8">
|
||||
<div className="space-y-4 transform transition-all duration-500 group-hover:translate-y-0">
|
||||
{/* Category */}
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
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 */}
|
||||
<h3 className="text-2xl md:text-3xl font-bold text-white leading-tight tracking-tight">
|
||||
{project.title}
|
||||
</h3>
|
||||
|
||||
{/* View Project Link */}
|
||||
<Link
|
||||
to={project.liveUrl}
|
||||
className="flex items-center gap-3 text-white/80 group-hover:text-white transition-colors"
|
||||
>
|
||||
<span className="font-medium text-sm tracking-wider">live</span>
|
||||
<ExternalLink className="w-5 h-5 transition-transform group-hover:rotate-45" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Neon Border Glow */}
|
||||
<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>
|
||||
</motion.div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useProjects } from "@/hooks/queires/useProjects";
|
||||
import { motion, useInView } from "framer-motion";
|
||||
import { ArrowRight, ExternalLink } from "lucide-react";
|
||||
import { useRef, useState } from "react";
|
||||
import { ArrowRight } from "lucide-react";
|
||||
import { useRef } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { ProjectCard } from "./ProjectCard";
|
||||
|
||||
const gradientColors = [
|
||||
"from-neon-blue/90",
|
||||
@@ -98,99 +99,3 @@ export default function ProjectsSection() {
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function ProjectCard({ project, color, index, isInView }) {
|
||||
const [mousePosition, setMousePosition] = useState({ x: 50, y: 50 });
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
const rect = e.currentTarget.getBoundingClientRect();
|
||||
const x = ((e.clientX - rect.left) / rect.width) * 100;
|
||||
const y = ((e.clientY - rect.top) / rect.height) * 100;
|
||||
setMousePosition({ x, y });
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 60, rotateX: 15 }}
|
||||
animate={isInView ? { opacity: 1, y: 0, rotateX: 0 } : {}}
|
||||
transition={{ duration: 0.6, delay: index * 0.08 }}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
className="group relative rounded-3xl overflow-hidden cursor-pointer h-full"
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => {
|
||||
setIsHovered(false);
|
||||
setMousePosition({ x: 50, y: 50 });
|
||||
}}
|
||||
>
|
||||
{/* Main Card Container with 3D Tilt */}
|
||||
<div
|
||||
className="relative h-full rounded-3xl overflow-hidden shadow-2xl transition-transform duration-300"
|
||||
style={{
|
||||
transform: isHovered
|
||||
? `perspective(1000px) rotateX(${(50 - mousePosition.y) * 0.12}deg) rotateY(${(mousePosition.x - 50) * 0.15}deg)`
|
||||
: "perspective(1000px) rotateX(0deg) rotateY(0deg)",
|
||||
transition: isHovered
|
||||
? "transform 0.1s ease-out"
|
||||
: "transform 0.4s ease-out",
|
||||
}}
|
||||
>
|
||||
{/* Image Container */}
|
||||
<div className="aspect-[16/13] overflow-hidden relative">
|
||||
<img
|
||||
src={project.image}
|
||||
alt={project.title}
|
||||
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 */}
|
||||
<div className="absolute inset-0 flex flex-col justify-end p-8">
|
||||
<div className="space-y-4 transform transition-all duration-500 group-hover:translate-y-0">
|
||||
{/* Category */}
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
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 */}
|
||||
<h3 className="text-2xl md:text-3xl font-bold text-white leading-tight tracking-tight">
|
||||
{project.title}
|
||||
</h3>
|
||||
|
||||
{/* View Project Link */}
|
||||
<Link
|
||||
to={project.liveUrl}
|
||||
className="flex items-center gap-3 text-white/80 group-hover:text-white transition-colors"
|
||||
>
|
||||
<span className="font-medium text-sm tracking-wider">live</span>
|
||||
<ExternalLink className="w-5 h-5 transition-transform group-hover:rotate-45" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Neon Border Glow */}
|
||||
<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>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
+304
-276
@@ -1,6 +1,8 @@
|
||||
import Footer from "@/components/Footer";
|
||||
import Navbar from "@/components/Navbar";
|
||||
import PageTransition from "@/components/PageTransition";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { getProjectBySlug, getRelatedProjects } from "@/data/projectData";
|
||||
import { useProjectById } from "@/hooks/queires/useProjects";
|
||||
import { motion } from "framer-motion";
|
||||
import {
|
||||
Calendar,
|
||||
@@ -14,21 +16,77 @@ import ReactMarkdown from "react-markdown";
|
||||
import { Link, useNavigate, useParams } from "react-router-dom";
|
||||
import remarkGfm from "remark-gfm";
|
||||
|
||||
export default function ProjectDetails() {
|
||||
const { slug } = useParams<{ slug: string }>();
|
||||
const navigate = useNavigate();
|
||||
const project = getProjectBySlug(slug || "");
|
||||
interface Result {
|
||||
label: string;
|
||||
value: string | number;
|
||||
}
|
||||
|
||||
if (!project) {
|
||||
interface ProjectData {
|
||||
id: string;
|
||||
category: string;
|
||||
title: string;
|
||||
description: string;
|
||||
year: string | number;
|
||||
duration: string;
|
||||
team: string | number;
|
||||
client: string;
|
||||
liveUrl: string;
|
||||
codeUrl?: string;
|
||||
image: string;
|
||||
results?: Result[];
|
||||
fullDescription: string;
|
||||
features?: string[];
|
||||
technologies?: string[];
|
||||
challenges?: string[];
|
||||
gallery?: string[];
|
||||
}
|
||||
|
||||
const CATEGORY_LABELS: Record<string, string> = {
|
||||
web: "Web Development",
|
||||
mobile: "Mobile App",
|
||||
ai: "AI & Machine Learning",
|
||||
cloud: "Cloud Solutions",
|
||||
};
|
||||
|
||||
export default function ProjectDetails() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
const { data, isLoading, isError } = useProjectById(id || "");
|
||||
|
||||
const project: ProjectData | null =
|
||||
data?.data?.data || data?.data || data || null;
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<PageTransition>
|
||||
<div className="min-h-screen bg-background flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<h1 className="text-4xl font-bold mb-4">Project Not Found</h1>
|
||||
<p className="text-muted-foreground mb-8">
|
||||
The project you're looking for doesn't exist.
|
||||
<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" />
|
||||
<span className="text-sm font-medium text-muted-foreground animate-pulse">
|
||||
Loading project details...
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</PageTransition>
|
||||
);
|
||||
}
|
||||
|
||||
if (isError || !project) {
|
||||
return (
|
||||
<PageTransition>
|
||||
<div className="min-h-screen bg-background flex items-center justify-center p-4">
|
||||
<div className="text-center max-w-md border border-primary/20 bg-secondary/30 backdrop-blur-xl p-8 rounded-3xl shadow-2xl">
|
||||
<h1 className="text-3xl font-bold tracking-tight mb-3">
|
||||
Project Not Found
|
||||
</h1>
|
||||
<p className="text-muted-foreground mb-6">
|
||||
The project you're looking for does not exist or could not be
|
||||
loaded.
|
||||
</p>
|
||||
<Button onClick={() => navigate("/projects")}>
|
||||
<Button
|
||||
onClick={() => navigate("/projects")}
|
||||
className="rounded-full w-full bg-primary text-primary-foreground hover:opacity-90 transition-all shadow-lg"
|
||||
>
|
||||
Back to Projects
|
||||
</Button>
|
||||
</div>
|
||||
@@ -37,203 +95,229 @@ export default function ProjectDetails() {
|
||||
);
|
||||
}
|
||||
|
||||
const relatedProjects = getRelatedProjects(project);
|
||||
const categoryLabels: Record<string, string> = {
|
||||
web: "Web Development",
|
||||
mobile: "Mobile App",
|
||||
ai: "AI & Machine Learning",
|
||||
cloud: "Cloud Solutions",
|
||||
};
|
||||
|
||||
return (
|
||||
<PageTransition>
|
||||
<div className="min-h-screen bg-background overflow-x-hidden">
|
||||
{/* Hero */}
|
||||
<section className="pt-32 pb-16 relative overflow-hidden">
|
||||
<div className="absolute inset-0">
|
||||
<div className="absolute top-1/4 left-1/4 w-96 h-96 bg-primary/20 rounded-full blur-3xl" />
|
||||
<div className="absolute bottom-0 right-1/4 w-64 h-64 bg-neon-purple/20 rounded-full blur-3xl" />
|
||||
<div className="min-h-screen bg-background text-foreground overflow-x-hidden selection:bg-primary/30 selection:text-primary">
|
||||
<Navbar />
|
||||
|
||||
{/* Hero Section */}
|
||||
<section className="pt-36 pb-20 relative overflow-hidden">
|
||||
{/* Ambient Background Glows */}
|
||||
<div className="absolute inset-0 pointer-events-none z-0">
|
||||
<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]" />
|
||||
<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" />
|
||||
</div>
|
||||
|
||||
<div className="container mx-auto px-4 relative z-10">
|
||||
{/* Breadcrumb */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="flex items-center gap-2 text-sm text-muted-foreground mb-8"
|
||||
<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-4 h-4" />
|
||||
<ChevronRight className="w-3.5 h-3.5 opacity-50" />
|
||||
<Link
|
||||
to="/projects"
|
||||
className="hover:text-primary transition-colors"
|
||||
>
|
||||
Projects
|
||||
</Link>
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
<span className="text-foreground">{project.title}</span>
|
||||
</motion.div>
|
||||
<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 */}
|
||||
<div>
|
||||
{/* Content Block */}
|
||||
<div className="flex flex-col">
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
initial={{ opacity: 0, y: 15 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="inline-block px-4 py-1.5 rounded-full bg-primary/20 text-primary text-sm font-medium mb-6"
|
||||
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"
|
||||
>
|
||||
{categoryLabels[project.category]}
|
||||
{CATEGORY_LABELS[project.category] || project.category}
|
||||
</motion.span>
|
||||
|
||||
<motion.h1
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
initial={{ opacity: 0, y: 15 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.1 }}
|
||||
className="text-4xl md:text-5xl font-bold mb-6"
|
||||
transition={{ duration: 0.5, delay: 0.1 }}
|
||||
className="text-4xl md:text-5xl lg:text-6xl font-black tracking-tight leading-none mb-6"
|
||||
>
|
||||
{project.title}
|
||||
</motion.h1>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
initial={{ opacity: 0, y: 15 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.2 }}
|
||||
className="text-lg text-muted-foreground mb-8"
|
||||
transition={{ duration: 0.5, delay: 0.2 }}
|
||||
className="text-base md:text-lg text-muted-foreground leading-relaxed max-w-2xl mb-10"
|
||||
>
|
||||
{project.description}
|
||||
</motion.p>
|
||||
|
||||
{/* Meta */}
|
||||
{/* Project Metadata */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
initial={{ opacity: 0, y: 15 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.3 }}
|
||||
className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8"
|
||||
transition={{ duration: 0.5, delay: 0.3 }}
|
||||
className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-10"
|
||||
>
|
||||
<div className="glass rounded-xl p-4 text-center">
|
||||
<Calendar className="w-5 h-5 mx-auto mb-2 text-primary" />
|
||||
<p className="text-sm text-muted-foreground">Year</p>
|
||||
<p className="font-bold">{project.year}</p>
|
||||
</div>
|
||||
<div className="glass rounded-xl p-4 text-center">
|
||||
<Clock className="w-5 h-5 mx-auto mb-2 text-primary" />
|
||||
<p className="text-sm text-muted-foreground">Duration</p>
|
||||
<p className="font-bold">{project.duration}</p>
|
||||
</div>
|
||||
<div className="glass rounded-xl p-4 text-center">
|
||||
<Users className="w-5 h-5 mx-auto mb-2 text-primary" />
|
||||
<p className="text-sm text-muted-foreground">Team</p>
|
||||
<p className="font-bold">{project.team}</p>
|
||||
</div>
|
||||
<div className="glass rounded-xl p-4 text-center">
|
||||
<p className="text-sm text-muted-foreground mb-1">Client</p>
|
||||
<p className="font-bold text-sm">{project.client}</p>
|
||||
</div>
|
||||
{[
|
||||
{ label: "Year", value: project.year, icon: Calendar },
|
||||
{ label: "Duration", value: project.duration, icon: Clock },
|
||||
{ label: "Team Size", value: project.team, icon: Users },
|
||||
{ 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>
|
||||
|
||||
{/* Actions */}
|
||||
{/* Call to Actions */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
initial={{ opacity: 0, y: 15 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.4 }}
|
||||
className="flex gap-4"
|
||||
transition={{ duration: 0.5, delay: 0.4 }}
|
||||
className="flex flex-wrap gap-4"
|
||||
>
|
||||
<Button className="rounded-full neon-glow">
|
||||
<ExternalLink className="w-4 h-4 mr-2" />
|
||||
View Live
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="rounded-full glass border-primary/30"
|
||||
<a
|
||||
href={project.liveUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
<Github className="w-4 h-4 mr-2" />
|
||||
View Code
|
||||
</Button>
|
||||
<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">
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
View Live
|
||||
</Button>
|
||||
</a>
|
||||
{project.codeUrl && (
|
||||
<a
|
||||
href={project.codeUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
<Button
|
||||
variant="outline"
|
||||
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"
|
||||
>
|
||||
<Github className="w-4 h-4 text-muted-foreground" />
|
||||
View Code
|
||||
</Button>
|
||||
</a>
|
||||
)}
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Image */}
|
||||
{/* Project Cover Visual */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ duration: 0.6, delay: 0.3 }}
|
||||
className="rounded-3xl overflow-hidden neon-glow"
|
||||
transition={{ duration: 0.6, delay: 0.2 }}
|
||||
className="relative group rounded-3xl overflow-hidden border border-border/60 shadow-2xl bg-secondary/30 aspect-[4/3] lg:aspect-auto h-[450px]"
|
||||
>
|
||||
<img
|
||||
src={project.image}
|
||||
alt={project.title}
|
||||
className="w-full h-[400px] object-cover"
|
||||
className="w-full h-full object-cover object-center transition-transform duration-700 ease-out group-hover:scale-[1.02]"
|
||||
loading="lazy"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-background/80 via-transparent to-transparent opacity-80" />
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Results */}
|
||||
<section className="py-16 bg-secondary/30">
|
||||
<div className="container mx-auto px-4">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
className="text-center mb-12"
|
||||
>
|
||||
<h2 className="text-3xl font-bold">Key Results</h2>
|
||||
</motion.div>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-6 max-w-4xl mx-auto">
|
||||
{project.results.map((result, index) => (
|
||||
<motion.div
|
||||
key={result.label}
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ delay: index * 0.1 }}
|
||||
className="glass-strong rounded-2xl p-6 text-center"
|
||||
>
|
||||
<p className="text-3xl md:text-4xl font-bold text-primary neon-text-glow mb-2">
|
||||
{result.value}
|
||||
</p>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{result.label}
|
||||
</p>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Details */}
|
||||
<section className="py-24">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="grid lg:grid-cols-2 gap-16 max-w-6xl mx-auto">
|
||||
{/* Full Description */}
|
||||
{/* Key Results Section */}
|
||||
{project.results && project.results.length > 0 && (
|
||||
<section className="py-20 border-t border-b border-border/50 bg-secondary/20">
|
||||
<div className="container mx-auto px-4 md:px-6">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -30 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
initial={{ opacity: 0, y: 15 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
className="text-center max-w-2xl mx-auto mb-14"
|
||||
>
|
||||
<h3 className="text-2xl font-bold mb-6">About the Project</h3>
|
||||
<div className="prose-content">
|
||||
<h2 className="text-3xl font-bold tracking-tight mb-3">
|
||||
Key Performance Outcomes
|
||||
</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Measurable impact and business value generated by this
|
||||
project.
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-6 max-w-5xl mx-auto">
|
||||
{project.results.map((result, index) => (
|
||||
<motion.div
|
||||
key={result.label}
|
||||
initial={{ opacity: 0, y: 25 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ delay: index * 0.08, duration: 0.4 }}
|
||||
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"
|
||||
>
|
||||
<span className="text-3xl md:text-4xl font-black text-primary tracking-tight mb-2">
|
||||
{result.value}
|
||||
</span>
|
||||
<span className="text-xs font-medium text-center text-muted-foreground">
|
||||
{result.label}
|
||||
</span>
|
||||
</motion.div>
|
||||
))}
|
||||
</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-muted-foreground leading-relaxed mb-4">
|
||||
<p className="text-base md:text-lg text-muted-foreground leading-relaxed mb-6">
|
||||
{children}
|
||||
</p>
|
||||
),
|
||||
strong: ({ children }) => (
|
||||
<strong className="font-bold text-foreground">
|
||||
<strong className="font-semibold text-foreground">
|
||||
{children}
|
||||
</strong>
|
||||
),
|
||||
ul: ({ children }) => (
|
||||
<ul className="list-disc list-inside text-muted-foreground space-y-2 mb-4 ml-4">
|
||||
<ul className="list-disc list-inside space-y-2 mb-6 ml-2 text-muted-foreground">
|
||||
{children}
|
||||
</ul>
|
||||
),
|
||||
@@ -245,160 +329,104 @@ export default function ProjectDetails() {
|
||||
{project.fullDescription}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Features & Tech */}
|
||||
<div className="space-y-12">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: 30 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
viewport={{ once: true }}
|
||||
>
|
||||
<h3 className="text-2xl font-bold mb-6">Key Features</h3>
|
||||
<ul className="space-y-3">
|
||||
{project.features.map((feature, index) => (
|
||||
<li key={index} className="flex items-start gap-3">
|
||||
<span className="w-2 h-2 rounded-full bg-primary mt-2 flex-shrink-0" />
|
||||
<span className="text-muted-foreground">{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: 30 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ delay: 0.1 }}
|
||||
>
|
||||
<h3 className="text-2xl font-bold mb-6">Technologies Used</h3>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{project.technologies.map((tech) => (
|
||||
<span
|
||||
key={tech}
|
||||
className="px-4 py-2 rounded-full glass text-sm font-medium border border-primary/30"
|
||||
>
|
||||
{tech}
|
||||
</span>
|
||||
))}
|
||||
{/* Sidebar Capabilities */}
|
||||
<div className="lg:col-span-5 flex flex-col gap-12">
|
||||
{/* Features */}
|
||||
{project.features && project.features.length > 0 && (
|
||||
<div>
|
||||
<h3 className="text-xl font-bold tracking-tight mb-6">
|
||||
Key Capabilities
|
||||
</h3>
|
||||
<ul className="space-y-4">
|
||||
{project.features.map((feature, index) => (
|
||||
<li key={index} className="flex items-start gap-4">
|
||||
<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>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: 30 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ delay: 0.2 }}
|
||||
>
|
||||
<h3 className="text-2xl font-bold mb-6">Challenges Solved</h3>
|
||||
<ul className="space-y-3">
|
||||
{project.challenges.map((challenge, index) => (
|
||||
<li key={index} className="flex items-start gap-3">
|
||||
<span className="w-2 h-2 rounded-full bg-neon-purple mt-2 flex-shrink-0" />
|
||||
<span className="text-muted-foreground">
|
||||
{challenge}
|
||||
{/* Technologies Used */}
|
||||
{project.technologies && project.technologies.length > 0 && (
|
||||
<div className="border-t border-border/40 pt-8">
|
||||
<h3 className="text-xl font-bold tracking-tight mb-6">
|
||||
Technologies Used
|
||||
</h3>
|
||||
<div className="flex flex-wrap gap-2.5">
|
||||
{project.technologies.map((tech) => (
|
||||
<span
|
||||
key={tech}
|
||||
className="px-3.5 py-1.5 bg-secondary text-xs font-medium rounded-full border border-border/60 text-muted-foreground"
|
||||
>
|
||||
{tech}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Challenges Solved */}
|
||||
{project.challenges && project.challenges.length > 0 && (
|
||||
<div className="border-t border-border/40 pt-8">
|
||||
<h3 className="text-xl font-bold tracking-tight mb-6">
|
||||
Challenges Solved
|
||||
</h3>
|
||||
<ul className="space-y-4">
|
||||
{project.challenges.map((challenge, index) => (
|
||||
<li key={index} className="flex items-start gap-4">
|
||||
<span className="flex-shrink-0 w-1.5 h-1.5 rounded-full bg-purple-500 mt-2.5" />
|
||||
<span className="text-sm md:text-base text-muted-foreground leading-snug">
|
||||
{challenge}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Gallery */}
|
||||
<section className="py-16 bg-secondary/30">
|
||||
<div className="container mx-auto px-4">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
className="text-center mb-12"
|
||||
>
|
||||
<h2 className="text-3xl font-bold">Project Gallery</h2>
|
||||
</motion.div>
|
||||
{/* Gallery Section */}
|
||||
{project.gallery && project.gallery.length > 0 && (
|
||||
<section className="py-20 border-t border-border/50 bg-secondary/30">
|
||||
<div className="container mx-auto px-4 md:px-6">
|
||||
<div className="text-center max-w-2xl mx-auto mb-14">
|
||||
<h2 className="text-3xl font-bold tracking-tight mb-3">
|
||||
Project Gallery
|
||||
</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
A visual overview of the implementation and user interface.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-3 gap-6 max-w-5xl mx-auto">
|
||||
{project.gallery.map((image, index) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
whileInView={{ opacity: 1, scale: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ delay: index * 0.1 }}
|
||||
className="rounded-2xl overflow-hidden hover:neon-glow transition-all duration-500"
|
||||
>
|
||||
<img
|
||||
src={image}
|
||||
alt={`${project.title} screenshot ${index + 1}`}
|
||||
className="w-full h-48 object-cover hover:scale-105 transition-transform duration-500"
|
||||
/>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Related Projects */}
|
||||
{relatedProjects.length > 0 && (
|
||||
<section className="py-24">
|
||||
<div className="container mx-auto px-4">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
className="text-center mb-12"
|
||||
>
|
||||
<h2 className="text-3xl font-bold">Related Projects</h2>
|
||||
</motion.div>
|
||||
|
||||
<div className="grid md:grid-cols-3 gap-8 max-w-5xl mx-auto">
|
||||
{relatedProjects.map((relatedProject, index) => (
|
||||
<motion.article
|
||||
key={relatedProject.id}
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
<div className="grid md:grid-cols-3 gap-6 max-w-6xl mx-auto">
|
||||
{project.gallery.map((image, index) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
initial={{ opacity: 0, scale: 0.97 }}
|
||||
whileInView={{ opacity: 1, scale: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ delay: index * 0.1 }}
|
||||
className="group glass rounded-2xl overflow-hidden hover:neon-glow transition-all duration-500"
|
||||
transition={{ delay: index * 0.06, duration: 0.4 }}
|
||||
className="group relative aspect-[4/3] rounded-3xl overflow-hidden border border-border/60 bg-background/40"
|
||||
>
|
||||
<Link to={`/projects/${relatedProject.slug}`}>
|
||||
<div className="relative h-40 overflow-hidden">
|
||||
<img
|
||||
src={relatedProject.image}
|
||||
alt={relatedProject.title}
|
||||
className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110"
|
||||
/>
|
||||
</div>
|
||||
<div className="p-5">
|
||||
<h3 className="font-bold mb-2 group-hover:text-primary transition-colors">
|
||||
{relatedProject.title}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground line-clamp-2">
|
||||
{relatedProject.description}
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
</motion.article>
|
||||
<img
|
||||
src={image}
|
||||
alt={`${project.title} visual display ${index + 1}`}
|
||||
className="w-full h-full object-cover object-center transition-all duration-500 group-hover:scale-105"
|
||||
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>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
className="text-center mt-12"
|
||||
>
|
||||
<Link to="/projects">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="rounded-full px-8 glass border-primary/30 hover:neon-glow"
|
||||
>
|
||||
View All Projects
|
||||
</Button>
|
||||
</Link>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user