-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #34 from ishtails/redesign-builder-page
init - new builder page
- Loading branch information
Showing
20 changed files
with
543 additions
and
157 deletions.
There are no files selected for viewing
128 changes: 128 additions & 0 deletions
128
...js/app/builders/0x5D56b71abE6cA1Dc208Ed85926178f9758fa879c/_components/shooting-stars.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
"use client"; | ||
|
||
import React, { useEffect, useRef, useState } from "react"; | ||
import { cn } from "../_lib/utils"; | ||
|
||
interface ShootingStar { | ||
id: number; | ||
x: number; | ||
y: number; | ||
angle: number; | ||
scale: number; | ||
speed: number; | ||
distance: number; | ||
} | ||
|
||
interface ShootingStarsProps { | ||
minSpeed?: number; | ||
maxSpeed?: number; | ||
minDelay?: number; | ||
maxDelay?: number; | ||
starColor?: string; | ||
trailColor?: string; | ||
starWidth?: number; | ||
starHeight?: number; | ||
className?: string; | ||
} | ||
|
||
const getRandomStartPoint = () => { | ||
const side = Math.floor(Math.random() * 4); | ||
const offset = Math.random() * window.innerWidth; | ||
|
||
switch (side) { | ||
case 0: | ||
return { x: offset, y: 0, angle: 45 }; | ||
case 1: | ||
return { x: window.innerWidth, y: offset, angle: 135 }; | ||
case 2: | ||
return { x: offset, y: window.innerHeight, angle: 225 }; | ||
case 3: | ||
return { x: 0, y: offset, angle: 315 }; | ||
default: | ||
return { x: 0, y: 0, angle: 45 }; | ||
} | ||
}; | ||
export const ShootingStars: React.FC<ShootingStarsProps> = ({ | ||
minSpeed = 10, | ||
maxSpeed = 30, | ||
minDelay = 1200, | ||
maxDelay = 4200, | ||
starColor = "#9E00FF", | ||
trailColor = "#2EB9DF", | ||
starWidth = 10, | ||
starHeight = 1, | ||
className, | ||
}) => { | ||
const [star, setStar] = useState<ShootingStar | null>(null); | ||
const svgRef = useRef<SVGSVGElement>(null); | ||
|
||
useEffect(() => { | ||
const createStar = () => { | ||
const { x, y, angle } = getRandomStartPoint(); | ||
const newStar: ShootingStar = { | ||
id: Date.now(), | ||
x, | ||
y, | ||
angle, | ||
scale: 1, | ||
speed: Math.random() * (maxSpeed - minSpeed) + minSpeed, | ||
distance: 0, | ||
}; | ||
setStar(newStar); | ||
|
||
const randomDelay = Math.random() * (maxDelay - minDelay) + minDelay; | ||
setTimeout(createStar, randomDelay); | ||
}; | ||
|
||
createStar(); | ||
}, [minSpeed, maxSpeed, minDelay, maxDelay]); | ||
|
||
useEffect(() => { | ||
const moveStar = () => { | ||
if (star) { | ||
setStar(prevStar => { | ||
if (!prevStar) return null; | ||
const newX = prevStar.x + prevStar.speed * Math.cos((prevStar.angle * Math.PI) / 180); | ||
const newY = prevStar.y + prevStar.speed * Math.sin((prevStar.angle * Math.PI) / 180); | ||
const newDistance = prevStar.distance + prevStar.speed; | ||
const newScale = 1 + newDistance / 100; | ||
if (newX < -20 || newX > window.innerWidth + 20 || newY < -20 || newY > window.innerHeight + 20) { | ||
return null; | ||
} | ||
return { | ||
...prevStar, | ||
x: newX, | ||
y: newY, | ||
distance: newDistance, | ||
scale: newScale, | ||
}; | ||
}); | ||
} | ||
}; | ||
|
||
const animationFrame = requestAnimationFrame(moveStar); | ||
return () => cancelAnimationFrame(animationFrame); | ||
}, [star]); | ||
|
||
return ( | ||
<svg ref={svgRef} className={cn("w-full h-full absolute inset-0", className)}> | ||
{star && ( | ||
<rect | ||
key={star.id} | ||
x={star.x} | ||
y={star.y} | ||
width={starWidth * star.scale} | ||
height={starHeight} | ||
fill="url(#gradient)" | ||
transform={`rotate(${star.angle}, ${star.x + (starWidth * star.scale) / 2}, ${star.y + starHeight / 2})`} | ||
/> | ||
)} | ||
<defs> | ||
<linearGradient id="gradient" x1="0%" y1="0%" x2="100%" y2="100%"> | ||
<stop offset="0%" style={{ stopColor: trailColor, stopOpacity: 0 }} /> | ||
<stop offset="100%" style={{ stopColor: starColor, stopOpacity: 1 }} /> | ||
</linearGradient> | ||
</defs> | ||
</svg> | ||
); | ||
}; |
114 changes: 114 additions & 0 deletions
114
.../app/builders/0x5D56b71abE6cA1Dc208Ed85926178f9758fa879c/_components/stars-background.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
"use client"; | ||
|
||
import React, { RefObject, useCallback, useEffect, useRef, useState } from "react"; | ||
import { cn } from "../_lib/utils"; | ||
|
||
interface StarProps { | ||
x: number; | ||
y: number; | ||
radius: number; | ||
opacity: number; | ||
twinkleSpeed: number | null; | ||
} | ||
|
||
interface StarBackgroundProps { | ||
starDensity?: number; | ||
allStarsTwinkle?: boolean; | ||
twinkleProbability?: number; | ||
minTwinkleSpeed?: number; | ||
maxTwinkleSpeed?: number; | ||
className?: string; | ||
} | ||
|
||
export const StarsBackground: React.FC<StarBackgroundProps> = ({ | ||
starDensity = 0.00015, | ||
allStarsTwinkle = true, | ||
twinkleProbability = 0.7, | ||
minTwinkleSpeed = 0.5, | ||
maxTwinkleSpeed = 1, | ||
className, | ||
}) => { | ||
const [stars, setStars] = useState<StarProps[]>([]); | ||
const canvasRef: RefObject<HTMLCanvasElement> = useRef<HTMLCanvasElement>(null); | ||
|
||
const generateStars = useCallback( | ||
(width: number, height: number): StarProps[] => { | ||
const area = width * height; | ||
const numStars = Math.floor(area * starDensity); | ||
return Array.from({ length: numStars }, () => { | ||
const shouldTwinkle = allStarsTwinkle || Math.random() < twinkleProbability; | ||
return { | ||
x: Math.random() * width, | ||
y: Math.random() * height, | ||
radius: Math.random() * 0.05 + 0.5, | ||
opacity: Math.random() * 0.5 + 0.5, | ||
twinkleSpeed: shouldTwinkle ? minTwinkleSpeed + Math.random() * (maxTwinkleSpeed - minTwinkleSpeed) : null, | ||
}; | ||
}); | ||
}, | ||
[starDensity, allStarsTwinkle, twinkleProbability, minTwinkleSpeed, maxTwinkleSpeed], | ||
); | ||
|
||
useEffect(() => { | ||
const updateStars = () => { | ||
if (canvasRef.current) { | ||
const canvas = canvasRef.current; | ||
const ctx = canvas.getContext("2d"); | ||
if (!ctx) return; | ||
|
||
const { width, height } = canvas.getBoundingClientRect(); | ||
canvas.width = width; | ||
canvas.height = height; | ||
setStars(generateStars(width, height)); | ||
} | ||
}; | ||
|
||
updateStars(); | ||
|
||
const resizeObserver = new ResizeObserver(updateStars); | ||
const currentCanvas = canvasRef.current; // Copy the ref value to a variable | ||
if (currentCanvas) { | ||
resizeObserver.observe(currentCanvas); | ||
} | ||
|
||
return () => { | ||
if (currentCanvas) { | ||
resizeObserver.unobserve(currentCanvas); | ||
} | ||
}; | ||
}, [starDensity, allStarsTwinkle, twinkleProbability, minTwinkleSpeed, maxTwinkleSpeed, generateStars]); | ||
|
||
useEffect(() => { | ||
const canvas = canvasRef.current; | ||
if (!canvas) return; | ||
|
||
const ctx = canvas.getContext("2d"); | ||
if (!ctx) return; | ||
|
||
let animationFrameId: number; | ||
|
||
const render = () => { | ||
ctx.clearRect(0, 0, canvas.width, canvas.height); | ||
stars.forEach(star => { | ||
ctx.beginPath(); | ||
ctx.arc(star.x, star.y, star.radius, 0, Math.PI * 2); | ||
ctx.fillStyle = `rgba(255, 255, 255, ${star.opacity})`; | ||
ctx.fill(); | ||
|
||
if (star.twinkleSpeed !== null) { | ||
star.opacity = 0.5 + Math.abs(Math.sin((Date.now() * 0.001) / star.twinkleSpeed) * 0.5); | ||
} | ||
}); | ||
|
||
animationFrameId = requestAnimationFrame(render); | ||
}; | ||
|
||
render(); | ||
|
||
return () => { | ||
cancelAnimationFrame(animationFrameId); | ||
}; | ||
}, [stars]); | ||
|
||
return <canvas ref={canvasRef} className={cn("h-full w-full absolute inset-0", className)} />; | ||
}; |
131 changes: 131 additions & 0 deletions
131
packages/nextjs/app/builders/_components/BuilderCard.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
import { useEffect, useState } from "react"; | ||
import Image from "next/image"; | ||
import { normalize } from "path"; | ||
import { getAddress, isAddress } from "viem"; | ||
import { useEnsAvatar, useEnsName } from "wagmi"; | ||
import Arrow_Icon from "~~/components/Arrow_Icon"; | ||
import { Address, BlockieAvatar } from "~~/components/scaffold-eth"; | ||
import useBuilderExist from "~~/hooks/user/useBuilderExist"; | ||
import { Builder, Mentor } from "~~/types/builders"; | ||
|
||
type Props = { | ||
mentor?: Mentor; | ||
builder?: Builder; | ||
}; | ||
|
||
const banners = [ | ||
"/banner_1.jpg", | ||
"/banner_2.jpg", | ||
"/banner_3.jpg", | ||
"/banner_4.jpg", | ||
"/banner_5.jpg", | ||
"/banner_6.jpg", | ||
"/banner_7.jpg", | ||
]; | ||
|
||
const getRandomBanner = () => { | ||
const randomIndex = Math.floor(Math.random() * banners.length); | ||
return banners[randomIndex]; | ||
}; | ||
|
||
const BuilderCard = ({ mentor, builder }: Props) => { | ||
const [ensAvatar, setEnsAvatar] = useState<string | null>(); | ||
const checkSumAddress = builder?.address ? getAddress(builder.address) : undefined; | ||
const [banner, setBanner] = useState<string>(""); | ||
const builderPageExists = useBuilderExist({ address: checkSumAddress }); | ||
|
||
useEffect(() => { | ||
setBanner(getRandomBanner()); | ||
}, []); | ||
|
||
const { data: fetchedEns } = useEnsName({ | ||
address: checkSumAddress, | ||
chainId: 1, | ||
query: { | ||
enabled: isAddress(checkSumAddress ?? ""), | ||
}, | ||
}); | ||
|
||
const { data: fetchedEnsAvatar } = useEnsAvatar({ | ||
name: fetchedEns ? normalize(fetchedEns) : undefined, | ||
chainId: 1, | ||
query: { | ||
enabled: Boolean(fetchedEns), | ||
gcTime: 30_000, | ||
}, | ||
}); | ||
|
||
useEffect(() => { | ||
setEnsAvatar(fetchedEnsAvatar); | ||
}, [fetchedEnsAvatar]); | ||
|
||
return ( | ||
<div className="z-10 border rounded-xl h-[15rem] min-w-[17rem] w-full font-light dark:border-zinc-700 border-zinc-400 flex flex-col text-text-zinc-700 dark:text-zinc-300 shadow-md"> | ||
<div className="w-full relative flex flex-col flex-grow-[20] rounded-t-xl"> | ||
{banner ? ( | ||
<Image | ||
src={banner} | ||
alt="banner" | ||
priority={true} | ||
width={1000} | ||
height={1000} | ||
className="rounded-t-xl object-cover max-h-24" | ||
/> | ||
) : ( | ||
<div className="bg-gradient-to-r from-st_cyan/10 to-st_purple/10 rounded-t-xl h-24 w-full" /> | ||
)} | ||
|
||
<div className="rounded-full z-10 top-10 left-5 absolute dark:border-zinc-700 scale-90 sm:scale-100 border-zinc-400 border-4"> | ||
{mentor && <Image src={mentor.image} alt="builder" width={110} height={110} className="rounded-full" />} | ||
|
||
{builder && <BlockieAvatar address={builder.address as `0x${string}`} ensImage={ensAvatar} size={100} />} | ||
</div> | ||
|
||
<div className="text-end"> | ||
<div | ||
className="bg-clip-text text-transparent | ||
dark:bg-[linear-gradient(to_right,theme(colors.indigo.500),theme(colors.indigo.200),theme(colors.rose.500),theme(colors.fuchsia.500),theme(colors.sky.500),theme(colors.indigo.200),theme(colors.indigo.500))] | ||
bg-[linear-gradient(to_right,theme(colors.indigo.500),theme(colors.indigo.800),theme(colors.sky.500),theme(colors.fuchsia.500),theme(colors.sky.500),theme(colors.indigo.800),theme(colors.indigo.500))] bg-[length:200%_auto] animate-gradient font-bold text-2xl" | ||
> | ||
{mentor && <p className="pr-4 sm:pr-8 uppercase">{mentor.name}</p>} | ||
|
||
{builder ? ( | ||
<div className="w-full flex justify-end pt-14 sm:pt-12 pr-4"> | ||
<Address address={builder.address} size="sm" /> | ||
</div> | ||
) : null} | ||
</div> | ||
</div> | ||
</div> | ||
|
||
<hr className="border-t dark:border-zinc-700 border-zinc-400" /> | ||
|
||
<div | ||
className={`${ | ||
(mentor || builderPageExists) && "hover:bg-st_cyan/10" | ||
} " rounded-b-xl duration-75 transition-all dark:border-zinc-700 border-zinc-400 flex-grow"`} | ||
> | ||
<div | ||
className={`${ | ||
mentor || builderPageExists ? "cursor-pointer" : "cursor-not-allowed" | ||
} flex items-center justify-between h-full px-4`} | ||
onClick={() => { | ||
if (mentor) { | ||
window.open(mentor.profileLink, "_blank"); | ||
} else if (builder && builderPageExists) { | ||
window.open(`/builders/${builder.address}`, "_blank"); | ||
} else { | ||
alert("Builder page does not exist.."); | ||
} | ||
}} | ||
> | ||
<p className="font-medium flex flex-col items-center justify-center">View Profile</p> | ||
|
||
<Arrow_Icon /> | ||
</div> | ||
</div> | ||
</div> | ||
); | ||
}; | ||
|
||
export default BuilderCard; |
Oops, something went wrong.