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:
parent
cfea9a90ea
commit
964c1a651f
|
|
@ -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',
|
||||
},
|
||||
};
|
||||
|
|
|
|||
15
ui/README.md
15
ui/README.md
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { Octokit } from 'octokit';
|
||||
import React, { forwardRef } from 'react';
|
||||
import { Flex } from '../layout';
|
||||
import { CardStyles } from './card.styles';
|
||||
|
|
|
|||
|
|
@ -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
|
||||
>;
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
@ -0,0 +1 @@
|
|||
export * from './avatar';
|
||||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -2,3 +2,5 @@ export * from './button';
|
|||
export * from './combobox';
|
||||
export * from './icon';
|
||||
export * from './input';
|
||||
export * from './avatar';
|
||||
export * from './separator.styles';
|
||||
|
|
|
|||
|
|
@ -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', {
|
||||
|
|
|
|||
|
|
@ -2,3 +2,4 @@ export * from './core';
|
|||
export * from './layout';
|
||||
export * from './form';
|
||||
export * from './card';
|
||||
export * from './spinner';
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
export * from './spinner';
|
||||
|
|
@ -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
|
||||
>;
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
|
|
@ -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 || '',
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1 @@
|
|||
export * from './env';
|
||||
|
|
@ -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'));
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
@ -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'));
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
@ -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'));
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
export * from './login';
|
||||
export * from './fetch-repositories';
|
||||
export * from './fetch-branches';
|
||||
export * from './fetch-user-organizations';
|
||||
|
|
@ -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'));
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export * from './github-slice';
|
||||
|
|
@ -1 +1,2 @@
|
|||
export * from './wallet';
|
||||
export * from './github';
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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 />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
1339
ui/yarn.lock
1339
ui/yarn.lock
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue