Compare commits
16
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42b97daca6 | ||
|
|
b5452bc4c1 | ||
|
|
11387520e1 | ||
|
|
b1b5d65679 | ||
|
|
6fc99f1fd7 | ||
|
|
b3bf4b7639 | ||
|
|
3dc8ae135a | ||
|
|
3821fdf81d | ||
|
|
3b96e99a66 | ||
|
|
ed52b412ba | ||
|
|
354a9d9703 | ||
|
|
6bf26e9c86 | ||
|
|
009cab20f3 | ||
|
|
e7818eeff9 | ||
|
|
556d39b9e3 | ||
|
|
cb843ea9ab |
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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]);
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"Asset (1)": "Asset (1)",
|
||||
"Assets": "Assets",
|
||||
"Available to withdraw this epoch": "Available to withdraw this epoch",
|
||||
"Avatar URL": "Avatar URL",
|
||||
"Base commission rate": "Base commission rate",
|
||||
"Base rate": "Base rate",
|
||||
"Best bid": "Best bid",
|
||||
@@ -55,6 +56,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",
|
||||
"Dark mode": "Dark mode",
|
||||
"Date Joined": "Date Joined",
|
||||
@@ -127,6 +130,8 @@
|
||||
"Inactive": "Inactive",
|
||||
"Index Price": "Index Price",
|
||||
"Indicators": "Indicators",
|
||||
"Invalid image URL": "Invalid image URL",
|
||||
"Invalid URL": "Invalid URL",
|
||||
"Infrastructure": "Infrastructure",
|
||||
"Interval: {{interval}}": "Interval: {{interval}}",
|
||||
"INTERVAL_I1M": "1m",
|
||||
@@ -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",
|
||||
@@ -232,7 +238,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",
|
||||
@@ -253,6 +262,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",
|
||||
@@ -293,6 +303,8 @@
|
||||
"Supplied stake": "Supplied stake",
|
||||
"Suspended due to price or liquidity monitoring trigger": "Suspended due to price or liquidity monitoring trigger",
|
||||
"Target stake": "Target stake",
|
||||
"Team": "Team",
|
||||
"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.",
|
||||
@@ -337,6 +349,8 @@
|
||||
"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>",
|
||||
@@ -351,6 +365,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",
|
||||
@@ -399,6 +414,7 @@
|
||||
"userActive": "{{active}} trader: {{count}} epochs so far",
|
||||
"(Tier {{tier}} as of last epoch)": "(Tier {{tier}} as of last epoch)",
|
||||
"Team": "Team",
|
||||
"Team name": "Team name",
|
||||
"Ends in": "Ends in",
|
||||
"Assessed over": "Assessed over",
|
||||
"No rows": "No rows"
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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]
|
||||
);
|
||||
};
|
||||
|
||||
@@ -444,6 +444,7 @@ export type CreateReferralSet = {
|
||||
teamUrl?: string;
|
||||
avatarUrl?: string;
|
||||
closed: boolean;
|
||||
allowList: string[];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user