Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add achievements page #130

Merged
merged 15 commits into from
Sep 5, 2023
2 changes: 1 addition & 1 deletion components/UI/footer.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { FunctionComponent } from "react";
import styles from "../styles/components/footer.module.css";
import styles from "../../styles/components/footer.module.css";

const Footer: FunctionComponent = () => {
return (
Expand Down
27 changes: 16 additions & 11 deletions components/UI/navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,14 +113,19 @@ const Navbar: FunctionComponent = () => {
}

function onTopButtonClick(): void {
if (available.length > 0) {
if (available.length === 1) {
connect(available[0]);
if (!isConnected) {
if (available.length > 0) {
if (available.length === 1) {
connect(available[0]);
} else {
setHasWallet(true);
}
} else {
setHasWallet(true);
}
} else {
setHasWallet(true);
// disconnectByClick();
fricoben marked this conversation as resolved.
Show resolved Hide resolved
setShowWallet(true);
}
}

Expand Down Expand Up @@ -166,12 +171,12 @@ const Navbar: FunctionComponent = () => {
</div>
<div>
<ul className="hidden lg:flex uppercase items-center">
<Link href="/partnership">
<li className={styles.menuItem}>Partnership</li>
</Link>
<Link href="/">
<li className={styles.menuItem}>Quests</li>
</Link>
<Link href="/achievements">
<li className={styles.menuItem}>Achievements</li>
</Link>
<Link href={`/${address ? addressOrDomain : "not-connected"}`}>
<li className={styles.menuItem}>My profile</li>
</Link>
Expand Down Expand Up @@ -259,20 +264,20 @@ const Navbar: FunctionComponent = () => {
</div>
<div className="py-4 flex flex-col">
<ul className="uppercase text-babe-blue">
<Link href="/partnership">
<Link href="/">
<li
onClick={() => setNav(false)}
className={styles.menuItemSmall}
>
Partnership
Quests
</li>
</Link>
<Link href="/">
<Link href="/achievements">
<li
onClick={() => setNav(false)}
className={styles.menuItemSmall}
>
Quests
Achievements
</li>
</Link>
fricoben marked this conversation as resolved.
Show resolved Hide resolved
<Link
Expand Down
15 changes: 15 additions & 0 deletions components/UI/tooltip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import React from "react";
import styled from "@emotion/styled";
import { Tooltip, TooltipProps, tooltipClasses } from "@mui/material";

export const CustomTooltip = styled(({ className, ...props }: TooltipProps) => (
<Tooltip {...props} classes={{ popper: className }} />
))(() => ({
[`& .${tooltipClasses.tooltip}`]: {
backgroundColor: "#191527",
borderRadius: "12px",
color: "#E1DCEA",
maxWidth: 206,
padding: 12,
},
}));
47 changes: 47 additions & 0 deletions components/achievements/achievement.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import React, { FunctionComponent, useMemo } from "react";
import styles from "../../styles/achievements.module.css";
import {
AchievementDocument,
AchievementsDocument,
} from "../../types/backTypes";
import Level from "./level";

type AchievementProps = {
achievements: AchievementsDocument;
index: number;
};

const Achievement: FunctionComponent<AchievementProps> = ({
achievements,
index,
}) => {
const position = useMemo(() => {
return index % 2 === 0 ? "right center" : "left center";
}, [index]);
return (
<div className={styles.card}>
<div
className={styles.background}
style={{
backgroundImage: `url('/${achievements.category_img_url}')`,
backgroundPosition: `${position}`,
backgroundSize: "30%",
}}
Copy link
Contributor

Choose a reason for hiding this comment

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

Might be worth putting that in a const before the return

/>
<div className={styles.backgroundFilter} />
<div className={styles.cardContainer}>
<div>
<h2 className={styles.cardTitle}>{achievements.category_name}</h2>
<p className={styles.cardSubtitle}>/{achievements.category_desc}</p>
</div>
<div className={styles.levels}>
{achievements.achievements.map((achievement: AchievementDocument) => {
return <Level key={achievement.id} achievement={achievement} />;
})}
</div>
</div>
</div>
);
};

export default Achievement;
42 changes: 42 additions & 0 deletions components/achievements/level.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React, { FunctionComponent } from "react";
import styles from "../../styles/achievements.module.css";
import { AchievementDocument } from "../../types/backTypes";
import { CustomTooltip } from "../UI/tooltip";

type AchievementLevelProps = {
achievement: AchievementDocument;
};

const AchievementLevel: FunctionComponent<AchievementLevelProps> = ({
achievement,
}) => {
return (
<CustomTooltip
title={
<React.Fragment>
fricoben marked this conversation as resolved.
Show resolved Hide resolved
<div>
<div className={styles.tooltipTitle}>{achievement.title}</div>
<div className={styles.tooltipSub}>{achievement.desc}</div>
</div>
</React.Fragment>
}
placement="bottom-end"
>
<div className={styles.levelContainer}>
<div className={styles.levelInfo}>
<p className={styles.levelDesc}>{achievement.short_desc}</p>
<h3 className={styles.levelTitle}>{achievement.name}</h3>
</div>
<div
className={`${styles.levelImg} ${
!achievement.completed ? styles.disabled : ""
}`}
>
<img src={achievement.img_url} alt="achievement level image" />
</div>
</div>
</CustomTooltip>
);
};

export default AchievementLevel;
1 change: 1 addition & 0 deletions pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { InjectedConnector, StarknetConfig } from "@starknet-react/core";
import { Analytics } from "@vercel/analytics/react";
import { StarknetIdJsProvider } from "../context/StarknetIdJsProvider";
import { createTheme } from "@mui/material/styles";
import Footer from "../components/UI/footer";

function MyApp({ Component, pageProps }: AppProps) {
const connectors = [
Expand Down
124 changes: 124 additions & 0 deletions pages/achievements.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { NextPage } from "next";
import React, { useEffect, useState } from "react";
import styles from "../styles/achievements.module.css";
import { useAccount } from "@starknet-react/core";
import {
AchievementsDocument,
CompletedDocument,
QueryError,
} from "../types/backTypes";
import Achievement from "../components/achievements/achievement";
import { hexToDecimal } from "../utils/feltService";

const Achievements: NextPage = () => {
const { address } = useAccount();
const [userAchievements, setUserAchievements] = useState<
AchievementsDocument[]
>([]);
const [hasChecked, setHasChecked] = useState<boolean>(false);

useEffect(() => {
// If a call was made with an address in the first second, the call with 0 address should be cancelled
fricoben marked this conversation as resolved.
Show resolved Hide resolved
let shouldFetchWithZeroAddress = true;

// Set a 1-second timer to allow time for address loading
const timer = setTimeout(() => {
// If address isn't loaded after 1 second, make the API call with the zero address
if (shouldFetchWithZeroAddress) {
fetch(`${process.env.NEXT_PUBLIC_API_LINK}/achievements/fetch?addr=0`)
.then((response) => response.json())
.then((data: AchievementsDocument[] | QueryError) => {
if (data as AchievementsDocument[])
setUserAchievements(data as AchievementsDocument[]);
});
}
}, 1000);

// If the address is loaded before the 1-second timer, make the API call with the loaded address
if (address) {
shouldFetchWithZeroAddress = false;
clearTimeout(timer);
fetch(
`${
process.env.NEXT_PUBLIC_API_LINK
}/achievements/fetch?addr=${hexToDecimal(address)}`
)
.then((response) => response.json())
.then((data: AchievementsDocument[] | QueryError) => {
if (data as AchievementsDocument[])
setUserAchievements(data as AchievementsDocument[]);
});
}
// Clear the timer when component unmounts or dependencies change to prevent memory leaks
return () => {
clearTimeout(timer);
};
}, [address, hasChecked]);

// Map through user achievements and check if any are completed
fricoben marked this conversation as resolved.
Show resolved Hide resolved
useEffect(() => {
if (userAchievements.length > 0 && !hasChecked && address) {
const promises: Promise<void>[] = [];
userAchievements.forEach((achievementCategory, index) => {
achievementCategory.achievements.forEach((achievement, aIndex) => {
if (!achievement.completed) {
if (achievement.verify_type === "default") {
const fetchPromise = fetch(
`${
process.env.NEXT_PUBLIC_API_LINK
}/achievements/verify_default?addr=${hexToDecimal(
address
)}&id=${achievement.id}`
)
.then((response) => response.json())
.then((data: CompletedDocument) => {
if (data?.achieved) {
const newUserAchievements = [...userAchievements];
newUserAchievements[index].achievements[aIndex].completed =
true;
setUserAchievements(newUserAchievements);
}
});
promises.push(fetchPromise);
}
}
});
});
// Wait for all promises to resolve before setting hasChecked to true
Promise.all(promises).then(() => {
setHasChecked(true);
});
}
}, [userAchievements.length, address]);

return (
<div className={styles.screen}>
<div className={styles.container}>
<div className={styles.headerContent}>
<h1 className={styles.title}>Achievements</h1>
<p className={styles.subtitle}>
Complete achievements and grow your Starknet on-chain reputation
</p>
</div>
<div className={styles.cardWrapper}>
<div className={styles.cards}>
{userAchievements &&
fricoben marked this conversation as resolved.
Show resolved Hide resolved
userAchievements.map(
(achievementCategory: AchievementsDocument, index: number) => {
return (
<Achievement
achievements={achievementCategory}
key={achievementCategory.category_name}
index={index}
/>
);
}
)}
</div>
</div>
</div>
</div>
);
};

export default Achievements;
Binary file added public/achievements/argent/argent_1.webp
Binary file not shown.
Binary file added public/achievements/argent/argent_2.webp
Binary file not shown.
Binary file added public/achievements/argent/argent_3.png
Copy link
Member

Choose a reason for hiding this comment

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

why not using webp?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

because it doesn't work as a background image

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/achievements/argent/argent_3.webp
Binary file not shown.
Binary file added public/achievements/braavos/braavos_1.webp
Binary file not shown.
Binary file added public/achievements/braavos/braavos_2.webp
Binary file not shown.
Binary file added public/achievements/braavos/braavos_3.png
fricoben marked this conversation as resolved.
Show resolved Hide resolved
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/achievements/braavos/braavos_3.webp
Binary file not shown.
Loading