- 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();
-
- const sectionSelectors = [
- ".description-section",
- ".trailer-section",
- ".skill-section",
- ".details-section",
- ".links-section",
- ];
- {
-
- }
- const containerVariants = {
- visible: {
- opacity: 1,
- scale: 1,
- transition: {
- staggerChildren: 0.2,
- },
- },
- hidden: {
- opacity: 0,
- scale: 0,
- },
- };
- const itemVariants = {
- visible: {
- opacity: 1,
- scale: 1,
- },
- hidden: {
- opacity: 0,
- scale: 0,
- },
- };
-
- const isElementVisible = ElementObserver(sectionSelectors);
- useEffect(() => {
- const fetchProjectData = async () => {
- try {
-
- 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;
-
- setProjectDetails([]);
- if (projectData?.start_date) {
- const parseDate = (dateString) => {
- const parts = dateString.split(" ");
- if (parts.length === 3) {
-
- return new Date(dateString);
- } else if (parts.length === 2) {
-
- 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]);
-
- const timeDifferenceInDays = Math.floor(
- (lastDate - startDate) / (1000 * 60 * 60 * 24)
- );
-
- const timeDifferenceInWeeks = Math.floor(timeDifferenceInDays / 7);
- const timeDifferenceInMonths =
- (lastDate.getFullYear() - startDate.getFullYear()) * 12 +
- (lastDate.getMonth() - startDate.getMonth());
- let durationText = "";
- if (timeDifferenceInMonths === 0) {
-
- 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();
- return () => clearInterval(intervalRef.current);
- }, [projectData]);
- const nextCommit = () => {
- setDirection(1);
- setCurrentIndex(
- (prevIndex) => (prevIndex + 1) % projectData?.repository?.commits.length
- );
- resetTimer();
- };
- const prevCommit = () => {
- setDirection(-1);
- setCurrentIndex((prevIndex) =>
- prevIndex === 0
- ? projectData?.repository?.commits.length - 1
- : prevIndex - 1
- );
- resetTimer();
- };
- 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;