Newer
Older
My-Portfolio / frontend / src / components / sections / projects / Project.jsx
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;