Skip to content

Commit

Permalink
Merge pull request #10 from citydaoproject/mnorman-issue3
Browse files Browse the repository at this point in the history
chore: [#3] disable claim button when no more to claim
  • Loading branch information
trkaplan authored May 14, 2022
2 parents 95acff8 + 4fb3feb commit 783d77a
Show file tree
Hide file tree
Showing 22 changed files with 18,513 additions and 1,046 deletions.
1 change: 1 addition & 0 deletions .eslintrc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ plugins:
- prettier
rules:
'prettier/prettier': warn
'@next/next/no-img-element': off
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ jobs:
- name: Check out code
uses: actions/checkout@v3
- name: Install dependencies
run: npm ci
run: yarn install --frozen-lockfile
- name: Build
run: npm run build
run: yarn build
- name: Lint
run: npm run lint
run: yarn lint
1 change: 0 additions & 1 deletion components/Button/index.tsx

This file was deleted.

36 changes: 36 additions & 0 deletions components/ClaimButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import React from 'react';
import DefaultButton, { DefaultButtonProps } from './common/DefaultButton';

export interface ClaimButtonProps extends DefaultButtonProps {
allowance: number;
walletAlreadyClaimed: number;
withinClaimPeriod: boolean;
}

const ClaimButton = ({ allowance, walletAlreadyClaimed, withinClaimPeriod, disabled, ...rest }: ClaimButtonProps) => {
const getClaimButtonText = () => {
if (!withinClaimPeriod) {
return 'CLAIM PLOTS';
}

if (allowance > 0 && allowance > walletAlreadyClaimed) {
return `CLAIM ${allowance - walletAlreadyClaimed} PLOTS`;
}

if (walletAlreadyClaimed > 0) {
return `${walletAlreadyClaimed} PLOTS CLAIMED`;
}

return 'CLAIM PLOTS';
};

return (
<DefaultButton
{...rest}
disabled={disabled || !withinClaimPeriod || (walletAlreadyClaimed === 0 && allowance === 0)}
>
{getClaimButtonText()}
</DefaultButton>
);
};
export default ClaimButton;
4 changes: 1 addition & 3 deletions components/ClaimModal/ClaimModal.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
/* eslint-disable @next/next/no-img-element */
// TODO trkaplan disable no-img-element in eslint config
import { FC, useState } from 'react';
import Modal from 'react-modal';

import { useModal } from '../../hooks/useModal';
import { AGREEMENT_IPFS_URL } from '../../contants';
import { AGREEMENT_IPFS_URL } from '../../constants/other';

type ClaimModalProps = {
eligibleNftsCount: number;
Expand Down
63 changes: 63 additions & 0 deletions components/ClaimPeriodCountdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import React from 'react';
import Countdown from 'react-countdown';

export interface ClaimPeriodCountdownProps {
claimPeriodStart: number;
claimPeriodEnd: number;
}

interface RendererProps {
days: number;
hours: number;
minutes: number;
seconds: number;
completed: boolean;
}

const ClaimPeriodCountdown = ({ claimPeriodStart, claimPeriodEnd }: ClaimPeriodCountdownProps) => {
const ClaimPeriodEnded = () => <span>Claim Period has ended</span>;

const buildRenderer =
(beforeStart: boolean) =>
// eslint-disable-next-line react/display-name
({ days, hours, minutes, seconds, completed }: RendererProps) => {
if (completed) {
if (beforeStart) {
return <span>Claim Period has started</span>;
}

return <ClaimPeriodEnded />;
}

const numberText = (number: number, singular: string, plural: string = singular + 's') =>
number > 0 ? number + ' ' + (number === 1 ? singular : plural) : '';

const daysText = numberText(days, 'day');
const hoursText = numberText(hours, 'hour');
const minutesText = numberText(minutes, 'minute');
const secondsText = numberText(seconds, 'second');

const allText = [daysText, hoursText, minutesText, secondsText].filter((text) => text).join(', ');

return <span className="remaining-time">{allText}</span>;
};

if (claimPeriodStart > Date.now()) {
return (
<span>
Claim Period starts in <Countdown date={claimPeriodStart} renderer={buildRenderer(true)} />
</span>
);
}

if (claimPeriodEnd > Date.now()) {
return (
<span>
Claim Period ends in <Countdown date={claimPeriodEnd} renderer={buildRenderer(false)} />
</span>
);
}

return <ClaimPeriodEnded />;
};
export default ClaimPeriodCountdown;
13 changes: 5 additions & 8 deletions components/ConnectButton/ConnectButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,8 @@ interface ConnectButtonProps {
text?: string;
}

export const ConnectButton: FC<ConnectButtonProps> = ({ enabled, onClick, address }) => {
// TODO trkaplan disable if wallet is not installed
return (
<button disabled={!enabled} onClick={onClick} className="text-button">
{address ? shortenWalletAddress(address) : 'Connect'}
</button>
);
};
export const ConnectButton: FC<ConnectButtonProps> = ({ enabled, onClick, address }) => (
<button disabled={!enabled} onClick={onClick} className="text-button">
{address ? shortenWalletAddress(address) : 'Connect'}
</button>
);
2 changes: 1 addition & 1 deletion components/MintedNftsView/MintedNftsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export const MintedNftsView: FC<MintedNftsViewProps> = ({ navigateToHome, number
<br />
<div className="mintedNftImagesWrapper">
{[...Array(numberOfNfts)].map((value: undefined, index: number) => (
<img className="mintedNftImage" key={index} src="/citydao-parcel-0-NFT-Art-sm2.png" />
<img className="mintedNftImage" alt="Parcel 0 NFT Art" key={index} src="/citydao-parcel-0-NFT-Art-sm2.png" />
))}
</div>
</div>
Expand Down
8 changes: 8 additions & 0 deletions components/common/DefaultButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import React from 'react';

export interface DefaultButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {}

const DefaultButton = ({ disabled, ...rest }: DefaultButtonProps) => (
<button {...rest} disabled={disabled} className={disabled ? 'border-button' : ''} />
);
export default DefaultButton;
17 changes: 17 additions & 0 deletions constants/other.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { ZERO_ADDRESS } from '@citydao/parcel-contracts/dist/src/constants/accounts';
import { addresses } from '../data/whiteListedAddresses';

export const MAX_NFT_TO_MINT = Object.values(addresses).reduce((prev, curr) => prev + curr, 0);

const AGREEMENT_IPFS_HASH = 'QmUbFb12ZEAyoqGUEsnS8fxh78nowEqNwvn7BbAfryRRay';
export const AGREEMENT_IPFS_URL = `https://ipfs.io/ipfs/${AGREEMENT_IPFS_HASH}`;

export const PARCEL0_NFT_CONTRACT_ADDRESSES: { [key: number]: string } = {
1: ZERO_ADDRESS,
4: '0x209723a65844093Ad769d557a22742e0f661959d',
};

export enum VIEWS {
'INITIAL_VIEW',
'MINTED_NFTS',
}
3 changes: 2 additions & 1 deletion containers/ParcelProperties/ParcelProperties.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { FC } from 'react';
import { ParcelProperty, ParcelPropertyProps } from '../../components/ParcelProperty';
import { AGREEMENT_IPFS_URL } from '../../contants';
import { AGREEMENT_IPFS_URL } from '../../constants/other';

export const ParcelProperties: FC<{
parcelProperties: ParcelPropertyProps[];
}> = ({ parcelProperties }) => (
Expand Down
11 changes: 0 additions & 11 deletions contants.ts

This file was deleted.

21 changes: 10 additions & 11 deletions context/StateProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { reducer, Actions } from "../reducer/";
import React, { createContext, ReactNode, useContext, useReducer } from "react";
import { Provider } from '@ethersproject/abstract-provider';
import { JsonRpcProvider } from '@ethersproject/providers';
import React, { createContext, ReactNode, useContext, useReducer } from 'react';
import { Actions, reducer } from '../reducer/';

export type InitialStateType = {
provider?: any;
web3Provider?: any;
account?: string | null;
chainId?: number | null;
provider: Provider | null;
web3Provider: JsonRpcProvider | null;
account: string | null;
chainId: number | null;
};

const initialState: InitialStateType = {
Expand All @@ -27,11 +30,7 @@ type AppProviderProps = {
export const AppProvider: React.FC<AppProviderProps> = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialState);

return (
<AppContext.Provider value={{ state, dispatch }}>
{children}
</AppContext.Provider>
);
return <AppContext.Provider value={{ state, dispatch }}>{children}</AppContext.Provider>;
};

export const useAppContext = () => useContext(AppContext);
157 changes: 157 additions & 0 deletions hooks/contractHooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import { ZERO_ADDRESS } from '@citydao/parcel-contracts/dist/src/constants/accounts';
import { JsonRpcProvider, Provider } from '@ethersproject/providers';
import { Contract, ContractFactory, Signer } from 'ethers';
import { useEffect, useMemo, useState } from 'react';
import useWallet from './useWallet';

export interface ContractLoaderHook<C extends Contract, K extends KeyOfGetterFunction<C>> {
contract?: C;
values?: ContractValues<C, K>;
refetch: () => Promise<void>;
}

export type ContractValues<C extends Contract, K extends KeyOfGetterFunction<C>> = {
[P in K]: Awaited<ReturnType<C[K]>>;
};

export type KeyOfType<T, V> = keyof {
[P in keyof T as T[P] extends V ? P : never]: any;
};

export type KeyOfGetterFunction<T> = KeyOfType<T, () => any>;

export const useContractLoader = <
F extends ContractFactory,
C extends FactoryContract<F>,
K extends KeyOfGetterFunction<C>,
>(
factory: F,
address: string,
keys: K[] = [],
): ContractLoaderHook<C, K> => {
const { web3Provider } = useWallet();

const contract = useMemo(
() => attachContract<F, C>(factory, address, web3Provider) || undefined,
[factory, address, web3Provider],
);
const [values, setValues] = useState<ContractValues<C, K>>();

useEffect(() => {
// noinspection JSIgnoredPromiseFromCall
fetchValues();
}, [contract]);

const fetchValues = async () => {
if (!contract) {
setValues(undefined);
return;
}

const fetchedValues = await Promise.all(keys.map((key) => contract[key]()));

setValues(
fetchedValues.reduce((acc, fetchedValue, index) => {
acc[keys[index]] = fetchedValue;
return acc;
}, {} as ContractValues<C, K>),
);
};

return { contract, values, refetch: fetchValues };
};

export const useInterfaceLoader = <C extends Contract, K extends KeyOfGetterFunction<C>>(
factory: InterfaceFactoryConnector<C>,
address: string,
keys: K[] = [],
): ContractLoaderHook<C, K> => {
const { web3Provider } = useWallet();

const contract = useMemo(
() => attachInterface<C>(factory, address, web3Provider) || undefined,
[address, web3Provider],
);
const [values, setValues] = useState<ContractValues<C, K>>();

useEffect(() => {
// noinspection JSIgnoredPromiseFromCall
fetchValues();
}, [contract]);

const fetchValues = async () => {
if (!contract) {
setValues(undefined);
return;
}

const fetchedValues = await Promise.all(keys.map((key) => contract[key]()));

setValues(
fetchedValues.reduce((acc, fetchedValue, index) => {
acc[keys[index]] = fetchedValue;
return acc;
}, {} as ContractValues<C, K>),
);
};

return { contract, values, refetch: fetchValues };
};

export interface EthereumProviderHook {
provider: Provider | null;
signer: Signer | null;
}

export const useEthereumProvider = (): EthereumProviderHook => {
const { web3Provider } = useWallet();
const signer = web3Provider?.getSigner();

return { provider: web3Provider, signer: signer || null };
};

export type FactoryContract<F extends ContractFactory> = Contract & Awaited<ReturnType<F['deploy']>>;

export const attachContract = <F extends ContractFactory, C extends FactoryContract<F>>(
factory: F,
address: string,
provider: JsonRpcProvider | null,
): C | null => {
if (!address || address === ZERO_ADDRESS) {
return null;
}

const signer = provider?.getSigner();
if (signer) {
return factory.attach(address).connect(signer) as C;
}

if (provider) {
return factory.attach(address).connect(provider) as C;
}

return null;
};

export type InterfaceFactoryConnector<C extends Contract> = (address: string, provider: Signer | Provider) => C;

export const attachInterface = <C extends Contract>(
connect: InterfaceFactoryConnector<C>,
address: string,
provider: JsonRpcProvider | null,
): C | null => {
if (!address || address === ZERO_ADDRESS) {
return null;
}

const signer = provider?.getSigner();
if (signer) {
return connect(address, signer) as C;
}

if (provider) {
return connect(address, provider) as C;
}

return null;
};
Loading

1 comment on commit 783d77a

@vercel
Copy link

@vercel vercel bot commented on 783d77a May 14, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.