Skip to content

Commit

Permalink
Animate card wall loading transition
Browse files Browse the repository at this point in the history
  • Loading branch information
foxyblocks committed Jul 18, 2023
1 parent e812bd5 commit 4e4e5f3
Show file tree
Hide file tree
Showing 5 changed files with 306 additions and 115 deletions.
12 changes: 12 additions & 0 deletions components/organisms/DevCardWall/dev-card-wall.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
@keyframes skeleton-loading {
0% {
background-color: hsla(200, 100%, 100%, 0.1);
}
100% {
background-color: hsla(200, 100%, 100%, 0.3);
}
}

.skeletonPulse {
animation: skeleton-loading 1s linear infinite alternate;
}
149 changes: 149 additions & 0 deletions components/organisms/DevCardWall/dev-card-wall.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import { useMeasure, usePrevious } from "react-use";
import { useEffect, useState } from "react";
import { animated, useSpring, useSprings } from "react-spring";
import DevCard, { DevCardProps } from "components/molecules/DevCard/dev-card";
import styles from "./dev-card-wall.module.css";

interface DevCardWallProps {
cards: DevCardProps[];
isLoadingUsernames?: boolean;
}

const LOADING_TILES_COUNT = 20;

const cellHeight = 348;
const cellWidth = 245;
const gapSize = 20;

const coordinatesForIndex = (height: number) => (index: number) => {
const extraYOffset = cellHeight * 0.4;
const tilesPerColumn = Math.floor((height - gapSize) / (cellHeight + gapSize)) + 1;
const columnRow = index % tilesPerColumn;
const columnIsOdd = Math.floor(index / tilesPerColumn) % 2 === 1;
const yOffsetForCell = columnIsOdd ? 0 : extraYOffset;
const y = columnRow * (cellHeight + gapSize) - yOffsetForCell;
const x = Math.floor(index / tilesPerColumn) * (cellWidth + gapSize);
return {
x,
y,
};
};

export default function DevCardWall({ isLoadingUsernames = false, cards }: DevCardWallProps) {
const [containerRef, { height }] = useMeasure<HTMLDivElement>();
const previousHeight = usePrevious(height);
const [activeCardIndex, setActiveCardIndex] = useState<number | null>(null);

const loadingOpacity = useSpring({
opacity: isLoadingUsernames ? 1 : 0,
config: {
duration: 1000,
},
});

const [cardTrails, api] = useSprings(
cards.length,
(index) => ({
opacity: 0,
x: 0,
y: 0,
}),
[cards.length]
);

useEffect(() => {
if (!height) {
return;
}
let stagger = 0;
if (previousHeight === 0) {
stagger = 100;
}
api.start((i) => ({
...coordinatesForIndex(height)(i),
delay: i * 100 + stagger,
}));
}, [height, previousHeight, api, cards]);

// useEffect(() => {
// if(cards.length) {
// api.start((i) => ({
// ...coordinatesForIndex(height)(i),
// delay: i * 100,
// }));
// }

// }, [cards]);

const loadingSkeleton = Array.from({ length: LOADING_TILES_COUNT }, (_, i) => {
const { x, y } = coordinatesForIndex(height)(i);
return (
<animated.div
className={`grid absolute rounded-3xl ${styles.skeletonPulse}`}
key={i}
style={{
width: `${cellWidth}px`,
height: `${cellHeight}px`,
left: `${x}px`,
top: `${y}px`,
opacity: loadingOpacity.opacity,
// ...style,
}}
></animated.div>
);
});

const cardElements = cardTrails.map((style, i) => {
const cardProps = cards[i];
const { x, y } = style;
console.log("card props", x, y);
return (
<animated.div
className="grid absolute"
key={i}
style={{
width: `${cellWidth}px`,
height: `${cellHeight}px`,
left: x.to((x) => `${x}px`),
top: y.to((y) => `${y}px`),
}}
>
<DevCard isInteractive={false} {...cardProps} />
</animated.div>
);
});

return (
<div className="relative overflow-hidden" ref={containerRef}>
{/* card wall surface area, should extend beyond the top and bottom */}
{/* {isLoadingUsernames ? loadingSkeleton : cardElements} */}
{loadingSkeleton}
{cardElements}
<div
style={{
width: "100%",
position: "absolute",
top: 0,
left: 0,
bottom: 0,
right: 0,
zIndex: 2,
pointerEvents: "none",
background: "linear-gradient(90deg, #000 0%, rgba(0, 0, 0, 0.00) 100%)",
}}
></div>
<div
style={{
width: "100%",
position: "absolute",
top: 0,
left: 0,
zIndex: 2,
pointerEvents: "none",
height: "72.42614145%",
background: "linear-gradient(180deg, #000 0%, rgba(0, 0, 0, 0.00) 40%)",
}}
></div>
</div>
);
}
116 changes: 103 additions & 13 deletions pages/404.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,53 @@
import Link from "next/link";
import { useEffect, useState } from "react";
import FullHeightContainer from "components/atoms/FullHeightContainer/full-height-container";
import HeaderLogo from "components/molecules/HeaderLogo/header-logo";
import DevCard from "components/molecules/DevCard/dev-card";
import { useFetchTopContributors } from "lib/hooks/useFetchTopContributors";
import DevCardWall from "components/organisms/DevCardWall/dev-card-wall";
import { DevCardProps } from "components/molecules/DevCard/dev-card";
import { fetchContributorPRs } from "lib/hooks/api/useContributorPullRequests";
import { getRepoList } from "lib/hooks/useRepoList";
import getContributorPullRequestVelocity from "lib/utils/get-contributor-pr-velocity";
import getPercent from "lib/utils/get-percent";
import { getAvatarByUsername } from "lib/utils/github";
import BubbleBG from "../img/bubble-bg.svg";

export default function Custom404() {
const cards = Array.from({ length: 5 }, (_, i) => (
<div key={i}>
<DevCard username={`test${i}`} name="test" avatarURL="https://avatars.githubusercontent.com/u/54212428?v=4" />
</div>
));
const { data, isLoading } = useFetchTopContributors();
const [cards, setCards] = useState<DevCardProps[]>([]);
const [isLoadingUsernames, setIsLoadingUsernames] = useState<boolean>(true);

console.log(cards);
console.log("top contributors data", data, isLoading);

useEffect(() => {
// setCards(
// data.map((user) => ({
// username: user.login,
// name: user.login,
// avatarURL: "https://avatars.githubusercontent.com/u/54212428?v=4",
// }))
// );

// setTimeout(() => {
// setIsLoadingUsernames(false);
// }, 1000);

async function loadCards() {
const cardData = await Promise.all(data.map((user) => getAllCardData(user.login)));

setCards(cardData);
setIsLoadingUsernames(false);
}

loadCards();
}, [data]);

return (
<FullHeightContainer className="text-white">
<div
className="grid w-full h-full md:pb-20"
style={{
background: `url(${BubbleBG.src}) no-repeat center center, linear-gradient(147deg, #212121 13.41%, #2E2E2E 86.8%)`,
background: `#010101 url(${BubbleBG.src}) no-repeat center center`,
backgroundSize: "cover",
gridTemplateRows: "auto 1fr auto",
}}
Expand All @@ -27,27 +56,88 @@ export default function Custom404() {
<HeaderLogo withBg={false} />
</div>
<main className="grid md:grid-cols-2 place-content-center py-6">
<div className="text-center px-6">
<div className="text-center px-6 relative z-10">
<h1 className="text-7xl font-bold mb-2">404</h1>
<div className="text-2xl mb-2">uh oh page not found</div>
<div className="mb-2">while you&apos;re here, you can check out some of our amazing users!</div>
<Link href="/" className="text-orange-600">
<Link href="/" className="text-orange-600 hover:text-orange-500">
Take me home &rarr;
</Link>
</div>
<div className="">
<div className="hidden md:grid">
<div
className="grid"
style={{
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
width: "50%",
}}
>
{cards}
<DevCardWall cards={cards} isLoadingUsernames={isLoadingUsernames} />
</div>
</div>
</main>
</div>
</FullHeightContainer>
);
}

async function getAllCardData(username: string): Promise<DevCardProps> {
const [basicData, contributorData] = await Promise.all([
fetchBasicCardData(username),
fetchContributorCardData(username),
]);

return {
...basicData,
...contributorData,
isLoading: false,
};
}

async function fetchUserData(username: string) {
const req = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/users/${username}`, {
headers: {
accept: "application/json",
},
});

return (await req.json()) as DbUser;
}

async function fetchBasicCardData(username: string): Promise<DevCardProps> {
const user = await fetchUserData(username);
const githubAvatar = getAvatarByUsername(username, 300);

const ageInDays = user.first_opened_pr_at
? Math.floor((Date.now() - Date.parse(user.first_opened_pr_at)) / 86400000)
: 0;

return {
username,
avatarURL: githubAvatar,
name: user.name || username,
bio: user.bio,
age: ageInDays,
};
}

async function fetchContributorCardData(
username: string
): Promise<Pick<DevCardProps, "prs" | "prVelocity" | "prMergePercentage" | "repos">> {
const { data, meta } = await fetchContributorPRs(username, undefined, "*", [], 100);
const prs = data.length;
const prVelocity = getContributorPullRequestVelocity(data);
const prTotal = meta.itemCount;
const mergedPrs = data.filter((prData) => prData.merged);
const prMergePercentage = getPercent(prTotal, mergedPrs.length || 0);
const repos = getRepoList(Array.from(new Set(data.map((prData) => prData.full_name))).join(",")).length;
return {
prs,
prVelocity,
prMergePercentage,
repos,
};
}
Loading

0 comments on commit 4e4e5f3

Please sign in to comment.