From 228fc48681718c5b3f17a8fe71913f36c2e7322e Mon Sep 17 00:00:00 2001 From: Nikos Kontakis Date: Thu, 19 Sep 2024 13:40:41 +0300 Subject: [PATCH] Create links and make them beautiful (#146) Co-authored-by: Thibaut Sardan <33178835+Tbaut@users.noreply.github.com> Co-authored-by: Thibaut Sardan --- src/Content.tsx | 18 +++++---- src/components/DelegateCard.tsx | 21 ++++++---- src/components/RedirectByName.tsx | 56 ++++++++++++++++++++++++++ src/contexts/DelegatesContext.tsx | 38 +++++++++++------- src/contexts/NetworkContext.tsx | 2 +- src/lib/utils.ts | 11 ++++++ src/pages/Delegate/index.tsx | 65 ++++++++++++++++++++----------- 7 files changed, 159 insertions(+), 52 deletions(-) create mode 100644 src/components/RedirectByName.tsx diff --git a/src/Content.tsx b/src/Content.tsx index 78dd2b8..d192034 100644 --- a/src/Content.tsx +++ b/src/Content.tsx @@ -5,6 +5,8 @@ import { Delegate } from '@/pages/Delegate' import { toast } from 'sonner' import { useEffect } from 'react' import { useNetwork } from './contexts/NetworkContext' +import { RedirectByName } from './components/RedirectByName' + const pages = [ { path: '', @@ -18,6 +20,10 @@ const pages = [ path: '/delegate/:address', element: , }, + { + path: '/:network/:name', + element: , + }, ] export const Content = () => { @@ -30,12 +36,10 @@ export const Content = () => { }, [isLight, lightClientLoaded]) return ( - <> - - {pages.map(({ path, element }, i) => { - return - })} - - + + {pages.map(({ path, element }, i) => { + return + })} + ) } diff --git a/src/components/DelegateCard.tsx b/src/components/DelegateCard.tsx index 9dcafa3..ab85c75 100644 --- a/src/components/DelegateCard.tsx +++ b/src/components/DelegateCard.tsx @@ -3,12 +3,15 @@ import { Card } from '@/components/ui/card' import { useLocation, useNavigate } from 'react-router-dom' import { Delegate } from '@/contexts/DelegatesContext' import { ContentReveal } from './ui/content-reveal' +import { sanitizeString } from '@/lib/utils' +import { useNetwork } from '@/contexts/NetworkContext' import { cn } from '@/lib/utils' import { toast } from 'sonner' import { LinkIcon } from 'lucide-react' import Markdown from 'react-markdown' import { H, H2, H3, Hr, P } from './ui/md' import { AnchorLink } from './ui/anchorLink' +import { useCallback, useMemo } from 'react' interface Props { delegate: Delegate @@ -23,22 +26,26 @@ export const DelegateCard = ({ hasShareButton, hasDelegateButton = true, }: Props) => { + const { network } = useNetwork() const navigate = useNavigate() const { search } = useLocation() + const copyLink = useMemo( + () => `${window.location.host}/${network}/${sanitizeString(name)}`, + [name, network], + ) const shouldHideLongDescription = !longDescription || longDescription === shortDescription - const onDelegate = () => { + const onDelegate = useCallback(() => { navigate(`/delegate/${address}${search}`) - } - const onCopy = () => { - navigator.clipboard.writeText( - window.location.origin + `/delegate/${address}${search}`, - ) + }, [address, navigate, search]) + + const onCopy = useCallback(() => { + navigator.clipboard.writeText(copyLink) toast.success('Copied to clipboard', { duration: 1000, }) - } + }, [copyLink]) const DelegateAvatar: React.FC = () => { const divStyle: React.CSSProperties = { diff --git a/src/components/RedirectByName.tsx b/src/components/RedirectByName.tsx new file mode 100644 index 0000000..5e48fa5 --- /dev/null +++ b/src/components/RedirectByName.tsx @@ -0,0 +1,56 @@ +import { useDelegates } from '@/contexts/DelegatesContext' +import { isSupportedNetwork, useNetwork } from '@/contexts/NetworkContext' +import { Loader2 } from 'lucide-react' +import { useEffect, useState } from 'react' +import { Navigate, useParams } from 'react-router-dom' + +export const RedirectByName = () => { + const { name, network } = useParams() + const { getDelegateByName, isLoading: isDelegateLoading } = useDelegates() + const [isDelegateMissing, setIsDelegateMissing] = useState(false) + const [address, setAddress] = useState('') + const { selectNetwork } = useNetwork() + + useEffect(() => { + if (!network || !isSupportedNetwork(network)) { + setIsDelegateMissing(true) + + return + } + + selectNetwork(network) + }, [network, selectNetwork]) + + useEffect(() => { + if (!name) { + setIsDelegateMissing(true) + return + } + + if (isDelegateLoading) return + + const delegate = getDelegateByName(name) + + if (!delegate) { + setIsDelegateMissing(true) + } else { + setAddress(delegate.address) + } + }, [getDelegateByName, isDelegateLoading, name, network, selectNetwork]) + + if (address && !!network && isSupportedNetwork(network)) { + return + } + + if (!isDelegateMissing) { + return + } + + return ( +
+ Delegate not found for name:{' '} + {name} and network:{' '} + {network} +
+ ) +} diff --git a/src/contexts/DelegatesContext.tsx b/src/contexts/DelegatesContext.tsx index ae45bf8..89e31e4 100644 --- a/src/contexts/DelegatesContext.tsx +++ b/src/contexts/DelegatesContext.tsx @@ -2,7 +2,7 @@ import { createContext, useContext, useEffect, useState } from 'react' import { useNetwork } from './NetworkContext' import { DelegateListKusama, DelegateListPolkadot } from '@/lib/constants' -import { shuffleArray } from '@/lib/utils' +import { sanitizeString, shuffleArray } from '@/lib/utils' type DelegatesContextProps = { children: React.ReactNode | React.ReactNode[] @@ -18,8 +18,10 @@ export type Delegate = { } export interface IDelegatesContext { + isLoading: boolean delegates: Delegate[] getDelegateByAddress: (address: string) => Delegate | undefined + getDelegateByName: (name: string) => Delegate | undefined } const DelegatesContext = createContext(undefined) @@ -27,26 +29,32 @@ const DelegatesContext = createContext(undefined) const DelegateContextProvider = ({ children }: DelegatesContextProps) => { const { network } = useNetwork() const [delegates, setDelegates] = useState([]) + const [isLoading, setIsLoading] = useState(true) useEffect(() => { - const fetchOpenPRs = async () => { - const response = await ( - await fetch( - network === 'polkadot' || network === 'polkadot-lc' - ? DelegateListPolkadot - : DelegateListKusama, - )! - ).json() + if (!network) return + setIsLoading(true) + const networkToFetch = + network === 'polkadot' || network === 'polkadot-lc' + ? DelegateListPolkadot + : DelegateListKusama - const randomized = shuffleArray(response) as Delegate[] - setDelegates(randomized) - } - fetchOpenPRs() + fetch(networkToFetch) + .then(async (response) => { + const result = await response.json() + const randomized = shuffleArray(result) as Delegate[] + setDelegates(randomized) + setIsLoading(false) + }) + .catch(console.error) }, [network]) const getDelegateByAddress = (address: string) => delegates.find((d) => d.address === address) + const getDelegateByName = (name: string) => + delegates.find((d) => sanitizeString(d.name) == name.toLowerCase()) + // Votes thingy - pause for now // useEffect(() => { // const a = async (delegates: any[]) => { @@ -61,7 +69,9 @@ const DelegateContextProvider = ({ children }: DelegatesContextProps) => { // }, [delegates]) return ( - + {children} ) diff --git a/src/contexts/NetworkContext.tsx b/src/contexts/NetworkContext.tsx index 2416f42..990b8f4 100644 --- a/src/contexts/NetworkContext.tsx +++ b/src/contexts/NetworkContext.tsx @@ -58,7 +58,7 @@ export interface INetworkContext { trackList: TrackList } -const isSupportedNetwork = ( +export const isSupportedNetwork = ( network: string, ): network is SupportedNetworkNames => !!descriptorName[network as SupportedNetworkNames] diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 1fae7bd..6c1477b 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -79,6 +79,17 @@ export const getLockTimes = async (api: ApiType) => { ) } +export const sanitizeString = (value: string) => + value + // remove all emojis + .replace( + /([\u2700-\u27BF]|[\uE000-\uF8FF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDFFF]|[\u2011-\u26FF]|\uD83E[\uDD10-\uDDFF])/g, + '', + ) + // replace all strange characters with underscores + .replace(/[\W_]+/g, '_') + .toLowerCase() + // thanks to https://stackoverflow.com/a/2450976/3086912 export const shuffleArray = (arrayToShuffle: unknown[]) => { const array = [...arrayToShuffle] diff --git a/src/pages/Delegate/index.tsx b/src/pages/Delegate/index.tsx index 57daa61..9eff044 100644 --- a/src/pages/Delegate/index.tsx +++ b/src/pages/Delegate/index.tsx @@ -1,6 +1,9 @@ import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' -import { useDelegates } from '@/contexts/DelegatesContext' +import { + type Delegate as DelegateType, + useDelegates, +} from '@/contexts/DelegatesContext' import { useNetwork } from '@/contexts/NetworkContext' import { VotingConviction } from '@polkadot-api/descriptors' import { SetStateAction, useEffect, useMemo, useState } from 'react' @@ -8,7 +11,7 @@ import { Button } from '@/components/ui/button' import { useAccounts } from '@/contexts/AccountsContext' import { Slider } from '@/components/ui/slider' import { Link, useLocation, useNavigate, useParams } from 'react-router-dom' -import { ArrowLeft } from 'lucide-react' +import { ArrowLeft, Loader2 } from 'lucide-react' import { msgs } from '@/lib/constants' import { evalUnits, planckToUnit } from '@polkadot-ui/utils' import { useLocks } from '@/contexts/LocksContext' @@ -23,12 +26,15 @@ import { DelegateCard } from '@/components/DelegateCard' export const Delegate = () => { const { api, assetInfo } = useNetwork() const { address } = useParams() - const { getConvictionLockTimeDisplay } = useLocks() + const { selectedAccount } = useAccounts() + const getDelegateTx = useGetDelegateTx() + const { getConvictionLockTimeDisplay, refreshLocks } = useLocks() + const getSubscriptionCallBack = useGetSigningCallback() + const navigate = useNavigate() + const { search } = useLocation() + const { getDelegateByAddress, isLoading: isLoadingDelegates } = useDelegates() + const [delegate, setDelegate] = useState() - const { getDelegateByAddress } = useDelegates() - const [delegate, setDelegate] = useState( - address && getDelegateByAddress(address), - ) const [isAmountDirty, setIsAmountDirty] = useState(false) const [amount, setAmount] = useState(0n) const [amountVisible, setAmountVisible] = useState('0') @@ -37,16 +43,27 @@ export const Delegate = () => { VotingConviction.Locked1x(), ) const [convictionNo, setConvictionNo] = useState(1) - const { selectedAccount } = useAccounts() const [isTxInitiated, setIsTxInitiated] = useState(false) - const getDelegateTx = useGetDelegateTx() - const navigate = useNavigate() - const { search } = useLocation() const { isExhaustsResources } = useTestTx() const [isMultiTxDialogOpen, setIsMultiTxDialogOpen] = useState(false) const [delegateTxs, setDelegateTxs] = useState({} as DelegateTxs) - const { refreshLocks } = useLocks() - const getSubscriptionCallBack = useGetSigningCallback() + const [noDelegateFound, setNoDelegateFound] = useState(false) + + useEffect(() => { + // the delegate list may still be loading + if (isLoadingDelegates || delegate) return + + const foundDelegate = address && getDelegateByAddress(address) + + // if no delegate is found based on the address + // or there's no address passed in the url + if (!foundDelegate || !address) { + setNoDelegateFound(true) + return + } + + setDelegate(foundDelegate) + }, [address, delegate, getDelegateByAddress, isLoadingDelegates]) const { display: convictionTimeDisplay, multiplier: convictionMultiplier } = getConvictionLockTimeDisplay(convictionNo) @@ -84,15 +101,6 @@ export const Delegate = () => { setAmountVisible('0') }, [api]) - useEffect(() => { - if (!address || delegate) return - - const res = getDelegateByAddress(address) - setDelegate(res) - }, [address, delegate, getDelegateByAddress]) - - if (!delegate || !api) return
No delegate found
- const onChangeAmount = ( e: React.ChangeEvent, decimals: number, @@ -118,7 +126,8 @@ export const Delegate = () => { } const onSign = async () => { - if (!selectedAccount || !amount) return + if (!delegate || !selectedAccount || !amount || !api) return + setIsTxInitiated(true) const allTracks = await api.constants.Referenda.Tracks() @@ -177,6 +186,16 @@ export const Delegate = () => { .subscribe(subscriptionCallBack) } + if (noDelegateFound) + return ( +
+ No delegate found for this address: {address} +
+ ) + + if (!delegate || !api) + return + return (
{!api && (