Skip to content
Merged
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
347 changes: 223 additions & 124 deletions components/Hero/Hero.tsx
Original file line number Diff line number Diff line change
@@ -1,140 +1,239 @@
"use client";

import { useState } from "react";
import "atropos/css";
import Atropos from "atropos/react";
import Image from "next/image";
import space from "public/images/home/space.jpg";
import rocketman from "public/images/home/rocketman.png";
import moon from "public/images/home/moon.png";
import React, { useState, useEffect } from "react";

export default function Hero() {
const [rocketLoaded, setRocketLoaded] = useState(false);
const [moonLoaded, setMoonLoaded] = useState(false);
const [starsLoaded, setStarsLoaded] = useState(false);

const isReady = rocketLoaded && moonLoaded && starsLoaded;
const CoduLogo = ({ className }: { className?: string }) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="463"
height="150"
fill="none"
viewBox="0 0 463 150"
className={className}
aria-label="Codú logo"
role="img"
>
<path
fill="#fff"
fillRule="evenodd"
d="M187 150a50 50 0 1 0 0-100 50 50 0 0 0 0 100Zm0-18a32 32 0 1 0 0-64 32 32 0 0 0 0 64Z"
clipRule="evenodd"
/>
<path
fill="#fff"
d="M415.75 6.36a9 9 0 0 1 12.73 12.73l-18.39 18.39a9 9 0 0 1-12.73-12.73l18.39-18.39Z"
/>
<path
fill="#fff"
fillRule="evenodd"
d="M341 0a9 9 0 0 0-9 9v52.58A50 50 0 1 0 350 100V9a9 9 0 0 0-9-9Zm-9 100a32 32 0 1 0-64 0 32 32 0 0 0 64 0Z"
clipRule="evenodd"
/>
<path
fill="#fff"
d="M121.46 121.88c3.5 3.53 3.5 9.28-.42 12.33a75 75 0 1 1-.05-118.45c3.93 3.05 3.93 8.8.44 12.33-3.5 3.53-9.17 3.5-13.2.6a57 57 0 1 0 .03 92.6c4.04-2.9 9.7-2.94 13.2.59ZM363 59a9 9 0 1 1 18 0v41a32 32 0 0 0 64 0V59a9 9 0 1 1 18 0v82a9 9 0 1 1-18 0v-2.58A50 50 0 0 1 363 100V59Z"
/>
</svg>
);
};

const handleScroll = (id: string) => {
const element = document.getElementById(id);
if (element) {
element.scrollIntoView({ behavior: "smooth" });
const NightSky = () => {
const styles = `
@keyframes twinkle {
0%, 100% { opacity: 0.3; }
50% { opacity: 0.7; }
}
@keyframes gentleMove1 {
0%, 100% { transform: translate(0, 0); }
50% { transform: translate(3px, 2px); }
}
@keyframes gentleMove2 {
0%, 100% { transform: translate(0, 0); }
50% { transform: translate(-2px, 4px); }
}
@keyframes gentleMove3 {
0%, 100% { transform: translate(0, 0); }
50% { transform: translate(4px, -3px); }
}
@keyframes shootingStar {
0% { transform: translate(0, 0); opacity: 1; }
100% { transform: translate(300px, 300px); opacity: 0; }
}
.twinkle { animation: twinkle 4s ease-in-out infinite; }
.gentle-move1 { animation: gentleMove1 25s ease-in-out infinite; }
.gentle-move2 { animation: gentleMove2 30s ease-in-out infinite; }
.gentle-move3 { animation: gentleMove3 35s ease-in-out infinite; }
.shooting-star {
position: absolute;
width: 4px;
height: 4px;
background: white;
border-radius: 50%;
top: -4px;
left: -4px;
animation: shootingStar 1.5s linear;
animation-iteration-count: 1;
}
`;
Comment on lines +42 to +78
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider Moving Inline Styles to External CSS

The styles variable contains CSS defined as a template literal and is injected into a <style> tag within the component. While this approach works, it can make the code harder to maintain and can lead to duplication if multiple components need similar styles.

Consider moving these styles to an external CSS file, a CSS module, or using a CSS-in-JS solution like styled-components or Emotion. This will improve code organization, enable reuse, and make it easier to manage styles across the project.


const [shootingStars, setShootingStars] = useState<number[]>([]);
useEffect(() => {
const createShootingStar = () => {
const id = Date.now();
setShootingStars((prev) => [...prev, id]);
setTimeout(() => {
setShootingStars((prev) => prev.filter((starId) => starId !== id));
}, 1500); // Match the duration of the shooting star animation
};

const interval = setInterval(() => {
if (Math.random() < 0.3) {
// 30% chance every 3 seconds
createShootingStar();
}
}, 3000);

return () => clearInterval(interval);
}, []);

const starPositions = [
{ x: 10, y: 15 },
{ x: 25, y: 30 },
{ x: 40, y: 10 },
{ x: 60, y: 40 },
{ x: 75, y: 20 },
{ x: 5, y: 50 },
{ x: 30, y: 70 },
{ x: 50, y: 85 },
{ x: 80, y: 60 },
{ x: 90, y: 35 },
{ x: 15, y: 25 },
{ x: 35, y: 45 },
{ x: 55, y: 15 },
{ x: 70, y: 55 },
{ x: 85, y: 30 },
{ x: 20, y: 65 },
{ x: 45, y: 80 },
{ x: 65, y: 5 },
{ x: 95, y: 45 },
{ x: 8, y: 90 },
{ x: 28, y: 18 },
{ x: 48, y: 38 },
{ x: 68, y: 78 },
{ x: 88, y: 22 },
{ x: 12, y: 72 },
{ x: 32, y: 92 },
{ x: 52, y: 62 },
{ x: 72, y: 42 },
{ x: 92, y: 82 },
{ x: 18, y: 52 },
{ x: 38, y: 32 },
{ x: 58, y: 72 },
{ x: 78, y: 12 },
{ x: 98, y: 58 },
{ x: 3, y: 83 },
{ x: 23, y: 3 },
{ x: 43, y: 93 },
{ x: 63, y: 33 },
{ x: 83, y: 73 },
{ x: 7, y: 37 },
];

const seededRandom = (function () {
const seed = 12345; // You can change this seed to get a different, but consistent, pattern
let state = seed;
return function () {
state = (state * 1664525 + 1013904223) % 4294967296;
return state / 4294967296;
};
})();
Comment on lines +143 to +150
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Move Seeded Random Function Outside Component to Avoid Re-initialization

The seededRandom function is defined inside the NightSky component, which means it will be re-initialized on every render. This can lead to inconsistent behavior or unnecessary computations.

Consider moving the seededRandom function outside of the component to ensure that the random sequence remains consistent across renders:

- const NightSky = () => {
-   const seededRandom = (function () {
+ // Simple pseudo-random number generator
+ const seededRandom = (function () {
    const seed = 12345; // You can change this seed to get a different, but consistent, pattern
    let state = seed;
    return function () {
      state = (state * 1664525 + 1013904223) % 4294967296;
      return state / 4294967296;
    };
  })();

+ const NightSky = () => {
    // ... rest of the component

This change ensures that the random number generator maintains its state between renders, leading to consistent star patterns.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const seededRandom = (function () {
const seed = 12345; // You can change this seed to get a different, but consistent, pattern
let state = seed;
return function () {
state = (state * 1664525 + 1013904223) % 4294967296;
return state / 4294967296;
};
})();
// Simple pseudo-random number generator
const seededRandom = (function () {
const seed = 12345; // You can change this seed to get a different, but consistent, pattern
let state = seed;
return function () {
state = (state * 1664525 + 1013904223) % 4294967296;
return state / 4294967296;
};
})();
const NightSky = () => {
// ... rest of the component


const generateStars = () => {
return starPositions.map((pos, i) => (
<circle
key={i}
cx={pos.x}
cy={pos.y}
r={seededRandom() * 0.15 + 0.05}
fill="white"
opacity={seededRandom() * 0.5 + 0.3}
className={seededRandom() > 0.7 ? "twinkle" : ""}
/>
));
};

const generateAnimatedStars = () => {
const animatedStarPositions = [
{ x: 20, y: 20 },
{ x: 45, y: 55 },
{ x: 70, y: 30 },
{ x: 85, y: 75 },
{ x: 15, y: 80 },
{ x: 55, y: 25 },
{ x: 35, y: 65 },
{ x: 65, y: 50 },
{ x: 10, y: 40 },
{ x: 90, y: 10 },
];
return animatedStarPositions.map((pos, i) => (
<circle
key={`animated-${i}`}
cx={pos.x}
cy={pos.y}
r={seededRandom() * 0.2 + 0.1}
fill="white"
opacity={seededRandom() * 0.5 + 0.5}
className={`gentle-move${(i % 3) + 1}`}
/>
));
};

return (
<main className="relative">
<Atropos
rotateXMax={0.4}
rotateYMax={0.4}
stretchX={1}
stretchY={0.2}
stretchZ={0.3}
highlight={false}
className="relative h-[calc(100vh_-_100px)] max-h-[calc(100svh_-_100px)] w-full overflow-hidden sm:h-[900px] [&>span.atropos-scale]:pointer-events-none [&_span.atropos-rotate]:pointer-events-auto"
<div className="night-sky-container relative h-full w-full">
<style>{styles}</style>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 100 100"
preserveAspectRatio="xMidYMid slice"
style={{ background: "transparent", width: "100%", height: "100%" }}
className="absolute inset-0"
>
<Image
placeholder="blur"
className="absolute -z-10 h-full w-full object-cover"
src={space}
data-atropos-offset="-2"
alt="Realistic space sky which is black with stars scattered across."
onLoad={() => {
setStarsLoaded(true);
}}
/>

<div className="absolute -bottom-28 left-0 right-0 -z-10 mx-auto max-h-[480px] max-w-[480px] sm:-bottom-60 sm:max-h-[800px] sm:max-w-[600px] md:-bottom-96 md:max-h-[800px] md:max-w-[800px]">
<div className="relative mx-auto brightness-75">
<Image
src={moon}
data-atropos-offset="1"
alt="Photograph of the moon"
sizes="100vw"
style={{
width: "100%",
height: "auto",
}}
onLoad={() => {
setMoonLoaded(true);
}}
/>
</div>
<div className="absolute right-0 top-10 h-[240px] w-[240px] md:-right-28 md:h-[350px] md:w-[350px]">
<Image
height={350}
width={350}
src={rocketman}
data-atropos-offset="8"
alt="3D claymation style model of a astronaut on a rocket"
sizes="100vw"
style={{
width: "100%",
height: "auto",
}}
onLoad={() => {
setRocketLoaded(true);
}}
/>
</div>
</div>
{generateStars()}
{generateAnimatedStars()}
</svg>
{shootingStars.map((id) => (
<div key={id} className="shooting-star" />
))}
</div>
);
};

<div
data-atropos-offset="0"
className="flex h-full flex-col justify-center"
>
<Image
width={340}
height={200}
src="/images/codu.svg"
alt="Codú logo"
className={`mx-auto w-[240px] object-contain transition duration-500 sm:w-[340px] ${
isReady ? "opacity-100" : "opacity-0"
}`}
/>
export default function Hero() {
return (
<div className="relative w-full bg-neutral-950">
<div className="absolute inset-0 bg-gradient-to-b from-black via-transparent to-black"></div>
<section
className="relative mx-auto h-[500px] max-w-5xl overflow-hidden rounded sm:h-[600px]"
aria-labelledby="hero-heading"
>
<NightSky />
<div className="absolute left-1/2 top-1/2 z-10 w-full max-w-3xl -translate-x-1/2 -translate-y-1/2 transform space-y-4 px-4 text-center sm:px-6 lg:px-8">
<CoduLogo className="mx-auto mb-8 h-16 sm:h-20" aria-hidden="true" />
<h1
className={`mt-8 text-center text-5xl font-extrabold tracking-tight text-white drop-shadow-2xl duration-500 sm:text-7xl ${
isReady ? "opacity-100" : "opacity-0"
}`}
id="hero-heading"
className="text-2xl font-semibold text-white sm:text-4xl md:text-4xl"
>
A{" "}
<span className="bg-gradient-to-r from-orange-400 to-pink-600 bg-clip-text text-transparent">
space
</span>{" "}
for coders
The <span className="font-extrabold text-pink-600">free</span> web
developer community
</h1>
<div className="mt-12 flex justify-center">
<button
aria-label="Scroll to call to action"
className="focus-style-rounded animate-bounce rounded-full border-2 bg-neutral-900 bg-opacity-60 p-4"
onClick={() => handleScroll("cta")}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="#ffffff"
className="h-8 w-8"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M19.5 5.25l-7.5 7.5-7.5-7.5m15 6l-7.5 7.5-7.5-7.5"
/>
</svg>
</button>
</div>
<p className="mx-auto max-w-2xl text-sm text-neutral-300 sm:text-base md:text-lg">
{`Codú's community offers hundreds of tutorials, an online community, and answers
questions on a wide range of web development topics. Sign up for a free account today
and join the community.`}
</p>
</div>
<div
className="absolute bottom-0 h-20 w-full bg-gradient-to-t from-black"
data-atropos-offset="-2"
/>
<div
className="absolute top-0 h-20 w-full bg-gradient-to-b from-black"
data-atropos-offset="-2"
/>
</Atropos>
</main>
</section>
</div>
);
}
9 changes: 2 additions & 7 deletions e2e/home.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,11 @@ test.describe("Confirm homepage content", () => {
await page.goto("http://localhost:3000/");
// Check headers

await expect(page.locator("h1")).toContainText("A space for coders");
await expect(page.locator("h1")).not.toContainText("Unwanted text");

await expect(page.locator("h2")).toContainText(
"Sign up today to become a writer and get a free invite to our Discord community",
await expect(page.locator("h1")).toContainText(
"The free web developer community",
);

await expect(page.locator("h3")).toContainText("Trending");

await expect(page.locator("h3")).toContainText("Trending");
});

test.describe("Confirm image accessibiliy content", () => {
Expand Down
Loading