Skip to content

Commit

Permalink
Setup Solana Boilerplate
Browse files Browse the repository at this point in the history
  • Loading branch information
mschneider committed Apr 25, 2021
1 parent 17d5d98 commit 235874b
Show file tree
Hide file tree
Showing 28 changed files with 1,922 additions and 259 deletions.
4 changes: 4 additions & 0 deletions @types/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
declare module '*.svg' {
const content: any
export default content
}
62 changes: 62 additions & 0 deletions @types/types.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import {
AccountInfo,
Connection,
PublicKey,
Transaction,
} from '@solana/web3.js'
import Wallet from '@project-serum/sol-wallet-adapter'

export interface ConnectionContextValues {
endpoint: string
setEndpoint: (newEndpoint: string) => void
connection: Connection
sendConnection: Connection
availableEndpoints: EndpointInfo[]
setCustomEndpoints: (newCustomEndpoints: EndpointInfo[]) => void
}

export interface EndpointInfo {
name: string
url: string
websocket: string
}

export interface WalletContextValues {
wallet: Wallet
connected: boolean
providerUrl: string
setProviderUrl: (newProviderUrl: string) => void
providerName: string
}

export interface TokenAccount {
pubkey: PublicKey
account: AccountInfo<Buffer> | null
effectiveMint: PublicKey
}

/**
* {tokenMint: preferred token account's base58 encoded public key}
*/
export interface SelectedTokenAccounts {
[tokenMint: string]: string
}

// Token infos
export interface KnownToken {
tokenSymbol: string
tokenName: string
icon?: string
mintAddress: string
}

export interface WalletAdapter {
publicKey: PublicKey
autoApprove: boolean
connected: boolean
signTransaction: (transaction: Transaction) => Promise<Transaction>
signAllTransactions: (transaction: Transaction[]) => Promise<Transaction[]>
connect: () => any
disconnect: () => any
on(event: string, fn: () => void): this
}
45 changes: 45 additions & 0 deletions components/ConnectWalletButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import styled from '@emotion/styled'
import useWalletStore from '../stores/useWalletStore'
import { WALLET_PROVIDERS, DEFAULT_PROVIDER } from '../hooks/useWallet'
import useLocalStorageState from '../hooks/useLocalStorageState'
import WalletSelect from './WalletSelect'
import WalletIcon from './WalletIcon'

const StyledWalletTypeLabel = styled.div`
font-size: 0.6rem;
`

const ConnectWalletButton = () => {
const wallet = useWalletStore((s) => s.current)
const [savedProviderUrl] = useLocalStorageState(
'walletProvider',
DEFAULT_PROVIDER.url
)

return (
<div className="flex justify-between border border-th-primary rounded-md h-11 w-48">
<button
onClick={() => wallet.connect()}
disabled={!wallet}
className="text-th-primary hover:text-th-fgd-1 focus:outline-none disabled:text-th-fgd-4 disabled:cursor-wait"
>
<div className="flex flex-row items-center px-2 justify-center h-full rounded-l default-transition hover:bg-th-primary hover:text-th-fgd-1">
<WalletIcon className="w-5 h-5 mr-3 fill-current" />
<div>
<span className="whitespace-nowrap">Connect Wallet</span>
<StyledWalletTypeLabel className="font-normal text-th-fgd-1 text-left leading-3">
{WALLET_PROVIDERS.filter((p) => p.url === savedProviderUrl).map(
({ name }) => name
)}
</StyledWalletTypeLabel>
</div>
</div>
</button>
<div className="relative h-full">
<WalletSelect isPrimary />
</div>
</div>
)
}

export default ConnectWalletButton
113 changes: 113 additions & 0 deletions components/Notification.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { useEffect, useState } from 'react'
import {
CheckCircleIcon,
InformationCircleIcon,
XCircleIcon,
} from '@heroicons/react/outline'
import useNotificationStore from '../stores/useNotificationStore'

const NotificationList = () => {
const { notifications, set: setNotificationStore } = useNotificationStore(
(s) => s
)

useEffect(() => {
if (notifications.length > 0) {
const id = setInterval(() => {
setNotificationStore((state) => {
state.notifications = notifications.slice(1, notifications.length)
})
}, 5000)

return () => {
clearInterval(id)
}
}
}, [notifications, setNotificationStore])

const reversedNotifications = [...notifications].reverse()

return (
<div
className={`fixed inset-0 flex items-end px-4 py-6 pointer-events-none sm:p-6`}
>
<div className={`flex flex-col w-full`}>
{reversedNotifications.map((n, idx) => (
<Notification
key={`${n.message}${idx}`}
type={n.type}
message={n.message}
description={n.description}
txid={n.txid}
/>
))}
</div>
</div>
)
}

const Notification = ({ type, message, description, txid }) => {
const [showNotification, setShowNotification] = useState(true)

if (!showNotification) return null

return (
<div
className={`max-w-sm w-full bg-th-bkg-3 shadow-lg rounded-md mt-2 pointer-events-auto ring-1 ring-black ring-opacity-5 overflow-hidden`}
>
<div className={`p-4`}>
<div className={`flex items-center`}>
<div className={`flex-shrink-0`}>
{type === 'success' ? (
<CheckCircleIcon className={`text-th-green h-9 w-9 mr-1`} />
) : null}
{type === 'info' && (
<XCircleIcon className={`text-th-primary h-9 w-9 mr-1`} />
)}
{type === 'error' && (
<InformationCircleIcon className={`text-th-red h-9 w-9 mr-1`} />
)}
</div>
<div className={`ml-2 w-0 flex-1`}>
<div className={`text-lg text-th-fgd-1`}>{message}</div>
{description ? (
<p className={`mt-0.5 text-base text-th-fgd-2`}>{description}</p>
) : null}
{txid ? (
<a
href={'https://explorer.solana.com/tx/' + txid}
className="text-th-primary"
>
View transaction {txid.slice(0, 8)}...
{txid.slice(txid.length - 8)}
</a>
) : null}
</div>
<div className={`ml-4 flex-shrink-0 self-start flex`}>
<button
onClick={() => setShowNotification(false)}
className={`bg-th-bkg-3 rounded-md inline-flex text-fgd-3 hover:text-th-fgd-4 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-th-primary`}
>
<span className={`sr-only`}>Close</span>
<svg
className={`h-5 w-5`}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg>
</button>
</div>
</div>
</div>
</div>
)
}

export default NotificationList
25 changes: 25 additions & 0 deletions components/WalletIcon.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
const WalletIcon = ({ className }) => {
return (
<svg
className={className}
width="20"
height="17"
viewBox="0 0 20 17"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M14.625 8.24561C13.7276 8.24561 13 8.97325 13 9.87061C13 10.768 13.7276 11.4956 14.625 11.4956C15.5224 11.4956 16.25 10.768 16.25 9.87061C16.25 8.97325 15.5224 8.24561 14.625 8.24561ZM14 9.87061C14 9.52554 14.2799 9.24561 14.625 9.24561C14.9701 9.24561 15.25 9.52554 15.25 9.87061C15.25 10.2157 14.9701 10.4956 14.625 10.4956C14.2799 10.4956 14 10.2157 14 9.87061Z"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M3.25 0.25C1.59301 0.25 0.25 1.59301 0.25 3.25L0.250676 13.8083C0.250702 15.4652 1.59371 16.8083 3.25068 16.8083H17.2147C18.5735 16.8083 19.7507 15.755 19.7507 14.3708V5.37076C19.7507 4.51126 19.2968 3.77937 18.6275 3.34799C18.6257 2.86554 18.5949 2.24606 18.3863 1.7108C18.2324 1.31604 17.973 0.930835 17.5462 0.652726C17.1244 0.377893 16.6042 0.25 16 0.25H3.25ZM17.6434 4.51627C17.6217 4.50923 17.6004 4.50122 17.5796 4.4923C17.4681 4.45439 17.3457 4.43326 17.2147 4.43326H4.81318C4.39896 4.43326 4.06318 4.09747 4.06318 3.68326C4.06318 3.26904 4.39896 2.93326 4.81318 2.93326H17.1143C17.0993 2.67796 17.0651 2.45157 16.9887 2.2555C16.9238 2.08899 16.8395 1.98262 16.7273 1.90947C16.61 1.83305 16.3958 1.75 16 1.75H3.25C2.42146 1.75 1.75003 2.42141 1.75 3.24995L1.75068 13.8082C1.75068 14.6368 2.42212 15.3083 3.25068 15.3083H17.2147C17.8262 15.3083 18.2507 14.8477 18.2507 14.3708V5.37076C18.2507 5.01586 18.0156 4.67002 17.6434 4.51627Z"
/>
</svg>
)
}

export default WalletIcon
64 changes: 64 additions & 0 deletions components/WalletSelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { Menu } from '@headlessui/react'
import {
ChevronDownIcon,
ChevronUpIcon,
CheckCircleIcon,
} from '@heroicons/react/outline'

import useWalletStore from '../stores/useWalletStore'
import { WALLET_PROVIDERS, DEFAULT_PROVIDER } from '../hooks/useWallet'
import useLocalStorageState from '../hooks/useLocalStorageState'

export default function WalletSelect({ isPrimary = false }) {
const setWalletStore = useWalletStore((s) => s.set)
const [savedProviderUrl] = useLocalStorageState(
'walletProvider',
DEFAULT_PROVIDER.url
)

const handleSelectProvider = (url) => {
setWalletStore((state) => {
state.providerUrl = url
})
}

return (
<Menu>
{({ open }) => (
<>
<Menu.Button
className={`flex justify-center items-center h-full rounded-r rounded-l-none focus:outline-none text-th-primary hover:text-th-fgd-1 ${
isPrimary
? 'px-3 hover:bg-th-primary'
: 'px-2 hover:bg-th-bkg-3 border-l border-th-fgd-4'
} cursor-pointer`}
>
{open ? (
<ChevronUpIcon className="h-5 w-5" />
) : (
<ChevronDownIcon className="h-5 w-5" />
)}
</Menu.Button>
<Menu.Items className="z-20 p-1 absolute right-0 top-11 bg-th-bkg-1 divide-y divide-th-bkg-3 shadow-lg outline-none rounded-md w-48">
{WALLET_PROVIDERS.map(({ name, url, icon }) => (
<Menu.Item key={name}>
<button
className="flex flex-row items-center justify-between w-full p-2 hover:bg-th-bkg-2 hover:cursor-pointer font-normal focus:outline-none"
onClick={() => handleSelectProvider(url)}
>
<div className="flex">
<img src={icon} className="w-5 h-5 mr-2" />
{name}
</div>
{savedProviderUrl === url ? (
<CheckCircleIcon className="h-4 w-4 text-th-green" />
) : null}{' '}
</button>
</Menu.Item>
))}
</Menu.Items>
</>
)}
</Menu>
)
}
39 changes: 39 additions & 0 deletions hooks/useInterval.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { useState, useRef, useEffect } from 'react'

export function useEffectAfterTimeout(effect, timeout) {
useEffect(() => {
const handle = setTimeout(effect, timeout)
return () => clearTimeout(handle)
})
}

export function useListener(emitter, eventName) {
const [, forceUpdate] = useState(0)
useEffect(() => {
const listener = () => forceUpdate((i) => i + 1)
emitter.on(eventName, listener)
return () => emitter.removeListener(eventName, listener)
}, [emitter, eventName])
}

export default function useInterval(callback, delay) {
const savedCallback = useRef<() => void>()

// Remember the latest callback.
useEffect(() => {
savedCallback.current = callback
}, [callback])

// Set up the interval.
useEffect(() => {
function tick() {
savedCallback.current && savedCallback.current()
}
if (delay !== null) {
const id = setInterval(tick, delay)
return () => {
clearInterval(id)
}
}
}, [delay])
}
Loading

0 comments on commit 235874b

Please sign in to comment.