diff --git a/packages/react-app/src/App.jsx b/packages/react-app/src/App.jsx index eab64028..a3933253 100644 --- a/packages/react-app/src/App.jsx +++ b/packages/react-app/src/App.jsx @@ -1,26 +1,21 @@ import React, { useCallback, useEffect, useRef, useState } from "react"; -import { Switch, Redirect, Route } from "react-router-dom"; +import { Switch, Route } from "react-router-dom"; import { Web3Provider, StaticJsonRpcProvider, InfuraProvider } from "@ethersproject/providers"; import "./App.css"; import Web3Modal from "web3modal"; import WalletConnectProvider from "@walletconnect/web3-provider"; import { useUserAddress } from "eth-hooks"; import axios from "axios"; +import { useIntl } from "react-intl"; import { useUserProvider } from "./hooks"; import { Header, ColorModeSwitcher } from "./components"; import { INFURA_ID, SERVER_URL as serverUrl } from "./constants"; -import { - BuilderListView, - ChallengeDetailView, - BuilderProfileView, - SubmissionReviewView, - HomeView, - ActivityView, -} from "./views"; -import { USER_ROLES } from "./helpers/constants"; +import { SUPPORTED_LANGS, USER_ROLES } from "./helpers/constants"; import { providerPromiseWrapper } from "./helpers/blockchainProviders"; import BlockchainProvidersContext from "./contexts/blockchainProvidersContext"; import SiteFooter from "./components/SiteFooter"; +import Routes from "./Routes"; +import useUrlLang from "./hooks/useUrlLang"; // ๐Ÿ˜ฌ Sorry for all the console logging const DEBUG = true; @@ -41,7 +36,7 @@ const web3Modal = new Web3Modal({ }, }); -function App() { +function App({ setLocale }) { const [providers, setProviders] = useState({ mainnet: { provider: null, isReady: false }, local: { provider: null, isReady: false }, @@ -165,6 +160,12 @@ function App() { } }, [address, fetchUserData]); + const intl = useIntl(); + const { lang: urlLang } = useUrlLang(); + if (urlLang !== intl.locale) { + setLocale(urlLang); + } + return (
@@ -179,41 +180,28 @@ function App() { setUserRole={setUserRole} /> - - - - - {address && } - - - - - - + + - - - - {/* ToDo: Protect this route on the frontend? */} - - - - - -
diff --git a/packages/react-app/src/Routes.jsx b/packages/react-app/src/Routes.jsx new file mode 100644 index 00000000..8f6116d6 --- /dev/null +++ b/packages/react-app/src/Routes.jsx @@ -0,0 +1,68 @@ +import React from "react"; +import { Redirect, Route, Switch } from "react-router-dom"; +import { + ActivityView, + BuilderListView, + BuilderProfileView, + ChallengeDetailView, + HomeView, + SubmissionReviewView, +} from "./views"; +import useUrlLang from "./hooks/useUrlLang"; + +const Routes = ({ + connectedBuilder, + userProvider, + address, + serverUrl, + mainnetProvider, + userRole, + fetchUserData, + loadWeb3Modal, +}) => { + const { langUrlPrefix, path } = useUrlLang(); + // INFO: pathPrefix is the path without trailing `/` + const pathPrefix = path.replace(/\/$/, ""); + + return ( + + + + + + {address && } + + + + + + + + + + + {/* ToDo: Protect this route on the frontend? */} + + + + + + + + ); +}; + +export default Routes; diff --git a/packages/react-app/src/components/Account.jsx b/packages/react-app/src/components/Account.jsx index 2d83fd80..1efdf031 100644 --- a/packages/react-app/src/components/Account.jsx +++ b/packages/react-app/src/components/Account.jsx @@ -23,6 +23,7 @@ import { FormattedMessage } from "react-intl"; import QRPunkBlockie from "./QrPunkBlockie"; import useDisplayAddress from "../hooks/useDisplayAddress"; import useCustomColorModes from "../hooks/useCustomColorModes"; +import useUrlLang from "../hooks/useUrlLang"; import { ellipsizedAddress } from "../helpers/strings"; import { USER_ROLES } from "../helpers/constants"; import HeroIconUser from "./icons/HeroIconUser"; @@ -80,6 +81,7 @@ export default function Account({ const openPopover = () => setIsPopoverOpen(true); const closePopover = () => setIsPopoverOpen(false); const { primaryFontColor, secondaryFontColor, dividerColor } = useCustomColorModes(); + const { langUrlPrefix } = useUrlLang(); if (!userRole && isWalletConnected) { return ; @@ -115,7 +117,7 @@ export default function Account({ const accountMenu = address && ( - + @@ -138,7 +140,7 @@ export default function Account({ description: ( <> Visit{" "} - + your portfolio {" "} to start building diff --git a/packages/react-app/src/components/ChallengeExpandedCard.jsx b/packages/react-app/src/components/ChallengeExpandedCard.jsx index 94c1b59f..5573d13b 100644 --- a/packages/react-app/src/components/ChallengeExpandedCard.jsx +++ b/packages/react-app/src/components/ChallengeExpandedCard.jsx @@ -99,7 +99,7 @@ const ChallengeExpandedCard = ({ overflow="hidden" >
- +
diff --git a/packages/react-app/src/components/Header.jsx b/packages/react-app/src/components/Header.jsx index bdb8e255..88365f8a 100644 --- a/packages/react-app/src/components/Header.jsx +++ b/packages/react-app/src/components/Header.jsx @@ -7,6 +7,7 @@ import { USER_ROLES } from "../helpers/constants"; import { ENVIRONMENT } from "../constants"; import useCustomColorModes from "../hooks/useCustomColorModes"; import HeaderLogo from "./icons/HeaderLogo"; +import useUrlLang from "../hooks/useUrlLang"; export default function Header({ injectedProvider, @@ -21,6 +22,7 @@ export default function Header({ const { linkColor, bgColor } = useCustomColorModes(); const primaryColorString = useColorModeValue("var(--chakra-colors-gray-700)", "var(--chakra-colors-gray-200)"); const location = useLocation(); + const { langUrlPrefix } = useUrlLang(); const isSignerProviderConnected = injectedProvider && injectedProvider.getSigner && injectedProvider.getSigner()._isSigner; @@ -59,7 +61,7 @@ export default function Header({ > {!isHomepage && ( - + @@ -78,7 +80,7 @@ export default function Header({ {userRole && USER_ROLES.anonymous !== userRole && ( location.pathname.includes("/builders/")} activeStyle={{ color: primaryColorString, @@ -93,7 +95,7 @@ export default function Header({ <> { @@ -34,7 +37,7 @@ export default function JoinBG({ text, connectedBuilder, isChallengeLocked, user }, { Link: chunks => ( - + {chunks} ), diff --git a/packages/react-app/src/components/builder/BuilderChallenges.jsx b/packages/react-app/src/components/builder/BuilderChallenges.jsx index 7165cfe1..ea650c42 100644 --- a/packages/react-app/src/components/builder/BuilderChallenges.jsx +++ b/packages/react-app/src/components/builder/BuilderChallenges.jsx @@ -22,6 +22,7 @@ import { getChallengeInfo } from "../../data/challenges"; import DateWithTooltip from "../DateWithTooltip"; import ChallengeStatusTag from "../ChallengeStatusTag"; import useCustomColorModes from "../../hooks/useCustomColorModes"; +import useUrlLang from "../../hooks/useUrlLang"; export const BuilderChallenges = ({ challenges, @@ -33,6 +34,7 @@ export const BuilderChallenges = ({ const { primaryFontColor, secondaryFontColor, borderColor, linkColor } = useCustomColorModes(); const intl = useIntl(); const challengeInfo = getChallengeInfo(intl); + const { langUrlPrefix } = useUrlLang(); return ( <> @@ -85,7 +87,12 @@ export const BuilderChallenges = ({ return ( - + {challengeInfo[challengeId].label} diff --git a/packages/react-app/src/components/builder/BuilderProfileCard.jsx b/packages/react-app/src/components/builder/BuilderProfileCard.jsx index c63c0e05..a89763d9 100644 --- a/packages/react-app/src/components/builder/BuilderProfileCard.jsx +++ b/packages/react-app/src/components/builder/BuilderProfileCard.jsx @@ -43,6 +43,7 @@ import { import { bySocialWeight, socials } from "../../data/socials"; import { USER_ROLES } from "../../helpers/constants"; import { validateSocials } from "../../helpers/validators"; +import useUrlLang from "../../hooks/useUrlLang"; const BuilderProfileCardSkeleton = ({ isLoaded, children }) => ( {isLoaded ? children() : } @@ -61,6 +62,8 @@ const BuilderProfileCard = ({ builder, mainnetProvider, isMyProfile, userProvide const shortAddress = ellipsizedAddress(builder?.id); const hasEns = ens !== shortAddress; + const { langUrlPrefix } = useUrlLang(); + const toast = useToast({ position: "top", isClosable: true }); const toastVariant = useColorModeValue("subtle", "solid"); @@ -250,7 +253,7 @@ const BuilderProfileCard = ({ builder, mainnetProvider, isMyProfile, userProvide maxW={{ base: "full", lg: "50%", xl: 60 }} margin="auto" > - + ({ defaultMessage: "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.", }), - previewImage: "assets/bg.png", + previewImage: "/assets/bg.png", dependencies: ["simple-nft-example", "decentralized-staking", "token-vendor"], externalLink: { link: "https://buidlguidl.com/", @@ -100,7 +100,7 @@ export const getChallengeInfo = intl => ({ defaultMessage: "๐Ÿ’ต 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...", }), - previewImage: "assets/challenges/dex.svg", + previewImage: "/assets/challenges/dex.svg", dependencies: ["simple-nft-example", "decentralized-staking", "token-vendor", "dice-game"], }, "state-channels": { @@ -116,7 +116,7 @@ export const getChallengeInfo = intl => ({ defaultMessage: "๐Ÿ›ฃ๏ธ 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.", }), - previewImage: "assets/challenges/state.svg", + previewImage: "/assets/challenges/state.svg", dependencies: ["simple-nft-example", "decentralized-staking", "token-vendor", "dice-game"], }, "learn-multisig": { @@ -132,7 +132,7 @@ export const getChallengeInfo = intl => ({ defaultMessage: '๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ง 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".', }), - previewImage: "assets/challenges/multiSig.svg", + previewImage: "/assets/challenges/multiSig.svg", // Challenge locked until the builder completed these challenges dependencies: ["simple-nft-example", "decentralized-staking", "token-vendor", "dice-game"], // Once the dependencies are completed, lock the challenge until @@ -155,7 +155,7 @@ export const getChallengeInfo = intl => ({ defaultMessage: "๐Ÿง™ Tinker around with cutting edge smart contracts that render SVGs in Solidity. ๐Ÿงซ We quickly discovered that the render function needs to be public... ๐Ÿค” This allows NFTs that own other NFTs to render their stash. Just wait until you see an Optimistic Loogie and a Fancy Loogie swimming around in the same Loogie Tank!", }), - previewImage: "assets/challenges/dynamicSvgNFT.svg", + previewImage: "/assets/challenges/dynamicSvgNFT.svg", // Challenge locked until the builder completed these challenges dependencies: ["simple-nft-example", "decentralized-staking", "token-vendor", "dice-game"], // Once the dependencies are completed, lock the challenge until diff --git a/packages/react-app/src/helpers/constants.js b/packages/react-app/src/helpers/constants.js index f81fd33e..521a16e0 100644 --- a/packages/react-app/src/helpers/constants.js +++ b/packages/react-app/src/helpers/constants.js @@ -31,3 +31,6 @@ export const userFunctionDescription = { [USER_FUNCTIONS.support]: { colorScheme: "blue", label: "Support" }, [USER_FUNCTIONS.mentor]: { colorScheme: "yellow", label: "Mentor" }, }; + +export const DEFAULT_LANG = "en"; +export const SUPPORTED_LANGS = ["es", "fr"]; diff --git a/packages/react-app/src/hooks/useUrlLang.js b/packages/react-app/src/hooks/useUrlLang.js new file mode 100644 index 00000000..669c1180 --- /dev/null +++ b/packages/react-app/src/hooks/useUrlLang.js @@ -0,0 +1,13 @@ +import { useRouteMatch } from "react-router-dom"; +import { DEFAULT_LANG, SUPPORTED_LANGS } from "../helpers/constants"; + +const useUrlLang = () => { + const langMatch = useRouteMatch(`/:lang(${SUPPORTED_LANGS.join("|")})`); + const lang = langMatch?.params.lang ?? DEFAULT_LANG; + const langUrlPrefix = lang === DEFAULT_LANG ? "" : `/${lang}`; + const path = langMatch?.path ?? "/"; + + return { lang, langUrlPrefix, path }; +}; + +export default useUrlLang; diff --git a/packages/react-app/src/index.jsx b/packages/react-app/src/index.jsx index 64033624..664a16aa 100644 --- a/packages/react-app/src/index.jsx +++ b/packages/react-app/src/index.jsx @@ -9,6 +9,7 @@ import { IntlProvider } from "react-intl"; import theme from "./theme"; import App from "./App"; +import { DEFAULT_LANG } from "./helpers/constants"; import translationEn from "./lang/en.json"; import translationEs from "./lang/es.json"; @@ -18,16 +19,20 @@ const translations = { }; // TODO: change from ui -const userLocale = "en"; +const defaultLocale = DEFAULT_LANG; -const Root = () => ( - - - - - - - - -); +const Root = () => { + const [locale, setLocale] = useState(defaultLocale); + + return ( + + + + + + + + + ); +}; ReactDOM.render(, document.getElementById("root")); diff --git a/packages/react-app/src/views/BuilderListView.jsx b/packages/react-app/src/views/BuilderListView.jsx index ea3be67c..ab231f63 100644 --- a/packages/react-app/src/views/BuilderListView.jsx +++ b/packages/react-app/src/views/BuilderListView.jsx @@ -32,6 +32,7 @@ import { getAcceptedChallenges } from "../helpers/builders"; import Address from "../components/Address"; import { bySocialWeight } from "../data/socials"; import { USER_ROLES } from "../helpers/constants"; +import useUrlLang from "../hooks/useUrlLang"; const serverPath = "/builders"; @@ -63,9 +64,9 @@ const BuilderSocialLinksCell = ({ builder, isAdmin }) => { ); }; -const BuilderAddressCell = ({ builderId, mainnetProvider }) => { +const BuilderAddressCell = ({ builderId, mainnetProvider, langUrlPrefix }) => { return ( - +
); @@ -75,6 +76,7 @@ export default function BuilderListView({ serverUrl, mainnetProvider, userRole } const [builders, setBuilders] = useState([]); const [isLoadingBuilders, setIsLoadingBuilders] = useState(false); const { secondaryFontColor, linkColor } = useCustomColorModes(); + const { langUrlPrefix } = useUrlLang(); const bgColor = useColorModeValue("sre.cardBackground", "sreDark.cardBackground"); const isAdmin = userRole === USER_ROLES.admin; @@ -84,7 +86,9 @@ export default function BuilderListView({ serverUrl, mainnetProvider, userRole } Header: "Builder", accessor: "builder", disableSortBy: true, - Cell: ({ value }) => , + Cell: ({ value }) => ( + + ), }, { Header: "Challenges",