feat: UI ap flow (#223)
* chore: AP flow based on designs * chore: preview ap creation * style: max-width for explore container * fix: fix setArgs * feat: add success step for access point creation * feat: add dots spinner * chore: AP flow finished * chore: release changes * feat: store ens once the user log in with wallet * feat: add mocked function for bunny cdn creation * chore: finish bunny cdn mocked functions * chore: run eslint * feat: add domain validator * style: move animation to stitches keyframes * chore: set ens only when is not set * Update ui/src/store/features/bunny-cdn/async-thunk/verify-ap.ts Co-authored-by: Felipe Mendes <zo.fmendes@gmail.com> * chore: remove nfa-picker * chore: get nfa name * chore: change verify thunk name * fix: fix verifyPullzone thunk --------- Co-authored-by: Felipe Mendes <zo.fmendes@gmail.com>
This commit is contained in:
parent
22a6d70e98
commit
21b9660164
|
|
@ -21,7 +21,6 @@ export const App: React.FC = () => {
|
|||
<Routes>
|
||||
<Route path="/" element={<Explore />} />
|
||||
<Route path="/mint" element={<Mint />} />
|
||||
<Route path="/create-ap" element={<CreateAP />} />
|
||||
<Route path="/create-ap/:id" element={<CreateAP />} />
|
||||
<Route path="/nfa/:id" element={<IndexedNFAView />} />
|
||||
{/** TODO remove for release */}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
import { styled } from '@/theme';
|
||||
|
||||
export const CardTag = styled('span', {
|
||||
fontSize: '$sm',
|
||||
backgroundColor: '$slate4',
|
||||
color: '$slate11',
|
||||
p: '$1 $3h',
|
||||
height: '$7',
|
||||
borderRadius: '$md',
|
||||
});
|
||||
|
|
@ -0,0 +1 @@
|
|||
export * from './card-tag';
|
||||
|
|
@ -24,6 +24,5 @@ export abstract class InputFileStyles {
|
|||
'&[aria-invalid=true], &[data-invalid]': {
|
||||
borderColor: '$red9',
|
||||
},
|
||||
//TODO add error state
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,4 +7,5 @@ export * from './toast';
|
|||
export * from './step';
|
||||
export * from './nfa-card';
|
||||
export * from './nfa-preview';
|
||||
export * from './card-tag';
|
||||
export * from './resolved-address';
|
||||
|
|
|
|||
|
|
@ -1,11 +1,29 @@
|
|||
import { Avatar, ConnectKitButton } from 'connectkit';
|
||||
|
||||
import { Button, Flex } from '@/components';
|
||||
import { ENSActions, useAppDispatch, useENSStore } from '@/store';
|
||||
|
||||
export const ConnectWalletButton: React.FC = () => {
|
||||
const { addressMap } = useENSStore();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const setEnsNameStore = (ensName: string, address: string): void => {
|
||||
const stored = addressMap[address] || {};
|
||||
if (typeof stored.state !== 'undefined') return;
|
||||
|
||||
dispatch(
|
||||
ENSActions.setAddress({
|
||||
key: address,
|
||||
value: { state: 'success', value: ensName },
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<ConnectKitButton.Custom>
|
||||
{({ isConnected, show, truncatedAddress, address, ensName }) => {
|
||||
if (ensName && address) setEnsNameStore(ensName, address);
|
||||
|
||||
return (
|
||||
<Button onClick={show}>
|
||||
{isConnected && !!address && !!truncatedAddress ? (
|
||||
|
|
|
|||
|
|
@ -9,19 +9,20 @@ export type ResolvedAddressProps = React.ComponentPropsWithRef<
|
|||
typeof RAS.Container
|
||||
> & {
|
||||
children: string;
|
||||
truncated?: boolean;
|
||||
};
|
||||
|
||||
export const ResolvedAddress = forwardStyledRef<
|
||||
HTMLSpanElement,
|
||||
ResolvedAddressProps
|
||||
>(({ children, ...props }, ref) => {
|
||||
>(({ children, truncated = false, ...props }, ref) => {
|
||||
const [resolvedAddress, loading] = useResolvedAddress(children);
|
||||
|
||||
const text = useMemo(() => {
|
||||
if (!resolvedAddress.endsWith('.eth'))
|
||||
if (!resolvedAddress.endsWith('.eth') && truncated)
|
||||
return `${resolvedAddress.slice(0, 6)}...${resolvedAddress.slice(-4)}`;
|
||||
return resolvedAddress;
|
||||
}, [resolvedAddress]);
|
||||
}, [resolvedAddress, truncated]);
|
||||
|
||||
return (
|
||||
<RAS.Container {...props} ref={ref} data-loading={loading}>
|
||||
|
|
|
|||
|
|
@ -1 +1,2 @@
|
|||
export * from './spinner';
|
||||
export * from './spinner-dot';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
import { SpinnerStyles } from './spinner.styles';
|
||||
|
||||
export const SpinnerDot: React.FC<SpinnerStyles.ContainerProps> = (props) => {
|
||||
return (
|
||||
<SpinnerStyles.Container
|
||||
{...props}
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g>
|
||||
<circle cx="12" cy="2.5" r="1" opacity=".14" fill="#FFFFFF" />
|
||||
<circle cx="16.75" cy="3.77" r="1" opacity=".29" fill="#FFFFFF" />
|
||||
<circle cx="20.23" cy="7.25" r="1" opacity=".43" fill="#FFFFFF" />
|
||||
<circle cx="21.50" cy="12.00" r="1" opacity=".57" fill="#FFFFFF" />
|
||||
<circle cx="20.23" cy="16.75" r="1" opacity=".71" fill="#FFFFFF" />
|
||||
<circle cx="16.75" cy="20.23" r="1" opacity=".86" fill="#FFFFFF" />
|
||||
<circle cx="12" cy="21.5" r="1" fill="#FFFFFF" />
|
||||
</g>
|
||||
</SpinnerStyles.Container>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,12 +1,57 @@
|
|||
import { styled } from '@/theme';
|
||||
import { keyframes, styled } from '@/theme';
|
||||
|
||||
export abstract class SpinnerStyles {
|
||||
static readonly Container = styled('svg', {
|
||||
const DotSpinner = keyframes({
|
||||
'8.3%': {
|
||||
transform: 'rotate(30deg)',
|
||||
},
|
||||
'16.6%': {
|
||||
transform: 'rotate(60deg)',
|
||||
},
|
||||
'25%': {
|
||||
transform: 'rotate(90deg)',
|
||||
},
|
||||
'33.3%': {
|
||||
transform: 'rotate(120deg)',
|
||||
},
|
||||
'41.6%': {
|
||||
transform: 'rotate(150deg)',
|
||||
},
|
||||
'50%': {
|
||||
transform: 'rotate(180deg)',
|
||||
},
|
||||
'58.3%': {
|
||||
transform: 'rotate(210deg)',
|
||||
},
|
||||
'66.6%': {
|
||||
transform: 'rotate(240deg)',
|
||||
},
|
||||
'75%': {
|
||||
transform: 'rotate(270deg)',
|
||||
},
|
||||
'83.3%': {
|
||||
transform: 'rotate(300deg)',
|
||||
},
|
||||
'91.6%': {
|
||||
transform: 'rotate(330deg)',
|
||||
},
|
||||
'100%': {
|
||||
transform: 'rotate(360deg)',
|
||||
},
|
||||
});
|
||||
|
||||
export const SpinnerStyles = {
|
||||
KeyFrames: {},
|
||||
Container: styled('svg', {
|
||||
fontSize: '1.5rem',
|
||||
width: '1em',
|
||||
height: '1em',
|
||||
});
|
||||
}
|
||||
|
||||
g: {
|
||||
transformOrigin: 'center',
|
||||
animation: `${DotSpinner} 0.75s step-end infinite`,
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
export namespace SpinnerStyles {
|
||||
export type ContainerProps = React.ComponentProps<
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
export const createBunnyCDNMock = async (
|
||||
domain: string,
|
||||
targetDomain: string
|
||||
): Promise<{ bunnyURL: string }> => {
|
||||
return new Promise((resolved, reject) => {
|
||||
setTimeout(() => {
|
||||
resolved({
|
||||
bunnyURL: '8c12c649402442d88b5f.b-cdn.net',
|
||||
});
|
||||
}, 3000);
|
||||
});
|
||||
};
|
||||
|
||||
export const verifyBunnyCDNMock = async (domain: string): Promise<boolean> => {
|
||||
return new Promise((resolved, reject) => {
|
||||
setTimeout(() => {
|
||||
resolved(true);
|
||||
}, 3000);
|
||||
});
|
||||
};
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
export * from './mint-site';
|
||||
export * from './detail';
|
||||
export * from './list';
|
||||
export * from './bunny-cdn';
|
||||
export * from './nfa';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||
|
||||
import { createBunnyCDNMock } from '@/mocks';
|
||||
import { RootState } from '@/store';
|
||||
import { AppLog } from '@/utils';
|
||||
|
||||
import { bunnyCDNActions } from '../bunny-cdn-slice';
|
||||
|
||||
type CNAMERecord = {
|
||||
domain: string;
|
||||
targetDomain: string;
|
||||
};
|
||||
|
||||
export const createBunnyCDN = createAsyncThunk<void, CNAMERecord>(
|
||||
'BunnyCDN/CreateCDN',
|
||||
async ({ domain, targetDomain }, { dispatch, getState }) => {
|
||||
const { state } = (getState() as RootState).bunnyCDN;
|
||||
|
||||
if (state === 'loading') return;
|
||||
|
||||
try {
|
||||
dispatch(bunnyCDNActions.setState('loading'));
|
||||
|
||||
const CDNRecord = await createBunnyCDNMock(domain, targetDomain);
|
||||
|
||||
dispatch(bunnyCDNActions.setCDNRecordData(CDNRecord.bunnyURL));
|
||||
} catch (error) {
|
||||
AppLog.errorToast(
|
||||
'Failed to create the CDN record. Please, try again',
|
||||
error
|
||||
);
|
||||
dispatch(bunnyCDNActions.setState('failed'));
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export * from './create-cdn';
|
||||
export * from './verify-pullzone';
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||
|
||||
import { verifyBunnyCDNMock } from '@/mocks';
|
||||
import { RootState } from '@/store';
|
||||
import { AppLog } from '@/utils';
|
||||
|
||||
import { bunnyCDNActions } from '../bunny-cdn-slice';
|
||||
|
||||
export const verifyBunnyPullzone = createAsyncThunk<void, string>(
|
||||
'BunnyCDN/VerifyPullzone',
|
||||
async (domain, { dispatch, getState }): Promise<void> => {
|
||||
const { state } = (getState() as RootState).bunnyCDN;
|
||||
|
||||
if (state === 'loading') return;
|
||||
|
||||
try {
|
||||
dispatch(bunnyCDNActions.setState('loading'));
|
||||
|
||||
const verifyAPState = await verifyBunnyCDNMock(domain);
|
||||
|
||||
if (verifyAPState) dispatch(bunnyCDNActions.setState('success'));
|
||||
else throw new Error('Invalid AP state');
|
||||
} catch (error) {
|
||||
AppLog.errorToast(
|
||||
'There was an error trying to verify the domain. Please, try again',
|
||||
error
|
||||
);
|
||||
dispatch(bunnyCDNActions.setState('failed'));
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
|
||||
import { RootState } from '@/store';
|
||||
import { useAppSelector } from '@/store/hooks';
|
||||
|
||||
import * as asyncThunk from './async-thunk';
|
||||
|
||||
export namespace BunnyCDNState {
|
||||
export type CreateCDNState =
|
||||
| undefined
|
||||
| 'loading'
|
||||
| 'unferified'
|
||||
| 'failed'
|
||||
| 'success';
|
||||
}
|
||||
|
||||
export interface BunnyCDNState {
|
||||
state: BunnyCDNState.CreateCDNState;
|
||||
bunnyURL: string;
|
||||
}
|
||||
|
||||
const initialState: BunnyCDNState = {
|
||||
state: undefined,
|
||||
bunnyURL: '',
|
||||
};
|
||||
|
||||
export const bunnyCDNSlice = createSlice({
|
||||
name: 'BunnyCDNSlice',
|
||||
initialState,
|
||||
reducers: {
|
||||
setState: (state, action: PayloadAction<BunnyCDNState.CreateCDNState>) => {
|
||||
state.state = action.payload;
|
||||
},
|
||||
setCDNRecordData: (state, action: PayloadAction<string>) => {
|
||||
state.bunnyURL = action.payload;
|
||||
state.state = 'success';
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const bunnyCDNActions = {
|
||||
...bunnyCDNSlice.actions,
|
||||
...asyncThunk,
|
||||
};
|
||||
|
||||
const selectENSState = (state: RootState): BunnyCDNState => state.bunnyCDN;
|
||||
|
||||
export const useBunnyCDNStore = (): BunnyCDNState =>
|
||||
useAppSelector(selectENSState);
|
||||
|
||||
export default bunnyCDNSlice.reducer;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export * from './bunny-cdn-slice';
|
||||
|
|
@ -2,3 +2,4 @@ export * from './fleek-erc721';
|
|||
export * from './github';
|
||||
export * from './toasts';
|
||||
export * from './ens';
|
||||
export * from './bunny-cdn';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { configureStore } from '@reduxjs/toolkit';
|
||||
|
||||
import bunnyCDNReducer from './features/bunny-cdn/bunny-cdn-slice';
|
||||
import ENSReducer from './features/ens/ens-slice';
|
||||
import fleekERC721Reducer from './features/fleek-erc721/fleek-erc721-slice';
|
||||
import githubReducer from './features/github/github-slice';
|
||||
|
|
@ -7,10 +8,11 @@ import toastsReducer from './features/toasts/toasts-slice';
|
|||
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
bunnyCDN: bunnyCDNReducer,
|
||||
ENS: ENSReducer,
|
||||
fleekERC721: fleekERC721Reducer,
|
||||
github: githubReducer,
|
||||
toasts: toastsReducer,
|
||||
ENS: ENSReducer,
|
||||
},
|
||||
middleware: (getDefaultMiddleware) =>
|
||||
getDefaultMiddleware({
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
/**
|
||||
* Converts a hex color string to a number.
|
||||
*/
|
||||
export const parseColorToNumber = (color: string): number => {
|
||||
const hexColor = color.replace('#', '');
|
||||
return parseInt(hexColor, 16);
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts string number to hex color string.
|
||||
*/
|
||||
export const parseNumberToHexColor = (color: number): string => {
|
||||
const hexColor = color.toString(16);
|
||||
return hexColor;
|
||||
};
|
||||
|
|
@ -54,12 +54,23 @@ const hasSpecialCharacters: StringValidator = {
|
|||
message: 'This field has special characters',
|
||||
};
|
||||
|
||||
const isValidDomain: StringValidator = {
|
||||
name: 'isValidDomain',
|
||||
validate: (value = '') => {
|
||||
const regex =
|
||||
/(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]/;
|
||||
return regex.test(value);
|
||||
},
|
||||
message: 'This field is not a valid domain',
|
||||
};
|
||||
|
||||
export const StringValidators = {
|
||||
required,
|
||||
maxLength,
|
||||
isUrl,
|
||||
maxFileSize,
|
||||
hasSpecialCharacters,
|
||||
isValidDomain,
|
||||
};
|
||||
|
||||
export const hasValidator = <
|
||||
|
|
|
|||
|
|
@ -0,0 +1,156 @@
|
|||
import { useQuery } from '@apollo/client';
|
||||
import { ethers } from 'ethers';
|
||||
import { useEffect } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useAccount } from 'wagmi';
|
||||
|
||||
import {
|
||||
Button,
|
||||
CardTag,
|
||||
Flex,
|
||||
Form,
|
||||
Spinner,
|
||||
Stepper,
|
||||
Text,
|
||||
} from '@/components';
|
||||
import { getNFADocument } from '@/graphclient';
|
||||
import { useAppDispatch } from '@/store';
|
||||
import { bunnyCDNActions, useBunnyCDNStore } from '@/store/features/bunny-cdn';
|
||||
import { AppLog } from '@/utils';
|
||||
|
||||
import { CreateAccessPoint } from '../create-ap.context';
|
||||
import { NFAIconFragment } from '../nfa-icon';
|
||||
import { useAccessPointFormContext } from './create-ap.form.context';
|
||||
|
||||
export const SelectedNFA: React.FC = () => {
|
||||
const { nfa } = CreateAccessPoint.useContext();
|
||||
|
||||
return (
|
||||
<Flex
|
||||
css={{
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
<Flex css={{ alignItems: 'center', maxWidth: '65%' }}>
|
||||
<NFAIconFragment image={nfa.logo} color={nfa.color} />
|
||||
<Text
|
||||
css={{
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{nfa.name}
|
||||
</Text>
|
||||
</Flex>
|
||||
<CardTag css={{ minWidth: '$28' }}>Selected NFA</CardTag>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export const CreateAccessPointFormBody: React.FC = () => {
|
||||
const { id } = useParams();
|
||||
const { address } = useAccount();
|
||||
const { nextStep } = Stepper.useContext();
|
||||
const { nfa, setNfa, billing } = CreateAccessPoint.useContext();
|
||||
const { setArgs } = CreateAccessPoint.useTransactionContext();
|
||||
const { state } = useBunnyCDNStore();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const {
|
||||
form: {
|
||||
domain: {
|
||||
value: [domain],
|
||||
},
|
||||
isValid: [isValid],
|
||||
},
|
||||
} = useAccessPointFormContext();
|
||||
|
||||
const {
|
||||
form: { domain: domainContext },
|
||||
} = useAccessPointFormContext();
|
||||
|
||||
const { loading: nfaLoading } = useQuery(getNFADocument, {
|
||||
skip: id === undefined,
|
||||
variables: {
|
||||
id: ethers.utils.hexlify(Number(id)),
|
||||
},
|
||||
onCompleted(data) {
|
||||
if (data.token && id) {
|
||||
const { name, tokenId, logo, color, externalURL: domain } = data.token;
|
||||
setNfa({ name, tokenId, logo, color, domain });
|
||||
} else {
|
||||
AppLog.errorToast("We couldn't find the NFA you are looking for");
|
||||
}
|
||||
},
|
||||
onError(error) {
|
||||
AppLog.errorToast('Error fetching NFA', error);
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (state === 'success') {
|
||||
nextStep();
|
||||
dispatch(bunnyCDNActions.setState(undefined));
|
||||
}
|
||||
}, [state, nextStep, dispatch]);
|
||||
|
||||
if (nfaLoading) {
|
||||
return (
|
||||
<Flex
|
||||
css={{
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
height: '$48',
|
||||
}}
|
||||
>
|
||||
<Spinner />
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
const handleContinueClick = (): void => {
|
||||
if (!address) {
|
||||
AppLog.errorToast('No address found. Please connect your wallet.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (nfa && domain) {
|
||||
try {
|
||||
setArgs([Number(nfa.tokenId), domain, { value: billing }]);
|
||||
dispatch(
|
||||
bunnyCDNActions.createBunnyCDN({
|
||||
domain: 'domain',
|
||||
targetDomain: domain,
|
||||
})
|
||||
);
|
||||
} catch (e) {
|
||||
AppLog.errorToast('Error setting transaction arguments');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex css={{ flexDirection: 'column', gap: '$6' }}>
|
||||
<SelectedNFA />
|
||||
<Text css={{ fontSize: '$sm', color: '$slate11' }}>
|
||||
Enter the domain you want to host the NFA. You will need access to the
|
||||
DNS settings in the next step.
|
||||
</Text>
|
||||
<Form.Field context={domainContext}>
|
||||
<Form.Label>Domain</Form.Label>
|
||||
<Form.Input placeholder="mydomain.com" />
|
||||
<Form.Overline />
|
||||
</Form.Field>
|
||||
<Button
|
||||
disabled={!isValid || nfa.tokenId === ''}
|
||||
isLoading={state === 'loading'}
|
||||
colorScheme="blue"
|
||||
variant="solid"
|
||||
onClick={handleContinueClick}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { Card, Grid, Icon, IconButton, Stepper } from '@/components';
|
||||
import { Card, Flex, Icon, IconButton, Stepper } from '@/components';
|
||||
|
||||
import { CreateAccessPointFormBody } from './create-ap.form-body';
|
||||
import { CreateAccessPointFormBody } from './create-ap-form-body';
|
||||
|
||||
export const CreateAccessPointForm: React.FC = () => {
|
||||
const { prevStep } = Stepper.useContext();
|
||||
|
|
@ -8,7 +8,7 @@ export const CreateAccessPointForm: React.FC = () => {
|
|||
return (
|
||||
<Card.Container css={{ width: '$107h' }}>
|
||||
<Card.Heading
|
||||
title="Create Access Point"
|
||||
title="Enter Domain"
|
||||
leftIcon={
|
||||
<IconButton
|
||||
aria-label="Add"
|
||||
|
|
@ -29,13 +29,14 @@ export const CreateAccessPointForm: React.FC = () => {
|
|||
}
|
||||
/>
|
||||
<Card.Body>
|
||||
<Grid
|
||||
<Flex
|
||||
css={{
|
||||
rowGap: '$6',
|
||||
flexDirection: 'column',
|
||||
gap: '$6',
|
||||
}}
|
||||
>
|
||||
<CreateAccessPointFormBody />
|
||||
</Grid>
|
||||
</Flex>
|
||||
</Card.Body>
|
||||
</Card.Container>
|
||||
);
|
||||
|
|
@ -5,7 +5,7 @@ import { createContext, StringValidators } from '@/utils';
|
|||
|
||||
export type CreateAccessPointFormContext = {
|
||||
form: {
|
||||
appName: FormField;
|
||||
domain: FormField;
|
||||
isValid: ReactState<boolean>;
|
||||
};
|
||||
};
|
||||
|
|
@ -20,9 +20,9 @@ export const [CreateAccessPointFormProvider, useAccessPointFormContext] =
|
|||
export const useAccessPointFormContextInit =
|
||||
(): CreateAccessPointFormContext => ({
|
||||
form: {
|
||||
appName: useFormField('appName', [
|
||||
domain: useFormField('domain', [
|
||||
StringValidators.required,
|
||||
StringValidators.maxLength(50),
|
||||
StringValidators.isValidDomain,
|
||||
]),
|
||||
isValid: useState(false),
|
||||
},
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export * from './create-ap.form.context';
|
||||
export * from './create-ap-form';
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
import { useEffect, useMemo } from 'react';
|
||||
|
||||
import { Button, Card, Grid, SpinnerDot, Stepper, Text } from '@/components';
|
||||
import { bunnyCDNActions, useAppDispatch, useBunnyCDNStore } from '@/store';
|
||||
|
||||
import { useAccessPointFormContext } from '../ap-form-step';
|
||||
import { CreateAccessPoint } from '../create-ap.context';
|
||||
import { DisplayText } from '../display-text';
|
||||
import { isSubdomain } from './record-step.utils';
|
||||
|
||||
export const APRecordCardBody: React.FC = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { bunnyURL, state } = useBunnyCDNStore();
|
||||
const {
|
||||
nfa: { domain: nfaDomain },
|
||||
} = CreateAccessPoint.useContext();
|
||||
const {
|
||||
form: {
|
||||
domain: {
|
||||
value: [accesPointDomain],
|
||||
},
|
||||
},
|
||||
} = useAccessPointFormContext();
|
||||
const { nextStep } = Stepper.useContext();
|
||||
|
||||
const isSudomain = useMemo(
|
||||
() => isSubdomain(accesPointDomain),
|
||||
[accesPointDomain]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (state === 'success') {
|
||||
dispatch(bunnyCDNActions.setState(undefined));
|
||||
nextStep();
|
||||
}
|
||||
}, [state, nextStep, dispatch]);
|
||||
|
||||
const handleContinueClick = (): void => {
|
||||
dispatch(bunnyCDNActions.verifyBunnyPullzone(nfaDomain));
|
||||
};
|
||||
|
||||
return (
|
||||
<Card.Body>
|
||||
{state === 'loading' ? (
|
||||
<Card.Text css={{ p: '$12 $10', gap: '$7' }}>
|
||||
<SpinnerDot css={{ fontSize: '$7xl' }} />
|
||||
<Text css={{ fontSize: '$md' }}>
|
||||
Waiting for DNS propagation, allow a few minutes.
|
||||
</Text>
|
||||
</Card.Text>
|
||||
) : (
|
||||
<Grid
|
||||
css={{
|
||||
rowGap: '$6',
|
||||
}}
|
||||
>
|
||||
<Text>
|
||||
{`Create a ${
|
||||
isSudomain ? 'CNAME' : 'ANAME'
|
||||
} record in your DNS provider pointing to our CDN
|
||||
endpoint.`}
|
||||
</Text>
|
||||
<DisplayText
|
||||
label="Record Type"
|
||||
value={isSudomain ? 'CNAME' : 'ANAME'}
|
||||
/>
|
||||
<DisplayText label="Host" value={isSudomain ? 'App' : '@'} />
|
||||
<DisplayText label="Data (Points to)" value={bunnyURL} />
|
||||
<Button
|
||||
colorScheme="blue"
|
||||
variant="solid"
|
||||
onClick={handleContinueClick}
|
||||
>
|
||||
I added the record
|
||||
</Button>
|
||||
</Grid>
|
||||
)}
|
||||
</Card.Body>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
import { Card, Icon, IconButton, Stepper } from '@/components';
|
||||
|
||||
export const APRecordCardHeader: React.FC = () => {
|
||||
const { prevStep } = Stepper.useContext();
|
||||
|
||||
return (
|
||||
<Card.Heading
|
||||
title="Create Record"
|
||||
leftIcon={
|
||||
<IconButton
|
||||
aria-label="Add"
|
||||
colorScheme="gray"
|
||||
variant="link"
|
||||
icon={<Icon name="back" />}
|
||||
css={{ mr: '$2' }}
|
||||
onClick={prevStep}
|
||||
/>
|
||||
}
|
||||
rightIcon={
|
||||
<IconButton
|
||||
aria-label="Add"
|
||||
colorScheme="gray"
|
||||
variant="link"
|
||||
icon={<Icon name="info" />}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import { Card } from '@/components';
|
||||
|
||||
import { APRecordCardBody } from './ap-record-body';
|
||||
import { APRecordCardHeader } from './ap-record-header';
|
||||
|
||||
export const APRecordStep: React.FC = () => {
|
||||
return (
|
||||
<Card.Container css={{ width: '$107h' }}>
|
||||
<APRecordCardHeader />
|
||||
<APRecordCardBody />
|
||||
</Card.Container>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1 @@
|
|||
export * from './ap-record-step';
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
export const isSubdomain = (url: string): boolean => {
|
||||
const urlParts = url.split('.');
|
||||
|
||||
return urlParts.length > 2;
|
||||
};
|
||||
|
|
@ -1,36 +1,71 @@
|
|||
import { ethers } from 'ethers';
|
||||
import { useMemo } from 'react';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { useAccount } from 'wagmi';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Flex,
|
||||
Grid,
|
||||
Icon,
|
||||
IconButton,
|
||||
ResolvedAddress,
|
||||
Stepper,
|
||||
Text,
|
||||
} from '@/components';
|
||||
import { useTransactionCost } from '@/hooks';
|
||||
import { FleekERC721 } from '@/integrations';
|
||||
import { AppLog } from '@/utils';
|
||||
|
||||
import { useAccessPointFormContext } from './ap-form-step/create-ap.form.context';
|
||||
import { SelectedNFA } from './ap-form-step/create-ap-form-body';
|
||||
import { CreateAccessPoint } from './create-ap.context';
|
||||
import { useAccessPointFormContext } from './create-ap.form.context';
|
||||
import { DisplayText } from './display-text';
|
||||
|
||||
export const AccessPointDataFragment: React.FC = () => {
|
||||
const { address, status } = useAccount();
|
||||
const {
|
||||
form: {
|
||||
domain: {
|
||||
value: [domain],
|
||||
},
|
||||
},
|
||||
} = useAccessPointFormContext();
|
||||
|
||||
if (status === 'connecting') return <div>Loading...</div>; //TODO replace with spinner
|
||||
|
||||
return (
|
||||
<>
|
||||
<SelectedNFA />
|
||||
<DisplayText
|
||||
label="Owner"
|
||||
value={
|
||||
address ? (
|
||||
<ResolvedAddress truncated={false}>{address || ''}</ResolvedAddress>
|
||||
) : (
|
||||
'Please connect to wallet'
|
||||
)
|
||||
}
|
||||
/>
|
||||
<DisplayText label="Frontend URL" value={domain} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const CreateAccessPointPreview: React.FC = () => {
|
||||
const { prevStep } = Stepper.useContext();
|
||||
const { address } = useAccount();
|
||||
|
||||
const {
|
||||
prepare: { status: prepareStatus, data: prepareData, error: prepareError },
|
||||
write: { status: writeStatus, write },
|
||||
transaction: { status: transactionStatus },
|
||||
} = CreateAccessPoint.useTransactionContext();
|
||||
|
||||
const {
|
||||
form: {
|
||||
appName: {
|
||||
value: [appName],
|
||||
},
|
||||
isValid: [isValid],
|
||||
},
|
||||
} = useAccessPointFormContext();
|
||||
const { nfa } = CreateAccessPoint.useContext();
|
||||
|
||||
const [cost, currency, isCostLoading] = useTransactionCost(
|
||||
prepareData?.request.value,
|
||||
|
|
@ -66,10 +101,19 @@ export const CreateAccessPointPreview: React.FC = () => {
|
|||
[prepareStatus, writeStatus, transactionStatus]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const error = [writeStatus, transactionStatus].some(
|
||||
(status) => status === 'error'
|
||||
);
|
||||
if (error) {
|
||||
AppLog.errorToast('An error occurred while minting the NFA');
|
||||
}
|
||||
}, [writeStatus, transactionStatus]);
|
||||
|
||||
return (
|
||||
<Card.Container css={{ width: '$107h' }}>
|
||||
<Card.Heading
|
||||
title={`Create Access Point ${nfa.label || ''}`}
|
||||
title="Review Details"
|
||||
leftIcon={
|
||||
<IconButton
|
||||
aria-label="Add"
|
||||
|
|
@ -90,26 +134,19 @@ export const CreateAccessPointPreview: React.FC = () => {
|
|||
}
|
||||
/>
|
||||
<Card.Body>
|
||||
<Grid
|
||||
css={{
|
||||
rowGap: '$6',
|
||||
}}
|
||||
>
|
||||
<Flex css={{ flexDirection: 'column' }}>
|
||||
<span>NFA: {nfa.value}</span>
|
||||
<span>{appName}</span>
|
||||
<span className="text-slate11 text-sm">{message}</span>
|
||||
</Flex>
|
||||
<Flex css={{ flexDirection: 'column', gap: '$6' }}>
|
||||
<AccessPointDataFragment />
|
||||
<Text>{message}</Text>
|
||||
<Button
|
||||
disabled={!!prepareError || !nfa}
|
||||
isLoading={isLoading}
|
||||
isDisabled={isLoading || !isValid || !address}
|
||||
colorScheme="blue"
|
||||
variant="solid"
|
||||
onClick={write}
|
||||
isLoading={isLoading}
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
</Grid>
|
||||
</Flex>
|
||||
</Card.Body>
|
||||
</Card.Container>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,47 @@
|
|||
import { Button, Card, Flex, Icon, IconButton, Text } from '@/components';
|
||||
|
||||
import { CreateAccessPoint } from './create-ap.context';
|
||||
import { AccessPointDataFragment } from './create-ap-preview';
|
||||
|
||||
export const CreateAccessPointSuccess: React.FC = () => {
|
||||
const { nfa } = CreateAccessPoint.useContext();
|
||||
return (
|
||||
<Card.Container css={{ width: '$107h' }}>
|
||||
<Card.Heading
|
||||
title="Hosting Successful"
|
||||
leftIcon={
|
||||
<Icon
|
||||
name="check-circle"
|
||||
css={{ color: '$green11', fontSize: '$xl', mr: '$2' }}
|
||||
/>
|
||||
}
|
||||
rightIcon={
|
||||
<IconButton
|
||||
aria-label="Add"
|
||||
colorScheme="gray"
|
||||
variant="link"
|
||||
icon={<Icon name="info" />}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Card.Body>
|
||||
<Flex css={{ flexDirection: 'column', gap: '$6' }}>
|
||||
<Text css={{ fontSize: '$sm', color: '$slate11' }}>
|
||||
{`You have successfully hosted a ${nfa.name} frontend on your own domain.`}
|
||||
</Text>
|
||||
<AccessPointDataFragment />
|
||||
<Flex css={{ flexDirection: 'column', gap: '$4' }}>
|
||||
<Button
|
||||
colorScheme="blue"
|
||||
variant="solid"
|
||||
leftIcon={<Icon name="twitter" />}
|
||||
>
|
||||
Tweet about your frontend!
|
||||
</Button>
|
||||
<Button>Manage Frontend</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Card.Body>
|
||||
</Card.Container>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,16 +1,23 @@
|
|||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { useState } from 'react';
|
||||
|
||||
import { Token } from '@/graphclient';
|
||||
import { EthereumHooks } from '@/integrations';
|
||||
import { useFleekERC721Billing } from '@/store';
|
||||
import { AppLog, createContext, pushToast } from '@/utils';
|
||||
import { AppLog, createContext } from '@/utils';
|
||||
|
||||
type NFA = Pick<Token, 'id' | 'name'>;
|
||||
export type NFA = {
|
||||
tokenId: string;
|
||||
name: string;
|
||||
logo: string;
|
||||
color: number;
|
||||
domain: string;
|
||||
};
|
||||
|
||||
export type AccessPointContext = {
|
||||
billing: string | undefined;
|
||||
nfa: NFA | undefined;
|
||||
setNfa: ReactState<NFA | undefined>[1];
|
||||
nfa: NFA;
|
||||
setNfa: (nfa: NFA) => void;
|
||||
};
|
||||
|
||||
const [CreateAPProvider, useContext] = createContext<AccessPointContext>({
|
||||
|
|
@ -31,7 +38,13 @@ export abstract class CreateAccessPoint {
|
|||
children,
|
||||
}) => {
|
||||
const [billing] = useFleekERC721Billing('AddAccessPoint');
|
||||
const [nfa, setNfa] = useState<NFA>();
|
||||
const [nfa, setNfa] = useState<NFA>({
|
||||
tokenId: '',
|
||||
name: '',
|
||||
logo: '',
|
||||
color: 0,
|
||||
domain: '',
|
||||
});
|
||||
|
||||
const value = {
|
||||
billing,
|
||||
|
|
@ -44,12 +57,10 @@ export abstract class CreateAccessPoint {
|
|||
<TransactionProvider
|
||||
config={{
|
||||
transaction: {
|
||||
onSuccess: (data) => {
|
||||
onSuccess: (data: any) => {
|
||||
AppLog.info('Transaction:', data);
|
||||
pushToast('success', 'Your transaction was successful!');
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
onError: (error) => {
|
||||
onError: (error: any) => {
|
||||
AppLog.errorToast(
|
||||
'There was an error trying to create the Access Point. Please try again'
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,100 +0,0 @@
|
|||
import { useQuery } from '@apollo/client';
|
||||
import { ethers } from 'ethers';
|
||||
import { useEffect } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { Button, Flex, Form, Spinner, Stepper } from '@/components';
|
||||
import { getNFADocument } from '@/graphclient';
|
||||
import { AppLog } from '@/utils';
|
||||
|
||||
import { CreateAccessPoint } from './create-ap.context';
|
||||
import { useAccessPointFormContext } from './create-ap.form.context';
|
||||
import { NfaPicker } from './nfa-picker';
|
||||
|
||||
export const CreateAccessPointFormBody: React.FC = () => {
|
||||
const { id } = useParams();
|
||||
const { nextStep } = Stepper.useContext();
|
||||
const { nfa, setNfa, billing } = CreateAccessPoint.useContext();
|
||||
const { setArgs } = CreateAccessPoint.useTransactionContext();
|
||||
|
||||
const {
|
||||
form: {
|
||||
appName: {
|
||||
value: [appName],
|
||||
},
|
||||
isValid: [isValid],
|
||||
},
|
||||
} = useAccessPointFormContext();
|
||||
|
||||
const {
|
||||
form: { appName: appNameContext },
|
||||
} = useAccessPointFormContext();
|
||||
|
||||
const {
|
||||
data: nfaData,
|
||||
error: nfaError,
|
||||
loading: nfaLoading,
|
||||
} = useQuery(getNFADocument, {
|
||||
skip: id === undefined,
|
||||
variables: {
|
||||
id: ethers.utils.hexlify(Number(id)),
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (nfaError) {
|
||||
AppLog.errorToast('Error fetching NFA');
|
||||
}
|
||||
}, [nfaError]);
|
||||
|
||||
useEffect(() => {
|
||||
if (nfaData) {
|
||||
if (nfaData.token && id) {
|
||||
const { name } = nfaData.token;
|
||||
setNfa({ value: id, label: name });
|
||||
} else {
|
||||
AppLog.errorToast("We couldn't find the NFA you are looking for");
|
||||
}
|
||||
}
|
||||
}, [nfaData, id, setNfa]);
|
||||
|
||||
if (nfaLoading) {
|
||||
return (
|
||||
<Flex
|
||||
css={{
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
height: '$48',
|
||||
}}
|
||||
>
|
||||
<Spinner />
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
const handleContinueClick = (): void => {
|
||||
if (nfa && appName) {
|
||||
setArgs([Number(nfa.value), appName, { value: billing }]);
|
||||
nextStep();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* TODO will have to do some changes on the Form.Combobox if we use this component for the NFA picker */}
|
||||
{id === undefined && <NfaPicker />}
|
||||
<Form.Field context={appNameContext}>
|
||||
<Form.Label>App Name</Form.Label>
|
||||
<Form.Input />
|
||||
</Form.Field>
|
||||
<Button
|
||||
disabled={!isValid}
|
||||
colorScheme="blue"
|
||||
variant="solid"
|
||||
onClick={handleContinueClick}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,16 +1,29 @@
|
|||
import { Form, Step, Stepper } from '@/components';
|
||||
|
||||
import { WalletStep } from '../mint/wallet-step';
|
||||
import { useAccessPointFormContext } from './create-ap.form.context';
|
||||
import { CreateAccessPointForm } from './create-ap-form';
|
||||
import { useAccessPointFormContext } from './ap-form-step/create-ap.form.context';
|
||||
import { CreateAccessPointForm } from './ap-form-step/create-ap-form';
|
||||
import { APRecordStep } from './ap-record-step/ap-record-step';
|
||||
import { isSubdomain } from './ap-record-step/record-step.utils';
|
||||
import { CreateAccessPoint } from './create-ap.context';
|
||||
import { CreateAccessPointPreview } from './create-ap-preview';
|
||||
import { CreateAccessPointSuccess } from './create-ap-success';
|
||||
|
||||
export const CreateApStepper: React.FC = () => {
|
||||
const {
|
||||
transaction: { isSuccess },
|
||||
} = CreateAccessPoint.useTransactionContext();
|
||||
const {
|
||||
form: {
|
||||
domain: {
|
||||
value: [accesPointDomain],
|
||||
},
|
||||
isValid: [, setIsValid],
|
||||
},
|
||||
} = useAccessPointFormContext();
|
||||
|
||||
if (isSuccess) return <CreateAccessPointSuccess />;
|
||||
|
||||
return (
|
||||
<Stepper.Root initialStep={1}>
|
||||
<Form.Root onValidationChange={setIsValid}>
|
||||
|
|
@ -22,13 +35,23 @@ export const CreateApStepper: React.FC = () => {
|
|||
</Stepper.Step>
|
||||
|
||||
<Stepper.Step>
|
||||
<Step header="Set Access Point">
|
||||
<Step header="Enter the domain you want to host the NFA">
|
||||
<CreateAccessPointForm />
|
||||
</Step>
|
||||
</Stepper.Step>
|
||||
|
||||
<Stepper.Step>
|
||||
<Step header="Create Access Point">
|
||||
<Step
|
||||
header={`Add a ${
|
||||
isSubdomain(accesPointDomain) ? 'CNAME' : 'ANAME'
|
||||
} record to your DNS provider`}
|
||||
>
|
||||
<APRecordStep />
|
||||
</Step>
|
||||
</Stepper.Step>
|
||||
|
||||
<Stepper.Step>
|
||||
<Step header="Review your hosted frontend and confirm">
|
||||
<CreateAccessPointPreview />
|
||||
</Step>
|
||||
</Stepper.Step>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { Flex } from '@/components';
|
||||
|
||||
import { CreateAccessPoint } from './create-ap.context';
|
||||
import {
|
||||
CreateAccessPointFormProvider,
|
||||
useAccessPointFormContextInit,
|
||||
} from './create-ap.form.context';
|
||||
} from './ap-form-step/create-ap.form.context';
|
||||
import { CreateAccessPoint } from './create-ap.context';
|
||||
import { CreateApStepper } from './create-ap.stepper';
|
||||
|
||||
export const CreateAP: React.FC = () => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
import { styled } from '@/theme';
|
||||
|
||||
import { Flex } from '../../../components/layout';
|
||||
|
||||
export const DisplayTextStyles = {
|
||||
Container: styled(Flex, {
|
||||
flexDirection: 'column',
|
||||
}),
|
||||
Label: styled('label', {
|
||||
color: '$slate11',
|
||||
mb: '$1h',
|
||||
|
||||
fontSize: '$xs',
|
||||
//TODO add variants
|
||||
}),
|
||||
Input: styled('span', {
|
||||
backgroundColor: '$slate1',
|
||||
borderColor: '$slate1',
|
||||
color: '$slate12',
|
||||
borderRadius: '$lg',
|
||||
fontSize: '$sm',
|
||||
height: '$11',
|
||||
p: '$3 $3h',
|
||||
}),
|
||||
};
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
import { DisplayTextStyles as S } from './display-text.styles';
|
||||
type DisplayTextProps = {
|
||||
label: string;
|
||||
value: string | React.ReactNode;
|
||||
};
|
||||
|
||||
export const DisplayText: React.FC<DisplayTextProps> = ({
|
||||
label,
|
||||
value,
|
||||
}: DisplayTextProps) => {
|
||||
return (
|
||||
<S.Container>
|
||||
<S.Label>{label}</S.Label>
|
||||
<S.Input>{value}</S.Input>
|
||||
</S.Container>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1 @@
|
|||
export * from './display-text';
|
||||
|
|
@ -0,0 +1 @@
|
|||
export * from './nfa-icon';
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
import { styled } from '@/theme';
|
||||
|
||||
export const NFAIconStyles = {
|
||||
Container: styled('span', {
|
||||
p: '$1h',
|
||||
borderRadius: '$full',
|
||||
width: '$7',
|
||||
height: '$7',
|
||||
mr: '$2',
|
||||
}),
|
||||
Image: styled('img', {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}),
|
||||
};
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import { parseNumberToHexColor } from '@/utils/color';
|
||||
|
||||
import { NFAIconStyles as NS } from './nfa-icon.styles';
|
||||
|
||||
type NFAIconProps = {
|
||||
image: string;
|
||||
color: number;
|
||||
};
|
||||
|
||||
export const NFAIconFragment: React.FC<NFAIconProps> = ({
|
||||
image,
|
||||
color,
|
||||
}: NFAIconProps) => {
|
||||
return (
|
||||
<NS.Container
|
||||
css={{ backgroundColor: `#${parseNumberToHexColor(color)}57` }}
|
||||
>
|
||||
<NS.Image src={image} />
|
||||
</NS.Container>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
import { useQuery } from '@apollo/client';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { Combobox } from '@/components';
|
||||
import { getLatestNFAsDocument } from '@/graphclient';
|
||||
import { AppLog } from '@/utils';
|
||||
|
||||
import { CreateAccessPoint } from './create-ap.context';
|
||||
|
||||
export const NfaPicker: React.FC = () => {
|
||||
const { nfa, setNfa } = CreateAccessPoint.useContext();
|
||||
const { data, loading, error } = useQuery(getLatestNFAsDocument);
|
||||
|
||||
const items = useMemo(() => data?.tokens || [], [data]);
|
||||
|
||||
if (error) {
|
||||
AppLog.errorToast('Error loading NFA list', error);
|
||||
}
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
isLoading={loading}
|
||||
items={items}
|
||||
selected={[nfa, setNfa]}
|
||||
queryKey={['name', 'id']}
|
||||
>
|
||||
{({ Field, Options }) => (
|
||||
<>
|
||||
<Field>{(selected) => selected?.name || 'Select NFA'}</Field>
|
||||
|
||||
<Options>{(item) => item.name}</Options>
|
||||
</>
|
||||
)}
|
||||
</Combobox>
|
||||
);
|
||||
};
|
||||
|
|
@ -2,13 +2,15 @@ import { Flex, ResolvedAddress } from '@/components';
|
|||
|
||||
import { ColorPickerTest } from './color-picker';
|
||||
import { ComboboxTest } from './combobox-test';
|
||||
import { SpinnerTest } from './spinner-test';
|
||||
import { ToastTest } from './toast-test';
|
||||
|
||||
export const ComponentsTest: React.FC = () => {
|
||||
return (
|
||||
<Flex css={{ flexDirection: 'column' }}>
|
||||
<SpinnerTest />
|
||||
<ResolvedAddress css={{ alignSelf: 'center' }}>
|
||||
{'0x7ed735b7095c05d78df169f991f2b7f1a1f1a049'}
|
||||
{'0x7ed735b7095c05d78df169f991f2b7f1a1f1a049a'}
|
||||
</ResolvedAddress>
|
||||
<ComboboxTest />
|
||||
<ColorPickerTest />
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
import { Flex, Spinner, SpinnerDot } from '@/components';
|
||||
|
||||
export const SpinnerTest: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
<Flex css={{ alignItems: 'center' }}>
|
||||
<SpinnerDot css={{ fontSize: '$6xl' }} />
|
||||
<SpinnerDot css={{ fontSize: '$4xl' }} />
|
||||
<SpinnerDot css={{ fontSize: '$lg' }} />
|
||||
</Flex>
|
||||
<Flex css={{ alignItems: 'center' }}>
|
||||
<Spinner css={{ fontSize: '$6xl' }} />
|
||||
<Spinner css={{ fontSize: '$4xl' }} />
|
||||
<Spinner css={{ fontSize: '$lg' }} />
|
||||
</Flex>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -4,7 +4,7 @@ import { styled } from '@/theme';
|
|||
export abstract class Explore {
|
||||
static readonly Container = styled(Flex, {
|
||||
flexDirection: 'column',
|
||||
width: '64.75rem', //TODO replace for max-width
|
||||
width: '63.4rem', //TODO replace for max-width
|
||||
margin: '0 auto',
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Button, Card, Flex, Stepper } from '@/components';
|
||||
import { Button, Card, CardTag, Flex, Stepper } from '@/components';
|
||||
import { Mint } from '@/views/mint/mint.context';
|
||||
import { useMintFormContext } from '@/views/mint/nfa-step/form-step';
|
||||
|
||||
|
|
@ -25,17 +25,8 @@ export const RepoConfigurationBody: React.FC = () => {
|
|||
<Flex css={{ rowGap: '$6', flexDirection: 'column' }}>
|
||||
<RepoRow
|
||||
repo={repositoryName.name}
|
||||
css={{ mb: '0' }}
|
||||
button={
|
||||
<Button
|
||||
colorScheme="gray"
|
||||
disabled
|
||||
variant="outline"
|
||||
css={{ py: '$1', height: '$5', borderRadius: '$md' }}
|
||||
>
|
||||
Use for NFA
|
||||
</Button>
|
||||
}
|
||||
css={{ mb: '0', cursor: 'default' }}
|
||||
button={<CardTag>Use for NFA</CardTag>}
|
||||
/>
|
||||
<RepoBranchCommitFields />
|
||||
<Button
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ export const Repository: React.FC<RepositoryProps> = ({
|
|||
<RepoRow
|
||||
onClick={handleSelectRepo}
|
||||
repo={repository.name}
|
||||
css={{ cursor: 'pointer' }}
|
||||
button={
|
||||
<Button
|
||||
colorScheme="blue"
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ export const RepoRow = forwardRef<HTMLDivElement, RepoRowProps>(
|
|||
justifyContent: 'space-between',
|
||||
my: '$4',
|
||||
...props.css,
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
<Flex css={{ alignItems: 'center' }}>
|
||||
|
|
|
|||
|
|
@ -18,39 +18,37 @@ export const MintStepper: React.FC = () => {
|
|||
},
|
||||
} = useMintFormContext();
|
||||
|
||||
if (!isSuccess) {
|
||||
return (
|
||||
<Stepper.Root initialStep={1}>
|
||||
<Form.Root onValidationChange={setIsValid}>
|
||||
<Stepper.Container>
|
||||
<Stepper.Step>
|
||||
<Step header="Connect your Ethereum Wallet to mint an NFA">
|
||||
<WalletStep />
|
||||
</Step>
|
||||
</Stepper.Step>
|
||||
if (isSuccess) return <NftMinted />;
|
||||
|
||||
<Stepper.Step>
|
||||
<Step header="Connect GitHub and select repository">
|
||||
<GithubStep />
|
||||
</Step>
|
||||
</Stepper.Step>
|
||||
return (
|
||||
<Stepper.Root initialStep={1}>
|
||||
<Form.Root onValidationChange={setIsValid}>
|
||||
<Stepper.Container>
|
||||
<Stepper.Step>
|
||||
<Step header="Connect your Ethereum Wallet to mint an NFA">
|
||||
<WalletStep />
|
||||
</Step>
|
||||
</Stepper.Step>
|
||||
|
||||
<Stepper.Step>
|
||||
<Step header="Finalize a few key things for your NFA">
|
||||
<NFAStep />
|
||||
</Step>
|
||||
</Stepper.Step>
|
||||
<Stepper.Step>
|
||||
<Step header="Connect GitHub and select repository">
|
||||
<GithubStep />
|
||||
</Step>
|
||||
</Stepper.Step>
|
||||
|
||||
<Stepper.Step>
|
||||
<Step header="Review your NFA and mint it on Ethereum">
|
||||
<MintPreview />
|
||||
</Step>
|
||||
</Stepper.Step>
|
||||
</Stepper.Container>
|
||||
</Form.Root>
|
||||
</Stepper.Root>
|
||||
);
|
||||
} else {
|
||||
return <NftMinted />;
|
||||
}
|
||||
<Stepper.Step>
|
||||
<Step header="Finalize a few key things for your NFA">
|
||||
<NFAStep />
|
||||
</Step>
|
||||
</Stepper.Step>
|
||||
|
||||
<Stepper.Step>
|
||||
<Step header="Review your NFA and mint it on Ethereum">
|
||||
<MintPreview />
|
||||
</Step>
|
||||
</Stepper.Step>
|
||||
</Stepper.Container>
|
||||
</Form.Root>
|
||||
</Stepper.Root>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -8,11 +8,3 @@ export const fileToBase64 = (file: File): Promise<string> =>
|
|||
reader.onload = () => resolve(reader.result?.toString() || '');
|
||||
reader.onerror = reject;
|
||||
});
|
||||
|
||||
/**
|
||||
* Converts a hex color string to a number.
|
||||
*/
|
||||
export const parseColorToNumber = (color: string): number => {
|
||||
const hexColor = color.replace('#', '');
|
||||
return parseInt(hexColor, 16);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { useAccount } from 'wagmi';
|
|||
|
||||
import { Button, Card, Grid, Stepper } from '@/components';
|
||||
import { AppLog } from '@/utils';
|
||||
import { parseColorToNumber } from '@/utils/color';
|
||||
|
||||
import { Mint } from '../../mint.context';
|
||||
import { MintCardHeader } from '../../mint-card';
|
||||
|
|
@ -11,7 +12,6 @@ import {
|
|||
EnsDomainField,
|
||||
LogoField,
|
||||
} from './fields';
|
||||
import { parseColorToNumber } from './form.utils';
|
||||
import { useMintFormContext } from './mint-form.context';
|
||||
|
||||
export const MintFormStep: React.FC = () => {
|
||||
|
|
@ -54,7 +54,7 @@ export const MintFormStep: React.FC = () => {
|
|||
AppLog.errorToast('No address found. Please connect your wallet.');
|
||||
return;
|
||||
}
|
||||
// setting the args otherwise mint may fail
|
||||
|
||||
setArgs([
|
||||
address,
|
||||
appName,
|
||||
|
|
@ -62,7 +62,7 @@ export const MintFormStep: React.FC = () => {
|
|||
domainURL,
|
||||
ens,
|
||||
gitCommit,
|
||||
`${repositoryName.url}/tree/${gitBranch}`,
|
||||
`${repositoryName?.url}/tree/${gitBranch}`,
|
||||
appLogo,
|
||||
parseColorToNumber(logoColor),
|
||||
verifyNFA,
|
||||
|
|
|
|||
Loading…
Reference in New Issue