feat: integrate firebase for github login (#132)

* wip: form for mint

* style: change bgcolor for disabled button

* fix: add key to list items

* styles: add some spacings and border radius

* refactor: change type file and move file validation to form

* feat: add minted nft card. add wallet step

* refactor: add mint card header to not repeat code

* styles: add border radius to svg

* styles: fix styles on mint view

* style: fix height mint view

* fix: fix save repository config

* chore: changes based on PR review

* wip: connecting with gh login

* chore: add env variables

* wip: gh login wiht auth0

* feat: add gh login integration

* chore: remove web3auth packages

* doc: add info on readme to know how to get firebase credentials

* feat: add spinner component (#133)

* fix: fix for polyfills

* refactor: remove loading state cause it was causing a loop

* chore: change placeholder

* feat: add constants env file

* fix: fix polyfills

* refactor: implement async thunk for github login

* wip: add async thunk for github api calls

* feat: implemented async thunk for github api calls

* chore: add promise.all to improve api call performance

* fix: fix console log error
This commit is contained in:
Camila Sosa Morales 2023-02-23 13:38:51 -05:00 committed by GitHub
parent cfea9a90ea
commit 964c1a651f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 2287 additions and 215 deletions

View File

@ -8,6 +8,7 @@ module.exports = {
'plugin:react/recommended',
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
'plugin:react-hooks/recommended',
],
overrides: [],
parser: '@typescript-eslint/parser',
@ -27,5 +28,6 @@ module.exports = {
'simple-import-sort/imports': 2,
'@typescript-eslint/explicit-function-return-type': 'off',
'no-console': 'error',
'unused-imports/no-unused-imports-ts': 'error',
},
};

View File

@ -23,8 +23,21 @@ To run the UI localy follow the steps:
```bash
$ yarn
```
3. You'll need to set up your firebase cretendials to make work the github login. Set the .env file with the following variables
3. Start the local server running the app:
```bash
VITE_FIREBASE_API_KEY
VITE_FIREBASE_AUTH_DOMAIN
VITE_FIREBASE_PROJECT_ID
VITE_FIREBASE_STORAGE_BUCKET
VITE_FIREBASE_MESSAGING_SENDER_ID
VITE_FIREBASE_APP_ID
VITE_FIREBASE_MEASUREMENT_ID
```
Get them from the project settings on the firebase dashboard. Read [this article](https://support.google.com/firebase/answer/7015592?hl=en#zippy=%2Cin-this-article) to know how to get your porject config
4. Start the local server running the app:
```bash
$ yarn dev

View File

@ -1,6 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<script type="module">
import { Buffer } from 'buffer';
import process from 'process';
window.Buffer = Buffer;
window.process = process;
</script>
<meta charset="UTF-8" />
<link
rel="icon"

View File

@ -16,11 +16,14 @@
"@ethersproject/providers": "^5.7.2",
"@headlessui/react": "^1.7.8",
"@radix-ui/colors": "^0.1.8",
"@radix-ui/react-avatar": "^1.0.1",
"@react-icons/all-files": "^4.1.0",
"@reduxjs/toolkit": "^1.9.1",
"@stitches/react": "^1.2.8",
"colorthief": "^2.3.2",
"firebase": "^9.17.1",
"formik": "^2.2.9",
"octokit": "^2.0.14",
"path": "^0.12.7",
"react": "^18.2.0",
"react-dom": "^18.2.0",
@ -29,6 +32,8 @@
},
"devDependencies": {
"@babel/core": "^7.20.12",
"@esbuild-plugins/node-globals-polyfill": "^0.2.3",
"@esbuild-plugins/node-modules-polyfill": "^0.2.2",
"@storybook/addon-actions": "^6.5.15",
"@storybook/addon-essentials": "^6.5.15",
"@storybook/addon-interactions": "^6.5.15",
@ -44,18 +49,23 @@
"@types/react-dom": "^18.0.9",
"@typescript-eslint/eslint-plugin": "^5.45.0",
"@typescript-eslint/parser": "^5.45.0",
"@vitejs/plugin-react": "^2.2.0",
"@vitejs/plugin-react": "2.2.0",
"autoprefixer": "^10.4.13",
"babel-loader": "^8.3.0",
"buffer": "^6.0.3",
"eslint": "^8.28.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-jest": "^27.1.6",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react": "^7.31.11",
"eslint-plugin-react-hooks": "^4.6.0",
"ethers": "^5.7.2",
"postcss": "^8.4.21",
"prettier": "^2.8.0",
"process": "^0.11.10",
"react-query": "^3.39.2",
"rollup-plugin-node-builtins": "^2.1.2",
"rollup-plugin-node-polyfills": "^0.2.1",
"storybook-dark-mode": "^2.0.5",
"tailwindcss": "^3.2.4",
"ts-loader": "^9.4.1",

View File

@ -1,8 +1,7 @@
import { BrowserRouter, Route, Routes, Navigate } from 'react-router-dom';
import { initializeWallet } from './store';
import { themeGlobals } from '@/theme/globals';
import { Home } from './views';
import { Mint } from './views/mint';
import { Home, Mint } from './views';
import { SVGTestScreen } from './views/svg-test'; // TODO: remove when done
initializeWallet();

View File

@ -1,3 +1,4 @@
import { Octokit } from 'octokit';
import React, { forwardRef } from 'react';
import { Flex } from '../layout';
import { CardStyles } from './card.styles';

View File

@ -0,0 +1,73 @@
import { dripStitches } from '@/theme';
import * as Avatar from '@radix-ui/react-avatar';
const { styled } = dripStitches;
export abstract class AvatarStyles {
static readonly Root = styled(Avatar.Root, {
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
verticalAlign: 'middle',
overflow: 'hidden',
userSelect: 'none',
width: '$5',
height: '$5',
borderRadius: '100%',
backgroundColor: '$slate2',
mr: '$2',
});
static readonly Image = styled(Avatar.Image, {
width: '100%',
height: '100%',
objectFit: 'cover',
borderRadius: 'inherit',
});
static readonly Fallback = styled(Avatar.Fallback, {
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '$slate2',
color: '$slate12',
fontSize: '$sm',
fontWeight: '$medium',
});
}
export type AvatarProps = React.ComponentProps<typeof AvatarStyles.Root> & {
/**
* Fallback node.
* In case of string, transformed to upper case and sliced to second letter.
*/
fallback?: React.ReactNode;
/**
* Source of the image.
* If not provided, fallback will be used.
*/
src?: AvatarImageProps['src'];
/**
* Alt text of the image.
*/
alt?: AvatarImageProps['alt'];
/**
* Props of the image tag.
* @see {@link AvatarImageProps}
* @default {}
*/
imageProps?: AvatarImageProps;
/**
* Props of the fallback tag.
* @see {@link AvatarFallbackProps}
* @default {}
*/
fallbackProps?: AvatarFallbackProps;
};
export type AvatarImageProps = React.ComponentProps<typeof AvatarStyles.Image>;
export type AvatarFallbackProps = React.ComponentProps<
typeof AvatarStyles.Fallback
>;

View File

@ -0,0 +1,15 @@
import { forwardRef } from 'react';
import { AvatarProps, AvatarStyles } from './avatar.styles';
export const Avatar = forwardRef<HTMLDivElement, AvatarProps>(
(
{ fallback, fallbackProps, imageProps = {}, src, alt, css, ...rootProps },
ref
) => {
return (
<AvatarStyles.Root {...rootProps} ref={ref} css={css}>
<AvatarStyles.Image src={src} alt={alt} {...imageProps} />
</AvatarStyles.Root>
);
}
);

View File

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

View File

@ -1,6 +1,6 @@
import { Fragment, useRef, useState } from 'react';
import React, { Fragment, useRef, useState } from 'react';
import { Combobox as ComboboxLib, Transition } from '@headlessui/react';
import { Icon, IconName } from '@/components/core/icon';
import { Icon } from '@/components/core/icon';
import { Flex } from '@/components/layout';
type ComboboxInputProps = {
@ -53,7 +53,7 @@ const ComboboxOption = ({ option }: ComboboxOptionProps) => (
{({ selected, active }) => (
<Flex css={{ justifyContent: 'space-between' }}>
<Flex css={{ flexDirection: 'row' }}>
{option.icon && <Icon name={option.icon} css={{ mr: '$2' }} />}
{option.icon}
<span className={`${active ? 'text-slate12' : 'text-slate11'}`}>
{option.label}
</span>
@ -75,7 +75,7 @@ export const NoResults = ({ css }: { css?: string }) => (
export type ComboboxItem = {
value: string;
label: string;
icon?: IconName;
icon?: React.ReactNode;
};
export type ComboboxProps = {

View File

@ -10,7 +10,7 @@ type DropdownOptionProps = {
const DropdownOption = ({ option }: DropdownOptionProps) => (
<Listbox.Option
className={({ active }) =>
`relative cursor-default select-none py-2 px-3.5 text-slate11 rounded-xl mb-2 text-sm ${
`relative cursor-default select-none py-2 px-3.5 text-slate11 rounded-xl mb-2 text-sm max-w-full ${
active ? 'bg-slate5 text-slate12' : 'bg-transparent'
}`
}
@ -18,10 +18,28 @@ const DropdownOption = ({ option }: DropdownOptionProps) => (
>
{({ selected, active }) => (
<Flex css={{ justifyContent: 'space-between' }}>
<span className={`${active ? 'text-slate12' : 'text-slate11'}`}>
<span
className={`${
active ? 'text-slate12' : 'text-slate11'
} max-w-full break-words pr-5`}
>
{option.label}
</span>
{selected && <Icon name="check" color="white" />}
{selected && (
<Icon
name="check"
color="white"
css={{
position: 'absolute',
top: '$0',
bottom: '$0',
right: '$0',
display: 'flex',
alignItems: 'center',
pr: '$4',
}}
/>
)}
</Flex>
)}
</Listbox.Option>
@ -41,7 +59,7 @@ const DropdownButton = ({ selectedValue, open }: DropdownButtonProps) => (
<span
className={`block truncate ${
selectedValue && selectedValue.label ? 'text-slate12' : 'text-slate11'
}`}
} break-words`}
>
{selectedValue && selectedValue.label ? selectedValue.label : 'Select'}
</span>
@ -74,7 +92,7 @@ export const Dropdown: React.FC<DropdownProps> = ({
return (
<Listbox value={selectedValue} by="value" onChange={handleDropdownChange}>
{({ open }) => (
<div className="relative">
<div className="relative max-w-full">
<DropdownButton selectedValue={selectedValue} open={open} />
<Transition
as={Fragment}
@ -82,7 +100,7 @@ export const Dropdown: React.FC<DropdownProps> = ({
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options className="absolute max-h-fit w-full z-10 overflow-auto rounded-b-xl bg-black px-3 pt-2 border-solid border-slate6 border text-base focus:outline-none sm:text-sm">
<Listbox.Options className="absolute max-h-32 w-full z-10 overflow-auto rounded-b-xl bg-black px-3 pt-2 border-solid border-slate6 border text-base focus:outline-none sm:text-sm">
{items.map((option: DropdownItem) => (
<DropdownOption key={option.value} option={option} />
))}

View File

@ -2,3 +2,5 @@ export * from './button';
export * from './combobox';
export * from './icon';
export * from './input';
export * from './avatar';
export * from './separator.styles';

View File

@ -5,6 +5,7 @@ const { styled } = dripStitches;
export abstract class FormStyles {
static readonly Field = styled(Flex, {
flexDirection: 'column',
maxWidth: '100%',
});
static readonly Label = styled('label', {

View File

@ -2,3 +2,4 @@ export * from './core';
export * from './layout';
export * from './form';
export * from './card';
export * from './spinner';

View File

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

View File

@ -0,0 +1,17 @@
import { dripStitches } from '@/theme';
const { styled } = dripStitches;
export abstract class SpinnerStyles {
static readonly Container = styled('svg', {
fontSize: '1.5rem',
width: '1em',
height: '1em',
});
}
export namespace SpinnerStyles {
export type ContainerProps = React.ComponentProps<
typeof SpinnerStyles.Container
>;
}

View File

@ -0,0 +1,38 @@
/* eslint-disable react/no-unknown-property */
import { SpinnerStyles } from './spinner.styles';
export const Spinner: React.FC<SpinnerStyles.ContainerProps> = (props) => (
<SpinnerStyles.Container
{...props}
viewBox="0 0 40 40"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle
cx="20"
cy="20"
r="17"
stroke="currentColor"
strokeWidth="5"
strokeLinecap="round"
strokeDasharray="125.6"
>
<animate
attributeName="stroke-dashoffset"
values="26.4;125.6;26.4"
dur="4s"
repeatCount="indefinite"
keyTimes="0;0.5;1"
/>
<animateTransform
attributeName="transform"
type="rotate"
from="0 20 20"
to="1080 20 20"
dur="2s"
repeatCount="indefinite"
/>
</circle>
</SpinnerStyles.Container>
);

11
ui/src/constants/env.ts Normal file
View File

@ -0,0 +1,11 @@
export const env = Object.freeze({
firebase: {
apiKey: import.meta.env.VITE_FIREBASE_API_KEY || '',
authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN || '',
projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID || '',
storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET || '',
messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID || '',
appId: import.meta.env.VITE_FIREBASE_APP_ID || '',
measurementId: import.meta.env.VITE_FIREBASE_MEASUREMENT_ID || '',
},
});

View File

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

View File

@ -0,0 +1,31 @@
import { DropdownItem } from '@/components';
import { githubActions, RootState } from '@/store';
import { createAsyncThunk } from '@reduxjs/toolkit';
import { GithubClient } from '../github-client';
type FetchBranches = {
owner: string;
repository: string;
};
export const fetchBranchesThunk = createAsyncThunk<void, FetchBranches>(
'github/fetchBranches',
async ({ owner, repository }, { dispatch, getState }) => {
const { token, queryLoading } = (getState() as RootState).github;
if (queryLoading === 'loading') return;
try {
dispatch(githubActions.setQueryState('loading'));
const githubClient = new GithubClient(token);
const branches = await githubClient.fetchBranches(owner, repository);
dispatch(githubActions.setBranches(branches as DropdownItem[]));
} catch (error) {
console.log(error);
dispatch(githubActions.setQueryState('failed'));
}
}
);

View File

@ -0,0 +1,31 @@
import { githubActions, Repository, RootState } from '@/store';
import { createAsyncThunk } from '@reduxjs/toolkit';
import { GithubClient } from '../github-client';
export const fetchRepositoriesThunk = createAsyncThunk(
'github/fetchRepositories',
async (url: string, { dispatch, getState }) => {
if ((getState() as RootState).github.queryLoading === 'loading') return;
try {
dispatch(githubActions.setQueryState('loading'));
const githubClient = new GithubClient(
(getState() as RootState).github.token
);
const repositories = await githubClient.fetchRepos(url);
dispatch(
githubActions.setRepositoires(
repositories.map(
(repo: any) => ({ name: repo.name, url: repo.url } as Repository)
)
)
);
} catch (error) {
console.log(error);
dispatch(githubActions.setQueryState('failed'));
}
}
);

View File

@ -0,0 +1,43 @@
import { ComboboxItem } from '@/components';
import { RootState } from '@/store';
import { createAsyncThunk } from '@reduxjs/toolkit';
import { GithubClient, UserData } from '../github-client';
import { githubActions } from '../github-slice';
export const fetchUserAndOrgsThunk = createAsyncThunk(
'github/fetchUserAndOrgs',
async (_, { dispatch, getState }) => {
const { token, queryUserAndOrganizations } = (getState() as RootState)
.github;
if (queryUserAndOrganizations === 'loading') return;
try {
dispatch(githubActions.setQueryUserState('loading'));
const githubClient = new GithubClient(token);
const response = await Promise.all([
githubClient.fetchUser(),
githubClient.fetchOrgs(),
]);
const userResponse = response[0];
const orgsResponse = response[1];
let comboboxItems: UserData[] = [];
if (userResponse) {
comboboxItems.push(userResponse);
}
if (orgsResponse) {
comboboxItems = [...comboboxItems, ...orgsResponse];
}
dispatch(githubActions.setUserAndOrgs(comboboxItems));
} catch (error) {
console.log(error);
dispatch(githubActions.setQueryState('failed'));
}
}
);

View File

@ -0,0 +1,4 @@
export * from './login';
export * from './fetch-repositories';
export * from './fetch-branches';
export * from './fetch-user-organizations';

View File

@ -0,0 +1,50 @@
import { createAsyncThunk } from '@reduxjs/toolkit';
import { env } from '@/constants';
import { initializeApp } from 'firebase/app';
import { getAuth, signInWithPopup, GithubAuthProvider } from 'firebase/auth';
import { githubActions, RootState } from '@/store';
const GithubScopes = ['repo', 'read:org', 'read:user', 'public_repo', 'user'];
const firebaseConfig = {
apiKey: env.firebase.apiKey,
authDomain: env.firebase.authDomain,
projectId: env.firebase.projectId,
storageBucket: env.firebase.storageBucket,
messagingSenderId: env.firebase.messagingSenderId,
appId: env.firebase.appId,
measurementId: env.firebase.measurementId,
};
// Initialize Firebase
const app = initializeApp(firebaseConfig);
const provider = new GithubAuthProvider();
GithubScopes.forEach((scope) => provider.addScope(scope));
const auth = getAuth(app);
export const login = createAsyncThunk(
'github/login',
async (_, { dispatch, getState }) => {
if ((getState() as RootState).github.state === 'loading') return;
try {
dispatch(githubActions.setState('loading'));
const response = await signInWithPopup(auth, provider);
// This gives you a GitHub Access Token. You can use it to access the GitHub API.
const credential = GithubAuthProvider.credentialFromResult(response);
if (credential && credential.accessToken) {
dispatch(githubActions.setToken(credential.accessToken));
} else {
//something went wrong and have no token
throw Error('Invalid response type');
}
} catch (error) {
console.log('Could not connect to GitHub', error);
dispatch(githubActions.setState('disconnected'));
}
}
);

View File

@ -0,0 +1,76 @@
import { DropdownItem } from '@/components';
import { Octokit } from 'octokit';
export type UserData = {
value: string;
label: string;
avatar: string;
};
export class GithubClient {
octokit: Octokit;
token: string;
constructor(token: string) {
(this.token = token),
(this.octokit = new Octokit({
auth: token,
}));
}
async fetchUser(): Promise<UserData> {
const { data: userData } = await this.octokit.request('GET /user');
return {
value: userData.repos_url,
label: userData.login,
avatar: userData.avatar_url,
};
}
async fetchOrgs(): Promise<UserData[]> {
const { data: organizationsData } = await this.octokit.request(
'GET /user/orgs'
);
return organizationsData.map((org) => {
return {
label: org.login,
value: org.repos_url,
avatar: org.avatar_url,
};
});
}
async fetchRepos(url: string) {
try {
const repos = await fetch(url, {
headers: {
Authorization: `Bearer ${this.token}`,
},
}).then((res) => res.json());
return repos;
} catch (error) {
return error;
}
}
async fetchBranches(owner: string, repo: string): Promise<DropdownItem[]> {
const branches = await this.octokit
.request('GET /repos/{owner}/{repo}/branches', {
owner,
repo,
})
.then((res) =>
res.data.map((branch) => {
return {
label: branch.name,
value: branch.commit.sha,
};
})
);
return branches;
}
}

View File

@ -0,0 +1,115 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { RootState } from '@/store';
import { useAppSelector } from '@/store/hooks';
import * as asyncThunk from './async-thunk';
import { ComboboxItem, DropdownItem } from '@/components';
import { UserData } from './github-client';
export type Repository = {
name: string;
url: string;
};
export namespace GithubState {
export type Token = string;
export type State = 'disconnected' | 'loading' | 'connected';
export type QueryUserAndOrganizations =
| 'idle'
| 'loading'
| 'failed'
| 'success';
export type QueryLoading = 'idle' | 'loading' | 'failed' | 'success';
export type UserAndOrganizations = Array<UserData>;
export type Repositories = Array<Repository>;
export type Branches = Array<DropdownItem>;
}
export interface GithubState {
token: GithubState.Token;
state: GithubState.State;
userAndOrganizations: GithubState.UserAndOrganizations;
queryUserAndOrganizations: GithubState.QueryUserAndOrganizations;
queryLoading: GithubState.QueryLoading;
repositories: GithubState.Repositories;
branches: GithubState.Branches;
}
const initialState: GithubState = {
token: '',
state: 'disconnected',
queryUserAndOrganizations: 'idle',
queryLoading: 'idle',
userAndOrganizations: [],
repositories: [],
branches: [],
};
export const githubSlice = createSlice({
name: 'github',
initialState,
reducers: {
setToken: (state, action: PayloadAction<GithubState.Token>) => {
state.token = action.payload;
state.state = 'connected';
},
setState: (
state,
action: PayloadAction<Exclude<GithubState.State, 'connected'>>
) => {
state.token = '';
state.state = action.payload;
},
setRepositoires: (
state,
action: PayloadAction<GithubState.Repositoires>
) => {
state.repositories = action.payload;
state.queryLoading = 'success';
},
setBranches: (state, action: PayloadAction<GithubState.Branches>) => {
state.branches = action.payload;
state.queryLoading = 'success';
},
setUserAndOrgs: (
state,
action: PayloadAction<GithubState.UserAndOrganizations>
) => {
state.userAndOrganizations = action.payload;
state.queryUserAndOrganizations = 'success';
},
setQueryUserState: (
state,
action: PayloadAction<Exclude<GithubState.QueryLoading, 'success'>>
) => {
state.queryUserAndOrganizations = action.payload;
},
setQueryState: (
state,
action: PayloadAction<Exclude<GithubState.QueryLoading, 'success'>>
) => {
state.queryLoading = action.payload;
},
disconnect: (state) => {
state.token = '';
state.state = 'disconnected';
},
},
});
export const githubActions = {
...githubSlice.actions,
...asyncThunk,
};
const selectGithubState = (state: RootState): GithubState => state.github;
export const useGithubStore = (): GithubState =>
useAppSelector(selectGithubState);
export default githubSlice.reducer;

View File

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

View File

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

View File

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

View File

@ -0,0 +1,39 @@
import { Button, Icon } from '@/components';
import { githubActions, useAppDispatch, useGithubStore } from '@/store';
import { Mint } from '@/views/mint/mint.context';
import { useCallback } from 'react';
export const GithubButton = () => {
const { state } = useGithubStore();
const dispatch = useAppDispatch();
const { setGithubStep } = Mint.useContext();
const handleGithubLogin = useCallback(() => {
dispatch(githubActions.login())
.then(() => setGithubStep(2))
.catch((error) => {
//TODO show toast with error message
console.log(error);
});
}, [dispatch]);
return (
<Button
iconSpacing="59"
size="lg"
variant="ghost"
css={{
backgroundColor: '$slate4',
color: '$slate12',
py: '$2h',
}}
onClick={handleGithubLogin}
disabled={state === 'loading'}
rightIcon={
<Icon name="github" css={{ color: 'white', fontSize: '$4xl' }} />
}
>
GitHub
</Button>
);
};

View File

@ -1,14 +1,7 @@
import { Button, Card, Grid, Icon, IconButton } from '@/components';
import { Mint } from '../../../mint.context';
import { Card, Grid, Icon, IconButton } from '@/components';
import { GithubButton } from './github-button';
export const GithubConnect: React.FC = () => {
const { setGithubStep } = Mint.useContext();
const handleNextStep = () => {
//TODO when we integrate GH login, we'll need to set the step to 2 after login
setGithubStep(2);
};
return (
export const GithubConnect: React.FC = () => (
<Card.Container>
<Card.Heading
title="Connect GitHub"
@ -23,22 +16,7 @@ export const GithubConnect: React.FC = () => {
/>
<Card.Body>
<Grid css={{ rowGap: '$6' }}>
<Button
iconSpacing="59"
size="lg"
variant="ghost"
css={{
backgroundColor: '$slate4',
color: '$slate12',
py: '$2h',
}}
onClick={handleNextStep}
rightIcon={
<Icon name="github" css={{ color: 'white', fontSize: '$4xl' }} />
}
>
GitHub
</Button>
<GithubButton />
<Card.Text
css={{ height: '$46h', width: '$95', fontSize: '$md', px: '$12' }}
>
@ -49,5 +27,4 @@ export const GithubConnect: React.FC = () => {
</Grid>
</Card.Body>
</Card.Container>
);
};
);

View File

@ -0,0 +1,73 @@
import { Dropdown, DropdownItem, Flex, Form, Spinner } from '@/components';
import { githubActions, useAppDispatch, useGithubStore } from '@/store';
import { Mint } from '@/views/mint/mint.context';
import { useEffect } from 'react';
export const RepoBranchCommitFields = () => {
const { queryLoading, branches } = useGithubStore();
const dispatch = useAppDispatch();
const {
repositoryName,
selectedUserOrg,
branchName,
commitHash,
setBranchName,
setCommitHash,
} = Mint.useContext();
useEffect(() => {
if (queryLoading === 'idle') {
dispatch(
githubActions.fetchBranchesThunk({
owner: selectedUserOrg.label,
repository: repositoryName.name,
})
);
}
}, [queryLoading, dispatch]);
const handleBranchChange = (dorpdownOption: DropdownItem) => {
setBranchName(dorpdownOption);
setCommitHash(dorpdownOption.value);
};
const handleCommitHashChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setCommitHash(e.target.value);
};
if (queryLoading === 'loading') {
return (
<Flex
css={{
height: '9.75rem',
justifyContent: 'center',
alignItems: 'center',
}}
>
<Spinner />
</Flex>
);
}
return (
<>
<Form.Field>
<Form.Label>Git Branch</Form.Label>
<Dropdown
items={branches}
selectedValue={branchName}
onChange={handleBranchChange}
/>
</Form.Field>
<Form.Field>
<Form.Label>Git Commit</Form.Label>
<Form.Input
placeholder="Select branch to get last commit"
value={commitHash}
onChange={handleCommitHashChange}
/>
</Form.Field>
</>
);
};

View File

@ -1,60 +1,22 @@
import {
Button,
Card,
Dropdown,
DropdownItem,
Form,
Grid,
Stepper,
} from '@/components';
import { Button, Card, Flex, Stepper } from '@/components';
import { Mint } from '@/views/mint/mint.context';
import { useState } from 'react';
import { RepoRow } from '../github-repository-selection';
//TODO remove once it's integrated with GH login
const branches: DropdownItem[] = [
{
label: 'master',
value: 'master',
},
{
label: 'develop',
value: 'develop',
},
{
label: 'feature/branch',
value: 'feature/branch',
},
];
import { RepoBranchCommitFields } from './repo-branch-commit-fields';
export const RepoConfigurationBody = () => {
const { repositoryName, branchName, commitHash, setRepositoryConfig } =
Mint.useContext();
const { repositoryName, branchName, commitHash } = Mint.useContext();
const { nextStep } = Stepper.useContext();
const [branchSelected, setBranchSelected] = useState(branchName);
const [commitHashSelected, setCommitHashSelected] = useState(commitHash);
console.log(branchSelected);
const handleBranchChange = (dorpdownOption: DropdownItem) => {
//TODO we'll have to check the data that GH API returns
console.log(dorpdownOption);
setBranchSelected(dorpdownOption);
};
const handleCommitHashChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setCommitHashSelected(e.target.value);
};
const handleContinueClick = () => {
setRepositoryConfig(branchSelected, commitHashSelected);
nextStep();
};
return (
<Card.Body css={{ pt: '$2' }}>
<Grid css={{ rowGap: '$6' }}>
<Flex css={{ rowGap: '$6', flexDirection: 'column' }}>
<RepoRow
repo={repositoryName}
repo={repositoryName.name}
css={{ mb: '0' }}
button={
<Button
@ -67,31 +29,16 @@ export const RepoConfigurationBody = () => {
</Button>
}
/>
<Form.Field>
<Form.Label>Git Branch</Form.Label>
<Dropdown
items={branches}
selectedValue={branchSelected}
onChange={handleBranchChange}
/>
</Form.Field>
<Form.Field>
<Form.Label>Git Commit</Form.Label>
<Form.Input
placeholder="693f89763dbb7a6c9ce0711cc34591a4c8c77198"
value={commitHashSelected}
onChange={handleCommitHashChange}
/>
</Form.Field>
<RepoBranchCommitFields />
<Button
disabled={!branchSelected || !commitHashSelected}
disabled={!branchName.value || !commitHash}
colorScheme="blue"
variant="solid"
onClick={handleContinueClick}
>
Continue
</Button>
</Grid>
</Flex>
</Card.Body>
);
};

View File

@ -3,11 +3,12 @@ import { MintCardHeader } from '@/views/mint/mint-card';
import { Mint } from '@/views/mint/mint.context';
export const RepoConfigurationHeader = () => {
const { setGithubStep, setRepositoryConfig } = Mint.useContext();
const { setGithubStep, setBranchName, setCommitHash } = Mint.useContext();
const handlePrevStepClick = () => {
setGithubStep(2);
setRepositoryConfig({} as DropdownItem, '');
setBranchName({} as DropdownItem);
setCommitHash('');
};
return (

View File

@ -1,42 +1,23 @@
import {
Button,
Card,
Combobox,
ComboboxItem,
DropdownItem,
Flex,
Grid,
Icon,
IconButton,
NoResults,
} from '@/components';
import { Card, ComboboxItem, Flex, Grid, Icon, Spinner } from '@/components';
import { Input } from '@/components/core/input';
import { Separator } from '@/components/core/separator.styles';
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 { RepositoriesList } from './repositories-list';
import { UserOrgsCombobox } from './users-orgs-combobox';
//TODO remove once it's integrated with GH login
const repos = [
'DyDx',
'Testing',
'Hello World',
'Portofolio',
'NFA',
'NFT',
'NFTs',
];
//TODO remove once it's integrated with GH login
const users: ComboboxItem[] = [
{ label: 'DyDx', value: 'DyDx', icon: 'github' },
{ label: 'Testing', value: 'Testing', icon: 'github' },
{ label: 'Hello World', value: 'Hello World', icon: 'github' },
{ label: 'Portofolio', value: 'Portofolio', icon: 'github' },
{ label: 'NFA', value: 'NFA', icon: 'github' },
{ label: 'NFT', value: 'NFT', icon: 'github' },
{ label: 'NFTs', value: 'NFTs', icon: 'github' },
];
export const Loading = () => (
<Flex
css={{
justifyContent: 'center',
alignItems: 'center',
height: '$60',
}}
>
<Spinner />
</Flex>
);
type RepoRowProps = {
repo: string;
@ -60,10 +41,10 @@ export const RepoRow = forwardRef<HTMLDivElement, RepoRowProps>(
);
export const GithubRepositoryConnection: React.FC = () => {
const { queryLoading, queryUserAndOrganizations } = useGithubStore();
const [searchValue, setSearchValue] = useState('');
const [selectedUser, setSelectedUser] = useState<ComboboxItem | undefined>();
const { setGithubStep, setRepositoryName, setRepositoryConfig } =
Mint.useContext();
const { setGithubStep, setSelectedUserOrg } = Mint.useContext();
const timeOutRef = useRef<NodeJS.Timeout>();
@ -77,21 +58,9 @@ export const GithubRepositoryConnection: React.FC = () => {
const handlePrevStepClick = () => {
setGithubStep(1);
setSelectedUserOrg({} as ComboboxItem);
};
const handleSelectRepo = (repo: string) => {
setRepositoryName(repo);
setGithubStep(3);
setRepositoryConfig({} as DropdownItem, '');
};
const filteredRepositories =
searchValue === ''
? repos
: repos.filter(
(item) => item.toUpperCase().indexOf(searchValue.toUpperCase()) != -1
);
return (
<Card.Container css={{ maxWidth: '$107h', maxHeight: '$95h', pb: '$0h' }}>
<MintCardHeader
@ -101,49 +70,19 @@ export const GithubRepositoryConnection: React.FC = () => {
<Card.Body css={{ pt: '$4' }}>
<Grid css={{ rowGap: '$2' }}>
<Flex css={{ gap: '$4' }}>
<Combobox
items={users}
selectedValue={selectedUser}
onChange={setSelectedUser}
/>
<UserOrgsCombobox />
<Input
leftIcon="search"
placeholder="Search"
onChange={handleSearchChange}
/>
</Flex>
<Flex
css={{
minHeight: '$40',
maxHeight: '$60',
overflowX: 'hidden',
overflowY: 'scroll',
flexDirection: 'column',
}}
>
{filteredRepositories.length > 0 ? (
filteredRepositories.map((repo, index, { length }) => (
<React.Fragment key={repo}>
<RepoRow
repo={repo}
button={
<Button
colorScheme="blue"
variant="outline"
css={{ py: '$1', height: '$5', borderRadius: '$md' }}
onClick={() => handleSelectRepo(repo)}
>
Use for NFA
</Button>
}
/>
{index < length - 1 && <Separator />}
</React.Fragment>
))
) : (
<NoResults css="text-center" />
)}
</Flex>
{queryLoading === 'loading' ||
queryUserAndOrganizations === 'loading' ? (
<Loading />
) : (
<RepositoriesList searchValue={searchValue} />
)}
</Grid>
</Card.Body>
</Card.Container>

View File

@ -0,0 +1,58 @@
import { useEffect, useMemo } from 'react';
import { Flex, NoResults } from '@/components';
import { Mint } from '@/views/mint/mint.context';
import { githubActions, useAppDispatch, useGithubStore } from '@/store';
import { Repository } from './repository';
type RepositoriesListProps = {
searchValue: string;
};
export const RepositoriesList = ({ searchValue }: RepositoriesListProps) => {
const { selectedUserOrg } = Mint.useContext();
const { queryLoading, repositories } = useGithubStore();
const dispatch = useAppDispatch();
const filteredRepositories = useMemo(() => {
return searchValue === ''
? repositories
: repositories.filter(
(item) =>
item.name.toUpperCase().indexOf(searchValue.toUpperCase()) != -1
);
}, [searchValue, repositories]);
useEffect(() => {
if (queryLoading === 'idle' && selectedUserOrg.value) {
dispatch(githubActions.fetchRepositoriesThunk(selectedUserOrg.value));
}
}, [queryLoading, dispatch, selectedUserOrg]);
if (queryLoading === 'failed') {
return <span>Error</span>;
}
return (
<Flex
css={{
height: '$60',
overflowX: 'hidden',
overflowY: 'scroll',
flexDirection: 'column',
}}
>
{filteredRepositories.length > 0 ? (
filteredRepositories.map((repo, index, { length }) => (
<Repository
key={repo.name}
repository={repo}
index={index}
length={length}
/>
))
) : (
<NoResults css="text-center" />
)}
</Flex>
);
};

View File

@ -0,0 +1,43 @@
import { Button, Separator } from '@/components';
import {
githubActions,
Repository as RepositoryType,
useAppDispatch,
} from '@/store';
import { Mint } from '@/views/mint/mint.context';
import { RepoRow } from './github-repository-selection';
type RepositoryProps = {
repository: RepositoryType;
index: number;
length: number;
};
export const Repository = ({ repository, index, length }: RepositoryProps) => {
const { setGithubStep, setRepositoryName } = Mint.useContext();
const dispatch = useAppDispatch();
const handleSelectRepo = () => {
setRepositoryName(repository);
setGithubStep(3);
dispatch(githubActions.setQueryState('idle'));
};
return (
<>
<RepoRow
repo={repository.name}
button={
<Button
colorScheme="blue"
variant="outline"
css={{ py: '$1', height: '$5', borderRadius: '$md' }}
onClick={handleSelectRepo}
>
Use for NFA
</Button>
}
/>
{index < length - 1 && <Separator />}
</>
);
};

View File

@ -0,0 +1,48 @@
import { Avatar, Combobox, ComboboxItem } from '@/components';
import { githubActions, useAppDispatch, useGithubStore } from '@/store';
import { Mint } from '@/views/mint/mint.context';
import { useEffect } from 'react';
export const UserOrgsCombobox = () => {
const { queryUserAndOrganizations, userAndOrganizations } = useGithubStore();
const dispatch = useAppDispatch();
const { selectedUserOrg, setSelectedUserOrg } = Mint.useContext();
useEffect(() => {
if (queryUserAndOrganizations === 'idle') {
dispatch(githubActions.fetchUserAndOrgsThunk());
}
}, [dispatch, queryUserAndOrganizations]);
const handleUserOrgChange = (item: ComboboxItem) => {
dispatch(githubActions.fetchRepositoriesThunk(item.value));
setSelectedUserOrg(item);
};
useEffect(() => {
if (
queryUserAndOrganizations === 'success' &&
selectedUserOrg.value === undefined &&
userAndOrganizations.length > 0
) {
//SET first user
setSelectedUserOrg(userAndOrganizations[0]);
}
}, [queryUserAndOrganizations]);
return (
<Combobox
items={userAndOrganizations.map(
(item) =>
({
label: item.label,
value: item.value,
icon: <Avatar src={item.avatar} />,
} as ComboboxItem)
)}
selectedValue={selectedUserOrg}
onChange={handleUserOrgChange}
/>
);
};

View File

@ -1,9 +1,11 @@
import { DropdownItem } from '@/components';
import { ComboboxItem, DropdownItem } from '@/components';
import { Repository } from '@/store';
import { createContext } from '@/utils';
import { useState } from 'react';
export type MintContext = {
repositoryName: string;
selectedUserOrg: ComboboxItem;
repositoryName: Repository;
branchName: DropdownItem; //get value from DropdownItem to mint
commitHash: string;
githubStep: number;
@ -11,13 +13,15 @@ export type MintContext = {
appDescription: string;
appLogo: string;
logoColor: string;
ens: DropdownItem; //maybe it would be a DropdownItem
ens: DropdownItem;
domain: string;
verifyNFA: boolean;
sucessMint: boolean | undefined;
setGithubStep: (step: number) => void;
setRepositoryName: (repo: string) => void;
setRepositoryConfig: (branch: DropdownItem, hash: string) => void;
setSelectedUserOrg: (userOrg: ComboboxItem) => void;
setRepositoryName: (repo: Repository) => void;
setBranchName: (branch: DropdownItem) => void;
setCommitHash: (hash: string) => void;
setAppName: (name: string) => void;
setAppDescription: (description: string) => void;
setAppLogo: (logo: string) => void;
@ -39,7 +43,10 @@ export abstract class Mint {
static readonly Provider: React.FC<Mint.ProviderProps> = ({ children }) => {
//Github Connection
const [repositoryName, setRepositoryName] = useState('');
const [selectedUserOrg, setSelectedUserOrg] = useState({} as ComboboxItem);
const [repositoryName, setRepositoryName] = useState<Repository>(
{} as Repository
);
const [branchName, setBranchName] = useState({} as DropdownItem);
const [commitHash, setCommitHash] = useState('');
const [githubStep, setGithubStepContext] = useState(1);
@ -64,14 +71,10 @@ export abstract class Mint {
}
};
const setRepositoryConfig = (branch: DropdownItem, hash: string) => {
setBranchName(branch);
setCommitHash(hash);
};
return (
<MintProvider
value={{
selectedUserOrg,
repositoryName,
branchName,
commitHash,
@ -84,9 +87,11 @@ export abstract class Mint {
domain,
verifyNFA,
sucessMint,
setSelectedUserOrg,
setGithubStep,
setRepositoryConfig,
setRepositoryName,
setBranchName,
setCommitHash,
setAppName,
setAppDescription,
setAppLogo,

View File

@ -2,7 +2,68 @@ import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import tsconfigPaths from 'vite-tsconfig-paths';
import { NodeGlobalsPolyfillPlugin } from '@esbuild-plugins/node-globals-polyfill';
import { NodeModulesPolyfillPlugin } from '@esbuild-plugins/node-modules-polyfill';
import rollupNodePolyFill from 'rollup-plugin-node-polyfills';
import builtins from 'rollup-plugin-node-builtins';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react(), tsconfigPaths()],
resolve: {
alias: {
// This Rollup aliases are extracted from @esbuild-plugins/node-modules-polyfill,
// see https://github.com/remorses/esbuild-plugins/blob/master/node-modules-polyfill/src/polyfills.ts
// process and buffer are excluded because already managed
// by node-globals-polyfill
util: 'rollup-plugin-node-polyfills/polyfills/util',
sys: 'util',
events: 'rollup-plugin-node-polyfills/polyfills/events',
stream: 'rollup-plugin-node-polyfills/polyfills/stream',
path: 'rollup-plugin-node-polyfills/polyfills/path',
querystring: 'rollup-plugin-node-polyfills/polyfills/qs',
punycode: 'rollup-plugin-node-polyfills/polyfills/punycode',
url: 'rollup-plugin-node-polyfills/polyfills/url',
string_decoder: 'rollup-plugin-node-polyfills/polyfills/string-decoder',
http: 'rollup-plugin-node-polyfills/polyfills/http',
https: 'rollup-plugin-node-polyfills/polyfills/http',
os: 'rollup-plugin-node-polyfills/polyfills/os',
assert: 'rollup-plugin-node-polyfills/polyfills/assert',
constants: 'rollup-plugin-node-polyfills/polyfills/constants',
_stream_duplex:
'rollup-plugin-node-polyfills/polyfills/readable-stream/duplex',
_stream_passthrough:
'rollup-plugin-node-polyfills/polyfills/readable-stream/passthrough',
_stream_readable:
'rollup-plugin-node-polyfills/polyfills/readable-stream/readable',
_stream_writable:
'rollup-plugin-node-polyfills/polyfills/readable-stream/writable',
_stream_transform:
'rollup-plugin-node-polyfills/polyfills/readable-stream/transform',
timers: 'rollup-plugin-node-polyfills/polyfills/timers',
console: 'rollup-plugin-node-polyfills/polyfills/console',
vm: 'rollup-plugin-node-polyfills/polyfills/vm',
zlib: 'rollup-plugin-node-polyfills/polyfills/zlib',
tty: 'rollup-plugin-node-polyfills/polyfills/tty',
domain: 'rollup-plugin-node-polyfills/polyfills/domain',
},
},
optimizeDeps: {
esbuildOptions: {
target: 'es2020',
supported: { bigint: true },
plugins: [NodeModulesPolyfillPlugin()],
},
},
build: {
target: 'es2020',
rollupOptions: {
plugins: [
// Enable rollup polyfills plugin
// used during production bundling
builtins(),
rollupNodePolyFill(),
],
},
},
});

File diff suppressed because it is too large Load Diff