import React, { useRef, useState, useEffect } from "react"; import { motion } from "framer-motion"; import { useParams } from "react-router-dom"; import { fetchData } from "../../../api"; import HoverableIconList from "./HoverableIcon"; import Container from "./Container"; import Slideshow from "./Slideshow"; import ElementObserver from "../ElementObserver"; import MouseTracker from "../../MouseTracker"; import RightArrow from "../../../assets/buttons/RightArrow.svg"; import LeftArrow from "../../../assets/buttons/LeftArrow.svg"; import { clock_icon } from "../../../assets"; import styles from "./Project.module.css"; import useIsMobile from "../../../constants/useIsMobile"; import { floattingImages } from "../../../constants"; import FloatingSVG from "../../design/FloatingSVG"; const Project = () => { const { title } = useParams(); const [projectData, setProjectData] = useState(null); const [projectDetails, setProjectDetails] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [hoverState, setHoverState] = useState({ section: null, index: null, text: null, }); const [currentIndex, setCurrentIndex] = useState(0); const [direction, setDirection] = useState(1); const intervalRef = useRef(null); const GET_PROJECTS = import.meta.env.VITE_GET_PROJECTS; const GET_WORK_PAGE = import.meta.env.VITE_GET_WORK_PAGE; const isMobile = useIsMobile(); // Define selectors for the sections you want to animate const sectionSelectors = [ ".description-section", ".trailer-section", ".skill-section", ".details-section", ".links-section", ]; { /* Framer Motion Variants */ } const containerVariants = { visible: { opacity: 1, scale: 1, transition: { staggerChildren: 0.2, // Stagger delay for each child }, }, hidden: { opacity: 0, scale: 0, }, }; const itemVariants = { visible: { opacity: 1, scale: 1, }, hidden: { opacity: 0, scale: 0, }, }; // Use ElementObserver to track visibility of sections const isElementVisible = ElementObserver(sectionSelectors); useEffect(() => { const fetchProjectData = async () => { try { // Fetch all projects and find the one with the matching title const allProjects = await fetchData(GET_PROJECTS); const project = allProjects.find((p) => { const projectSlug = p.title .toLowerCase() .replace(/[^a-z0-9 -]/g, "") .replace(/\s+/g, "-") .replace(/-+/g, "-"); return projectSlug === title; }); if (project) { const data = await fetchData(`${GET_WORK_PAGE}/${project.id}`); setProjectData(data); } else { setError("Project not found"); } } catch (err) { setError(err.message); } finally { setLoading(false); } }; fetchProjectData(); }, [title]); const resetTimer = () => { clearInterval(intervalRef.current); intervalRef.current = setInterval(() => { setDirection(1); setCurrentIndex( (prevIndex) => (prevIndex + 1) % projectData?.repository?.commits?.length ); }, 6000); }; useEffect(() => { if (!projectData) return; // Reset projectDetails before adding new data setProjectDetails([]); if (projectData?.start_date) { const parseDate = (dateString) => { const parts = dateString.split(" "); if (parts.length === 3) { // Format: "Month Day, Year" (e.g., "March 15, 2024") return new Date(dateString); } else if (parts.length === 2) { // Format: "Month Year" (Default to 1st of the month) const [month, year] = parts; return new Date(`${month} 1, ${year}`); } return null; }; const startDate = parseDate(projectData.start_date); const isPresent = projectData.end_date === "Present"; const endDate = isPresent ? "Present" : parseDate(projectData.end_date); const lastDate = isPresent ? new Date() : endDate; const formatDate = (date) => { return date.toLocaleString("default", { month: "long", day: "numeric", year: "numeric", }); }; const partialFormatDate = (date) => { return date.toLocaleString("default", { month: "long", year: "numeric", }); }; const partialDate = isPresent ? `${partialFormatDate(startDate)} - Present` : `${partialFormatDate(startDate)} - ${partialFormatDate(endDate)}`; const date = isPresent ? `${formatDate(startDate)} - Present` : `${formatDate(startDate)} - ${formatDate(endDate)}`; const dateDetail = { text: partialDate, logo: projectData.date_logo }; setProjectDetails((prevDetails) => [...prevDetails, dateDetail]); // Calculate the total time difference in days const timeDifferenceInDays = Math.floor( (lastDate - startDate) / (1000 * 60 * 60 * 24) ); // Convert days to weeks and months const timeDifferenceInWeeks = Math.floor(timeDifferenceInDays / 7); const timeDifferenceInMonths = (lastDate.getFullYear() - startDate.getFullYear()) * 12 + (lastDate.getMonth() - startDate.getMonth()); let durationText = ""; if (timeDifferenceInMonths === 0) { // If in the same month, show weeks (or days if less than a week) if (timeDifferenceInWeeks > 0) { durationText = `${timeDifferenceInWeeks} week${ timeDifferenceInWeeks > 1 ? "s" : "" }`; } else { durationText = `${timeDifferenceInDays} day${ timeDifferenceInDays > 1 ? "s" : "" }`; } } else if (timeDifferenceInMonths < 12) { durationText = `${timeDifferenceInMonths} month${ timeDifferenceInMonths > 1 ? "s" : "" }`; } else { const years = Math.floor(timeDifferenceInMonths / 12); const months = timeDifferenceInMonths % 12; durationText = `${years} year${years > 1 ? "s" : ""}${ months > 0 ? ` ${months} month${months > 1 ? "s" : ""}` : "" }`; } const durationDetail = { text: durationText, logo: clock_icon, }; setProjectDetails((prevDetails) => [...prevDetails, durationDetail]); } if (projectData?.context && projectData.context.project_context) { const contextDetail = { text: projectData.context.project_context, logo: projectData.context.context_logo, }; setProjectDetails((prevDetails) => [...prevDetails, contextDetail]); } if (projectData?.category) { const detail = { text: projectData.category.name, logo: projectData.category.logo, }; setProjectDetails((prevDetails) => [...prevDetails, detail]); } if (projectData?.tags && projectData.tags.length > 0) { const tagsDetails = projectData.tags .filter((tag) => tag.logo) .map((tag) => ({ text: tag.name, logo: tag.logo, })); setProjectDetails((prevDetails) => [...prevDetails, ...tagsDetails]); } resetTimer(); // Start the interval when data loads return () => clearInterval(intervalRef.current); // Cleanup on unmount }, [projectData]); const nextCommit = () => { setDirection(1); setCurrentIndex( (prevIndex) => (prevIndex + 1) % projectData?.repository?.commits.length ); resetTimer(); // Restart the timer }; const prevCommit = () => { setDirection(-1); setCurrentIndex((prevIndex) => prevIndex === 0 ? projectData?.repository?.commits.length - 1 : prevIndex - 1 ); resetTimer(); // Restart the timer }; if (loading) return <div>Loading...</div>; if (error) return <div>Error: {error}</div>; if (!projectData) return <div>No data found</div>; return ( <div className="min-h-screen text-white "> {/* MouseTracker */} {!isMobile && <MouseTracker text={hoverState.text} />} {/* Background Image */} <div className="absolute top-0 left-0 w-full h-full z-0"> {projectData?.background_image ? ( <img className="object-cover w-full h-full" src={projectData?.background_image} alt="Project Background" /> ) : ( <div className="absolute w-full h-full bg-[#181818]"> {/* Add floating SVG images to the background */} {floattingImages.map((image, index) => ( <FloatingSVG key={index} svg={image.svg} position={image.position} speed={image.speed} amplitude={image.amplitude} resetTrigger={true} /> ))} </div> )} </div> <div className="relative z-10 w-full max-w-[100rem] mx-auto px-4 justify-center"> {/* Project Logo */} <header className="flex flex-col sm:flex-row items-center gap-6 mb-10"> <img src={projectData?.project_logo} alt="Project Logo" className="w-full h-full rounded-md mt-14" /> <h1 className="text-2xl sm:text-4xl font-bold text-center sm:text-left"> {projectData?.title} </h1> </header> {/* Project Description */} <motion.div className="description-section" initial={{ scale: 0, opacity: 0 }} animate={ isElementVisible(document.querySelector(".description-section")) ? { scale: 1, opacity: 1 } : { scale: 0, opacity: 0 } } transition={{ duration: 0.5 }} > <Container className="bg-black/90 mb-10"> <div className="flex flex-col lg:flex-row flex-grow sm:gap-6"> {/* Thumbnail */} <img src={projectData?.project_thumbnail} alt="Project Thumbnail" className="w-full lg:w-1/3 rounded-lg object-cover aspect-video" /> {/* Description */} <div className="w-full text-lg lg:text-xl mt-4 sm:mt-0"> <h2 className="text-2xl sm:text-3xl lg:text-4xl font-semibold mb-2 lg:mb-6 sm:text-left text-orange-400"> Description </h2> <p>{projectData?.project_description}</p> </div> </div> </Container> </motion.div> {/* Trailer */} <motion.div className="trailer-section" initial={{ scale: 0, opacity: 0 }} animate={ isElementVisible(document.querySelector(".trailer-section")) ? { scale: 1, opacity: 1 } : { scale: 0, opacity: 0 } } transition={{ duration: 0.5 }} > {projectData?.trailer && ( <Container className="bg-black/90 mb-10 max-w-auto mx-auto w-full"> <h2 className="text-2xl sm:text-3xl lg:text-4xl font-semibold mb-6 text-center"> Trailer </h2> <div className="w-full aspect-video"> <iframe className="w-full h-full rounded-lg" src={projectData.trailer} title={`${projectData.title} Trailer`} allowFullScreen ></iframe> </div> </Container> )} </motion.div> {/* Tech, Languages, Tools and Databases Section */} <div className="flex flex-wrap gap-6 justify-center mb-10"> <motion.div className="skill-section" initial={{ scale: 0, opacity: 0 }} animate={ isElementVisible(document.querySelector(".skill-section")) ? { scale: 1, opacity: 1 } : { scale: 0, opacity: 0 } } transition={{ duration: 0.5 }} > {projectData?.skills.length > 0 && ( <div className="flex flex-wrap justify-center gap-6"> {projectData.skills.map((skillCategory, categoryIndex) => Object.keys(skillCategory).map((categoryKey) => { const groupedItems = skillCategory[categoryKey].reduce( (acc, item) => { if (!acc[item.skill_name]) acc[item.skill_name] = []; acc[item.skill_name].push(item); return acc; }, {} ); return Object.entries(groupedItems).map( ([skillName, items], groupIndex) => ( <Container key={`${categoryIndex}-${categoryKey}-${skillName}`} className="bg-black/90 p-4 min-w-[260px]" > <h3 className="text-xl text-center font-semibold text-orange-400"> {skillName} </h3> <div className="flex flex-wrap gap-3"> {items.map((item, itemIndex) => ( <HoverableIconList key={itemIndex} items={[item]} getKey={(icon) => icon.id} sectionId={`skills-${categoryKey}-${groupIndex}`} onHoverStart={(section, index, text) => setHoverState({ section, index, text }) } onHoverEnd={() => setHoverState({ section: null, index: null, text: null, }) } /> ))} </div> </Container> ) ); }) )} </div> )} </motion.div> {/* Details Section */} <motion.div className="details-section" initial={{ scale: 0, opacity: 0 }} animate={ isElementVisible(document.querySelector(".details-section")) ? { scale: 1, opacity: 1 } : { scale: 0, opacity: 0 } } transition={{ duration: 0.5, delay: 0.1 }} > <div className="relative z-10 w-full flex flex-wrap sm:flex-row gap-4 md:gap-6 justify-center mb-10 px-4"> {projectDetails.map((detail, index) => ( <Container key={index} className="bg-black/90 max-w-[200px] min-w-[200px] px-4 py-6 flex flex-col items-center justify-center text-center transition-shadow duration-150 hover:shadow-[0_0_30px_rgba(255,255,255,0.6)]" whileHover={{ scale: 1.1 }} transition={{ duration: 0.1 }} > <img className="relative mb-4 w-16 md:w-20 object-contain" src={detail.logo} alt="Detail Logo" /> <h2 className="text-base md:text-lg mb-4 text-white"> {detail.text} </h2> </Container> ))} </div> </motion.div> </div> {projectData?.repository && projectData?.repository?.url && ( <div className="flex flex-col items-center justify-center min-h-[300px]"> <div className={styles["activity-card"]}> <h2 className="text-3xl md:4xl lg:text-4xl font-bold mb-6 text-center"> Development Activity </h2> <motion.div key={currentIndex} initial={{ x: direction * 100, opacity: 0 }} animate={{ x: 0, opacity: 1 }} exit={{ x: -direction * 100, opacity: 0 }} transition={{ duration: 0.1 }} className={styles["activity-content"]} > <img src={ projectData?.repository?.commits?.[currentIndex]?.logo || "default-logo-url" } alt="GitBucket Logo" className={styles["activity-logo"]} /> <div> <h3 className={styles["activity-title"]}> {projectData?.repository?.commits?.[currentIndex] ?.message || "No Title"} </h3> <p className={styles["activity-details"]}> Author:{" "} {projectData?.repository?.commits?.[currentIndex]?.author} -{" "} {new Date( projectData?.repository?.commits?.[currentIndex]?.date ).toLocaleString()} </p> </div> </motion.div> <a href={projectData?.repository?.commits?.[currentIndex]?.url} className={styles["activity-link"]} target="_blank" rel="noopener noreferrer" > View Commit </a> </div> {/* Buttons */} <div className={styles["activity-buttons"]}> <button onClick={prevCommit} className={styles["activity-button"]} > <img src={LeftArrow} alt="Previous" /> </button> <button onClick={nextCommit} className={styles["activity-button"]} > <img src={RightArrow} alt="Next" /> </button> </div> </div> )} {projectData?.screenshots && projectData.screenshots.length > 0 && ( <Slideshow images={projectData.screenshots} projectName={projectData.project_title} /> )} {/* Links Section */} <motion.div className="links-section" variants={containerVariants} initial="hidden" animate={ isElementVisible(document.querySelector(".links-section")) ? "visible" : "hidden" } > {projectData?.links && projectData.links.length > 0 && ( <motion.div className="relative justify-center flex flex-wrap gap-4 md:gap-6 mt-20 mb-10" variants={containerVariants} > {projectData.links.map((link, index) => ( <motion.a key={link.url} href={link.url} target="_blank" rel="noopener noreferrer" variants={itemVariants} onMouseEnter={() => setHoverState({ section: "links", index, text: link.name, }) } onMouseLeave={() => setHoverState({ section: null, index: null, text: null, }) } > <div className={styles["button-container"]}> <img className={styles["button-logo"]} src={link.logo} alt={link.name} /> </div> </motion.a> ))} </motion.div> )} </motion.div> </div> </div> ); }; export default Project;