Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions app/challenge/landing-page/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import styles from "./LandingPage.module.scss";
import { useParallaxScrollY } from "@/hooks/use-parallax-scrollY";
import clsx from "clsx";
import { motion, Variants } from "framer-motion";
import { ShootingStar } from "@/components/ShootingStars/ShootingStar";

const ProChallenge: React.FC = () => {
const { offsetY, ref } = useParallaxScrollY();
Expand Down Expand Up @@ -120,6 +121,11 @@ const ProChallenge: React.FC = () => {
}}
/>

{/* Shooting Stars */}
<ShootingStar />
<ShootingStar />
<ShootingStar />

{/* OVERLAY LAYER */}
<Box sx={{ position: "absolute", inset: 0 }}>
{/* pill and text */}
Expand Down
6 changes: 6 additions & 0 deletions app/landing/About.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import Image from "next/image";
import styles from "./About.module.scss";
import { useParallaxScrollY } from "@/hooks/use-parallax-scrollY";
import { motion, Variants } from "framer-motion";
import { ShootingStar } from "@/components/ShootingStars/ShootingStar";

const About = () => {
const { offsetY, ref } = useParallaxScrollY();
Expand Down Expand Up @@ -99,6 +100,11 @@ const About = () => {
/>
</div>

{/* Shooting Stars */}
<ShootingStar />
<ShootingStar />
<ShootingStar />

{/* 3. Convert container to motion.div and apply container variants */}
<motion.div
className={styles.content}
Expand Down
6 changes: 6 additions & 0 deletions app/landing/HackVoyagers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Image from "next/image";
import styles from "./HackVoyagers.module.scss";
import { useParallaxScrollY } from "@/hooks/use-parallax-scrollY";
import { motion, Variants } from "framer-motion";
import { ShootingStar } from "@/components/ShootingStars/ShootingStar";

const HackVoyagers = () => {
const { offsetY, ref } = useParallaxScrollY();
Expand Down Expand Up @@ -82,6 +83,11 @@ const HackVoyagers = () => {
/>
</div>

{/* Shooting Stars */}
<ShootingStar />
<ShootingStar />
<ShootingStar />

{/* 3. Wrap Robot container with motion div */}
<motion.div
className={styles.robotContainer}
Expand Down
6 changes: 6 additions & 0 deletions app/landing/Hero.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import Image from "next/image";
import { START_END_DATES } from "./constants";
import styles from "./Hero.module.scss";
import { motion, Variants } from "framer-motion";
import { ShootingStar } from "@/components/ShootingStars/ShootingStar";

// Create a motion-enabled version of the Next.js Image component
const MotionImage = motion(Image);
Expand Down Expand Up @@ -104,6 +105,11 @@ const Hero = () => {
/>
</div>

{/* Shooting Stars */}
<ShootingStar />
<ShootingStar />
<ShootingStar />

{/* 3. Apply the container variants to the main content wrapper */}
<motion.div
className={styles.heroSectionContent}
Expand Down
28 changes: 28 additions & 0 deletions components/ShootingStars/ShootingStar.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
.shootingStarsContainer {
position: absolute;
inset: 0;
pointer-events: none;
z-index: 5;
overflow: hidden;
}

.shootingStar {
position: absolute;
pointer-events: none;
animation: shootingStarFade 2s ease-out forwards;
}

@keyframes shootingStarFade {
0% {
opacity: 0;
}
20% {
opacity: 1;
}
80% {
opacity: 1;
}
100% {
opacity: 0;
}
}
157 changes: 157 additions & 0 deletions components/ShootingStars/ShootingStar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
"use client";

import { DotLottieReact } from "@lottiefiles/dotlottie-react";
import { useState, useEffect, useRef } from "react";
import styles from "./ShootingStar.module.scss";

interface ShootingStar {
id: number;
x: number;
y: number;
rotation: number;
size: number;
flipped: boolean;
}

interface ShootingStarProps {
/** Base delay between shooting stars in milliseconds */
delay?: number;
/** Random variance in delay between shooting stars in milliseconds */
jitter?: number;
}

// Cache the lottie data at module level to avoid re-fetching
let cachedLottieData: ArrayBuffer | null = null;
let fetchPromise: Promise<ArrayBuffer> | null = null;

const fetchLottieData = async (): Promise<ArrayBuffer> => {
if (cachedLottieData) {
return cachedLottieData;
}
if (fetchPromise) {
return fetchPromise;
}
fetchPromise = fetch("/assets/shooting_star.lottie")
.then(res => res.arrayBuffer())
.then(data => {
cachedLottieData = data;
return data;
});
return fetchPromise;
};

export const ShootingStar = ({
delay = 2000,
jitter = 1600
}: ShootingStarProps) => {
const [star, setStar] = useState<ShootingStar | null>(null);
const [lottieData, setLottieData] = useState<ArrayBuffer | null>(null);
const containerRef = useRef<HTMLDivElement>(null);

const spawnShootingStar = () => {
const id = Date.now();

// Calculate the visible portion of the container
const container = containerRef.current;
if (!container) return;

const rect = container.getBoundingClientRect();
const containerHeight = container.offsetHeight;
const containerWidth = container.offsetWidth;

// Scale down star size on smaller screens (max 70% of container width)
const size = 600;
const effectiveSize = Math.min(size, containerWidth * 0.7);

// Calculate visible area within the container
const visibleTop = Math.max(0, -rect.top);
const visibleBottom = Math.min(
containerHeight,
window.innerHeight - rect.top
);
const visibleHeight = visibleBottom - visibleTop;

// Don't spawn if container isn't visible
if (visibleHeight <= 0) return;

// Calculate spawn position as percentage of container
const starHeightPercent = (effectiveSize / containerHeight) * 100;
const starWidthPercent = (effectiveSize / containerWidth) * 100;

// X position: across the full width (with padding)
const maxX = 100 - starWidthPercent;
const x = Math.random() * Math.max(0, maxX - 10) + 5;

// Y position: only within the visible area
const visibleTopPercent = (visibleTop / containerHeight) * 100;
const visibleHeightPercent = (visibleHeight / containerHeight) * 100;
const maxYInVisible = visibleHeightPercent - starHeightPercent;

if (maxYInVisible <= 0) return;

const y =
visibleTopPercent +
Math.random() * Math.max(0, maxYInVisible - 5) +
2.5;

const rotation = Math.random() * -25; // 0 to -25 degrees
const flipped = Math.random() < 0.5; // 50% chance to flip

const newStar: ShootingStar = {
id,
x,
y,
rotation,
size: effectiveSize,
flipped
};
setStar(newStar);
};

// Preload lottie data once
useEffect(() => {
fetchLottieData().then(setLottieData);
}, []);

useEffect(() => {
// Spawn a shooting star at random intervals
const intervalId = setInterval(() => {
const variance = Math.random() * jitter;
setTimeout(() => {
spawnShootingStar();
}, variance);
}, delay);

return () => clearInterval(intervalId);
}, [delay, jitter]);

return (
<div ref={containerRef} className={styles.shootingStarsContainer}>
{lottieData && star && (
<div
key={star.id}
className={styles.shootingStar}
style={{
left: `${star.x}%`,
top: `${star.y}%`,
transform: `rotate(${star.rotation}deg) scaleX(${star.flipped ? -1 : 1})`
}}
>
{/* Size is adjusted to account for large padding in the asset */}
<DotLottieReact
data={lottieData}
loop={false}
autoplay
style={{
transform: star.flipped
? `translateY(-${star.size / 2}px)`
: `translate(-${star.size}px, -${star.size / 2}px)`,
width: `${star.size * 2}px`,
height: `${star.size * 2}px`
}}
/>
</div>
)}
</div>
);
};
19 changes: 19 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@lottiefiles/dotlottie-react": "^0.17.10",
"@mui/icons-material": "^7.3.4",
"@mui/material": "^7.3.4",
"@mui/material-nextjs": "^7.3.5",
Expand Down
Binary file added public/assets/shooting_star.lottie
Binary file not shown.