From e5d28251c45cb7116a4c26f572311856bcb8b837 Mon Sep 17 00:00:00 2001 From: Camila Sosa Morales Date: Tue, 7 Mar 2023 08:03:53 -0500 Subject: [PATCH] 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 --- subgraph/src/fleek-nfa.ts | 132 +++++++++++----- ui/package.json | 1 + ui/src/app.tsx | 4 +- ui/src/components/core/combobox/combobox.tsx | 148 +++++++++++++++--- .../core/combobox/combobox.utils.ts | 2 + ui/src/constants/env.ts | 4 + ui/src/hooks/use-debounce.ts | 15 ++ ui/src/integrations/ethereum/ethereum.ts | 31 ++++ .../ens/async-thunk/fetch-ens-names.ts | 24 +++ .../store/features/ens/async-thunk/index.ts | 1 + ui/src/store/features/ens/ens-slice.ts | 47 ++++++ ui/src/store/features/ens/index.ts | 1 + .../github/async-thunk/fetch-repositories.ts | 2 - ui/src/store/features/index.ts | 1 + ui/src/store/store.ts | 2 + .../views/components-test/components-test.tsx | 50 ++++++ ui/src/views/components-test/index.ts | 1 + ui/src/views/index.ts | 1 + .../views/mint/form-step/form.validations.ts | 12 ++ .../github-connect/github-connect-step.tsx | 51 +++--- .../github-repository-selection.tsx | 16 +- .../users-orgs-combobox.tsx | 1 + ui/src/views/mint/mint-stepper.tsx | 8 +- ui/src/views/mint/mint.context.tsx | 13 +- .../fields/ens-domain-field/ens-field.tsx | 40 +++-- ui/src/views/mint/wallet-step/wallet-step.tsx | 16 +- ui/tailwind.config.js | 3 + ui/yarn.lock | 115 +++++++++++++- 28 files changed, 602 insertions(+), 140 deletions(-) create mode 100644 ui/src/components/core/combobox/combobox.utils.ts create mode 100644 ui/src/hooks/use-debounce.ts create mode 100644 ui/src/store/features/ens/async-thunk/fetch-ens-names.ts create mode 100644 ui/src/store/features/ens/async-thunk/index.ts create mode 100644 ui/src/store/features/ens/ens-slice.ts create mode 100644 ui/src/store/features/ens/index.ts create mode 100644 ui/src/views/components-test/components-test.tsx create mode 100644 ui/src/views/components-test/index.ts create mode 100644 ui/src/views/mint/form-step/form.validations.ts diff --git a/subgraph/src/fleek-nfa.ts b/subgraph/src/fleek-nfa.ts index 80562ba..19b84d1 100644 --- a/subgraph/src/fleek-nfa.ts +++ b/subgraph/src/fleek-nfa.ts @@ -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] + ); } } diff --git a/ui/package.json b/ui/package.json index d71dbb3..8cf4965 100644 --- a/ui/package.json +++ b/ui/package.json @@ -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", diff --git a/ui/src/app.tsx b/ui/src/app.tsx index f75b169..2d08bf1 100644 --- a/ui/src/app.tsx +++ b/ui/src/app.tsx @@ -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 = () => { } /> } /> } /> + {/** TODO remove for release */} + } /> } /> } /> diff --git a/ui/src/components/core/combobox/combobox.tsx b/ui/src/components/core/combobox/combobox.tsx index e7f276a..e688b18 100644 --- a/ui/src/components/core/combobox/combobox.tsx +++ b/ui/src/components/core/combobox/combobox.tsx @@ -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) => 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) => (
selectedValue.label} onChange={handleInputChange} onClick={handleInputClick} /> + + +
); @@ -52,9 +73,13 @@ const ComboboxOption = ({ option }: ComboboxOptionProps) => ( > {({ selected, active }) => ( - + {option.icon} - + {option.label} @@ -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 = ({ items, selectedValue = { value: '', label: '' }, + withAutocomplete = false, + leftIcon = 'search', onChange, }) => { - const [query, setQuery] = useState(''); + const [filteredItems, setFilteredItems] = useState([]); + const [autocompleteItems, setAutocompleteItems] = useState( + [] + ); + + 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(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) => { - setQuery(event.target.value); + event.stopPropagation(); + handleSearch(event.target.value); }; const handleInputClick = () => { @@ -116,7 +200,11 @@ export const Combobox: React.FC = ({ }; const handleLeaveTransition = () => { - setQuery(''); + setFilteredItems(items); + if (selectedValue.value === undefined && withAutocomplete) { + setAutocompleteItems([]); + handleComboboxChange({} as ComboboxItem); + } }; return ( @@ -131,6 +219,7 @@ export const Combobox: React.FC = ({ handleInputChange={handleInputChange} handleInputClick={handleInputClick} open={open} + leftIcon={leftIcon} /> @@ -144,12 +233,25 @@ export const Combobox: React.FC = ({ afterLeave={handleLeaveTransition} > - {filteredItems.length === 0 && query !== '' ? ( + {[...autocompleteItems, ...filteredItems].length === 0 || + filteredItems === undefined ? ( ) : ( - filteredItems.map((option: ComboboxItem) => { - return ; - }) + <> + {autocompleteItems.length > 0 && Create new} + {autocompleteItems.map((autocompleteOption: ComboboxItem) => ( + + ))} + {autocompleteItems.length > 0 && filteredItems.length > 0 && ( + + )} + {filteredItems.map((option: ComboboxItem) => ( + + ))} + )} diff --git a/ui/src/components/core/combobox/combobox.utils.ts b/ui/src/components/core/combobox/combobox.utils.ts new file mode 100644 index 0000000..2b4de14 --- /dev/null +++ b/ui/src/components/core/combobox/combobox.utils.ts @@ -0,0 +1,2 @@ +export const cleanString = (str: string) => + str.toLowerCase().replace(/\s+/g, ''); diff --git a/ui/src/constants/env.ts b/ui/src/constants/env.ts index a3a470d..12ba134 100644 --- a/ui/src/constants/env.ts +++ b/ui/src/constants/env.ts @@ -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 || '', diff --git a/ui/src/hooks/use-debounce.ts b/ui/src/hooks/use-debounce.ts new file mode 100644 index 0000000..7528399 --- /dev/null +++ b/ui/src/hooks/use-debounce.ts @@ -0,0 +1,15 @@ +import { useRef } from 'react'; + +export const useDebounce = void>( + f: F, + t = 500 +) => { + const timeOutRef = useRef(); + + return (...args: A) => { + timeOutRef.current && clearTimeout(timeOutRef.current); + timeOutRef.current = setTimeout(() => { + f(...args); + }, t); + }; +}; diff --git a/ui/src/integrations/ethereum/ethereum.ts b/ui/src/integrations/ethereum/ethereum.ts index c61b16f..cb2ae65 100644 --- a/ui/src/integrations/ethereum/ethereum.ts +++ b/ui/src/integrations/ethereum/ethereum.ts @@ -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; + + validateEnsName: (name: string) => Promise; }; } diff --git a/ui/src/store/features/ens/async-thunk/fetch-ens-names.ts b/ui/src/store/features/ens/async-thunk/fetch-ens-names.ts new file mode 100644 index 0000000..be87beb --- /dev/null +++ b/ui/src/store/features/ens/async-thunk/fetch-ens-names.ts @@ -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( + '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')); + } + } +); diff --git a/ui/src/store/features/ens/async-thunk/index.ts b/ui/src/store/features/ens/async-thunk/index.ts new file mode 100644 index 0000000..e08e73a --- /dev/null +++ b/ui/src/store/features/ens/async-thunk/index.ts @@ -0,0 +1 @@ +export * from './fetch-ens-names'; diff --git a/ui/src/store/features/ens/ens-slice.ts b/ui/src/store/features/ens/ens-slice.ts new file mode 100644 index 0000000..0a9279d --- /dev/null +++ b/ui/src/store/features/ens/ens-slice.ts @@ -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) => { + state.ensNames = action.payload; + state.state = 'success'; + }, + setState: ( + state, + action: PayloadAction> + ) => { + 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; diff --git a/ui/src/store/features/ens/index.ts b/ui/src/store/features/ens/index.ts new file mode 100644 index 0000000..c58c438 --- /dev/null +++ b/ui/src/store/features/ens/index.ts @@ -0,0 +1 @@ +export * from './ens-slice'; diff --git a/ui/src/store/features/github/async-thunk/fetch-repositories.ts b/ui/src/store/features/github/async-thunk/fetch-repositories.ts index 152b109..f153e36 100644 --- a/ui/src/store/features/github/async-thunk/fetch-repositories.ts +++ b/ui/src/store/features/github/async-thunk/fetch-repositories.ts @@ -16,8 +16,6 @@ export const fetchRepositoriesThunk = createAsyncThunk( const repositories = await githubClient.fetchRepos(url); - console.log(repositories); - dispatch( githubActions.setRepositories( repositories.map((repo: any) => ({ diff --git a/ui/src/store/features/index.ts b/ui/src/store/features/index.ts index 8e3e83e..a587625 100644 --- a/ui/src/store/features/index.ts +++ b/ui/src/store/features/index.ts @@ -1 +1,2 @@ export * from './github'; +export * from './ens'; diff --git a/ui/src/store/store.ts b/ui/src/store/store.ts index 0a9377f..f86bae7 100644 --- a/ui/src/store/store.ts +++ b/ui/src/store/store.ts @@ -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({ diff --git a/ui/src/views/components-test/components-test.tsx b/ui/src/views/components-test/components-test.tsx new file mode 100644 index 0000000..a3871e1 --- /dev/null +++ b/ui/src/views/components-test/components-test.tsx @@ -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 ( + +

Components Test

+ + + + +
+ ); +}; diff --git a/ui/src/views/components-test/index.ts b/ui/src/views/components-test/index.ts new file mode 100644 index 0000000..689ba78 --- /dev/null +++ b/ui/src/views/components-test/index.ts @@ -0,0 +1 @@ +export * from './components-test'; diff --git a/ui/src/views/index.ts b/ui/src/views/index.ts index 75899f1..6319a92 100644 --- a/ui/src/views/index.ts +++ b/ui/src/views/index.ts @@ -1,3 +1,4 @@ export * from './home'; export * from './mint'; export * from './svg-test'; +export * from './components-test'; diff --git a/ui/src/views/mint/form-step/form.validations.ts b/ui/src/views/mint/form-step/form.validations.ts new file mode 100644 index 0000000..9241fb7 --- /dev/null +++ b/ui/src/views/mint/form-step/form.validations.ts @@ -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; +}; diff --git a/ui/src/views/mint/github-step/steps/github-connect/github-connect-step.tsx b/ui/src/views/mint/github-step/steps/github-connect/github-connect-step.tsx index 9ccc425..c9c8ce5 100644 --- a/ui/src/views/mint/github-step/steps/github-connect/github-connect-step.tsx +++ b/ui/src/views/mint/github-step/steps/github-connect/github-connect-step.tsx @@ -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 = () => ( - - } - /> - } - /> - - - - - - After connecting your GitHub, your repositories will show here. - - - - - -); +export const GithubConnect: React.FC = () => { + const { prevStep } = Stepper.useContext(); + + return ( + + + + + + + + After connecting your GitHub, your repositories will show here. + + + + + + ); +}; diff --git a/ui/src/views/mint/github-step/steps/github-repository-selection/github-repository-selection.tsx b/ui/src/views/mint/github-step/steps/github-repository-selection/github-repository-selection.tsx index ce79f9d..7b906bd 100644 --- a/ui/src/views/mint/github-step/steps/github-repository-selection/github-repository-selection.tsx +++ b/ui/src/views/mint/github-step/steps/github-repository-selection/github-repository-selection.tsx @@ -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(); + const setSearchValueDebounced = useDebounce( + (event: React.ChangeEvent) => + setSearchValue(event.target.value), + 500 + ); const handleSearchChange = (event: React.ChangeEvent) => { 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 = () => {
diff --git a/ui/src/views/mint/github-step/steps/github-repository-selection/users-orgs-combobox.tsx b/ui/src/views/mint/github-step/steps/github-repository-selection/users-orgs-combobox.tsx index c5fb89b..8cd591d 100644 --- a/ui/src/views/mint/github-step/steps/github-repository-selection/users-orgs-combobox.tsx +++ b/ui/src/views/mint/github-step/steps/github-repository-selection/users-orgs-combobox.tsx @@ -43,6 +43,7 @@ export const UserOrgsCombobox = () => { )} selectedValue={selectedUserOrg} onChange={handleUserOrgChange} + leftIcon="github" /> ); }; diff --git a/ui/src/views/mint/mint-stepper.tsx b/ui/src/views/mint/mint-stepper.tsx index 1196781..454e0dc 100644 --- a/ui/src/views/mint/mint-stepper.tsx +++ b/ui/src/views/mint/mint-stepper.tsx @@ -10,14 +10,14 @@ export const MintStepper = () => { - - + + - - + + diff --git a/ui/src/views/mint/mint.context.tsx b/ui/src/views/mint/mint.context.tsx index fc2894e..07c57b8 100644 --- a/ui/src/views/mint/mint.context.tsx +++ b/ui/src/views/mint/mint.context.tsx @@ -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({ @@ -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(''); + 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, }} > { - 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 ( ENS - ({ + label: ens, + value: ens, + }))} selectedValue={ens} onChange={handleEnsChange} + withAutocomplete /> + {ensError && {ensError}} ); }; diff --git a/ui/src/views/mint/wallet-step/wallet-step.tsx b/ui/src/views/mint/wallet-step/wallet-step.tsx index 5cf7cbd..c89df1b 100644 --- a/ui/src/views/mint/wallet-step/wallet-step.tsx +++ b/ui/src/views/mint/wallet-step/wallet-step.tsx @@ -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 ( - + } + /> + } + /> diff --git a/ui/tailwind.config.js b/ui/tailwind.config.js index 0e6b121..bb7ae2b 100644 --- a/ui/tailwind.config.js +++ b/ui/tailwind.config.js @@ -18,6 +18,9 @@ module.exports = { borderRadius: { xhl: '1.25rem', }, + maxWidth: { + 70: '70%', + }, space: { '1h': '0.375rem', }, diff --git a/ui/yarn.lock b/ui/yarn.lock index 4db5e04..f0623a1 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -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"