diff --git a/packages/nextjs/app/_assets/icons/CrossedSwordsIcon.tsx b/packages/nextjs/app/_assets/icons/CrossedSwordsIcon.tsx new file mode 100644 index 00000000..a0d9a8bd --- /dev/null +++ b/packages/nextjs/app/_assets/icons/CrossedSwordsIcon.tsx @@ -0,0 +1,168 @@ +const CrossedSwordsIcon = ({ className }: { className: string }) => ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +); + +export default CrossedSwordsIcon; diff --git a/packages/nextjs/app/_assets/icons/HeroDiamond.tsx b/packages/nextjs/app/_assets/icons/HeroDiamond.tsx new file mode 100644 index 00000000..26da9b00 --- /dev/null +++ b/packages/nextjs/app/_assets/icons/HeroDiamond.tsx @@ -0,0 +1,61 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useTheme } from "next-themes"; + +const HeroDiamond = ({ className }: { className?: string }) => { + const { resolvedTheme, theme } = useTheme(); + const [mounted, setMounted] = useState(false); + + useEffect(() => { + setMounted(true); + }, []); + + if (!mounted) { + return ( + + ); + } + + const currentTheme = resolvedTheme || theme || "light"; + const isDarkMode = currentTheme === "dark"; + const fillColor = isDarkMode ? "#DABFFF" : "#551D98"; + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default HeroDiamond; diff --git a/packages/nextjs/app/_assets/icons/HeroLogo.tsx b/packages/nextjs/app/_assets/icons/HeroLogo.tsx new file mode 100644 index 00000000..5af4ba75 --- /dev/null +++ b/packages/nextjs/app/_assets/icons/HeroLogo.tsx @@ -0,0 +1,56 @@ +const HeroLogo = ({ className }: { className?: string }) => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default HeroLogo; diff --git a/packages/nextjs/app/_assets/icons/PadLockIcon.tsx b/packages/nextjs/app/_assets/icons/PadLockIcon.tsx new file mode 100644 index 00000000..21c73035 --- /dev/null +++ b/packages/nextjs/app/_assets/icons/PadLockIcon.tsx @@ -0,0 +1,12 @@ +const PadLockIcon = ({ className }: { className: string }) => ( + + + +); + +export default PadLockIcon; diff --git a/packages/nextjs/app/_assets/icons/QuestionIcon.tsx b/packages/nextjs/app/_assets/icons/QuestionIcon.tsx new file mode 100644 index 00000000..239687a8 --- /dev/null +++ b/packages/nextjs/app/_assets/icons/QuestionIcon.tsx @@ -0,0 +1,31 @@ +const QuestionIcon = ({ className }: { className: string }) => { + return ( + + + + + + + + + + + + + + + + + + + ); +}; + +export default QuestionIcon; diff --git a/packages/nextjs/app/_components/ChallengeExpandedCard.tsx b/packages/nextjs/app/_components/ChallengeExpandedCard.tsx new file mode 100644 index 00000000..5f4ce5c7 --- /dev/null +++ b/packages/nextjs/app/_components/ChallengeExpandedCard.tsx @@ -0,0 +1,151 @@ +import Image from "next/image"; +import Link from "next/link"; +import { getChallengeDependenciesInfo } from "../../utils/dependent-challenges"; +import CrossedSwordsIcon from "../_assets/icons/CrossedSwordsIcon"; +import PadLockIcon from "../_assets/icons/PadLockIcon"; +import QuestionIcon from "../_assets/icons/QuestionIcon"; +import { ChallengeData, challengesData } from "../_data/_hardcoded"; +import { ReviewAction } from "~~/services/database/config/types"; +import { getChallengeById } from "~~/services/database/repositories/challenges"; +import { UserChallenges } from "~~/services/database/repositories/userChallenges"; + +type ChallengeExpandedCardProps = { + challengeId: string; + userChallenges: UserChallenges; +}; + +const ChallengeExpandedCard: React.FC = async ({ challengeId, userChallenges }) => { + const fetchedChallenge = await getChallengeById(challengeId); + + const userChallenge = userChallenges.find(userChallenge => userChallenge.challengeId === challengeId); + if (!fetchedChallenge) { + return null; + } + + const additionalChallengeData = challengesData.find(c => c.id === challengeId); + // Define a merged type for better type safety + type FullChallenge = typeof fetchedChallenge & ChallengeData; + const challenge = { ...fetchedChallenge, ...additionalChallengeData } as FullChallenge; + + const { sortOrder } = challenge; + const { completed: builderHasCompletedDependenciesChallenges, lockReasonToolTip } = getChallengeDependenciesInfo({ + dependencies: challenge.dependencies || [], + userChallenges, + }); + + const reviewAction = userChallenge?.reviewAction; + const isChallengeLocked = challenge.disabled || !builderHasCompletedDependenciesChallenges; + + return ( +
+
+
+
+
+ {reviewAction && ( + + {reviewAction.toLowerCase()} + + )} + + Challenge #{sortOrder} +

+ {challenge.label.split(": ")[1] ? challenge.label.split(": ")[1] : challenge.label} +

+
+
+ {challenge.description} + {challenge.externalLink?.link ? ( + // Redirect to externalLink if set (instead of challenge detail view) +
+ + {!builderHasCompletedDependenciesChallenges && ( +
+ +
+ )} +
+ ) : ( +
+ {isChallengeLocked ? ( + + ) : ( + +
+ + Quest +
+ + )} + {!builderHasCompletedDependenciesChallenges && ( +
+ +
+ )} +
+ )} +
+
+
+ {challenge.previewImage ? ( + {challenge.label} + ) : ( +

{challengeId} image

+ )} +
+ + {challenge.icon && ( + {challenge.icon} + )} +
+
+ ); +}; + +export { ChallengeExpandedCard }; diff --git a/packages/nextjs/app/_components/Hero.tsx b/packages/nextjs/app/_components/Hero.tsx new file mode 100644 index 00000000..d614e54c --- /dev/null +++ b/packages/nextjs/app/_components/Hero.tsx @@ -0,0 +1,32 @@ +import HeroDiamond from "../_assets/icons/HeroDiamond"; +import HeroLogo from "../_assets/icons/HeroLogo"; +import { StartBuildingButton } from "./StartBuildingButton"; + +export const Hero = ({ firstChallengeId }: { firstChallengeId: string }) => { + return ( +
+
+ +
+
+ +
+ +

+ Learn how to build on Ethereum; the superpowers and the gotchas. +

+ +
+ +
+ + +
+ +
+
+
+
+
+ ); +}; diff --git a/packages/nextjs/app/_components/JoinBGButton.tsx b/packages/nextjs/app/_components/JoinBGButton.tsx new file mode 100644 index 00000000..91e1a672 --- /dev/null +++ b/packages/nextjs/app/_components/JoinBGButton.tsx @@ -0,0 +1,164 @@ +"use client"; + +import React from "react"; +import Link from "next/link"; +import PadLockIcon from "../_assets/icons/PadLockIcon"; +import QuestionIcon from "../_assets/icons/QuestionIcon"; +import { useAccount, useWalletClient } from "wagmi"; +import { UserByAddress } from "~~/services/database/repositories/users"; +import { notification } from "~~/utils/scaffold-eth"; +import { getUserSocialsList } from "~~/utils/socials"; + +type JoinBGProps = { + isLocked: boolean; + user: UserByAddress; + lockReasonToolTip: string; +}; + +// TODO: Basic implementation. Update when user schema is ready. +const JoinBGButton: React.FC = ({ isLocked, user, lockReasonToolTip }) => { + const { address: connectedAddress } = useAccount(); + const { data: walletClient } = useWalletClient(); + // const [isJoining, setIsJoining] = useState(false); + // // Optimistic update. + // const [joined, setJoined] = useState(false); + + const address = user?.userAddress; + + const onJoin = async () => { + // setIsJoining(true); + + const socialsList = getUserSocialsList(user); + if (socialsList.length === 0) { + notification.error( +
+ Can't join the BuidlGuidl. + + In order to join the BuildGuidl you need to set your socials in{" "} + + your portfolio + + . It's our way to contact you. + +
, + ); + // setIsJoining(false); + + return; + } + + let signMessage; + try { + const signMessageResponse = await fetch(`/sign-message?messageId=bgJoin&address=${address}`); + if (!signMessageResponse.ok) { + throw new Error("Failed to fetch sign message"); + } + const data = await signMessageResponse.json(); + signMessage = data; + } catch (error) { + notification.error( +
+ Error Getting Signature + Can't get the message to sign. Please try again +
, + ); + return; + } + + let signature; + try { + signature = await walletClient?.signMessage({ message: signMessage }); + } catch (error) { + notification.error( +
+ Signature Cancelled + The signature was cancelled +
, + ); + console.error(error); + return; + } + + try { + const response = await fetch("/bg/join", { + method: "POST", + headers: { + "Content-Type": "application/json", + address: address, + }, + body: JSON.stringify({ signature }), + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData); + } + } catch (error) { + notification.error( +
+ Submission Failed + Submission Error. Please try again. +
, + ); + console.error(error); + return; + } + + notification.success( +
+ Welcome to BuidlGuidl! + + Visit{" "} + + BuidlGuidl + {" "} + and start crafting your Web3 portfolio by submitting your DEX, Multisig or SVG NFT build. + +
, + ); + // setIsJoining(false); + // setJoined(true); + }; + + // const builderAlreadyJoined = !!user?.joinedBg; + + if (!walletClient || !connectedAddress) { + // TODO: Add connect wallet functionality + return ( + + ); + } + + if (!user) { + return ( + + Apply to ๐Ÿฐ๏ธ BuidlGuidl + + ); + } + + return ( + <> + + {isLocked && ( +
+ +
+ )} + + ); +}; + +export default JoinBGButton; diff --git a/packages/nextjs/app/_components/JoinBGCard.tsx b/packages/nextjs/app/_components/JoinBGCard.tsx new file mode 100644 index 00000000..ea46ef17 --- /dev/null +++ b/packages/nextjs/app/_components/JoinBGCard.tsx @@ -0,0 +1,76 @@ +import Image from "next/image"; +import { getChallengeDependenciesInfo } from "../../utils/dependent-challenges"; +import JoinBGButton from "./JoinBGButton"; +import { UserChallenges } from "~~/services/database/repositories/userChallenges"; +import { UserByAddress } from "~~/services/database/repositories/users"; + +// TODO: use later +// "buidl-guidl": {, +// label: "Eligible to join ๐Ÿฐ๏ธ BuidlGuidl", +// previewImage: "assets/bg.png", +// externalLink: { +// link: "https://buidlguidl.com/", +// }, +// }, + +const JOIN_BG_DEPENDENCIES = [ + "simple-nft-example", + "decentralized-staking", + "token-vendor", + "dice-game", + "minimum-viable-exchange", +]; + +export const JoinBGCard = ({ userChallenges, user }: { userChallenges: UserChallenges; user: UserByAddress }) => { + const { completed: builderHasCompletedDependenciesChallenges, lockReasonToolTip } = getChallengeDependenciesInfo({ + dependencies: JOIN_BG_DEPENDENCIES, + userChallenges, + }); + + return ( +
+ bgBanner_joinBgClouds +
+
+ bgBanner_JoinBG +
+
+

+ The BuidlGuidl is a curated group of Ethereum builders creating products, prototypes, and tutorials to + enrich the web3 ecosystem. A place to show off your builds and meet other builders. Start crafting your Web3 + portfolio by submitting your DEX, Multisig or SVG NFT build. +

+
+ +
+
+ + + /assets/vault_icon.svg +
+
+ ); +}; diff --git a/packages/nextjs/app/_components/StartBuildingButton.tsx b/packages/nextjs/app/_components/StartBuildingButton.tsx new file mode 100644 index 00000000..de25a28f --- /dev/null +++ b/packages/nextjs/app/_components/StartBuildingButton.tsx @@ -0,0 +1,24 @@ +"use client"; + +import { useCallback } from "react"; +import Link from "next/link"; +import { usePlausible } from "next-plausible"; + +export const StartBuildingButton = ({ firstChallengeId }: { firstChallengeId: string }) => { + const plausible = usePlausible(); + + // TODO: test this later + const handleCtaClick = useCallback(() => { + plausible("cta"); + }, [plausible]); + + return ( + + Start Building on Ethereum + + ); +}; diff --git a/packages/nextjs/app/_data/_hardcoded.ts b/packages/nextjs/app/_data/_hardcoded.ts new file mode 100644 index 00000000..60f5aac2 --- /dev/null +++ b/packages/nextjs/app/_data/_hardcoded.ts @@ -0,0 +1,72 @@ +export type ChallengeData = { + id: string; + label: string; + previewImage: string; + dependencies: string[]; + icon?: string; + externalLink?: { + link: string; + claim: string; + }; +}; + +// TODO: Think where to move this +export const challengesData: ChallengeData[] = [ + { + id: "simple-nft-example", + label: "๐Ÿšฉ Challenge 0: ๐ŸŽŸ Simple NFT Example", + previewImage: "/assets/challenges/simpleNFT.svg", + dependencies: [], + }, + { + id: "decentralized-staking", + label: "๐Ÿšฉ Challenge 1: ๐Ÿ” Decentralized Staking App ", + previewImage: "/assets/challenges/stakingToken.svg", + dependencies: [], + }, + { + id: "token-vendor", + label: "๐Ÿšฉ Challenge 2: ๐Ÿต Token Vendor", + icon: "/assets/key_icon.svg", + previewImage: "/assets/challenges/tokenVendor.svg", + dependencies: [], + }, + { + id: "dice-game", + label: "๐Ÿšฉ Challenge 3: ๐ŸŽฒ Dice Game", + previewImage: "/assets/challenges/diceGame.svg", + dependencies: ["simple-nft-example", "decentralized-staking", "token-vendor"], + }, + { + id: "minimum-viable-exchange", + label: "๐Ÿšฉ Challenge 4: โš–๏ธ Build a DEX", + previewImage: "assets/challenges/dex.svg", + dependencies: ["simple-nft-example", "decentralized-staking", "token-vendor", "dice-game"], + }, + { + id: "state-channels", + label: "๐Ÿšฉ Challenge 5: ๐Ÿ“บ A State Channel Application", + previewImage: "assets/challenges/state.svg", + dependencies: ["simple-nft-example", "decentralized-staking", "token-vendor", "dice-game"], + }, + { + id: "multisig", + label: "๐Ÿ‘› Multisig Wallet Challenge", + previewImage: "assets/challenges/multiSig.svg", + dependencies: ["simple-nft-example", "decentralized-staking", "token-vendor", "dice-game"], + externalLink: { + link: "https://t.me/+zKllN8OlGuxmYzFh", + claim: "Join the ๐Ÿ‘› Multisig Build cohort", + }, + }, + { + id: "svg-nft", + label: "๐ŸŽ SVG NFT ๐ŸŽซ Challenge", + previewImage: "assets/challenges/dynamicSvgNFT.svg", + dependencies: ["simple-nft-example", "decentralized-staking", "token-vendor", "dice-game"], + externalLink: { + link: "https://t.me/+mUeITJ5u7Ig0ZWJh", + claim: "Join the ๐ŸŽ SVG NFT ๐ŸŽซ Building Cohort", + }, + }, +]; diff --git a/packages/nextjs/app/api/challenges/[challengeId]/submit/route.ts b/packages/nextjs/app/api/challenges/[challengeId]/submit/route.ts index 5e5c00a2..2e442b6f 100644 --- a/packages/nextjs/app/api/challenges/[challengeId]/submit/route.ts +++ b/packages/nextjs/app/api/challenges/[challengeId]/submit/route.ts @@ -1,4 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; +import { EventType, ReviewAction } from "~~/services/database/config/types"; import { createEvent } from "~~/services/database/repositories/events"; import { upsertUserChallenge } from "~~/services/database/repositories/userChallenges"; import { findUserByAddress } from "~~/services/database/repositories/users"; @@ -70,12 +71,12 @@ export async function POST(req: NextRequest, { params }: { params: { challengeId challengeId, frontendUrl, contractUrl, - reviewAction: gradingResult.success ? "ACCEPTED" : "REJECTED", + reviewAction: gradingResult.success ? ReviewAction.ACCEPTED : ReviewAction.REJECTED, reviewComment: gradingResult.feedback, }); await createEvent({ - eventType: "CHALLENGE_AUTOGRADE", + eventType: EventType.CHALLENGE_AUTOGRADE, userAddress: userAddress, signature: signature, payload: { diff --git a/packages/nextjs/app/layout.tsx b/packages/nextjs/app/layout.tsx index b8d2d47d..dc994e86 100644 --- a/packages/nextjs/app/layout.tsx +++ b/packages/nextjs/app/layout.tsx @@ -1,14 +1,24 @@ +import { Space_Grotesk } from "next/font/google"; import "@rainbow-me/rainbowkit/styles.css"; +import PlausibleProvider from "next-plausible"; import { ScaffoldEthAppWithProviders } from "~~/components/ScaffoldEthAppWithProviders"; import { ThemeProvider } from "~~/components/ThemeProvider"; import "~~/styles/globals.css"; import { getMetadata } from "~~/utils/scaffold-eth/getMetadata"; -export const metadata = getMetadata({ title: "Scaffold-ETH 2 App", description: "Built with ๐Ÿ— Scaffold-ETH 2" }); +const spaceGrotesk = Space_Grotesk({ + subsets: ["latin"], + weight: ["400", "500", "700"], +}); + +export const metadata = getMetadata({ title: "Speed Run Ethereum", description: "Built with ๐Ÿ— Scaffold-ETH 2" }); const ScaffoldEthApp = ({ children }: { children: React.ReactNode }) => { return ( - + + + + {children} diff --git a/packages/nextjs/app/page.tsx b/packages/nextjs/app/page.tsx index 42154cf9..0b8cb70b 100644 --- a/packages/nextjs/app/page.tsx +++ b/packages/nextjs/app/page.tsx @@ -1,19 +1,48 @@ -import Link from "next/link"; -import type { NextPage } from "next"; +import { ChallengeExpandedCard } from "./_components/ChallengeExpandedCard"; +import { Hero } from "./_components/Hero"; +import { JoinBGCard } from "./_components/JoinBGCard"; +import { NextPage } from "next"; import { getAllChallenges } from "~~/services/database/repositories/challenges"; +import { findUserChallengesByAddress } from "~~/services/database/repositories/userChallenges"; +import { findUserByAddress } from "~~/services/database/repositories/users"; const Home: NextPage = async () => { + const user = (await findUserByAddress("0x45334F41aAA464528CD5bc0F582acadC49Eb0Cd1"))[0]; const challenges = await getAllChallenges(); + + const userChallenges = (await findUserChallengesByAddress(user.userAddress)).filter( + userChallenge => userChallenge.reviewAction, + ); + return ( - <> -
- {challenges.map(challenge => ( - - {challenge.id} - - ))} +
+ +
+ + + + + + + + + + +
- +
); }; diff --git a/packages/nextjs/components/Footer.tsx b/packages/nextjs/components/Footer.tsx index 92b3c62d..df0c27d3 100644 --- a/packages/nextjs/components/Footer.tsx +++ b/packages/nextjs/components/Footer.tsx @@ -1,19 +1,17 @@ import React from "react"; import Link from "next/link"; import { hardhat } from "viem/chains"; -import { CurrencyDollarIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline"; +import { MagnifyingGlassIcon } from "@heroicons/react/24/outline"; import { HeartIcon } from "@heroicons/react/24/outline"; import { SwitchTheme } from "~~/components/SwitchTheme"; import { BuidlGuidlLogo } from "~~/components/assets/BuidlGuidlLogo"; import { Faucet } from "~~/components/scaffold-eth"; import { useTargetNetwork } from "~~/hooks/scaffold-eth/useTargetNetwork"; -import { useGlobalState } from "~~/services/store/store"; /** * Site footer */ export const Footer = () => { - const nativeCurrencyPrice = useGlobalState(state => state.nativeCurrency.price); const { targetNetwork } = useTargetNetwork(); const isLocalNetwork = targetNetwork.id === hardhat.id; @@ -22,14 +20,6 @@ export const Footer = () => {
- {nativeCurrencyPrice > 0 && ( -
-
- - {nativeCurrencyPrice.toFixed(2)} -
-
- )} {isLocalNetwork && ( <> @@ -47,7 +37,12 @@ export const Footer = () => {
    diff --git a/packages/nextjs/components/Header.tsx b/packages/nextjs/components/Header.tsx index 0981bc9a..8e020f61 100644 --- a/packages/nextjs/components/Header.tsx +++ b/packages/nextjs/components/Header.tsx @@ -1,11 +1,10 @@ "use client"; import React, { useCallback, useRef, useState } from "react"; -import Image from "next/image"; import Link from "next/link"; import { usePathname } from "next/navigation"; -import { Bars3Icon, BugAntIcon } from "@heroicons/react/24/outline"; -import { FaucetButton, RainbowKitCustomConnectButton } from "~~/components/scaffold-eth"; +import { Bars3Icon } from "@heroicons/react/24/outline"; +import { RainbowKitCustomConnectButton } from "~~/components/scaffold-eth"; import { useOutsideClick } from "~~/hooks/scaffold-eth"; type HeaderMenuLink = { @@ -14,16 +13,11 @@ type HeaderMenuLink = { icon?: React.ReactNode; }; +// TODO: Hardcoded. Fix later export const menuLinks: HeaderMenuLink[] = [ { - label: "Home", - href: "/", - }, - - { - label: "Debug Contracts", - href: "/debug", - icon: , + label: "Portfolio", + href: "/portfolio", }, ]; @@ -41,7 +35,7 @@ export const HeaderMenuLinks = () => { passHref className={`${ isActive ? "bg-secondary shadow-md" : "" - } hover:bg-secondary hover:shadow-md focus:!bg-secondary active:!text-neutral py-1.5 px-3 text-sm rounded-full gap-2 grid grid-flow-col`} + } hover:bg-secondary hover:shadow-md focus:!bg-secondary active:!text-neutral py-1.5 lg:py-2 px-3 lg:px-4 text-sm rounded-full gap-2 grid grid-flow-col`} > {icon} {label} @@ -65,7 +59,7 @@ export const Header = () => { ); return ( -
    +
    - -
    - SE2 logo -
    -
    - Scaffold-ETH - Ethereum dev stack -
    -
    -
    ); diff --git a/packages/nextjs/components/scaffold-eth/RainbowKitCustomConnectButton/AddressInfoDropdown.tsx b/packages/nextjs/components/scaffold-eth/RainbowKitCustomConnectButton/AddressInfoDropdown.tsx index a9e31251..06937df2 100644 --- a/packages/nextjs/components/scaffold-eth/RainbowKitCustomConnectButton/AddressInfoDropdown.tsx +++ b/packages/nextjs/components/scaffold-eth/RainbowKitCustomConnectButton/AddressInfoDropdown.tsx @@ -6,7 +6,6 @@ import { Address } from "viem"; import { useDisconnect } from "wagmi"; import { ArrowLeftOnRectangleIcon, - ArrowTopRightOnSquareIcon, ArrowsRightLeftIcon, CheckCircleIcon, ChevronDownIcon, @@ -30,6 +29,7 @@ export const AddressInfoDropdown = ({ address, ensAvatar, displayName, + // eslint-disable-next-line @typescript-eslint/no-unused-vars blockExplorerAddressLink, }: AddressInfoDropdownProps) => { const { disconnect } = useDisconnect(); @@ -48,9 +48,12 @@ export const AddressInfoDropdown = ({ return ( <>
    - + - + {isENS(displayName) ? displayName : checkSumAddress?.slice(0, 6) + "..." + checkSumAddress?.slice(-4)} @@ -95,19 +98,6 @@ export const AddressInfoDropdown = ({ View QR Code -
  • - -
  • {allowedNetworks.length > 1 ? (
  • ); @@ -50,7 +51,11 @@ export const RainbowKitCustomConnectButton = () => { if (!user) { return ( - ); @@ -58,12 +63,6 @@ export const RainbowKitCustomConnectButton = () => { return ( <> -
    - - - {chain.name} - -
    + + + + + + + + + + + + + + + diff --git a/packages/nextjs/public/assets/bgBanner_castlePlatform.svg b/packages/nextjs/public/assets/bgBanner_castlePlatform.svg new file mode 100644 index 00000000..569cae5b --- /dev/null +++ b/packages/nextjs/public/assets/bgBanner_castlePlatform.svg @@ -0,0 +1,177 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/nextjs/public/assets/bgBanner_joinBgClouds.svg b/packages/nextjs/public/assets/bgBanner_joinBgClouds.svg new file mode 100644 index 00000000..d2fd573e --- /dev/null +++ b/packages/nextjs/public/assets/bgBanner_joinBgClouds.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/packages/nextjs/public/assets/challenges/dex.svg b/packages/nextjs/public/assets/challenges/dex.svg new file mode 100644 index 00000000..b3f64b41 --- /dev/null +++ b/packages/nextjs/public/assets/challenges/dex.svg @@ -0,0 +1,212 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/nextjs/public/assets/challenges/diceGame.svg b/packages/nextjs/public/assets/challenges/diceGame.svg new file mode 100644 index 00000000..d2c746d0 --- /dev/null +++ b/packages/nextjs/public/assets/challenges/diceGame.svg @@ -0,0 +1,338 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/nextjs/public/assets/challenges/dynamicSvgNFT.svg b/packages/nextjs/public/assets/challenges/dynamicSvgNFT.svg new file mode 100644 index 00000000..b99b0f32 --- /dev/null +++ b/packages/nextjs/public/assets/challenges/dynamicSvgNFT.svg @@ -0,0 +1,217 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/nextjs/public/assets/challenges/multiSig.svg b/packages/nextjs/public/assets/challenges/multiSig.svg new file mode 100644 index 00000000..c7f931f1 --- /dev/null +++ b/packages/nextjs/public/assets/challenges/multiSig.svg @@ -0,0 +1,378 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/nextjs/public/assets/challenges/simpleNFT.svg b/packages/nextjs/public/assets/challenges/simpleNFT.svg new file mode 100644 index 00000000..2d2a773d --- /dev/null +++ b/packages/nextjs/public/assets/challenges/simpleNFT.svg @@ -0,0 +1,353 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/nextjs/public/assets/challenges/stakingToken.svg b/packages/nextjs/public/assets/challenges/stakingToken.svg new file mode 100644 index 00000000..fa38d775 --- /dev/null +++ b/packages/nextjs/public/assets/challenges/stakingToken.svg @@ -0,0 +1,196 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/nextjs/public/assets/challenges/state.svg b/packages/nextjs/public/assets/challenges/state.svg new file mode 100644 index 00000000..8a212db9 --- /dev/null +++ b/packages/nextjs/public/assets/challenges/state.svg @@ -0,0 +1,233 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/nextjs/public/assets/challenges/tokenVendor.svg b/packages/nextjs/public/assets/challenges/tokenVendor.svg new file mode 100644 index 00000000..183f8526 --- /dev/null +++ b/packages/nextjs/public/assets/challenges/tokenVendor.svg @@ -0,0 +1,593 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/nextjs/public/assets/header_platform.svg b/packages/nextjs/public/assets/header_platform.svg new file mode 100644 index 00000000..15778dbc --- /dev/null +++ b/packages/nextjs/public/assets/header_platform.svg @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/nextjs/public/assets/home_header_clouds.svg b/packages/nextjs/public/assets/home_header_clouds.svg new file mode 100644 index 00000000..ac339df5 --- /dev/null +++ b/packages/nextjs/public/assets/home_header_clouds.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/nextjs/public/assets/key_icon.svg b/packages/nextjs/public/assets/key_icon.svg new file mode 100644 index 00000000..50f84ceb --- /dev/null +++ b/packages/nextjs/public/assets/key_icon.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/nextjs/public/assets/vault_icon.svg b/packages/nextjs/public/assets/vault_icon.svg new file mode 100644 index 00000000..4ad7cdaf --- /dev/null +++ b/packages/nextjs/public/assets/vault_icon.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/nextjs/public/favicon.ico b/packages/nextjs/public/favicon.ico new file mode 100644 index 00000000..e803b933 Binary files /dev/null and b/packages/nextjs/public/favicon.ico differ diff --git a/packages/nextjs/public/favicon.png b/packages/nextjs/public/favicon.png deleted file mode 100644 index 4bef7f2f..00000000 Binary files a/packages/nextjs/public/favicon.png and /dev/null differ diff --git a/packages/nextjs/public/thumbnail.jpg b/packages/nextjs/public/thumbnail.jpg deleted file mode 100644 index a3bf231f..00000000 Binary files a/packages/nextjs/public/thumbnail.jpg and /dev/null differ diff --git a/packages/nextjs/public/thumbnail.png b/packages/nextjs/public/thumbnail.png new file mode 100644 index 00000000..397c1dd0 Binary files /dev/null and b/packages/nextjs/public/thumbnail.png differ diff --git a/packages/nextjs/services/database/config/schema.ts b/packages/nextjs/services/database/config/schema.ts index b652a630..d2a24c3a 100644 --- a/packages/nextjs/services/database/config/schema.ts +++ b/packages/nextjs/services/database/config/schema.ts @@ -1,7 +1,9 @@ +import { EventType, ReviewAction, UserRole } from "./types"; import { SQL, relations, sql } from "drizzle-orm"; import { AnyPgColumn, boolean, + integer, jsonb, pgEnum, pgTable, @@ -17,15 +19,23 @@ export function lower(address: AnyPgColumn): SQL { return sql`lower(${address})`; } -export const reviewActionEnum = pgEnum("review_action_enum", ["REJECTED", "ACCEPTED", "SUBMITTED"]); -export const eventTypeEnum = pgEnum("event_type_enum", ["CHALLENGE_SUBMIT", "CHALLENGE_AUTOGRADE", "USER_CREATE"]); -export const userRoleEnum = pgEnum("user_role_enum", ["USER", "ADMIN"]); +export const reviewActionEnum = pgEnum("review_action_enum", [ + ReviewAction.REJECTED, + ReviewAction.ACCEPTED, + ReviewAction.SUBMITTED, +]); +export const eventTypeEnum = pgEnum("event_type_enum", [ + EventType.CHALLENGE_SUBMIT, + EventType.CHALLENGE_AUTOGRADE, + EventType.USER_CREATE, +]); +export const userRoleEnum = pgEnum("user_role_enum", [UserRole.USER, UserRole.ADMIN]); export const users = pgTable( "users", { userAddress: varchar({ length: 42 }).primaryKey(), // Ethereum wallet address - role: userRoleEnum().default("USER"), // Using the enum and setting default + role: userRoleEnum().default(UserRole.USER), // Using the enum and setting default createdAt: timestamp().defaultNow(), socialTelegram: varchar({ length: 255 }), socialTwitter: varchar({ length: 255 }), @@ -40,8 +50,11 @@ export const users = pgTable( export const challenges = pgTable("challenges", { id: varchar({ length: 255 }).primaryKey(), // Unique identifier for the challenge challengeName: varchar({ length: 255 }).notNull(), + description: text().notNull(), + sortOrder: integer().notNull(), github: varchar({ length: 255 }), // Repository reference for the challenge autograding: boolean().default(false), // Whether the challenge supports automatic grading + disabled: boolean().default(false), }); export const userChallenges = pgTable( diff --git a/packages/nextjs/services/database/config/types.ts b/packages/nextjs/services/database/config/types.ts new file mode 100644 index 00000000..6caa9b4a --- /dev/null +++ b/packages/nextjs/services/database/config/types.ts @@ -0,0 +1,17 @@ +// Types to import from both schema/db and client +export enum ReviewAction { + REJECTED = "REJECTED", + ACCEPTED = "ACCEPTED", + SUBMITTED = "SUBMITTED", +} + +export enum EventType { + CHALLENGE_SUBMIT = "CHALLENGE_SUBMIT", + CHALLENGE_AUTOGRADE = "CHALLENGE_AUTOGRADE", + USER_CREATE = "USER_CREATE", +} + +export enum UserRole { + USER = "USER", + ADMIN = "ADMIN", +} diff --git a/packages/nextjs/services/database/repositories/challenges.ts b/packages/nextjs/services/database/repositories/challenges.ts index 57d00c14..61b68244 100644 --- a/packages/nextjs/services/database/repositories/challenges.ts +++ b/packages/nextjs/services/database/repositories/challenges.ts @@ -1,7 +1,10 @@ -import { eq } from "drizzle-orm"; +import { InferInsertModel, eq } from "drizzle-orm"; import { db } from "~~/services/database/config/postgresClient"; import { challenges } from "~~/services/database/config/schema"; +export type ChallengeInsert = InferInsertModel; +export type Challenges = Awaited>; + export async function getChallengeById(id: string) { const result = await db.select().from(challenges).where(eq(challenges.id, id)); return result[0]; diff --git a/packages/nextjs/services/database/seed.data.ts b/packages/nextjs/services/database/seed.data.ts index de9cf9ac..86d43f34 100644 --- a/packages/nextjs/services/database/seed.data.ts +++ b/packages/nextjs/services/database/seed.data.ts @@ -1,25 +1,26 @@ import { challenges, events, userChallenges, users } from "./config/schema"; +import { EventType, ReviewAction, UserRole } from "./config/types"; // Using Drizzle's inferred insert types to ensure seed data // matches database schema requirements export const seedUsers: (typeof users.$inferInsert)[] = [ { userAddress: "0xB4F53bd85c00EF22946d24Ae26BC38Ac64F5E7B1", - role: "USER", + role: UserRole.USER, createdAt: new Date(1679063274534), socialTwitter: "pabl0cks", socialTelegram: "pabl0cks", }, { userAddress: "0x000084821704d731438d2D06f4295e1AB0ace7D8", - role: "USER", + role: UserRole.USER, createdAt: new Date(1664777161512), socialEmail: "ryuufarhan7@gmail.com", socialTwitter: "FarhanRyuu", }, { userAddress: "0x014EC6296B3493f0f59a3FE90E0FFf377fb8826a", - role: "USER", + role: UserRole.USER, createdAt: new Date(1672731143934), socialEmail: "gokulkesavan5005@gmail.com", socialTwitter: "meta_Goku", @@ -27,12 +28,19 @@ export const seedUsers: (typeof users.$inferInsert)[] = [ }, { userAddress: "0x01B2686Bd146bFc3F4B3DD6F7F86f26ac7c2f7Fd", - role: "USER", + role: UserRole.USER, createdAt: new Date(1668050419502), socialEmail: "afo@wefa.app", socialTwitter: "Time_Is_Oba", socialGithub: "Oba-One", }, + { + userAddress: "0x45334F41aAA464528CD5bc0F582acadC49Eb0Cd1", + role: UserRole.USER, + createdAt: new Date(1679063274534), + socialTwitter: "rinat_eth", + socialTelegram: "rinat_eth", + }, ]; export const seedChallenges: (typeof challenges.$inferInsert)[] = [ @@ -41,48 +49,72 @@ export const seedChallenges: (typeof challenges.$inferInsert)[] = [ challengeName: "Simple NFT Example", github: "scaffold-eth/se-2-challenges:challenge-0-simple-nft", autograding: true, + description: + "๐ŸŽซ Create a simple NFT to learn basics of ๐Ÿ— scaffold-eth. You'll use ๐Ÿ‘ทโ€โ™€๏ธ HardHat to compile and deploy smart contracts. Then, you'll use a template React app full of important Ethereum components and hooks. Finally, you'll deploy an NFT to a public network to share with friends! ๐Ÿš€", + sortOrder: 0, }, { id: "decentralized-staking", challengeName: "Decentralized Staking App", github: "scaffold-eth/se-2-challenges:challenge-1-decentralized-staking", autograding: true, + description: + "๐Ÿฆธ A superpower of Ethereum is allowing you, the builder, to create a simple set of rules that an adversarial group of players can use to work together. In this challenge, you create a decentralized application where users can coordinate a group funding effort. The users only have to trust the code.", + sortOrder: 1, }, { id: "token-vendor", challengeName: "Token Vendor", github: "scaffold-eth/se-2-challenges:challenge-2-token-vendor", autograding: true, + description: + '๐Ÿค– Smart contracts are kind of like "always on" vending machines that anyone can access. Let\'s make a decentralized, digital currency (an ERC20 token). Then, let\'s build an unstoppable vending machine that will buy and sell the currency. We\'ll learn about the "approve" pattern for ERC20s and how contract to contract interactions work.', + sortOrder: 2, }, { id: "dice-game", challengeName: "Dice Game", github: "scaffold-eth/se-2-challenges:challenge-3-dice-game", autograding: true, + description: + "๐ŸŽฐ Randomness is tricky on a public deterministic blockchain. The block hash is the result proof-of-work (for now) and some builders use this as a weak form of randomness. In this challenge you will take advantage of a Dice Game contract by predicting the randomness in order to only roll winning dice!", + sortOrder: 3, }, { id: "minimum-viable-exchange", challengeName: "Build a DEX", github: "scaffold-eth/se-2-challenges:challenge-4-dex", autograding: true, + description: + "๐Ÿ’ต Build an exchange that swaps ETH to tokens and tokens to ETH. ๐Ÿ’ฐ This is possible because the smart contract holds reserves of both assets and has a price function based on the ratio of the reserves. Liquidity providers are issued a token that represents their share of the reserves and fees...", + sortOrder: 4, }, { id: "state-channels", challengeName: "A State Channel Application", github: "scaffold-eth/se-2-challenges:challenge-5-state-channels", autograding: true, + description: + "๐Ÿ›ฃ๏ธ The Ethereum blockchain has great decentralization & security properties but these properties come at a price: transaction throughput is low, and transactions can be expensive. This makes many traditional web applications infeasible on a blockchain... or does it? State channels look to solve these problems by allowing participants to securely transact off-chain while keeping interaction with Ethereum Mainnet at a minimum.", + sortOrder: 5, }, { id: "multisig", challengeName: "Multisig Wallet", github: "scaffold-eth/se-2-challenges:challenge-6-multisig", autograding: false, + description: + '๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ง Using a smart contract as a wallet we can secure assets by requiring multiple accounts to "vote" on transactions. The contract will keep track of transactions in an array of structs and owners will confirm or reject each one. Any transaction with enough confirmations can "execute".', + sortOrder: 6, }, { id: "svg-nft", challengeName: "SVG NFT", github: "scaffold-eth/se-2-challenges:challenge-7-svg-nft", autograding: false, + description: + "๐ŸŽจ Create a dynamic SVG NFT using a smart contract. Your contract will generate on-chain SVG images and allow users to mint their unique NFTs. โœจ Customize your SVG graphics and metadata directly within the smart contract. ๐Ÿš€ Share the minting URL once your project is live!", + sortOrder: 7, }, ]; @@ -94,7 +126,7 @@ export const seedUserChallenges: (typeof userChallenges.$inferInsert)[] = [ contractUrl: "https://sepolia.etherscan.io/address/0x7f918d7b7d0fe0d3a8de3c0570ed4e154c0096e0", reviewComment: "Dummy review, nice work", submittedAt: new Date(1679063312936), - reviewAction: "ACCEPTED", + reviewAction: ReviewAction.ACCEPTED, }, { userAddress: "0xB4F53bd85c00EF22946d24Ae26BC38Ac64F5E7B1", @@ -103,7 +135,7 @@ export const seedUserChallenges: (typeof userChallenges.$inferInsert)[] = [ contractUrl: "https://sepolia.etherscan.io/address/0xd08b984c3ee4a880112d81de5a4a074c857b7f2f", reviewComment: "Dummy review, it's working great", submittedAt: new Date(1679359244796), - reviewAction: "ACCEPTED", + reviewAction: ReviewAction.ACCEPTED, }, { userAddress: "0xB4F53bd85c00EF22946d24Ae26BC38Ac64F5E7B1", @@ -112,7 +144,7 @@ export const seedUserChallenges: (typeof userChallenges.$inferInsert)[] = [ contractUrl: "https://sepolia.etherscan.io/address/0xbF35fC995A2Cc4F1508B5F769922623dE7f220d6", reviewComment: "Dummy review, it's working great", submittedAt: new Date(1679185806311), - reviewAction: "ACCEPTED", + reviewAction: ReviewAction.ACCEPTED, }, { userAddress: "0xB4F53bd85c00EF22946d24Ae26BC38Ac64F5E7B1", @@ -121,7 +153,7 @@ export const seedUserChallenges: (typeof userChallenges.$inferInsert)[] = [ contractUrl: "https://sepolia.etherscan.io/address/0x57D312c3E4bF22F22d6A900Be8B38b5546b47C3f", reviewComment: "Dummy review, it's working great", submittedAt: new Date(1679317471852), - reviewAction: "ACCEPTED", + reviewAction: ReviewAction.ACCEPTED, }, { userAddress: "0x000084821704d731438d2D06f4295e1AB0ace7D8", @@ -130,7 +162,7 @@ export const seedUserChallenges: (typeof userChallenges.$inferInsert)[] = [ contractUrl: "https://goerli.etherscan.io/address/0x861346d67b728949bcb69595638a78723a2adae3", reviewComment: "Dummy review, nice work", submittedAt: new Date(1664778633262), - reviewAction: "ACCEPTED", + reviewAction: ReviewAction.ACCEPTED, }, { userAddress: "0x014EC6296B3493f0f59a3FE90E0FFf377fb8826a", @@ -139,7 +171,7 @@ export const seedUserChallenges: (typeof userChallenges.$inferInsert)[] = [ contractUrl: "https://goerli.etherscan.io/address/0xe3DF14f1482074916A8Aeb40d84898C879b2B5f6", reviewComment: "Dummy review, nice work", submittedAt: new Date(1672831270556), - reviewAction: "ACCEPTED", + reviewAction: ReviewAction.ACCEPTED, }, { userAddress: "0x014EC6296B3493f0f59a3FE90E0FFf377fb8826a", @@ -148,7 +180,7 @@ export const seedUserChallenges: (typeof userChallenges.$inferInsert)[] = [ contractUrl: "https://goerli.etherscan.io/address/0x71F80197032c9a07b966D3f8eCAFE601Af244F35", reviewComment: "Dummy review, it's working great", submittedAt: new Date(1673150820763), - reviewAction: "ACCEPTED", + reviewAction: ReviewAction.ACCEPTED, }, { userAddress: "0x014EC6296B3493f0f59a3FE90E0FFf377fb8826a", @@ -157,7 +189,7 @@ export const seedUserChallenges: (typeof userChallenges.$inferInsert)[] = [ contractUrl: "https://goerli.etherscan.io/address/0xEd2aF24B1a657000C9b4509BC91FCfA5E76D3d33", reviewComment: "Dummy review, it's working great", submittedAt: new Date(1672974794575), - reviewAction: "ACCEPTED", + reviewAction: ReviewAction.ACCEPTED, }, { userAddress: "0x014EC6296B3493f0f59a3FE90E0FFf377fb8826a", @@ -166,7 +198,7 @@ export const seedUserChallenges: (typeof userChallenges.$inferInsert)[] = [ contractUrl: "https://goerli.etherscan.io/address/0xf3384118b56827271979A879e2d5A4d28569eb48", reviewComment: "Dummy review, it's working great", submittedAt: new Date(1672830084301), - reviewAction: "ACCEPTED", + reviewAction: ReviewAction.ACCEPTED, }, { userAddress: "0x014EC6296B3493f0f59a3FE90E0FFf377fb8826a", @@ -175,7 +207,7 @@ export const seedUserChallenges: (typeof userChallenges.$inferInsert)[] = [ contractUrl: "https://goerli.etherscan.io/address/0x0752d3847601b506cBFB10330172821B495e6bB0", reviewComment: "Dummy review, it's working great", submittedAt: new Date(1673006384070), - reviewAction: "ACCEPTED", + reviewAction: ReviewAction.ACCEPTED, }, { userAddress: "0x014EC6296B3493f0f59a3FE90E0FFf377fb8826a", @@ -184,7 +216,7 @@ export const seedUserChallenges: (typeof userChallenges.$inferInsert)[] = [ contractUrl: "https://goerli.etherscan.io/address/0x9626C68Dd8d000BBB7B06f0782266976dEB91c35", reviewComment: "Dummy review, it's working great", submittedAt: new Date(1672819901984), - reviewAction: "ACCEPTED", + reviewAction: ReviewAction.ACCEPTED, }, { userAddress: "0x01B2686Bd146bFc3F4B3DD6F7F86f26ac7c2f7Fd", @@ -193,7 +225,7 @@ export const seedUserChallenges: (typeof userChallenges.$inferInsert)[] = [ contractUrl: "https://goerli.etherscan.io/address/0xb9487f8d9E336a9468fcbb50dAF39587D0EBCA63", reviewComment: "Dummy review, nice work", submittedAt: new Date(1668196973527), - reviewAction: "ACCEPTED", + reviewAction: ReviewAction.ACCEPTED, }, { userAddress: "0x01B2686Bd146bFc3F4B3DD6F7F86f26ac7c2f7Fd", @@ -202,13 +234,69 @@ export const seedUserChallenges: (typeof userChallenges.$inferInsert)[] = [ contractUrl: "https://goerli.etherscan.io/address/0xd68edd04EbB81c6f187A526F3656C18dD0258cd8", reviewComment: "Dummy review, it's working great", submittedAt: new Date(1668830120864), - reviewAction: "ACCEPTED", + reviewAction: ReviewAction.ACCEPTED, + }, + { + userAddress: "0x45334F41aAA464528CD5bc0F582acadC49Eb0Cd1", + challengeId: "token-vendor", + frontendUrl: "https://sepolia-optimism.etherscan.io/address/0xad5e878a62D5B77277aCDC321614a9727815B4C8#code", + contractUrl: "https://sepolia-optimism.etherscan.io/address/0xad5e878a62D5B77277aCDC321614a9727815B4C8#code", + submittedAt: new Date(1736441361988), + reviewComment: + "

    You have successfully passed challenge 2!

    You have passed the first three challenges on SpeedRunEthereum and can now join the BuidlGuidl!

    ", + reviewAction: ReviewAction.REJECTED, + }, + { + userAddress: "0x45334F41aAA464528CD5bc0F582acadC49Eb0Cd1", + challengeId: "simple-nft-example", + frontendUrl: "https://sepolia-optimism.etherscan.io/address/0x82b4935ebe7a5d802cf465a3495da1aff96f1153#code", + contractUrl: "https://sepolia-optimism.etherscan.io/address/0x82b4935ebe7a5d802cf465a3495da1aff96f1153#code", + submittedAt: new Date(1736437841322), + reviewComment: "

    You passed all tests on Challenge 0, keep it up!

    ", + reviewAction: ReviewAction.ACCEPTED, + }, + { + userAddress: "0x45334F41aAA464528CD5bc0F582acadC49Eb0Cd1", + challengeId: "decentralized-staking", + frontendUrl: "https://sepolia-optimism.etherscan.io/address/0x75CCfC494667c91F4926213A04619f93812885b2#code", + contractUrl: "https://sepolia-optimism.etherscan.io/address/0x75CCfC494667c91F4926213A04619f93812885b2#code", + submittedAt: new Date(1736440199150), + reviewComment: "

    You passed all tests on Challenge 1, keep it up!

    ", + reviewAction: ReviewAction.SUBMITTED, + }, + { + userAddress: "0x45334F41aAA464528CD5bc0F582acadC49Eb0Cd1", + challengeId: "dice-game", + frontendUrl: "https://sepolia-optimism.etherscan.io/address/0xB30b4D0AD811De445656FA49d3CbfD26f24Fa20f#code", + contractUrl: "https://sepolia-optimism.etherscan.io/address/0xB30b4D0AD811De445656FA49d3CbfD26f24Fa20f#code", + submittedAt: new Date(1736455465938), + reviewComment: + "

    This looks good! Demo site and contract code are solid and the dice only roll when it's a winner!

    ", + reviewAction: ReviewAction.ACCEPTED, + }, + { + userAddress: "0x45334F41aAA464528CD5bc0F582acadC49Eb0Cd1", + challengeId: "minimum-viable-exchange", + frontendUrl: "https://sepolia-optimism.etherscan.io/address/0xFD893C93f44fdCd16D4673072D2a071578e6C7Ee#code", + contractUrl: "https://sepolia-optimism.etherscan.io/address/0xFD893C93f44fdCd16D4673072D2a071578e6C7Ee#code", + submittedAt: new Date(1736523426599), + reviewComment: "

    You have successfully passed the Dex Challenge! Great work!

    ", + reviewAction: ReviewAction.ACCEPTED, + }, + { + userAddress: "0x45334F41aAA464528CD5bc0F582acadC49Eb0Cd1", + challengeId: "state-channels", + frontendUrl: "https://sepolia-optimism.etherscan.io/address/0x1BC437381EC1A36Eb30A0F3AfEFF1Fd5E0078256#code", + contractUrl: "https://sepolia-optimism.etherscan.io/address/0x1BC437381EC1A36Eb30A0F3AfEFF1Fd5E0078256#code", + submittedAt: new Date(1736524737219), + reviewComment: "

    You have successfully passed the State Channel Challenge! Great work!

    ", + reviewAction: ReviewAction.ACCEPTED, }, ]; export const seedEvents: (typeof events.$inferInsert)[] = [ { - eventType: "CHALLENGE_SUBMIT", + eventType: EventType.CHALLENGE_SUBMIT, eventAt: new Date(1679063312936), signature: "0x7a808ee181d8655f38c48e0154903bea229b91e32f6062f6c23ad9da5fa25c3008238b07e367e7904200015157a43378b39244140c43ea35e5eea11c055a170500", @@ -220,7 +308,7 @@ export const seedEvents: (typeof events.$inferInsert)[] = [ }, }, { - eventType: "CHALLENGE_AUTOGRADE", + eventType: EventType.CHALLENGE_AUTOGRADE, eventAt: new Date(1679063320289), signature: "0x7a808ee181d8655f38c48e0154903bea229b91e32f6062f6c23ad9da5fa25c3008238b07e367e7904200015157a43378b39244140c43ea35e5eea11c055a170500", @@ -228,12 +316,12 @@ export const seedEvents: (typeof events.$inferInsert)[] = [ payload: { autograding: true, challengeId: "simple-nft-example", - reviewAction: "ACCEPTED", + reviewAction: ReviewAction.ACCEPTED, reviewMessage: "Dummy review, nice work", }, }, { - eventType: "USER_CREATE", + eventType: EventType.USER_CREATE, eventAt: new Date(1679063274533), signature: "0xe6d747b3e4760aa9ceb15ae8274366ab010d057136782bdf77ea755c9f148fc85046b2ed24bcd6a39720e6e62db5cfc52b476099126ef6d90985115d494b606a01", @@ -243,7 +331,7 @@ export const seedEvents: (typeof events.$inferInsert)[] = [ }, }, { - eventType: "CHALLENGE_SUBMIT", + eventType: EventType.CHALLENGE_SUBMIT, eventAt: new Date(1672974794575), signature: "0x4eca15955283c15c1f9565945c34638c9943a313a79301d73178b814e82e3e183e3d89aebdafa10a1012912cb820950fb6eea6ba76e58bf875f226bdc8257dc700", @@ -255,7 +343,7 @@ export const seedEvents: (typeof events.$inferInsert)[] = [ }, }, { - eventType: "CHALLENGE_AUTOGRADE", + eventType: EventType.CHALLENGE_AUTOGRADE, eventAt: new Date(1679185806311), signature: "0x78e60403c8f0d5e8094de16f059d169e8e3a92e3a2df940589f2ce9c2b556d5d0dc0230c421df265fa8975f6175d0a7881a58e0e51b31107c6de1ee4a55a9eb601", @@ -268,7 +356,7 @@ export const seedEvents: (typeof events.$inferInsert)[] = [ }, }, { - eventType: "CHALLENGE_AUTOGRADE", + eventType: EventType.CHALLENGE_AUTOGRADE, eventAt: new Date(1679185814845), signature: "0x78e60403c8f0d5e8094de16f059d169e8e3a92e3a2df940589f2ce9c2b556d5d0dc0230c421df265fa8975f6175d0a7881a58e0e51b31107c6de1ee4a55a9eb601", @@ -276,12 +364,12 @@ export const seedEvents: (typeof events.$inferInsert)[] = [ payload: { autograding: true, challengeId: "decentralized-staking", - reviewAction: "ACCEPTED", + reviewAction: ReviewAction.ACCEPTED, reviewMessage: "Dummy review, it's working great", }, }, { - eventType: "CHALLENGE_AUTOGRADE", + eventType: EventType.CHALLENGE_AUTOGRADE, eventAt: new Date(1679317478533), signature: "0x4e681d003b6310a604944bd334966cd10348b283d1797db0f93b6ce55f9c02d149b53e59a331362b7f6468528fe4dfee1cd8d923199fc9949ac8386ec633213a01", @@ -289,12 +377,12 @@ export const seedEvents: (typeof events.$inferInsert)[] = [ payload: { autograding: true, challengeId: "token-vendor", - reviewAction: "ACCEPTED", + reviewAction: ReviewAction.ACCEPTED, reviewMessage: "Dummy review, it's working great", }, }, { - eventType: "CHALLENGE_AUTOGRADE", + eventType: EventType.CHALLENGE_AUTOGRADE, eventAt: new Date(1679359253588), signature: "0x4eca15955283c15c1f9565945c34638c9943a313a79301d73178b814e82e3e183e3d89aebdafa10a1012912cb820950fb6eea6ba76e58bf875f226bdc8257dc700", @@ -302,12 +390,12 @@ export const seedEvents: (typeof events.$inferInsert)[] = [ payload: { autograding: true, challengeId: "dice-game", - reviewAction: "ACCEPTED", + reviewAction: ReviewAction.ACCEPTED, reviewMessage: "Dummy review, it's working great", }, }, { - eventType: "USER_CREATE", + eventType: EventType.USER_CREATE, eventAt: new Date(1664777161511), signature: "0x8d319d59ce02619610376d6ebb2aae2581935d4176ec3ba2228d2b2cab720d260b4d4499f0000ec63d029df5271c10c92ccd68333ddedf506878511f79dd7fad1b", @@ -317,7 +405,7 @@ export const seedEvents: (typeof events.$inferInsert)[] = [ }, }, { - eventType: "CHALLENGE_AUTOGRADE", + eventType: EventType.CHALLENGE_AUTOGRADE, eventAt: new Date(1664778651266), signature: "0xc6d7cd95c4baf16851aec5817b08a07ca52f1682f37548c57173d3432df734d17d4e6d810ce55f151dcc5272ae07991305b8d40b0863b4308d5bd74788b645471c", @@ -325,12 +413,12 @@ export const seedEvents: (typeof events.$inferInsert)[] = [ payload: { autograding: true, challengeId: "simple-nft-example", - reviewAction: "ACCEPTED", + reviewAction: ReviewAction.ACCEPTED, reviewMessage: "Dummy review, nice work", }, }, { - eventType: "USER_CREATE", + eventType: EventType.USER_CREATE, eventAt: new Date(1668050419499), signature: "0x760bf0c7f94d6fdce07a72d07274e36a258ffe1c4386201b8d34ba7e14882c0432fbedfb77259cbf42599b9939db4fe4ba5a73a4deffef10aa99b757f6f5957e1c", @@ -340,7 +428,7 @@ export const seedEvents: (typeof events.$inferInsert)[] = [ }, }, { - eventType: "CHALLENGE_AUTOGRADE", + eventType: EventType.CHALLENGE_AUTOGRADE, eventAt: new Date(1668196991801), signature: "0x03699f6b44e96814d5bac9e4a8a53195216af7b3d4eb44a76206e75ccb7869594755cf526826153335a00e0b140ec3f498db454681bd34d245d801f065b4ccf91c", @@ -348,12 +436,12 @@ export const seedEvents: (typeof events.$inferInsert)[] = [ payload: { autograding: true, challengeId: "simple-nft-example", - reviewAction: "ACCEPTED", + reviewAction: ReviewAction.ACCEPTED, reviewMessage: "Dummy review, nice work", }, }, { - eventType: "CHALLENGE_AUTOGRADE", + eventType: EventType.CHALLENGE_AUTOGRADE, eventAt: new Date(1668830138768), signature: "0x40a5005851b26a87a1ed130da6b1eedcf938dda15703f6d7155db088f91cca42496cd84d53e949d0ecb89ed3ed82db3bae9705c6a91b9198693c646690d9dea01c", @@ -361,7 +449,7 @@ export const seedEvents: (typeof events.$inferInsert)[] = [ payload: { autograding: true, challengeId: "decentralized-staking", - reviewAction: "ACCEPTED", + reviewAction: ReviewAction.ACCEPTED, reviewMessage: "Dummy review, it's working great", }, }, diff --git a/packages/nextjs/tailwind.config.js b/packages/nextjs/tailwind.config.js index 876b557c..65adee3e 100644 --- a/packages/nextjs/tailwind.config.js +++ b/packages/nextjs/tailwind.config.js @@ -8,19 +8,19 @@ module.exports = { themes: [ { light: { - primary: "#93BBFB", - "primary-content": "#212638", - secondary: "#DAE8FF", - "secondary-content": "#212638", - accent: "#93BBFB", - "accent-content": "#212638", - neutral: "#212638", + primary: "#088484", + "primary-content": "#026262", + secondary: "#C8F5FF", + "secondary-content": "#026262", + accent: "#67DDDE", + "accent-content": "#026262", + neutral: "#026262", "neutral-content": "#ffffff", "base-100": "#ffffff", - "base-200": "#f4f8ff", - "base-300": "#DAE8FF", - "base-content": "#212638", - info: "#93BBFB", + "base-200": "#E9FBFF", + "base-300": "#C8F5FF", + "base-content": "#026262", + info: "#2FBABB", success: "#34EEB6", warning: "#FFCF72", error: "#FF8863", @@ -32,19 +32,19 @@ module.exports = { }, { dark: { - primary: "#212638", - "primary-content": "#F9FBFF", - secondary: "#323f61", - "secondary-content": "#F9FBFF", - accent: "#4969A6", - "accent-content": "#F9FBFF", - neutral: "#F9FBFF", - "neutral-content": "#385183", - "base-100": "#385183", - "base-200": "#2A3655", - "base-300": "#212638", - "base-content": "#F9FBFF", - info: "#385183", + primary: "#C8F5FF", + "primary-content": "#C8F5FF", + secondary: "#026262", + "secondary-content": "#C8F5FF", + accent: "#67DDDE", + "accent-content": "#C8F5FF", + neutral: "#E9FBFF", + "neutral-content": "#088484", + "base-100": "#088484", + "base-200": "#026262", + "base-300": "#088484", + "base-content": "#C8F5FF", + info: "#67DDDE", success: "#34EEB6", warning: "#FFCF72", error: "#FF8863", diff --git a/packages/nextjs/utils/dependent-challenges.ts b/packages/nextjs/utils/dependent-challenges.ts new file mode 100644 index 00000000..8282c35f --- /dev/null +++ b/packages/nextjs/utils/dependent-challenges.ts @@ -0,0 +1,45 @@ +import { ReviewAction } from "~~/services/database/config/types"; +import { UserChallenges } from "~~/services/database/repositories/userChallenges"; + +// TODO: update deps later +export const getIsDependencyChallengesCompleted = (deps: { dependencies: string[] }, userChallenges: UserChallenges) => + deps.dependencies?.every(name => { + const userChallenge = userChallenges.find(uc => uc.challengeId === name); + return userChallenge?.reviewAction === ReviewAction.ACCEPTED; + }); + +const getLockReasonTooltip = ({ + dependencies, + userChallenges, +}: { + dependencies?: string[]; + userChallenges: UserChallenges; +}) => { + const pendingDependenciesChallenges = dependencies?.filter(dependency => { + return ( + !userChallenges.find(userChallenge => userChallenge.challengeId === dependency)?.reviewAction || + userChallenges.find(userChallenge => userChallenge.challengeId === dependency)?.reviewAction !== + ReviewAction.ACCEPTED + ); + }); + + if (pendingDependenciesChallenges && pendingDependenciesChallenges.length > 0) { + return "The following challenges are not completed: " + pendingDependenciesChallenges?.join(", "); + } + return ""; +}; + +export const getChallengeDependenciesInfo = ({ + dependencies, + userChallenges, +}: { + dependencies: string[]; + userChallenges: UserChallenges; +}) => { + const lockReasonToolTip = getLockReasonTooltip({ + dependencies, + userChallenges, + }); + + return { completed: !lockReasonToolTip, lockReasonToolTip }; +}; diff --git a/packages/nextjs/utils/scaffold-eth/getMetadata.ts b/packages/nextjs/utils/scaffold-eth/getMetadata.ts index ad531aea..1294a686 100644 --- a/packages/nextjs/utils/scaffold-eth/getMetadata.ts +++ b/packages/nextjs/utils/scaffold-eth/getMetadata.ts @@ -1,9 +1,10 @@ +import Favicon from "/public/favicon.ico"; import type { Metadata } from "next"; const baseUrl = process.env.VERCEL_PROJECT_PRODUCTION_URL ? `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}` : `http://localhost:${process.env.PORT || 3000}`; -const titleTemplate = "%s | Scaffold-ETH 2"; +const titleTemplate = "%s | Speed Run Ethereum"; export const getMetadata = ({ title, @@ -43,8 +44,6 @@ export const getMetadata = ({ description: description, images: [imageUrl], }, - icons: { - icon: [{ url: "/favicon.png", sizes: "32x32", type: "image/png" }], - }, + icons: [{ rel: "icon", url: Favicon.src }], }; }; diff --git a/yarn.lock b/yarn.lock index 1fdad9e8..25a90e42 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2646,6 +2646,7 @@ __metadata: next: ~14.2.11 next-mdx-remote: ^5.0.0 next-nprogress-bar: ~2.3.13 + next-plausible: ^3.12.4 next-themes: ~0.3.0 pg: ^8.13.1 postcss: ~8.4.45 @@ -11647,6 +11648,17 @@ __metadata: languageName: node linkType: hard +"next-plausible@npm:^3.12.4": + version: 3.12.4 + resolution: "next-plausible@npm:3.12.4" + peerDependencies: + next: "^11.1.0 || ^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 " + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + checksum: f8ae32eae3b8f4f45e1bdf1ab1aaf6be9ff184468c90253f4920a0cca01b426610c7ac85d082970e614f6f05b3679b9e68aae13ee5081a3d5bd003baaf11005a + languageName: node + linkType: hard + "next-themes@npm:~0.3.0": version: 0.3.0 resolution: "next-themes@npm:0.3.0"