feat(trading): create team (#5610)

This commit is contained in:
Matthew Russell
2024-01-19 16:35:29 -05:00
parent 83bbd70fa7
commit f82262c8b3
27 changed files with 573 additions and 159 deletions
@@ -0,0 +1,318 @@
import { useSearchParams } from 'react-router-dom';
import { Controller, useForm } from 'react-hook-form';
import {
Intent,
TextArea,
TradingAnchorButton,
TradingButton,
TradingCheckbox,
TradingFormGroup,
TradingInput,
TradingInputError,
} from '@vegaprotocol/ui-toolkit';
import {
useVegaWallet,
type CreateReferralSet,
useVegaWalletDialogStore,
} from '@vegaprotocol/wallet';
import {
addDecimalsFormatNumber,
isValidVegaPublicKey,
URL_REGEX,
} from '@vegaprotocol/utils';
import { useT } from '../../lib/use-t';
import { useCreateReferralSet } from '../../lib/hooks/use-create-referral-set';
import { DApp, TokenStaticLinks, useLinks } from '@vegaprotocol/environment';
import { RainbowButton } from '../../components/rainbow-button';
import { usePageTitle } from '../../lib/hooks/use-page-title';
import { ErrorBoundary } from '../../components/error-boundary';
import { Box } from '../../components/competitions/box';
import { Links } from '../../lib/links';
interface FormFields {
name: string;
url: string;
avatarUrl: string;
private: boolean;
allowList: string;
}
export const CompetitionsCreateTeam = () => {
const [searchParams] = useSearchParams();
const isSolo = Boolean(searchParams.get('solo'));
const t = useT();
usePageTitle(t('Create a team'));
const { isReadOnly, pubKey } = useVegaWallet();
const openWalletDialog = useVegaWalletDialogStore(
(store) => store.openVegaWalletDialog
);
return (
<ErrorBoundary feature="create-team">
<div className="relative h-full pt-5 overflow-y-auto">
<div className="absolute top-0 left-0 w-full h-[40%] -z-10 bg-[40%_0px] bg-cover bg-no-repeat bg-local bg-[url(/cover.png)]">
<div className="absolute top-o left-0 w-full h-full bg-gradient-to-t from-white dark:from-vega-cdark-900 to-transparent from-20% to-60%" />
</div>
<div className="lg:gap-6 container p-4 mx-auto">
<div className="mx-auto md:w-2/3 max-w-xl">
<Box className="flex flex-col gap-4">
<h1 className="calt text-2xl lg:text-3xl xl:text-5xl">
{t('Create a team')}
</h1>
{pubKey && !isReadOnly ? (
<CreateTeamFormContainer isSolo={isSolo} />
) : (
<>
<p>
{t(
'Create a team to participate in team based rewards as well as access the discount benefits of the current referral program.'
)}
</p>
<RainbowButton variant="border" onClick={openWalletDialog}>
{t('Connect wallet')}
</RainbowButton>
</>
)}
</Box>
</div>
</div>
</div>
</ErrorBoundary>
);
};
const CreateTeamFormContainer = ({ isSolo }: { isSolo: boolean }) => {
const t = useT();
const createLink = useLinks(DApp.Governance);
const { err, status, code, isEligible, requiredStake, onSubmit } =
useCreateReferralSet({
onSuccess: (code) => {
// For some reason team creation takes a long time, too long even to make
// polling viable, so its not feasible to navigate to the team page
// after creation
//
// navigate(Links.COMPETITIONS_TEAM(code));
},
});
if (status === 'success') {
return (
<div className="flex flex-col items-start gap-2">
<p className="text-sm">{t('Team creation transaction successful')}</p>
{code && (
<>
<p className="text-sm">
Your team ID is:{' '}
<span className="font-mono break-all">{code}</span>
</p>
<TradingAnchorButton
href={Links.COMPETITIONS_TEAM(code)}
intent={Intent.Info}
size="small"
>
{t('View team')}
</TradingAnchorButton>
</>
)}
</div>
);
}
if (!isEligible) {
return (
<div className="flex flex-col gap-4">
{requiredStake !== undefined && (
<p>
{t(
'You need at least {{requiredStake}} VEGA staked to generate a referral code and participate in the referral program.',
{
requiredStake: addDecimalsFormatNumber(
requiredStake.toString(),
18
),
}
)}
</p>
)}
<TradingAnchorButton
href={createLink(TokenStaticLinks.ASSOCIATE)}
intent={Intent.Primary}
target="_blank"
>
{t('Stake some $VEGA now')}
</TradingAnchorButton>
</div>
);
}
return (
<CreateTeamForm
onSubmit={onSubmit}
status={status}
err={err}
isSolo={isSolo}
/>
);
};
const CreateTeamForm = ({
status,
err,
isSolo,
onSubmit,
}: {
status: ReturnType<typeof useCreateReferralSet>['status'];
err: ReturnType<typeof useCreateReferralSet>['err'];
isSolo: boolean;
onSubmit: (tx: CreateReferralSet) => void;
}) => {
const t = useT();
const {
register,
handleSubmit,
control,
watch,
formState: { errors },
} = useForm<FormFields>({
defaultValues: {
private: isSolo,
},
});
const isPrivate = watch('private');
const createTeam = (fields: FormFields) => {
onSubmit({
createReferralSet: {
isTeam: true,
team: {
name: fields.name,
teamUrl: fields.url,
avatarUrl: fields.avatarUrl,
closed: fields.private,
allowList: fields.private ? parseAllowListText(fields.allowList) : [],
},
},
});
};
return (
<form onSubmit={handleSubmit(createTeam)}>
<TradingFormGroup label={t('Team name')} labelFor="name">
<TradingInput {...register('name', { required: t('Required') })} />
{errors.name?.message && (
<TradingInputError forInput="name">
{errors.name.message}
</TradingInputError>
)}
</TradingFormGroup>
<TradingFormGroup
label={t('URL')}
labelFor="url"
labelDescription={t(
'Provide a link so users can learn more about your team'
)}
>
<TradingInput
{...register('url', {
pattern: { value: URL_REGEX, message: t('Invalid URL') },
})}
/>
{errors.url?.message && (
<TradingInputError forInput="url">
{errors.url.message}
</TradingInputError>
)}
</TradingFormGroup>
<TradingFormGroup
label={t('Avatar URL')}
labelFor="avatarUrl"
labelDescription={t('Provide a URL to a hosted image')}
>
<TradingInput
{...register('avatarUrl', {
pattern: {
value: URL_REGEX,
message: t('Invalid image URL'),
},
})}
/>
{errors.avatarUrl?.message && (
<TradingInputError forInput="avatarUrl">
{errors.avatarUrl.message}
</TradingInputError>
)}
</TradingFormGroup>
<TradingFormGroup
label={t('Make team private')}
labelFor="private"
hideLabel={true}
>
<Controller
name="private"
control={control}
render={({ field }) => {
return (
<TradingCheckbox
label={t('Make team private')}
checked={field.value}
onCheckedChange={(value) => {
field.onChange(value);
}}
disabled={isSolo}
/>
);
}}
/>
</TradingFormGroup>
{isPrivate && (
<TradingFormGroup
label={t('Public key allow list')}
labelFor="allowList"
labelDescription={t(
'Use a comma separated list to allow only specific public keys to join the team'
)}
>
<TextArea
{...register('allowList', {
required: t('Required'),
disabled: isSolo,
validate: {
allowList: (value) => {
const publicKeys = parseAllowListText(value);
if (publicKeys.every((pk) => isValidVegaPublicKey(pk))) {
return true;
}
return t('Invalid public key found in allow list');
},
},
})}
/>
{errors.allowList?.message && (
<TradingInputError forInput="avatarUrl">
{errors.allowList.message}
</TradingInputError>
)}
</TradingFormGroup>
)}
{err && <p className="text-danger text-xs mb-4 capitalize">{err}</p>}
<TradingButton
type="submit"
intent={Intent.Info}
disabled={status === 'loading'}
>
{status === 'loading' ? t('Confirm in wallet...') : t('Create')}
</TradingButton>
</form>
);
};
const parseAllowListText = (str: string) => {
return str
.split(',')
.map((v) => v.trim())
.filter(Boolean);
};
@@ -1,6 +1,3 @@
import { useEffect } from 'react';
import { titlefy } from '@vegaprotocol/utils';
import { usePageTitleStore } from '../../stores';
import { useT } from '../../lib/use-t';
import { ErrorBoundary } from '@sentry/react';
import { CompetitionsHeader } from '../../components/competitions/competitions-header';
@@ -18,11 +15,14 @@ import { GamesContainer } from '../../components/competitions/games-container';
import { CompetitionsLeaderboard } from '../../components/competitions/competitions-leaderboard';
import { useTeams } from './hooks/use-teams';
import take from 'lodash/take';
import { usePageTitle } from '../../lib/hooks/use-page-title';
export const CompetitionsHome = () => {
const t = useT();
const navigate = useNavigate();
usePageTitle(t('Competitions'));
const { data: epochData } = useCurrentEpochInfoQuery();
const currentEpoch = Number(epochData?.epoch.id);
@@ -36,13 +36,6 @@ export const CompetitionsHome = () => {
order: 'desc',
});
const { updateTitle } = usePageTitleStore((store) => ({
updateTitle: store.updateTitle,
}));
useEffect(() => {
updateTitle(titlefy([t('Competitions')]));
}, [updateTitle, t]);
return (
<ErrorBoundary>
<CompetitionsHeader title={t('Competitions')}>
@@ -86,7 +79,7 @@ export const CompetitionsHome = () => {
intent={Intent.Primary}
onClick={(e) => {
e.preventDefault();
navigate(Links.COMPETITIONS_CREATE_TEAM());
navigate(Links.COMPETITIONS_CREATE_TEAM_SOLO());
}}
>
{t('Create a private team')}
@@ -26,22 +26,31 @@ import {
import { DApp, EXPLORER_PARTIES, useLinks } from '@vegaprotocol/environment';
import BigNumber from 'bignumber.js';
import { TeamAvatar } from '../../components/competitions/team-avatar';
import { usePageTitle } from '../../lib/hooks/use-page-title';
import { ErrorBoundary } from '../../components/error-boundary';
export const CompetitionsTeam = () => {
const t = useT();
const { teamId } = useParams<{ teamId: string }>();
const { team, stats, partyInTeam, members, games } = useTeam(teamId);
usePageTitle([t('Competitions'), t('Team')]);
return (
<ErrorBoundary feature="team">
<TeamPageContainer teamId={teamId} />
</ErrorBoundary>
);
};
// const team = {
// teamId: '12345678909876543212345678765432345676543234567',
// referrer: '12345678909876543212345678765432345676543234567',
// name: 'The Kittens',
// teamUrl: 'http://placekitten.com/g/200/300',
// avatarUrl: 'http://placekitten.com/g/200/300',
// createdAt: '2024-01-01',
// createdAtEpoch: 123,
// closed: true,
// };
const TeamPageContainer = ({ teamId }: { teamId: string | undefined }) => {
const t = useT();
const { team, stats, partyInTeam, members, games, loading } = useTeam(teamId);
if (loading) {
return (
<Splash>
<p>Loading...</p>
</Splash>
);
}
if (!team) {
return (
@@ -62,7 +71,7 @@ export const CompetitionsTeam = () => {
);
};
export const TeamPage = ({
const TeamPage = ({
team,
stats,
partyInTeam,
@@ -79,7 +88,7 @@ export const TeamPage = ({
const [showGames, setShowGames] = useState(true);
return (
<div className="relative h-full overflow-y-auto">
<div className="relative h-full overflow-y-auto pt-5">
<div className="absolute top-0 left-0 w-full h-[40%] -z-10 bg-[40%_0px] bg-cover bg-no-repeat bg-local bg-[url(/cover.png)]">
<div className="absolute top-o left-0 w-full h-full bg-gradient-to-t from-white dark:from-vega-cdark-900 to-transparent from-20% to-60%" />
</div>
@@ -1,9 +1,7 @@
import { ErrorBoundary } from '@sentry/react';
import { CompetitionsHeader } from '../../components/competitions/competitions-header';
import { usePageTitleStore } from '../../stores';
import { useEffect, useRef, useState } from 'react';
import { useRef, useState } from 'react';
import { useT } from '../../lib/use-t';
import { titlefy } from '@vegaprotocol/utils';
import { useTeams } from './hooks/use-teams';
import { CompetitionsLeaderboard } from '../../components/competitions/competitions-leaderboard';
import {
@@ -12,15 +10,12 @@ import {
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import { usePageTitle } from '../../lib/hooks/use-page-title';
export const CompetitionsTeams = () => {
const t = useT();
const { updateTitle } = usePageTitleStore((store) => ({
updateTitle: store.updateTitle,
}));
useEffect(() => {
updateTitle(titlefy([t('Competitions'), t('Teams')]));
}, [updateTitle, t]);
usePageTitle([t('Competitions'), t('Teams')]);
const { data: teamsData, loading: teamsLoading } = useTeams({
sortByField: ['totalQuantumRewards'],
@@ -202,7 +197,7 @@ export const CompetitionsTeams = () => {
return (
<ErrorBoundary>
<CompetitionsHeader title={t('Join a team')}>
<p className="text-lg mb-1">{t('Choose a team to get involved')}</p>x
<p className="text-lg mb-1">{t('Choose a team to get involved')}</p>
</CompetitionsHeader>
<div className="mb-6 flex justify-end">
@@ -11,7 +11,7 @@ import classNames from 'classnames';
import { Navigate, useNavigate, useSearchParams } from 'react-router-dom';
import type { ButtonHTMLAttributes, MouseEventHandler } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import { RainbowButton } from './buttons';
import { RainbowButton } from '../../components/rainbow-button';
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
import { useIsInReferralSet, useReferral } from './hooks/use-referral';
import { Routes } from '../../lib/links';
@@ -4,41 +4,6 @@ import type { ComponentProps, ButtonHTMLAttributes } from 'react';
import { forwardRef } from 'react';
import { NavLink } from 'react-router-dom';
type RainbowButtonProps = {
variant?: 'full' | 'border';
};
export const RainbowButton = ({
variant = 'full',
children,
className,
...props
}: RainbowButtonProps & ButtonHTMLAttributes<HTMLButtonElement>) => (
<button
className={classNames(
'bg-rainbow rounded-lg overflow-hidden disabled:opacity-40',
'hover:bg-rainbow-180 hover:animate-spin-rainbow',
{
'px-5 py-3 text-white': variant === 'full',
'p-[0.125rem]': variant === 'border',
}
)}
{...props}
>
<div
className={classNames(
{
'bg-vega-clight-800 dark:bg-vega-cdark-800 text-black dark:text-white px-5 py-3 rounded-[0.35rem] overflow-hidden':
variant === 'border',
},
className
)}
>
{children}
</div>
</button>
);
const RAINBOW_TAB_STYLE = classNames(
'inline-block',
'bg-vega-clight-500 dark:bg-vega-cdark-500',
@@ -1,9 +1,5 @@
import {
useVegaWallet,
useVegaWalletDialogStore,
determineId,
} from '@vegaprotocol/wallet';
import { RainbowButton } from './buttons';
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
import { RainbowButton } from '../../components/rainbow-button';
import { useState } from 'react';
import {
CopyWithTooltip,
@@ -18,13 +14,13 @@ import {
} from '@vegaprotocol/ui-toolkit';
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
import { DApp, TokenStaticLinks, useLinks } from '@vegaprotocol/environment';
import { useStakeAvailable } from './hooks/use-stake-available';
import { ABOUT_REFERRAL_DOCS_LINK } from './constants';
import { useIsInReferralSet, useReferral } from './hooks/use-referral';
import { useT } from '../../lib/use-t';
import { Navigate } from 'react-router-dom';
import { Routes } from '../../lib/links';
import { useReferralProgram } from './hooks/use-referral-program';
import { useCreateReferralSet } from '../../lib/hooks/use-create-referral-set';
export const CreateCodeContainer = () => {
const { pubKey } = useVegaWallet();
@@ -95,56 +91,24 @@ const CreateCodeDialog = ({
}) => {
const t = useT();
const createLink = useLinks(DApp.Governance);
const { isReadOnly, pubKey, sendTx } = useVegaWallet();
const { pubKey } = useVegaWallet();
const { refetch } = useReferral({ pubKey, role: 'referrer' });
const [err, setErr] = useState<string | null>(null);
const [code, setCode] = useState<string | null>(null);
const [status, setStatus] = useState<
'idle' | 'loading' | 'success' | 'error'
>('idle');
const { stakeAvailable: currentStakeAvailable, requiredStake } =
useStakeAvailable();
const {
err,
code,
status,
stakeAvailable: currentStakeAvailable,
requiredStake,
onSubmit,
} = useCreateReferralSet();
const { details: programDetails } = useReferralProgram();
const onSubmit = () => {
if (isReadOnly || !pubKey) {
setErr('Not connected');
} else {
setErr(null);
setStatus('loading');
setCode(null);
sendTx(pubKey, {
createReferralSet: {
isTeam: false,
},
})
.then((res) => {
if (!res) {
setErr(`Invalid response: ${JSON.stringify(res)}`);
return;
}
const code = determineId(res.signature);
setCode(code);
setStatus('success');
})
.catch((err) => {
if (err.message.includes('user rejected')) {
setStatus('idle');
return;
}
setStatus('error');
setErr(err.message);
});
}
};
const getButtonProps = () => {
if (status === 'idle' || status === 'error') {
return {
children: t('Generate code'),
onClick: () => onSubmit(),
onClick: () => onSubmit({ createReferralSet: { isTeam: false } }),
};
}
@@ -240,7 +204,7 @@ const CreateCodeDialog = ({
<TradingButton
fill={true}
intent={Intent.Primary}
onClick={() => onSubmit()}
onClick={() => onSubmit({ createReferralSet: { isTeam: false } })}
{...getButtonProps()}
>
{t('Yes')}
@@ -1,5 +1,5 @@
import { isRouteErrorResponse, useNavigate, useRouteError } from 'react-router';
import { RainbowButton } from './buttons';
import { RainbowButton } from '../../components/rainbow-button';
import { AnimatedDudeWithWire } from './graphics/dude';
import { LayoutWithSky } from './layout';
import { Routes } from '../../lib/links';
@@ -8,7 +8,7 @@ import type {
ReferralSetsQueryVariables,
} from './__generated__/ReferralSets';
import { useReferralSetsQuery } from './__generated__/ReferralSets';
import { useStakeAvailable } from './use-stake-available';
import { useStakeAvailable } from '../../../lib/hooks/use-stake-available';
export const DEFAULT_AGGREGATION_DAYS = 30;
@@ -26,7 +26,7 @@ import {
import { useReferralSetStatsQuery } from './hooks/__generated__/ReferralSetStats';
import compact from 'lodash/compact';
import { useReferralProgram } from './hooks/use-referral-program';
import { useStakeAvailable } from './hooks/use-stake-available';
import { useStakeAvailable } from '../../lib/hooks/use-stake-available';
import sortBy from 'lodash/sortBy';
import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { useCurrentEpochInfoQuery } from './hooks/__generated__/Epoch';
@@ -0,0 +1,21 @@
import classNames from 'classnames';
import { type HTMLAttributes } from 'react';
export const BORDER_COLOR = 'border-vega-clight-500 dark:border-vega-cdark-500';
export const GRADIENT =
'bg-gradient-to-b from-vega-clight-800 dark:from-vega-cdark-800 to-transparent';
export const Box = (props: HTMLAttributes<HTMLDivElement>) => {
return (
<div
{...props}
className={classNames(
BORDER_COLOR,
GRADIENT,
'border rounded-lg',
'p-6',
props.className
)}
/>
);
};
@@ -1,11 +1,7 @@
import classNames from 'classnames';
import { Box } from './box';
import { type ComponentProps, type ReactElement, type ReactNode } from 'react';
import { DudeBadge } from './graphics/dude-badge';
export const BORDER_COLOR = 'border-vega-clight-500 dark:border-vega-cdark-500';
export const GRADIENT =
'bg-gradient-to-b from-vega-clight-800 dark:from-vega-cdark-800 to-transparent';
export const CompetitionsActionsContainer = ({
children,
}: {
@@ -32,18 +28,11 @@ export const CompetitionsAction = ({
children?: ReactNode;
}) => {
return (
<div
className={classNames(
BORDER_COLOR,
GRADIENT,
'border rounded-lg',
'p-6 flex flex-col items-center gap-6 text-center'
)}
>
<Box className="flex flex-col items-center gap-6 text-center">
<DudeBadge variant={variant} />
<h2 className="text-2xl">{title}</h2>
{description && <p className="text-muted">{description}</p>}
{actionElement}
</div>
</Box>
);
};
@@ -29,8 +29,8 @@ export const Rank = ({
<svg width="18" height="30" viewBox="0 0 18 30" fill="none">
<defs>
<linearGradient x1="0" y1="0" x2="100%" y2="100%" id="medal">
<stop offset="33%" stop-color="transparent" />
<stop offset="100%" stop-color="black" stop-opacity="50%" />
<stop offset="33%" stopColor="transparent" />
<stop offset="100%" stopColor="black" stopOpacity="50%" />
</linearGradient>
<clipPath id="shape">
<path d="M2 2H4V4H2V2Z" />
@@ -0,0 +1 @@
export { RainbowButton } from './rainbow-button';
@@ -0,0 +1,37 @@
import classNames from 'classnames';
import { type ButtonHTMLAttributes } from 'react';
type RainbowButtonProps = {
variant?: 'full' | 'border';
};
export const RainbowButton = ({
variant = 'full',
children,
className,
...props
}: RainbowButtonProps & ButtonHTMLAttributes<HTMLButtonElement>) => (
<button
className={classNames(
'bg-rainbow rounded-lg overflow-hidden disabled:opacity-40',
'hover:bg-rainbow-180 hover:animate-spin-rainbow',
{
'px-5 py-3 text-white': variant === 'full',
'p-[0.125rem]': variant === 'border',
}
)}
{...props}
>
<div
className={classNames(
{
'bg-vega-clight-800 dark:bg-vega-cdark-800 text-black dark:text-white px-5 py-3 rounded-[0.35rem] overflow-hidden':
variant === 'border',
},
className
)}
>
{children}
</div>
</button>
);
@@ -0,0 +1,63 @@
import {
determineId,
useVegaWallet,
type CreateReferralSet,
} from '@vegaprotocol/wallet';
import { useState } from 'react';
import { useStakeAvailable } from './use-stake-available';
/**
* Manages state for creating a referral set or team
*/
export const useCreateReferralSet = (opts?: {
onSuccess?: (code: string) => void;
onError?: (error: string) => void;
}) => {
const { pubKey, isReadOnly, sendTx } = useVegaWallet();
const [err, setErr] = useState<string | null>(null);
const [code, setCode] = useState<string | null>(null);
const [status, setStatus] = useState<
'idle' | 'loading' | 'success' | 'error'
>('idle');
const { stakeAvailable, requiredStake, isEligible } = useStakeAvailable();
const onSubmit = (tx: CreateReferralSet) => {
if (isReadOnly || !pubKey) {
setErr('Not connected');
} else {
setErr(null);
setStatus('loading');
setCode(null);
sendTx(pubKey, tx)
.then((res) => {
if (!res) {
throw new Error(`Invalid response: ${JSON.stringify(res)}`);
}
const code = determineId(res.signature);
setCode(code);
setStatus('success');
opts?.onSuccess && opts.onSuccess(code);
})
.catch((err) => {
if (err.message.includes('user rejected')) {
setStatus('idle');
return;
}
setStatus('error');
setErr(err.message);
opts?.onError && opts.onError(err.message);
});
}
};
return {
err,
code,
status,
stakeAvailable,
requiredStake,
onSubmit,
isEligible,
};
};
+18
View File
@@ -0,0 +1,18 @@
import { useEffect, useMemo } from 'react';
import { titlefy } from '@vegaprotocol/utils';
import { usePageTitleStore } from '../../stores';
export const usePageTitle = (title: string | string[]) => {
const { updateTitle } = usePageTitleStore((store) => ({
updateTitle: store.updateTitle,
}));
const memotitle = useMemo(
() => titlefy(Array.isArray(title) ? title : [title]),
[title]
);
useEffect(() => {
updateTitle(memotitle);
}, [updateTitle, memotitle]);
};
+2 -2
View File
@@ -16,10 +16,10 @@ export const Routes = {
REFERRALS: '/referrals',
REFERRALS_APPLY_CODE: '/referrals/apply-code',
REFERRALS_CREATE_CODE: '/referrals/create-code',
TEAM: '/competitions/team/:teamId',
COMPETITIONS: '/competitions',
COMPETITIONS_TEAMS: '/competitions/teams',
COMPETITIONS_CREATE_TEAM: '/competitions/teams/create',
COMPETITIONS_CREATE_TEAM_SOLO: '/competitions/teams/create?solo=true',
COMPETITIONS_TEAM: '/competitions/teams/:teamId',
FEES: '/fees',
REWARDS: '/rewards',
@@ -45,10 +45,10 @@ export const Links: ConsoleLinks = {
REFERRALS: () => Routes.REFERRALS,
REFERRALS_APPLY_CODE: () => Routes.REFERRALS_APPLY_CODE,
REFERRALS_CREATE_CODE: () => Routes.REFERRALS_CREATE_CODE,
TEAM: (teamId: string) => trimEnd(Routes.TEAM.replace(':teamId', teamId)),
COMPETITIONS: () => Routes.COMPETITIONS,
COMPETITIONS_TEAMS: () => Routes.COMPETITIONS_TEAMS,
COMPETITIONS_CREATE_TEAM: () => Routes.COMPETITIONS_CREATE_TEAM,
COMPETITIONS_CREATE_TEAM_SOLO: () => Routes.COMPETITIONS_CREATE_TEAM_SOLO,
COMPETITIONS_TEAM: (teamId: string) =>
Routes.COMPETITIONS_TEAM.replace(':teamId', teamId),
FEES: () => Routes.FEES,
+18 -15
View File
@@ -32,6 +32,7 @@ import { useT } from '../lib/use-t';
import { CompetitionsHome } from '../client-pages/competitions/competitions-home';
import { CompetitionsTeams } from '../client-pages/competitions/competitions-teams';
import { CompetitionsTeam } from '../client-pages/competitions/competitions-team';
import { CompetitionsCreateTeam } from '../client-pages/competitions/competitions-create-team';
// These must remain dynamically imported as pennant cannot be compiled by nextjs due to ESM
// Using dynamic imports is a workaround for this until pennant is published as ESM
@@ -49,7 +50,7 @@ const NotFound = () => {
export const useRouterConfig = (): RouteObject[] => {
const featureFlags = useFeatureFlags((state) => state.flags);
return compact([
const routeConfig = compact([
{
index: true,
element: <Home />,
@@ -100,26 +101,26 @@ export const useRouterConfig = (): RouteObject[] => {
path: AppRoutes.COMPETITIONS,
element: <LayoutWithSidebar sidebar={<PortfolioSidebar />} />,
children: [
// pages with planets and stars
{
element: <LayoutWithSky />,
children: [
// pages with planets and stars
{ index: true, element: <CompetitionsHome /> },
{
element: <LayoutWithSky />,
children: [
{ index: true, element: <CompetitionsHome /> },
{
path: AppRoutes.COMPETITIONS_TEAMS,
element: <CompetitionsTeams />,
},
],
},
// pages with blurred background
{
path: AppRoutes.COMPETITIONS_TEAM,
element: <CompetitionsTeam />,
path: AppRoutes.COMPETITIONS_TEAMS,
element: <CompetitionsTeams />,
},
],
},
// pages with blurred background
{
path: AppRoutes.COMPETITIONS_CREATE_TEAM,
element: <CompetitionsCreateTeam />,
},
{
path: AppRoutes.COMPETITIONS_TEAM,
element: <CompetitionsTeam />,
},
],
}
: undefined,
@@ -213,6 +214,8 @@ export const useRouterConfig = (): RouteObject[] => {
element: <NotFound />,
},
]);
return routeConfig;
};
export const ClientRouter = () => {
+16 -1
View File
@@ -20,6 +20,7 @@
"Assets": "Assets",
"Available to withdraw this epoch": "Available to withdraw this epoch",
"Average position": "Average position",
"Avatar URL": "Avatar URL",
"Base commission rate": "Base commission rate",
"Base rate": "Base rate",
"Best bid": "Best bid",
@@ -51,6 +52,8 @@
"Could not initialize app": "Could not initialize app",
"Countdown": "Countdown",
"Create a referral code": "Create a referral code",
"Create": "Create",
"Create a team": "Create a team",
"Current tier": "Current tier",
"DISCLAIMER_P1": "Vega is a decentralised peer-to-peer protocol that can be used to trade derivatives with cryptoassets. The Vega Protocol is an implementation layer (layer one) protocol made of free, public, open-source or source-available software. Use of the Vega Protocol involves various risks, including but not limited to, losses while digital assets are supplied to the Vega Protocol and losses due to the fluctuation of prices of assets.",
"DISCLAIMER_P2": "Before using the Vega Protocol, review the relevant documentation at docs.vega.xyz to make sure that you understand how it works. Conduct your own due diligence and consult your financial advisor before making any investment decisions.",
@@ -132,6 +135,8 @@
"Inactive": "Inactive",
"Index Price": "Index Price",
"Indicators": "Indicators",
"Invalid image URL": "Invalid image URL",
"Invalid URL": "Invalid URL",
"Individual": "Individual",
"Infrastructure": "Infrastructure",
"Interval: {{interval}}": "Interval: {{interval}}",
@@ -151,6 +156,7 @@
"Low fees and no cost to place orders": "Low fees and no cost to place orders",
"Mainnet status & incidents": "Mainnet status & incidents",
"Make withdrawal": "Make withdrawal",
"Make team private": "Make team private",
"Maker": "Maker",
"Mark Price": "Mark Price",
"Mark price": "Mark price",
@@ -225,7 +231,10 @@
"Propose a new market": "Propose a new market",
"Proposed final price is {{price}} {{assetSymbol}}.": "Proposed final price is {{price}} {{assetSymbol}}.",
"Proposed markets": "Proposed markets",
"Provide a link so users can learn more about your team": "Provide a link so users can learn more about your team",
"Provide a URL to a hosted image": "Provide a URL to a hosted image",
"Providing liquidity": "Providing liquidity",
"Public key allow list": "Public key allow list",
"Purpose built proof of stake blockchain": "Purpose built proof of stake blockchain",
"qUSD": "qUSD",
"qUSD provides a rough USD equivalent of balances across all assets using the value of \"Quantum\" for that asset": "qUSD provides a rough USD equivalent of balances across all assets using the value of \"Quantum\" for that asset",
@@ -243,6 +252,7 @@
"Referrers earn commission based on a percentage of the taker fees their referees pay": "Referrers earn commission based on a percentage of the taker fees their referees pay",
"Referrers generate a code assigned to their key via an on chain transaction": "Referrers generate a code assigned to their key via an on chain transaction",
"Rejected": "Rejected",
"Required": "Required",
"Required epochs": "Required epochs",
"Required for next tier": "Required for next tier",
"Reset Columns": "Reset Columns",
@@ -285,6 +295,8 @@
"Suspended due to price or liquidity monitoring trigger": "Suspended due to price or liquidity monitoring trigger",
"Target stake": "Target stake",
"Team": "Team",
"Team name": "Team name",
"Team creation transaction successful": "Team creation transaction successful",
"The amount of fees paid to liquidity providers across the whole market during the last epoch {{epoch}}.": "The amount of fees paid to liquidity providers across the whole market during the last epoch {{epoch}}.",
"The commission is taken from the infrastructure fee, maker fee, and liquidity provider fee, not from the referee": "The commission is taken from the infrastructure fee, maker fee, and liquidity provider fee, not from the referee",
"The external time weighted average price (TWAP) received from the data source defined in the data sourcing specification.": "The external time weighted average price (TWAP) received from the data source defined in the data sourcing specification.",
@@ -330,9 +342,11 @@
"Type": "Type",
"Unknown": "Unknown",
"Unknown settlement date": "Unknown settlement date",
"URL": "URL",
"Use a comma separated list to allow only specific public keys to join the team": "Use a comma separated list to allow only specific public keys to join the team",
"Vega chart": "Vega chart",
"Vega Reward pot": "Vega Reward pot",
"Vega Wallet <0>full featured<0>": "Vega Wallet <0>full featured<0>",
"Vega chart": "Vega chart",
"Vesting": "Vesting",
"Vesting multiplier": "Vesting multiplier",
"Vesting {{assetSymbol}}": "Vesting {{assetSymbol}}",
@@ -345,6 +359,7 @@
"View proposals": "View proposals",
"View settlement asset details": "View settlement asset details",
"View successor market": "View successor market",
"View team": "View team",
"Volume": "Volume",
"Volume (24h)": "Volume (24h)",
"Volume discount": "Volume discount",
+1
View File
@@ -6,6 +6,7 @@
"Expired on {{date}}": "Expired on {{date}}",
"Invalid Ethereum address": "Invalid Ethereum address",
"Invalid Vega key": "Invalid Vega key",
"Invalid URL": "Invalid URL",
"Mark": "Mark",
"Must be valid JSON": "Must be valid JSON",
"Not time-based": "Not time-based",
@@ -43,7 +43,7 @@ export const TradingFormGroup = ({
<label htmlFor={labelFor} className={labelClasses}>
{label}
{labelDescription && (
<div className="font-light mt-1">{labelDescription}</div>
<div className="font-light mt-1 text-muted">{labelDescription}</div>
)}
</label>
)}
+22 -1
View File
@@ -29,11 +29,14 @@ export const useEthereumAddress = () => {
};
export const VEGA_ID_REGEX = /^[A-Fa-f0-9]{64}$/i;
export const isValidVegaPublicKey = (value: string) => {
return VEGA_ID_REGEX.test(value);
};
export const useVegaPublicKey = () => {
const t = useT();
return useCallback(
(value: string) => {
if (!VEGA_ID_REGEX.test(value)) {
if (!isValidVegaPublicKey(value)) {
return t('Invalid Vega key');
}
return true;
@@ -91,3 +94,21 @@ export const useValidateJson = () => {
[t]
);
};
export const URL_REGEX =
/^(https?:\/\/)?([a-zA-Z0-9.-]+(\.[a-zA-Z]{2,})+)(:[0-9]{1,5})?(\/[^\s]*)?$/;
const isValidUrl = (value: string) => {
return URL_REGEX.test(value);
};
export const useValidateUrl = () => {
const t = useT();
return useCallback(
(value: string) => {
if (!isValidUrl(value)) {
return t('Invalid URL');
}
return true;
},
[t]
);
};
@@ -445,6 +445,7 @@ export type CreateReferralSet = {
teamUrl?: string;
avatarUrl?: string;
closed: boolean;
allowList: string[];
};
};
};