Skip to content

Commit

Permalink
add modals
Browse files Browse the repository at this point in the history
  • Loading branch information
scoronelhamilton committed Oct 9, 2022
1 parent 58725ca commit bf8dbbf
Show file tree
Hide file tree
Showing 16 changed files with 894 additions and 40 deletions.
2 changes: 2 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@
"cSpell.words": [
"dethcrypto",
"esnext",
"headlessui",
"keccak",
"Numberish",
"rainbowkit",
"tailwindcss",
Expand Down
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,16 @@
"prepare": "eth-sdk -p ./src/eth-sdk"
},
"dependencies": {
"@headlessui/react": "^1.7.3",
"@headlessui/tailwindcss": "^0.1.1",
"@rainbow-me/rainbowkit": "^0.7.1",
"ethers": "^5.7.1",
"keccak256": "^1.0.6",
"next": "12.3.1",
"party-js": "^2.2.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-icons": "^4.4.0",
"wagmi": "^0.6.8"
},
"devDependencies": {
Expand Down
2 changes: 1 addition & 1 deletion src/components/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Navbar from './Navbar'

export default function Layout({ children }: { children: ReactElement }) {
return (
<div>
<div className="flex min-h-screen w-full flex-col">
<Navbar />
{children}
</div>
Expand Down
110 changes: 107 additions & 3 deletions src/components/bill/FundModal.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
import Button from 'components/ui/Button'
import Input from 'components/ui/Input'
import Modal from 'components/ui/Modal'
import useEthereum from 'hooks/useEthereum'
import party from 'party-js'
import type { DynamicSourceType } from 'party-js/lib/systems/sources'
import { useRef, useState } from 'react'
import { Bill } from 'types/types'
import { hash } from 'utils/hash'
import { useAccount, useBalance } from 'wagmi'

export default function FundModal({
bill,
Expand All @@ -9,9 +18,104 @@ export default function FundModal({
isOpen: boolean
setIsOpen: (isOpen: boolean) => void
}) {
return (
<div>
<h3>Fund your bill</h3>
const containerRef = useRef<HTMLDivElement>(null)
const { allyu, dai } = useEthereum()
const [code, setCode] = useState('')
const [error, setError] = useState('')
const [isLoading, setIsLoading] = useState(false)
const [isSuccess, setIsSuccess] = useState(false)
const { address } = useAccount()
const { data: balance } = useBalance({
addressOrName: address
})

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value
if (value.includes(' ')) return
setCode(value)
}

const handleSubmit = async () => {
if (!code) return
if (!address) {
setError('Wallet not connected')
return
}

if (balance?.value.lt(bill.value)) {
setError('Insufficient funds')
return
}

setIsLoading(true)
setError('')
try {
const codeHash = `0x${hash(code)}`

await dai.approve(address, bill.value)
await allyu.fund(bill.id, bill.value, codeHash)

setIsSuccess(true)
if (containerRef.current) {
party.confetti(containerRef.current as DynamicSourceType, {
count: 250,
spread: 20
})
}
} catch (e) {
console.error(e)
setError('Something went wrong')
setIsLoading(false)
}
}

const handleClose = () => {
setIsOpen(false)
setCode('')
setIsSuccess(false)
setIsLoading(false)
setError('')
}

const formComponent = (
<div className="grid gap-6">
<div className="grid gap-1">
<h3 className="font-secondary text-2xl">Fund your bill</h3>
<p className="text-sm">
Please enter a secret code. Once the transaction is completed, write it down on your paper
bill.
</p>
</div>
<Input
onChange={handleChange}
value={code}
className="w-full border-b-2 border-dotted border-black pb-1 text-center font-secondary text-6xl"
/>
<div>
<Button onClick={handleSubmit} className="w-full">
{isLoading ? 'Loading...' : 'Submit'}
</Button>
<p className="mt-3 text-center text-sm font-bold text-red">{error}</p>
</div>
</div>
)

const successComponent = (
<div className="grid gap-6">
<div className="grid gap-1">
<h3 className="font-secondary text-2xl">Your bill has been funded!</h3>
<p className="text-sm">
Please write down the secret code on your paper bill. After that, you can start using it
as a payment method.
</p>
</div>
<div className="text-center font-secondary text-6xl">{code}</div>
</div>
)

return (
<Modal isOpen={isOpen} onClose={handleClose}>
<div ref={containerRef}>{isSuccess ? successComponent : formComponent}</div>
</Modal>
)
}
124 changes: 121 additions & 3 deletions src/components/bill/RedeemModal.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
import Button from 'components/ui/Button'
import Input from 'components/ui/Input'
import Modal from 'components/ui/Modal'
import { ETHERSCAN_URL } from 'config'
import { ContractReceipt } from 'ethers'
import useEthereum from 'hooks/useEthereum'
import party from 'party-js'
import type { DynamicSourceType } from 'party-js/lib/systems/sources'
import { useRef, useState } from 'react'
import { Bill } from 'types/types'
import { useAccount } from 'wagmi'

export default function RedeemModal({
bill,
Expand All @@ -9,9 +19,117 @@ export default function RedeemModal({
isOpen: boolean
setIsOpen: (isOpen: boolean) => void
}) {
return (
<div>
<h3>Fund your bill</h3>
const containerRef = useRef<HTMLDivElement>(null)
const [publicCode, setPublicCode] = useState('')
const [privateCode, setPrivateCode] = useState('')
const [error, setError] = useState('')
const [isLoading, setIsLoading] = useState(false)
const [isSuccess, setIsSuccess] = useState(false)
const [transaction, setTransaction] = useState<ContractReceipt | undefined>()
const { address, isConnected } = useAccount()
const { allyu } = useEthereum()

const handlePublicCodeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setPublicCode(e.target.value)
}

const handlePrivateCodeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setPrivateCode(e.target.value)
}

const handleSubmit = async () => {
if (!publicCode || !privateCode) return
if (!isConnected || !address) {
setError('Wallet not connected')
return
}

setIsLoading(true)
setError('')
try {
const response = await allyu.redeem(bill.id, privateCode, publicCode, address)
const tx = await response.wait()

setTransaction(tx)
setIsSuccess(true)
if (containerRef.current) {
party.confetti(containerRef.current as DynamicSourceType, {
count: party.variation.range(100, 200)
})
}
} catch (err) {
console.error(err)
setError('Something went wrong')
setIsLoading(false)
}
}

const handleClose = () => {
setIsOpen(false)
setIsSuccess(false)
setPublicCode('')
setPrivateCode('')
setError('')
}

const successComponent = (
<div className="grid gap-6">
<div className="grid gap-1">
<h3 className="font-secondary text-2xl">Your bill has been redeemed!</h3>
<p className="text-sm">
The paper bill has been redeemed and the funds have been transferred to your wallet.
Please discard this bill as it no longer holds any value.
</p>
</div>
<a
target="_blank"
rel="noreferrer"
href={`${ETHERSCAN_URL}/tx/${transaction?.transactionHash}`}
>
<Button className="w-full">View transaction</Button>
</a>
</div>
)

const formComponent = (
<div className="grid gap-6">
<div className="grid gap-1">
<h3 className="font-secondary text-2xl">Redeem your bill</h3>
<p className="text-sm">
Please scratch the private code on your paper bill. Enter the private code along with the
public code written on the bill.
</p>
</div>
<div className="grid gap-4">
<div className="grid gap-2">
<Input
value={publicCode}
onChange={handlePublicCodeChange}
className="w-full border-b-2 border-dotted border-black pb-1 text-center font-secondary text-6xl"
/>
<p className="text-center font-bold">Public code</p>
</div>
<div className="grid gap-2">
<Input
value={privateCode}
onChange={handlePrivateCodeChange}
className="w-full border-b-2 border-dotted border-black pb-1 text-center font-secondary text-6xl"
/>
<p className="text-center font-bold">Private code</p>
</div>
</div>
<div>
<Button onClick={handleSubmit} className="w-full">
{isLoading ? 'Loading...' : 'Redeem'}
</Button>
<p className="mt-3 text-center text-sm font-bold text-red">{error}</p>
</div>
</div>
)

return (
<Modal isOpen={isOpen} onClose={handleClose}>
<div ref={containerRef}>{isSuccess ? successComponent : formComponent}</div>
</Modal>
)
}
1 change: 0 additions & 1 deletion src/components/request/Step4.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import bill from 'assets/bill.png'
import Button from 'components/ui/Button'
import { ETHERSCAN_URL } from 'config'
import Image from 'next/image'
import Link from 'next/link'
import { RequestProps } from 'pages/request'
import party from 'party-js'
import type { DynamicSourceType } from 'party-js/lib/systems/sources'
Expand Down
6 changes: 4 additions & 2 deletions src/components/ui/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,19 @@ import { ReactNode } from 'react'
export default function Button({
onClick,
disabled,
children
children,
className
}: {
onClick?: (e: React.MouseEvent<HTMLButtonElement> | undefined) => void
disabled?: boolean
className?: string
children: ReactNode
}) {
return (
<button
disabled={disabled}
onClick={onClick}
className="rounded-md bg-primary px-5 py-3 text-lg font-bold font-normal outline-none transition-colors duration-200 hover:bg-blue-50"
className={`rounded-md bg-primary px-5 py-3 font-secondary text-lg font-bold font-normal outline-none transition-colors duration-200 hover:bg-blue-50 ${className}`}
>
{children}
</button>
Expand Down
60 changes: 58 additions & 2 deletions src/components/ui/Modal.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,59 @@
export default function Modal() {
return <div></div>
import { Dialog, Transition } from '@headlessui/react'
import { RiCloseLine as IconClose } from 'react-icons/ri'

import { Fragment, ReactNode } from 'react'

interface ModalProps {
children: ReactNode
isOpen?: boolean
onClose?: () => void
className?: string
hideCloseButton?: boolean
}

export default function Modal(props: ModalProps): JSX.Element {
return (
<Transition show={props.isOpen} as={Fragment}>
<Dialog onClose={() => props?.onClose?.()}>
<Transition.Child
as={Fragment}
enter="ease-linear duration-150"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-linear duration-150"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 z-20 bg-white/25 backdrop-blur-sm" aria-hidden="true" />
</Transition.Child>
<Transition.Child
as={Fragment}
enter="ease-rk-easing duration-350"
enterFrom="opacity-0 translate-y-[20%]"
enterTo="opacity-100 translate-y-0"
leave="ease-rk-easing duration-350"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-[20%]"
>
<div className="fixed inset-x-0 top-0 z-30 h-screen overflow-auto">
<div className="flex min-h-screen items-end justify-center sm:items-center">
<Dialog.Panel
className={`relative w-full bg-white/90 sm:max-w-[768px] sm:max-w-[480px] ${props.className} rounded-md`}
>
{!props.hideCloseButton && (
<div
className="absolute top-0 right-0 flex h-12 w-14 cursor-pointer items-center justify-center justify-center p-2 transition-colors duration-200"
onClick={() => props?.onClose?.()}
>
<IconClose className="h-[18px] w-[18px]" />
</div>
)}
<div className="p-8">{props.children}</div>
</Dialog.Panel>
</div>
</div>
</Transition.Child>
</Dialog>
</Transition>
)
}
Loading

0 comments on commit bf8dbbf

Please sign in to comment.