feat: UI integrate ens dropdown (#143)

* chore: get ens names from address

* wip: ens validation

* wip: combobox with option to add new items

* chore: add trim words

* chore: change order steps

* chore: add comments

* chore: change components test view

* chore: remove unused file

* chore: add alchemy-sdk as prod dependency

* chore: pr comments
This commit is contained in:
Camila Sosa Morales 2023-03-07 08:03:53 -05:00 committed by GitHub
parent 8e309ee04c
commit e5d28251c4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 602 additions and 140 deletions

View File

@ -1,4 +1,11 @@
import { Address, Bytes, log, store, ethereum, BigInt } from '@graphprotocol/graph-ts';
import {
Address,
Bytes,
log,
store,
ethereum,
BigInt,
} from '@graphprotocol/graph-ts';
// Event Imports [based on the yaml config]
import {
@ -35,11 +42,11 @@ import {
enum CollectionRoles {
Owner,
};
}
enum TokenRoles {
Controller,
};
}
export function handleApproval(event: ApprovalEvent): void {
let entity = new Approval(
@ -263,27 +270,29 @@ export function handleMetadataUpdateWithIntValue(
}
export function handleInitialized(event: InitializedEvent): void {
// This is the contract creation transaction.
log.warning('This is the contract creation transaction.', []);
if (event.receipt) {
let receipt = event.receipt as ethereum.TransactionReceipt;
log.warning('Contract address is: {}', [
receipt.contractAddress.toHexString(),
]);
// add owner
let owner = new Owner(event.transaction.from);
owner.collection = true;
owner.save();
}
// This is the contract creation transaction.
log.warning('This is the contract creation transaction.', []);
if (event.receipt) {
let receipt = event.receipt as ethereum.TransactionReceipt;
log.warning('Contract address is: {}', [
receipt.contractAddress.toHexString(),
]);
// add owner
let owner = new Owner(event.transaction.from);
owner.collection = true;
owner.save();
}
}
export function handleCollectionRoleChanged(event: CollectionRoleChangedEvent): void {
export function handleCollectionRoleChanged(
event: CollectionRoleChangedEvent
): void {
let toAddress = event.params.toAddress;
let byAddress = event.params.byAddress;
let role = event.params.role;
let status = event.params.status;
if (role === CollectionRoles.Owner) {
// Owner role
if (status) {
@ -298,14 +307,21 @@ export function handleCollectionRoleChanged(event: CollectionRoleChangedEvent):
// revoked
let owner = Owner.load(toAddress);
if (!owner) {
log.error('Owner entity not found. Role: {}, byAddress: {}, toAddress: {}', [role.toString(), byAddress.toHexString(), toAddress.toHexString()]);
log.error(
'Owner entity not found. Role: {}, byAddress: {}, toAddress: {}',
[role.toString(), byAddress.toHexString(), toAddress.toHexString()]
);
return;
}
owner.collection = false;
owner.save();
}
} else {
log.error('Role not supported. Role: {}, byAddress: {}, toAddress: {}', [role.toString(), byAddress.toHexString(), toAddress.toHexString()]);
log.error('Role not supported. Role: {}, byAddress: {}, toAddress: {}', [
role.toString(),
byAddress.toHexString(),
toAddress.toHexString(),
]);
}
}
@ -315,13 +331,13 @@ export function handleTokenRoleChanged(event: TokenRoleChangedEvent): void {
let byAddress = event.params.byAddress;
let role = event.params.role;
let status = event.params.status;
// load token
let token = Token.load(Bytes.fromByteArray(Bytes.fromBigInt(tokenId)));
if (!token) {
log.error('Token not found. TokenId: {}', [tokenId.toString()]);
return;
}
}
if (role === TokenRoles.Controller) {
// Controller role
@ -343,11 +359,17 @@ export function handleTokenRoleChanged(event: TokenRoleChangedEvent): void {
}
token.controllers = token_controllers;
} else {
log.error('Role not supported. Role: {}, byAddress: {}, toAddress: {}', [role.toString(), byAddress.toHexString(), toAddress.toHexString()]);
log.error('Role not supported. Role: {}, byAddress: {}, toAddress: {}', [
role.toString(),
byAddress.toHexString(),
toAddress.toHexString(),
]);
}
}
export function handleMetadataUpdateWithBooleanValue(event: MetadataUpdateEvent3): void {
export function handleMetadataUpdateWithBooleanValue(
event: MetadataUpdateEvent3
): void {
/**
* accessPointAutoApproval
*/
@ -364,7 +386,9 @@ export function handleMetadataUpdateWithBooleanValue(event: MetadataUpdateEvent3
entity.save();
let token = Token.load(Bytes.fromByteArray(Bytes.fromBigInt(event.params._tokenId)));
let token = Token.load(
Bytes.fromByteArray(Bytes.fromBigInt(event.params._tokenId))
);
if (token) {
if (event.params.key == 'accessPointAutoApproval') {
@ -420,14 +444,13 @@ export function handleTransfer(event: TransferEvent): void {
}
}
/**
* This handler will create and load entities in the following order:
* - AccessPoint [create]
* - Owner [load / create]
* Note to discuss later: Should a `NewAccessPoint` entity be also created and defined?
*/
export function handleNewAccessPoint(event: NewAccessPointEvent): void {
* This handler will create and load entities in the following order:
* - AccessPoint [create]
* - Owner [load / create]
* Note to discuss later: Should a `NewAccessPoint` entity be also created and defined?
*/
export function handleNewAccessPoint(event: NewAccessPointEvent): void {
// Create an AccessPoint entity
let accessPointEntity = new AccessPoint(event.params.apName);
accessPointEntity.score = BigInt.fromU32(0);
@ -435,7 +458,9 @@ export function handleTransfer(event: TransferEvent): void {
accessPointEntity.nameVerified = false;
accessPointEntity.creationStatus = 'DRAFT'; // Since a `ChangeAccessPointCreationStatus` event is emitted instantly after `NewAccessPoint`, the status will be updated in that handler.
accessPointEntity.owner = event.params.owner;
accessPointEntity.token = Bytes.fromByteArray(Bytes.fromBigInt(event.params.tokenId));
accessPointEntity.token = Bytes.fromByteArray(
Bytes.fromBigInt(event.params.tokenId)
);
// Load / Create an Owner entity
let ownerEntity = Owner.load(event.params.owner);
@ -453,7 +478,9 @@ export function handleTransfer(event: TransferEvent): void {
/**
* This handler will update the status of an access point entity.
*/
export function handleChangeAccessPointCreationStatus(event: ChangeAccessPointCreationStatusEvent): void {
export function handleChangeAccessPointCreationStatus(
event: ChangeAccessPointCreationStatusEvent
): void {
// Load the AccessPoint entity
let accessPointEntity = AccessPoint.load(event.params.apName);
let status = event.params.status;
@ -474,20 +501,28 @@ export function handleChangeAccessPointCreationStatus(event: ChangeAccessPointCr
break;
default:
// Unknown status
log.error('Unable to handle ChangeAccessPointCreationStatus. Unknown status. Status: {}, AccessPoint: {}', [status.toString(), event.params.apName]);
log.error(
'Unable to handle ChangeAccessPointCreationStatus. Unknown status. Status: {}, AccessPoint: {}',
[status.toString(), event.params.apName]
);
}
accessPointEntity.save();
} else {
// Unknown access point
log.error('Unable to handle ChangeAccessPointCreationStatus. Unknown access point. Status: {}, AccessPoint: {}', [status.toString(), event.params.apName]);
log.error(
'Unable to handle ChangeAccessPointCreationStatus. Unknown access point. Status: {}, AccessPoint: {}',
[status.toString(), event.params.apName]
);
}
}
/**
* This handler will update the score of an access point entity.
*/
export function handleChangeAccessPointScore(event: ChangeAccessPointCreationScoreEvent): void {
export function handleChangeAccessPointScore(
event: ChangeAccessPointCreationScoreEvent
): void {
// Load the AccessPoint entity
let accessPointEntity = AccessPoint.load(event.params.apName);
@ -496,14 +531,19 @@ export function handleChangeAccessPointCreationStatus(event: ChangeAccessPointCr
accessPointEntity.save();
} else {
// Unknown access point
log.error('Unable to handle ChangeAccessPointScore. Unknown access point. Score: {}, AccessPoint: {}', [event.params.score.toString(), event.params.apName]);
log.error(
'Unable to handle ChangeAccessPointScore. Unknown access point. Score: {}, AccessPoint: {}',
[event.params.score.toString(), event.params.apName]
);
}
}
/**
* This handler will update the nameVerified field of an access point entity.
*/
export function handleChangeAccessPointNameVerify(event: ChangeAccessPointNameVerifyEvent): void {
export function handleChangeAccessPointNameVerify(
event: ChangeAccessPointNameVerifyEvent
): void {
// Load the AccessPoint entity
let accessPointEntity = AccessPoint.load(event.params.apName);
@ -512,14 +552,19 @@ export function handleChangeAccessPointCreationStatus(event: ChangeAccessPointCr
accessPointEntity.save();
} else {
// Unknown access point
log.error('Unable to handle ChangeAccessPointNameVerify. Unknown access point. Verified: {}, AccessPoint: {}', [event.params.verified.toString(), event.params.apName]);
log.error(
'Unable to handle ChangeAccessPointNameVerify. Unknown access point. Verified: {}, AccessPoint: {}',
[event.params.verified.toString(), event.params.apName]
);
}
}
/**
* This handler will update the contentVerified field of an access point entity.
*/
export function handleChangeAccessPointContentVerify(event: ChangeAccessPointContentVerifyEvent): void {
export function handleChangeAccessPointContentVerify(
event: ChangeAccessPointContentVerifyEvent
): void {
// Load the AccessPoint entity
let accessPointEntity = AccessPoint.load(event.params.apName);
@ -528,6 +573,9 @@ export function handleChangeAccessPointCreationStatus(event: ChangeAccessPointCr
accessPointEntity.save();
} else {
// Unknown access point
log.error('Unable to handle ChangeAccessPointContentVerify. Unknown access point. Verified: {}, AccessPoint: {}', [event.params.verified.toString(), event.params.apName]);
log.error(
'Unable to handle ChangeAccessPointContentVerify. Unknown access point. Verified: {}, AccessPoint: {}',
[event.params.verified.toString(), event.params.apName]
);
}
}

View File

@ -21,6 +21,7 @@
"@reduxjs/toolkit": "^1.9.1",
"@stitches/react": "^1.2.8",
"abitype": "^0.5.0",
"alchemy-sdk": "^2.5.0",
"colorthief": "^2.3.2",
"connectkit": "^1.1.3",
"firebase": "^9.17.1",

View File

@ -1,6 +1,6 @@
import { BrowserRouter, Route, Routes, Navigate } from 'react-router-dom';
import { themeGlobals } from '@/theme/globals';
import { Home, Mint } from './views';
import { ComponentsTest, Home, Mint } from './views';
import { SVGTestScreen } from './views/svg-test'; // TODO: remove when done
import { ConnectKitButton } from 'connectkit';
import { MintTest } from './views/mint-test';
@ -18,6 +18,8 @@ export const App = () => {
<Route path="/home" element={<Home />} />
<Route path="/mint" element={<Mint />} />
<Route path="/svg" element={<SVGTestScreen />} />
{/** TODO remove for release */}
<Route path="/components-test" element={<ComponentsTest />} />
<Route path="/mint-test" element={<MintTest />} />
<Route path="*" element={<Navigate to="/home" />} />
</Routes>

View File

@ -1,39 +1,60 @@
import React, { Fragment, useRef, useState } from 'react';
import React, { Fragment, useEffect, useRef, useState } from 'react';
import { Combobox as ComboboxLib, Transition } from '@headlessui/react';
import { Icon } from '@/components/core/icon';
import { Icon, IconName } from '@/components/core/icon';
import { Flex } from '@/components/layout';
import { useDebounce } from '@/hooks/use-debounce';
import { Separator } from '../separator.styles';
import { cleanString } from './combobox.utils';
type ComboboxInputProps = {
/**
* If it's true, the list of options will be displayed
*/
open: boolean;
/**
* Name of the left icon to display in the input
*/
leftIcon: IconName;
/**
* Function to handle the input change
*/
handleInputChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
/**
* Function to handle the input click. When the user clicks on the input, the list of options will be displayed
*/
handleInputClick: () => void;
};
const ComboboxInput = ({
open,
leftIcon,
handleInputChange,
handleInputClick,
}: ComboboxInputProps) => (
<div className="relative w-full cursor-default ">
<Icon
name="search"
name={leftIcon}
size="sm"
css={{
position: 'absolute',
left: '$3',
top: '0.9375rem',
top: '$3',
fontSize: '$xl',
color: 'slate8',
}}
/>
<ComboboxLib.Input
placeholder="Search"
className={`w-full border-solid border border-slate7 h-11 py-3 pl-8 pr-10 text-sm bg-transparent leading-5 text-slate11 outline-none ${
className={`w-full border-solid border border-slate7 h-11 py-3 pl-10 pr-10 text-sm bg-transparent leading-5 text-slate11 outline-none ${
open ? 'border-b-0 rounded-t-xl bg-black border-slate6' : 'rounded-xl'
}`}
displayValue={(selectedValue: ComboboxItem) => selectedValue.label}
onChange={handleInputChange}
onClick={handleInputClick}
/>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-4">
<Icon name="chevron-down" css={{ fontSize: '$xs' }} />
</span>
</div>
);
@ -52,9 +73,13 @@ const ComboboxOption = ({ option }: ComboboxOptionProps) => (
>
{({ selected, active }) => (
<Flex css={{ justifyContent: 'space-between' }}>
<Flex css={{ flexDirection: 'row' }}>
<Flex css={{ flexDirection: 'row', maxWidth: '95%' }}>
{option.icon}
<span className={`${active ? 'text-slate12' : 'text-slate11'}`}>
<span
className={`${active ? 'text-slate12' : 'text-slate11'} ${
option.icon ? 'max-w-70' : 'max-w-full'
} whitespace-nowrap text-ellipsis overflow-hidden`}
>
{option.label}
</span>
</Flex>
@ -73,38 +98,97 @@ export const NoResults = ({ css }: { css?: string }) => (
);
export type ComboboxItem = {
/**
* The key of the item.
*/
value: string;
/**
* The label to display of the item.
*/
label: string;
/**
* Optional icon to display on the left of the item.
*/
icon?: React.ReactNode;
};
export type ComboboxProps = {
/**
* List of items to be displayed in the combobox.
*/
items: ComboboxItem[];
/**
* The selected value of the combobox.
*/
selectedValue: ComboboxItem | undefined;
/**
* If true, the combobox will add the input if it doesn't exist in the list of items.
*/
withAutocomplete?: boolean;
/**
* Name of the left icon to display in the input. Defualt is "search".
*/
leftIcon?: IconName;
/**
* Callback when the selected value changes.
*/
onChange(option: ComboboxItem): void;
};
export const Combobox: React.FC<ComboboxProps> = ({
items,
selectedValue = { value: '', label: '' },
withAutocomplete = false,
leftIcon = 'search',
onChange,
}) => {
const [query, setQuery] = useState('');
const [filteredItems, setFilteredItems] = useState<ComboboxItem[]>([]);
const [autocompleteItems, setAutocompleteItems] = useState<ComboboxItem[]>(
[]
);
useEffect(() => {
// If the selected value doesn't exist in the list of items, we add it
if (
items.filter((item) => item === selectedValue).length === 0 &&
selectedValue.value !== undefined &&
autocompleteItems.length === 0 &&
withAutocomplete
) {
setAutocompleteItems([selectedValue]);
}
}, [selectedValue]);
useEffect(() => {
setFilteredItems(items);
}, [items]);
const buttonRef = useRef<HTMLButtonElement>(null);
const filteredItems =
query === ''
? items
: items.filter((person) =>
person.label
.toLowerCase()
.replace(/\s+/g, '')
.includes(query.toLowerCase().replace(/\s+/g, ''))
);
const handleSearch = useDebounce((searchValue: string) => {
if (searchValue === '') {
setFilteredItems(items);
if (withAutocomplete) {
setAutocompleteItems([]);
handleComboboxChange({} as ComboboxItem);
}
} else {
const filteredValues = items.filter((item) =>
cleanString(item.label).startsWith(cleanString(searchValue))
);
if (withAutocomplete && filteredValues.length === 0) {
// If the search value doesn't exist in the list of items, we add it
setAutocompleteItems([{ value: searchValue, label: searchValue }]);
}
setFilteredItems(filteredValues);
}
}, 200);
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setQuery(event.target.value);
event.stopPropagation();
handleSearch(event.target.value);
};
const handleInputClick = () => {
@ -116,7 +200,11 @@ export const Combobox: React.FC<ComboboxProps> = ({
};
const handleLeaveTransition = () => {
setQuery('');
setFilteredItems(items);
if (selectedValue.value === undefined && withAutocomplete) {
setAutocompleteItems([]);
handleComboboxChange({} as ComboboxItem);
}
};
return (
@ -131,6 +219,7 @@ export const Combobox: React.FC<ComboboxProps> = ({
handleInputChange={handleInputChange}
handleInputClick={handleInputClick}
open={open}
leftIcon={leftIcon}
/>
<ComboboxLib.Button ref={buttonRef} className="hidden" />
@ -144,12 +233,25 @@ export const Combobox: React.FC<ComboboxProps> = ({
afterLeave={handleLeaveTransition}
>
<ComboboxLib.Options className="absolute max-h-60 w-full z-10 overflow-auto rounded-b-xl border-solid border-slate6 border bg-black pt-2 px-3 text-base focus:outline-none sm:text-sm">
{filteredItems.length === 0 && query !== '' ? (
{[...autocompleteItems, ...filteredItems].length === 0 ||
filteredItems === undefined ? (
<NoResults />
) : (
filteredItems.map((option: ComboboxItem) => {
return <ComboboxOption key={option.value} option={option} />;
})
<>
{autocompleteItems.length > 0 && <span>Create new</span>}
{autocompleteItems.map((autocompleteOption: ComboboxItem) => (
<ComboboxOption
key={autocompleteOption.value}
option={autocompleteOption}
/>
))}
{autocompleteItems.length > 0 && filteredItems.length > 0 && (
<Separator css={{ mb: '$2' }} />
)}
{filteredItems.map((option: ComboboxItem) => (
<ComboboxOption key={option.value} option={option} />
))}
</>
)}
</ComboboxLib.Options>
</Transition>

View File

@ -0,0 +1,2 @@
export const cleanString = (str: string) =>
str.toLowerCase().replace(/\s+/g, '');

View File

@ -3,6 +3,10 @@ export const env = Object.freeze({
id: import.meta.env.VITE_ALCHEMY_API_KEY || '',
appName: import.meta.env.VITE_ALCHEMY_APP_NAME || '',
},
ens: {
contractAddress: import.meta.env.VITE_ENS_ADDRESS || '',
validationEnsURL: import.meta.env.VITE_ENS_VALIDATION_URL || '',
},
firebase: {
apiKey: import.meta.env.VITE_FIREBASE_API_KEY || '',
authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN || '',

View File

@ -0,0 +1,15 @@
import { useRef } from 'react';
export const useDebounce = <A extends any[], F extends (...args: A) => void>(
f: F,
t = 500
) => {
const timeOutRef = useRef<NodeJS.Timeout>();
return (...args: A) => {
timeOutRef.current && clearTimeout(timeOutRef.current);
timeOutRef.current = setTimeout(() => {
f(...args);
}, t);
};
};

View File

@ -1,8 +1,18 @@
import { JsonRpcProvider, Networkish } from '@ethersproject/providers';
import { ethers } from 'ethers';
import * as Contracts from './contracts';
import { env } from '@/constants';
import { Alchemy, Network } from 'alchemy-sdk';
const config = {
apiKey: env.alchemy.id,
network: Network.ETH_MAINNET,
};
const alchemy = new Alchemy(config);
export const Ethereum: Ethereum.Core = {
//TODO remove
defaultNetwork: 'https://rpc-mumbai.maticvigil.com', // TODO: make it environment variable
provider: {
@ -20,6 +30,23 @@ export const Ethereum: Ethereum.Core = {
return new ethers.Contract(contract.address, contract.abi, provider);
},
async getEnsName(address) {
const ensAddresses = await alchemy.nft.getNftsForOwner(address, {
contractAddresses: [env.ens.contractAddress],
});
return ensAddresses.ownedNfts.map((nft) => nft.title);
},
//TODO remove if we're not gonna validate ens on the client side
async validateEnsName(name) {
const provider = new ethers.providers.JsonRpcProvider(
env.ens.validationEnsURL
);
return Boolean(await provider.resolveName(name));
},
};
export namespace Ethereum {
@ -36,5 +63,9 @@ export namespace Ethereum {
contractName: keyof typeof Contracts,
providerName?: Providers
) => ethers.Contract;
getEnsName: (address: string) => Promise<string[]>;
validateEnsName: (name: string) => Promise<boolean>;
};
}

View File

@ -0,0 +1,24 @@
import { Ethereum } from '@/integrations';
import { RootState } from '@/store';
import { createAsyncThunk } from '@reduxjs/toolkit';
import { ensActions } from '../ens-slice';
export const fetchEnsNamesThunk = createAsyncThunk<void, `0x${string}`>(
'ens/fetchEnsNames',
async (address, { dispatch, getState }) => {
const { state } = (getState() as RootState).ens;
if (state === 'loading') return;
try {
dispatch(ensActions.setState('loading'));
//fetch ens names for received addresses
const ensList = await Ethereum.getEnsName(address);
dispatch(ensActions.setEnsNames(ensList));
} catch (error) {
console.log(error);
dispatch(ensActions.setState('failed'));
}
}
);

View File

@ -0,0 +1 @@
export * from './fetch-ens-names';

View File

@ -0,0 +1,47 @@
import { RootState, useAppSelector } from '@/store';
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import * as asyncThunk from './async-thunk';
export namespace EnsState {
export type State = 'idle' | 'loading' | 'success' | 'failed';
export type EnsNames = string[];
}
export interface EnsState {
state: EnsState.State;
ensNames: EnsState.EnsNames;
}
const initialState: EnsState = {
state: 'idle',
ensNames: [],
};
export const ensSlice = createSlice({
name: 'ens',
initialState,
reducers: {
setEnsNames: (state, action: PayloadAction<EnsState.EnsNames>) => {
state.ensNames = action.payload;
state.state = 'success';
},
setState: (
state,
action: PayloadAction<Exclude<EnsState.State, 'success'>>
) => {
state.state = action.payload;
},
},
});
export const ensActions = {
...ensSlice.actions,
...asyncThunk,
};
const selectEnsState = (state: RootState): EnsState => state.ens;
export const useEnsStore = (): EnsState => useAppSelector(selectEnsState);
export default ensSlice.reducer;

View File

@ -0,0 +1 @@
export * from './ens-slice';

View File

@ -16,8 +16,6 @@ export const fetchRepositoriesThunk = createAsyncThunk(
const repositories = await githubClient.fetchRepos(url);
console.log(repositories);
dispatch(
githubActions.setRepositories(
repositories.map((repo: any) => ({

View File

@ -1 +1,2 @@
export * from './github';
export * from './ens';

View File

@ -1,9 +1,11 @@
import { configureStore } from '@reduxjs/toolkit';
import githubReducer from './features/github/github-slice';
import ensReducer from './features/ens/ens-slice';
export const store = configureStore({
reducer: {
github: githubReducer,
ens: ensReducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({

View File

@ -0,0 +1,50 @@
import { Combobox, ComboboxItem, Flex } from '@/components';
import { useState } from 'react';
const itemsCombobox = [
{ label: 'Item 1', value: 'item-1' },
{ label: 'Item 2', value: 'item-2' },
{ label: 'Item 3', value: 'item-3' },
];
export const ComponentsTest = () => {
const [selectedValue, setSelectedValue] = useState({} as ComboboxItem);
const [selectedValueAutocomplete, setSelectedValueAutocomplete] = useState(
{} as ComboboxItem
);
const handleComboboxChange = (item: ComboboxItem) => {
setSelectedValue(item);
};
const handleComboboxChangeAutocomplete = (item: ComboboxItem) => {
setSelectedValueAutocomplete(item);
};
return (
<Flex
css={{
flexDirection: 'column',
margin: '100px',
justifyContent: 'center',
gap: '10px',
}}
>
<h1>Components Test</h1>
<Flex css={{ width: '400px', gap: '$2' }}>
<Combobox
items={itemsCombobox}
selectedValue={selectedValue}
onChange={handleComboboxChange}
/>
<Combobox
items={itemsCombobox}
selectedValue={selectedValueAutocomplete}
onChange={handleComboboxChangeAutocomplete}
withAutocomplete
/>
</Flex>
</Flex>
);
};

View File

@ -0,0 +1 @@
export * from './components-test';

View File

@ -1,3 +1,4 @@
export * from './home';
export * from './mint';
export * from './svg-test';
export * from './components-test';

View File

@ -0,0 +1,12 @@
import { Ethereum } from '@/integrations';
//TODO remove if we're not gonna validate ens on the client side
export const validateEnsField = async (
ensName: string,
setError: (message: string) => void
) => {
const isValid = await Ethereum.validateEnsName(ensName);
if (!isValid) setError('Invalid ENS name');
else setError('');
return isValid;
};

View File

@ -1,30 +1,25 @@
import { Card, Grid, Icon, IconButton } from '@/components';
import { Card, Grid, Stepper } from '@/components';
import { MintCardHeader } from '@/views/mint/mint-card';
import { GithubButton } from './github-button';
export const GithubConnect: React.FC = () => (
<Card.Container>
<Card.Heading
title="Connect GitHub"
rightIcon={
<IconButton
aria-label="Add"
colorScheme="gray"
variant="link"
icon={<Icon name="info" />}
/>
}
/>
<Card.Body>
<Grid css={{ rowGap: '$6' }}>
<GithubButton />
<Card.Text
css={{ height: '$46h', width: '$95', fontSize: '$md', px: '$12' }}
>
<span>
After connecting your GitHub, your repositories will show here.
</span>
</Card.Text>
</Grid>
</Card.Body>
</Card.Container>
);
export const GithubConnect: React.FC = () => {
const { prevStep } = Stepper.useContext();
return (
<Card.Container>
<MintCardHeader title="Connect GitHub" onClickBack={prevStep} />
<Card.Body>
<Grid css={{ rowGap: '$6' }}>
<GithubButton />
<Card.Text
css={{ height: '$46h', width: '$95', fontSize: '$md', px: '$12' }}
>
<span>
After connecting your GitHub, your repositories will show here.
</span>
</Card.Text>
</Grid>
</Card.Body>
</Card.Container>
);
};

View File

@ -1,9 +1,10 @@
import { Card, ComboboxItem, Flex, Grid, Icon, Spinner } from '@/components';
import { Input } from '@/components/core/input';
import { useDebounce } from '@/hooks/use-debounce';
import { useGithubStore } from '@/store';
import { MintCardHeader } from '@/views/mint/mint-card';
import { Mint } from '@/views/mint/mint.context';
import React, { forwardRef, useRef, useState } from 'react';
import React, { forwardRef, useState } from 'react';
import { RepositoriesList } from './repositories-list';
import { UserOrgsCombobox } from './users-orgs-combobox';
@ -46,14 +47,15 @@ export const GithubRepositoryConnection: React.FC = () => {
const { setGithubStep, setSelectedUserOrg } = Mint.useContext();
const timeOutRef = useRef<NodeJS.Timeout>();
const setSearchValueDebounced = useDebounce(
(event: React.ChangeEvent<HTMLInputElement>) =>
setSearchValue(event.target.value),
500
);
const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
event.stopPropagation();
timeOutRef.current && clearTimeout(timeOutRef.current);
timeOutRef.current = setTimeout(() => {
setSearchValue(event.target.value);
}, 500);
setSearchValueDebounced(event);
};
const handlePrevStepClick = () => {
@ -73,7 +75,7 @@ export const GithubRepositoryConnection: React.FC = () => {
<UserOrgsCombobox />
<Input
leftIcon="search"
placeholder="Search"
placeholder="Search repo"
onChange={handleSearchChange}
/>
</Flex>

View File

@ -43,6 +43,7 @@ export const UserOrgsCombobox = () => {
)}
selectedValue={selectedUserOrg}
onChange={handleUserOrgChange}
leftIcon="github"
/>
);
};

View File

@ -10,14 +10,14 @@ export const MintStepper = () => {
<Stepper.Root initialStep={1}>
<Stepper.Container>
<Stepper.Step>
<MintStep header="Connect GitHub and select repository">
<GithubStep />
<MintStep header="Connect your Ethereum Wallet to mint an NFA">
<WalletStep />
</MintStep>
</Stepper.Step>
<Stepper.Step>
<MintStep header="Connect your Ethereum Wallet to mint an NFA">
<WalletStep />
<MintStep header="Connect GitHub and select repository">
<GithubStep />
</MintStep>
</Stepper.Step>

View File

@ -15,9 +15,10 @@ export type MintContext = {
appDescription: string;
appLogo: string;
logoColor: string;
ens: DropdownItem;
ens: ComboboxItem;
domain: string;
verifyNFA: boolean;
ensError: string;
setGithubStep: (step: number) => void;
setNfaStep: (step: number) => void;
setSelectedUserOrg: (userOrg: ComboboxItem) => void;
@ -28,9 +29,10 @@ export type MintContext = {
setAppDescription: (description: string) => void;
setAppLogo: (logo: string) => void;
setLogoColor: (color: string) => void;
setEns: (ens: DropdownItem) => void;
setEns: (ens: ComboboxItem) => void;
setDomain: (domain: string) => void;
setVerifyNFA: (verify: boolean) => void;
setEnsError: (error: string) => void;
};
const [MintProvider, useContext] = createContext<MintContext>({
@ -62,10 +64,13 @@ export abstract class Mint {
const [appDescription, setAppDescription] = useState('');
const [appLogo, setAppLogo] = useState('');
const [logoColor, setLogoColor] = useState('');
const [ens, setEns] = useState({} as DropdownItem);
const [ens, setEns] = useState({} as ComboboxItem);
const [domain, setDomain] = useState('');
const [verifyNFA, setVerifyNFA] = useState(true);
//Field validations
const [ensError, setEnsError] = useState<string>('');
const setGithubStep = (step: number): void => {
if (step > 0 && step <= 3) {
setGithubStepContext(step);
@ -88,6 +93,7 @@ export abstract class Mint {
ens,
domain,
verifyNFA,
ensError,
setSelectedUserOrg,
setGithubStep,
setNfaStep,
@ -101,6 +107,7 @@ export abstract class Mint {
setEns,
setDomain,
setVerifyNFA,
setEnsError,
}}
>
<TransactionProvider

View File

@ -1,37 +1,35 @@
import { Dropdown, DropdownItem, Form } from '@/components';
import { Combobox, ComboboxItem, Form } from '@/components';
import { ensActions, useAppDispatch, useEnsStore } from '@/store';
import { Mint } from '@/views/mint/mint.context';
// TODO remove after integration with wallet
const ensList: DropdownItem[] = [
{
value: 'fleek.eth',
label: 'fleek.eth',
},
{
value: 'ens.eth',
label: 'ens.eth',
},
{
value: 'cami.eth',
label: 'cami.eth',
},
];
import { useAccount } from 'wagmi';
export const EnsField = () => {
const { ens, setEns } = Mint.useContext();
const { ens, ensError, setEns } = Mint.useContext();
const { state, ensNames } = useEnsStore();
const dispatch = useAppDispatch();
const { address } = useAccount();
const handleEnsChange = (item: DropdownItem) => {
if (state === 'idle' && address) {
dispatch(ensActions.fetchEnsNamesThunk(address));
}
const handleEnsChange = (item: ComboboxItem) => {
setEns(item);
};
return (
<Form.Field css={{ flex: 1 }}>
<Form.Label>ENS</Form.Label>
<Dropdown
items={ensList}
<Combobox
items={ensNames.map((ens) => ({
label: ens,
value: ens,
}))}
selectedValue={ens}
onChange={handleEnsChange}
withAutocomplete
/>
{ensError && <Form.Error>{ensError}</Form.Error>}
</Form.Field>
);
};

View File

@ -1,12 +1,20 @@
import { Button, Card, Grid, Icon, Stepper } from '@/components';
import { MintCardHeader } from '../mint-card';
import { Card, Grid, Icon, IconButton } from '@/components';
import { ConnectWalletButton } from './connect-wallet-button';
export const WalletStep = () => {
const { prevStep } = Stepper.useContext();
return (
<Card.Container>
<MintCardHeader title="Connect Wallet" onClickBack={prevStep} />
<Card.Heading
title="Connect Wallet"
rightIcon={
<IconButton
aria-label="Add"
colorScheme="gray"
variant="link"
icon={<Icon name="info" />}
/>
}
/>
<Card.Body>
<Grid css={{ rowGap: '$6' }}>
<ConnectWalletButton />

View File

@ -18,6 +18,9 @@ module.exports = {
borderRadius: {
xhl: '1.25rem',
},
maxWidth: {
70: '70%',
},
space: {
'1h': '0.375rem',
},

View File

@ -1415,7 +1415,7 @@
dependencies:
"@ethersproject/bignumber" "^5.7.0"
"@ethersproject/contracts@5.7.0":
"@ethersproject/contracts@5.7.0", "@ethersproject/contracts@^5.7.0":
version "5.7.0"
resolved "https://registry.yarnpkg.com/@ethersproject/contracts/-/contracts-5.7.0.tgz#c305e775abd07e48aa590e1a877ed5c316f8bd1e"
integrity sha512-5GJbzEU3X+d33CdfPhcyS+z8MzsTrBGk/sc+G+59+tPa9yFkl6HQ9D6L0QMgNTA9q8dT0XKxxkyp883XsQvbbg==
@ -1518,7 +1518,7 @@
dependencies:
"@ethersproject/logger" "^5.7.0"
"@ethersproject/providers@5.7.2", "@ethersproject/providers@^5.7.2":
"@ethersproject/providers@5.7.2", "@ethersproject/providers@^5.7.0", "@ethersproject/providers@^5.7.2":
version "5.7.2"
resolved "https://registry.yarnpkg.com/@ethersproject/providers/-/providers-5.7.2.tgz#f8b1a4f275d7ce58cf0a2eec222269a08beb18cb"
integrity sha512-g34EWZ1WWAVgr4aptGlVBF8mhl3VWjv+8hoAnzStu8Ah22VHBsuGzP17eb6xDVRzw895G4W7vvx60lFFur/1Rg==
@ -1617,7 +1617,7 @@
"@ethersproject/rlp" "^5.7.0"
"@ethersproject/signing-key" "^5.7.0"
"@ethersproject/units@5.7.0":
"@ethersproject/units@5.7.0", "@ethersproject/units@^5.7.0":
version "5.7.0"
resolved "https://registry.yarnpkg.com/@ethersproject/units/-/units-5.7.0.tgz#637b563d7e14f42deeee39245275d477aae1d8b1"
integrity sha512-pD3xLMy3SJu9kG5xDGI7+xhTEmGXlEqXU4OfNapmfnxLVY4EMSSRp7j1k7eezutBPH7RBN/7QPnwR7hzNlEFeg==
@ -1626,7 +1626,7 @@
"@ethersproject/constants" "^5.7.0"
"@ethersproject/logger" "^5.7.0"
"@ethersproject/wallet@5.7.0":
"@ethersproject/wallet@5.7.0", "@ethersproject/wallet@^5.7.0":
version "5.7.0"
resolved "https://registry.yarnpkg.com/@ethersproject/wallet/-/wallet-5.7.0.tgz#4e5d0790d96fe21d61d38fb40324e6c7ef350b2d"
integrity sha512-MhmXlJXEJFBFVKrDLB4ZdDzxcBxQ3rLyCkhNqVu3CDYvR97E+8r01UgrI+TI99Le+aYm/in/0vp86guJuM7FCA==
@ -5373,6 +5373,26 @@ ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.2, ajv@^6.12.3, ajv@^6.12.4, ajv
json-schema-traverse "^0.4.1"
uri-js "^4.2.2"
alchemy-sdk@^2.5.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/alchemy-sdk/-/alchemy-sdk-2.5.0.tgz#20813b02eded16e9cbc56c5fc8259684dc324eb9"
integrity sha512-pgwyPiAmUp4uaBkkpdnlHxjB7yjeO88VhIIFo5aCiuPAxgpB5nKRqBQw2XXqiZEUF4xU1ybZ2s1ESQ6k/hc2MQ==
dependencies:
"@ethersproject/abi" "^5.7.0"
"@ethersproject/abstract-provider" "^5.7.0"
"@ethersproject/bignumber" "^5.7.0"
"@ethersproject/bytes" "^5.7.0"
"@ethersproject/contracts" "^5.7.0"
"@ethersproject/hash" "^5.7.0"
"@ethersproject/networks" "^5.7.0"
"@ethersproject/providers" "^5.7.0"
"@ethersproject/units" "^5.7.0"
"@ethersproject/wallet" "^5.7.0"
"@ethersproject/web" "^5.7.0"
axios "^0.26.1"
sturdy-websocket "^0.2.1"
websocket "^1.0.34"
ansi-align@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-3.0.1.tgz#0cdf12e111ace773a86e9a1fad1225c43cb19a59"
@ -5756,6 +5776,13 @@ axios@^0.21.0:
dependencies:
follow-redirects "^1.14.0"
axios@^0.26.1:
version "0.26.1"
resolved "https://registry.yarnpkg.com/axios/-/axios-0.26.1.tgz#1ede41c51fcf51bbbd6fd43669caaa4f0495aaa9"
integrity sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==
dependencies:
follow-redirects "^1.14.8"
babel-loader@^8.0.0, babel-loader@^8.3.0:
version "8.3.0"
resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.3.0.tgz#124936e841ba4fe8176786d6ff28add1f134d6a8"
@ -7086,6 +7113,14 @@ cyclist@^1.0.1:
resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9"
integrity sha512-NJGVKPS81XejHcLhaLJS7plab0fK3slPh11mESeeDq2W4ZI5kUKK/LRRdVDvjJseojbPB7ZwjnyOybg3Igea/A==
d@1, d@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/d/-/d-1.0.1.tgz#8698095372d58dbee346ffd0c7093f99f8f9eb5a"
integrity sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==
dependencies:
es5-ext "^0.10.50"
type "^1.0.1"
dashdash@^1.12.0:
version "1.14.1"
resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
@ -7716,11 +7751,29 @@ es-to-primitive@^1.2.1:
is-date-object "^1.0.1"
is-symbol "^1.0.2"
es5-ext@^0.10.35, es5-ext@^0.10.50:
version "0.10.62"
resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.62.tgz#5e6adc19a6da524bf3d1e02bbc8960e5eb49a9a5"
integrity sha512-BHLqn0klhEpnOKSrzn/Xsz2UIW8j+cGmo9JLzr8BiUapV8hPL9+FliFqjwr9ngW7jWdnxv6eO+/LqyhJVqgrjA==
dependencies:
es6-iterator "^2.0.3"
es6-symbol "^3.1.3"
next-tick "^1.1.0"
es5-shim@^4.5.13:
version "4.6.7"
resolved "https://registry.yarnpkg.com/es5-shim/-/es5-shim-4.6.7.tgz#bc67ae0fc3dd520636e0a1601cc73b450ad3e955"
integrity sha512-jg21/dmlrNQI7JyyA2w7n+yifSxBng0ZralnSfVZjoCawgNTCnS+yBCyVM9DL5itm7SUnDGgv7hcq2XCZX4iRQ==
es6-iterator@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7"
integrity sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==
dependencies:
d "1"
es5-ext "^0.10.35"
es6-symbol "^3.1.1"
es6-promise@^4.0.3:
version "4.2.8"
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a"
@ -7738,6 +7791,14 @@ es6-shim@^0.35.5:
resolved "https://registry.yarnpkg.com/es6-shim/-/es6-shim-0.35.7.tgz#db00f1cbb7d4de70b50dcafa45b157e9ba28f5d2"
integrity sha512-baZkUfTDSx7X69+NA8imbvGrsPfqH0MX7ADdIDjqwsI8lkTgLIiD2QWrUCSGsUQ0YMnSCA/4pNgSyXdnLHWf3A==
es6-symbol@^3.1.1, es6-symbol@^3.1.3:
version "3.1.3"
resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.3.tgz#bad5d3c1bcdac28269f4cb331e431c78ac705d18"
integrity sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==
dependencies:
d "^1.0.1"
ext "^1.1.2"
esbuild-android-64@0.15.18:
version "0.15.18"
resolved "https://registry.yarnpkg.com/esbuild-android-64/-/esbuild-android-64-0.15.18.tgz#20a7ae1416c8eaade917fb2453c1259302c637a5"
@ -8347,6 +8408,13 @@ express@^4.17.1:
utils-merge "1.0.1"
vary "~1.1.2"
ext@^1.1.2:
version "1.7.0"
resolved "https://registry.yarnpkg.com/ext/-/ext-1.7.0.tgz#0ea4383c0103d60e70be99e9a7f11027a33c4f5f"
integrity sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==
dependencies:
type "^2.7.2"
extend-shallow@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f"
@ -8667,7 +8735,7 @@ focus-lock@^0.8.0:
dependencies:
tslib "^1.9.3"
follow-redirects@^1.14.0:
follow-redirects@^1.14.0, follow-redirects@^1.14.8:
version "1.15.2"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13"
integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==
@ -11288,6 +11356,11 @@ nested-error-stacks@^2.0.0, nested-error-stacks@^2.1.0:
resolved "https://registry.yarnpkg.com/nested-error-stacks/-/nested-error-stacks-2.1.1.tgz#26c8a3cee6cc05fbcf1e333cd2fc3e003326c0b5"
integrity sha512-9iN1ka/9zmX1ZvLV9ewJYEk9h7RyRRtqdK0woXcqohu8EWIerfPUjYJPg0ULy0UqP7cslmdGc8xKDJcojlKiaw==
next-tick@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.1.0.tgz#1836ee30ad56d67ef281b22bd199f709449b35eb"
integrity sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==
nice-try@^1.0.4:
version "1.0.5"
resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
@ -13951,6 +14024,11 @@ strip-json-comments@3.1.1, strip-json-comments@^3.1.0, strip-json-comments@^3.1.
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
sturdy-websocket@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/sturdy-websocket/-/sturdy-websocket-0.2.1.tgz#20a58fd53372ef96eaa08f3c61c91a10b07c7c05"
integrity sha512-NnzSOEKyv4I83qbuKw9ROtJrrT6Z/Xt7I0HiP/e6H6GnpeTDvzwGIGeJ8slai+VwODSHQDooW2CAilJwT9SpRg==
style-loader@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-1.3.0.tgz#828b4a3b3b7e7aa5847ce7bae9e874512114249e"
@ -14466,6 +14544,16 @@ type-is@~1.6.18:
media-typer "0.3.0"
mime-types "~2.1.24"
type@^1.0.1:
version "1.2.0"
resolved "https://registry.yarnpkg.com/type/-/type-1.2.0.tgz#848dd7698dafa3e54a6c479e759c4bc3f18847a0"
integrity sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==
type@^2.7.2:
version "2.7.2"
resolved "https://registry.yarnpkg.com/type/-/type-2.7.2.tgz#2376a15a3a28b1efa0f5350dcf72d24df6ef98d0"
integrity sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==
typed-array-length@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.4.tgz#89d83785e5c4098bec72e08b319651f0eac9c1bb"
@ -15111,6 +15199,18 @@ websocket-extensions@>=0.1.1:
resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42"
integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==
websocket@^1.0.34:
version "1.0.34"
resolved "https://registry.yarnpkg.com/websocket/-/websocket-1.0.34.tgz#2bdc2602c08bf2c82253b730655c0ef7dcab3111"
integrity sha512-PRDso2sGwF6kM75QykIesBijKSVceR6jL2G8NGYyq2XrItNC2P5/qL5XeR056GhA+Ly7JMFvJb9I312mJfmqnQ==
dependencies:
bufferutil "^4.0.1"
debug "^2.2.0"
es5-ext "^0.10.50"
typedarray-to-buffer "^3.1.5"
utf-8-validate "^5.0.2"
yaeti "^0.0.6"
whatwg-url@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"
@ -15303,6 +15403,11 @@ y18n@^5.0.5:
resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==
yaeti@^0.0.6:
version "0.0.6"
resolved "https://registry.yarnpkg.com/yaeti/-/yaeti-0.0.6.tgz#f26f484d72684cf42bedfb76970aa1608fbf9577"
integrity sha512-MvQa//+KcZCUkBTIC9blM+CU9J2GzuTytsOUwf2lidtvkx/6gnEp1QvJv34t9vdjhFmha/mUiNDbN0D0mJWdug==
yallist@^3.0.2:
version "3.1.1"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"