Skip to content

Commit

Permalink
Merge pull request #52 from sean1832/dev
Browse files Browse the repository at this point in the history
Implement new features and settings for YouTube videos and add math helper for time conversion
  • Loading branch information
sean1832 authored Jun 30, 2024
2 parents 5a3da52 + 3cb9580 commit 98102cc
Show file tree
Hide file tree
Showing 9 changed files with 163 additions and 14 deletions.
2 changes: 1 addition & 1 deletion data/manifest.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "Zeke Zhang Portfolio",
"version": "1.5.5",
"version": "1.6.0",
"copyright": {
"name": "zekezhang.com"
},
Expand Down
48 changes: 42 additions & 6 deletions data/projects.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@
"src": "https://www.youtube.com/embed/q15wEzLeKXw?si=N_EUmE4cvzq_bukx",
"alt": "Shara Clarke | Video Presentation",
"isVideo": true,
"videoSettings": {
"isAutoplay": true,
"isMutted": true,
"isLoop": true
},
"isHero": true
},
{
Expand All @@ -76,7 +81,12 @@
"src": "https://www.youtube.com/embed/9m0n3p4oGuM?si=jqiFEiP3uotYyXx2",
"alt": "Shara Clarke | Boundary Expansion Diagram",
"className": "md:col-span-1 col-span-2 md:h-auto",
"isVideo": true
"isVideo": true,
"videoSettings": {
"isAutoplay": true,
"isMutted": true,
"isLoop": true
}
},
{
"src": "/projects/sharaClarke/diagram_3.webp",
Expand Down Expand Up @@ -215,7 +225,12 @@
"src": "https://www.youtube.com/embed/6B2L0CH7QGg?si=YKckKJ2IYIaZzzGH",
"alt": "Synthetic Dunescapes | Video Presentation",
"isVideo": true,
"isHero": true
"isHero": true,
"videoSettings": {
"isAutoplay": true,
"isMutted": true,
"isLoop": true
}
},
{
"src": "/projects/syntheticDunescapes/piezoelectric-wall.avif",
Expand Down Expand Up @@ -250,7 +265,12 @@
"text": "The aggregation process is driven by a series of atomic discrete units that self-assemble into larger structures. This process is informed by the principles of swarm intelligence, where individual agents interact with their environment and each other to achieve a collective goal. The aggregation algorithm is designed to optimize the distribution of units, ensuring structural stability and adaptability to changing environmental conditions."
},
"className": "md:col-span-3 col-span-6 md:h-auto",
"isVideo": true
"isVideo": true,
"videoSettings": {
"isAutoplay": true,
"isMutted": true,
"isLoop": true
}
},
{
"src": "/projects/syntheticDunescapes/2.webp",
Expand Down Expand Up @@ -449,7 +469,12 @@
"src": "https://www.youtube.com/embed/ov6RFqihmLQ?si=dZVZYMSjo_UtNDIV",
"alt": "Jakarta Rising | Video Presentation",
"isVideo": true,
"isHero": true
"isHero": true,
"videoSettings": {
"isAutoplay": true,
"isMutted": true,
"isLoop": true
}
},
{
"src": "/projects/jakartaRising/main.webp",
Expand Down Expand Up @@ -550,7 +575,13 @@
"src": "https://www.youtube.com/embed/ffDNh79mnEE?si=RPGW12y582pP_6ck",
"alt": "Terra//Form | Video Presentation",
"isVideo": true,
"isHero": true
"isHero": true,
"videoSettings": {
"isAutoplay": true,
"isMutted": true,
"isLoop": true,
"startAt": "1:50"
}
},
{
"src": "/projects/terraForm/main.webp",
Expand Down Expand Up @@ -671,7 +702,12 @@
{
"src": "https://www.youtube.com/embed/TwejWaHOx3Q?si=DhLvhsljq9CSON7E",
"alt": "Mongrel Assembly | ML-Agents in Unity with Grasshopper for Discrete Aggregation and Finite Element Analysis",
"isVideo": true
"isVideo": true,
"videoSettings": {
"isAutoplay": true,
"isMutted": true,
"isLoop": true
}
},
{
"src": "/projects/mongrelAssembly/4.webp",
Expand Down
32 changes: 32 additions & 0 deletions data/schema/projects-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,33 @@
"type": "boolean",
"description": "Media is video?",
"default": true
},
"videoSettings": {
"type": "object",
"description": "Video settings",
"additionalProperties": false,
"properties": {
"isAutoplay": {
"type": "boolean",
"description": "Autoplay video?",
"default": true
},
"isMutted": {
"type": "boolean",
"description": "Mute video by default?",
"default": true
},
"isLoop": {
"type": "boolean",
"description": "Loop video?",
"default": true
},
"startAt": {
"type": ["string", "number"],
"description": "Start video at (in minutes or seconds)",
"default": "0:00"
}
}
}
},
"required": ["src", "alt"],
Expand Down Expand Up @@ -286,6 +313,11 @@
"not": {
"required": ["sizes"]
}
},
"else": {
"not": {
"required": ["videoSettings"]
}
}
},
{
Expand Down
10 changes: 9 additions & 1 deletion src/components/layout/projectDetailSection.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import BlurImage from "../ui/media/blur";
import ScrollToTopButton from "../ui/button/scrollToTopButton";
import ExpandableText from "../ui/expandableText";
import CustomCarousel from "../ui/media/customCarousel";
import { ConvertMinutesToSeconds } from "@/lib/math";

const ProjectDetailsSection = ({ data, className }) => (
<div className={className}>
Expand Down Expand Up @@ -78,7 +79,14 @@ const ProjectInfo = ({ data }) => {
<div className="flex relative max-w-[2560px] mx-auto md:h-[800px] h-[300px]">
{/* If there is no video, show the image; otherwise, show the video */}
{heroVideo ? (
<YoutubeVideo src={heroVideo.src} alt={heroVideo.alt} mute />
<YoutubeVideo
src={heroVideo.src}
alt={heroVideo.alt}
isAutoplay={true}
isMutted={true}
isLoop={true}
startTime={ConvertMinutesToSeconds(heroVideo.videoSettings?.startAt)}
/>
) : (
<a href={heroImage.src} target="_blank" rel="noopener noreferrer">
<BlurImage
Expand Down
9 changes: 8 additions & 1 deletion src/components/ui/media/customVideo.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,14 @@ import { ConstructYoutubeAltText } from "@/lib/constructAltText";
const CustomVideo = ({ video }) => {
return (
<div className={cn("relative w-full col-span-2 h-[300px] md:h-[600px]", video.className)}>
<YoutubeVideo src={video.src} alt={video.alt || ConstructYoutubeAltText(video.src)} />
<YoutubeVideo
src={video.src}
alt={video.alt || ConstructYoutubeAltText(video.src)}
isMutted={video.videoSettings?.isMutted || true}
isAutoplay={video.videoSettings?.isAutoplay || false}
startTime={video.videoSettings?.startAt || 0}
isLoop={video.videoSettings?.isLoop || false}
/>
</div>
);
};
Expand Down
2 changes: 1 addition & 1 deletion src/components/ui/media/lightbox.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import {

import Image from "next/image";
import { YoutubeVideo } from "./youtube-video";
import { GetYoutubeThumbnail } from "@/lib/getYoutube";
import { GetYoutubeThumbnail } from "@/lib/youtubeUtil";
import { ConstructYoutubeAltText } from "@/lib/constructAltText";
import { cn } from "@/lib/utils";
import CarouselWraper from "../carousel/carouselWraper";
Expand Down
18 changes: 15 additions & 3 deletions src/components/ui/media/youtube-video.jsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,28 @@
import React from "react";
import { cn } from "@/utils/cn";
import { SetYoutubeUrl } from "@/lib/youtubeUtil";
import propTypes from "prop-types";

const YoutubeVideo = ({ src, alt, mute, className }) => {
const YoutubeVideo = ({ src, alt, className, startTime, isAutoplay, isLoop, isMutted }) => {
return (
<iframe
src={src + (mute ? "&mute=1" : "")}
src={SetYoutubeUrl(src, startTime, isAutoplay, isMutted, isLoop)}
title={alt}
className={cn("absolute inset-0 w-full h-full", className)}
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; web-share"
allowFullScreen
/>
);
};

YoutubeVideo.propTypes = {
src: propTypes.string.isRequired,
alt: propTypes.string.isRequired,
className: propTypes.string,
startTime: propTypes.number,
isAutoPlay: propTypes.bool,
isLoop: propTypes.bool,
isMuted: propTypes.bool,
};

export { YoutubeVideo };
27 changes: 27 additions & 0 deletions src/lib/math.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
function ConvertMinutesToSeconds(time) {
if (typeof time === "number") {
return time;
} else if (typeof time === "undefined") {
return 0;
} else if (typeof time !== "string") {
throw new Error("Invalid time format " + typeof time);
}
// Split the input by colon
const parts = time.split(":");

// Ensure we have two parts
if (parts.length < 2) {
return time; // Return the input if is seconds only
} else if (parts.length > 2) {
throw new Error("Invalid time format");
}

// Extract minutes and seconds from the parts
const minutes = parseInt(parts[0], 10);
const seconds = parseInt(parts[1], 10);

// Convert minutes to seconds and add to the seconds part
return minutes * 60 + seconds;
}

export { ConvertMinutesToSeconds };
29 changes: 28 additions & 1 deletion src/lib/getYoutube.js → src/lib/youtubeUtil.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,31 @@ const GetYoutubeThumbnail = (url, quality) => {
return `https://img.youtube.com/vi/${id}/${imgName}.jpg`;
};

export { GetYoutubeId, GetYoutubeThumbnail };
function SetYoutubeUrl(url, startTime = 0, isAutoPlay, isMutted, isLoop) {
isAutoPlay = isAutoPlay ? 1 : 0;
isMutted = isMutted ? 1 : 0;
isLoop = isLoop ? 1 : 0;
let playList = "";

const id = GetYoutubeId(url);
if (!id) {
console.error("Invalid Youtube URL");
return null;
}
const baseUrl = `https://www.youtube.com/embed/${id}`;

if (isAutoPlay && !isMutted) {
isMutted = 1;
}

if (isLoop) {
playList = id;
}

return (
`${baseUrl}?start=${startTime}&autoplay=${isAutoPlay}&mute=${isMutted}&loop=${isLoop}` +
(playList ? `&playlist=${playList}` : "")
);
}

export { GetYoutubeId, GetYoutubeThumbnail, SetYoutubeUrl };

0 comments on commit 98102cc

Please sign in to comment.