loading ui

This commit is contained in:
Matthias Kretschmann 2024-04-01 01:15:51 +01:00
parent e2c21cf497
commit 4850c19417
Signed by: m
GPG Key ID: 606EEEF3C479A91F
12 changed files with 166 additions and 51 deletions

View File

@ -1,6 +1,7 @@
import type { Metadata } from 'next' import type { Metadata } from 'next'
import { Hanken_Grotesk } from 'next/font/google' import { Hanken_Grotesk } from 'next/font/google'
import '@/styles/globals.css' import '@/styles/globals.css'
import '@/styles/loading-ui.css'
const hankenGrotesk = Hanken_Grotesk({ const hankenGrotesk = Hanken_Grotesk({
subsets: ['latin'], subsets: ['latin'],

View File

@ -6,34 +6,48 @@ import { usePrices } from '@/hooks'
import { Label } from '@/components/Label' import { Label } from '@/components/Label'
export function CalculationBase() { export function CalculationBase() {
const { prices, isValidating } = usePrices() const { prices, isValidating, isLoading } = usePrices()
const feedbackClasses = isLoading
? 'isLoading'
: isValidating
? 'isValidating'
: ''
return ( return (
<ul className={styles.calculationBase}> <ul className={styles.calculationBase}>
<li> <li>
<p>1 ASI</p> <p>1 ASI</p>
<p className={isValidating ? 'isValidating' : ''}>= ${prices.asi}</p> <p>
= <span className={feedbackClasses}>${prices.asi}</span>
</p>
</li> </li>
<li> <li>
<p> <p>
1 Fet = {ratioFetToAsi} ASI 1 Fet = {ratioFetToAsi} ASI
<Label>fixed</Label> <Label>fixed</Label>
</p> </p>
<p className={isValidating ? 'isValidating' : ''}>= ${prices.fet}</p> <p>
= <span className={feedbackClasses}>${prices.fet}</span>
</p>
</li> </li>
<li> <li>
<p> <p>
1 OCEAN = {ratioOceanToAsi} ASI 1 OCEAN = {ratioOceanToAsi} ASI
<Label>fixed</Label> <Label>fixed</Label>
</p> </p>
<p className={isValidating ? 'isValidating' : ''}>= ${prices.ocean}</p> <p>
= <span className={feedbackClasses}>${prices.ocean}</span>
</p>
</li> </li>
<li> <li>
<p> <p>
1 AGIX = {ratioAgixToAsi} ASI 1 AGIX = {ratioAgixToAsi} ASI
<Label>fixed</Label> <Label>fixed</Label>
</p> </p>
<p className={isValidating ? 'isValidating' : ''}>= ${prices.agix}</p> <p>
= <span className={feedbackClasses}>${prices.agix}</span>
</p>
</li> </li>
</ul> </ul>
) )

View File

@ -3,4 +3,5 @@
border: 1px solid rgba(var(--foreground-rgb), 0.15); border: 1px solid rgba(var(--foreground-rgb), 0.15);
border-radius: var(--border-radius); border-radius: var(--border-radius);
overflow: hidden; overflow: hidden;
margin: 0 0.25rem;
} }

View File

@ -3,6 +3,7 @@
width: 60px; width: 60px;
padding: 0 0.2rem; padding: 0 0.2rem;
text-align: center; text-align: center;
border-right: 1px solid rgba(var(--foreground-rgb), 0.15);
} }
.input:hover { .input:hover {

View File

@ -1,7 +1,26 @@
.select { .select {
display: inline-block; display: inline-block;
all: unset; all: unset;
padding: 0 0.75rem 0 0; padding: 0 0.5rem;
}
.select:hover:not(.select[disabled]) {
background-color: rgba(var(--background-rgb), 0.5);
}
.select:focus-within {
outline: none;
background-color: rgba(var(--background-rgb), 0.9);
}
@media (prefers-color-scheme: dark) {
.select:hover:not(.select[disabled]) {
background-color: rgba(var(--foreground-rgb), 0.1);
}
.select:focus-within {
background-color: rgba(var(--foreground-rgb), 0.2);
}
} }
.selectWrapper { .selectWrapper {
@ -10,7 +29,8 @@
.icon { .icon {
position: absolute; position: absolute;
right: 0.175rem; right: 0.1rem;
width: 1em;
height: 100%; height: 100%;
z-index: -1; z-index: -1;
} }

View File

@ -11,7 +11,7 @@
.resultLine { .resultLine {
display: inline-grid; display: inline-grid;
grid-template-columns: 24px 2fr 3fr; grid-template-columns: 24px 40% 40%;
gap: 0.5rem; gap: 0.5rem;
align-items: center; align-items: center;
width: 100%; width: 100%;

View File

@ -11,6 +11,7 @@ type Props = {
amountFiat: number amountFiat: number
amountOriginalFiat?: number amountOriginalFiat?: number
isValidating: boolean isValidating: boolean
isLoading: boolean
} }
export function Result({ export function Result({
@ -19,36 +20,48 @@ export function Result({
amountAsi, amountAsi,
amountFiat, amountFiat,
amountOriginalFiat, amountOriginalFiat,
isValidating isValidating,
isLoading
}: Props) { }: Props) {
const feedbackClasses = isLoading
? 'isLoading'
: isValidating
? 'isValidating'
: ''
return ( return (
<div className={styles.result}> <div className={styles.result}>
<div className={styles.resultLine}> <div className={styles.resultLine}>
<TokenLogo token={token} /> <TokenLogo token={token} />
<span className={isValidating ? 'isValidating' : ''}> <p>
<span className={feedbackClasses}>
{formatNumber(amount || 0, token?.symbol || '')} {formatNumber(amount || 0, token?.symbol || '')}
</span> </span>
</p>
{amountOriginalFiat ? ( {amountOriginalFiat ? (
<span className={styles.fiat}> <p>
<span className={`${styles.fiat} ${feedbackClasses}`}>
{formatNumber(amountOriginalFiat || 0, 'USD')} {formatNumber(amountOriginalFiat || 0, 'USD')}
</span> </span>
</p>
) : null} ) : null}
</div> </div>
<div className={styles.resultLine}> <div className={styles.resultLine}>
<ArrowRightIcon className={styles.iconArrow} /> <ArrowRightIcon className={styles.iconArrow} />
<strong
title={`${amountAsi}`} <p>
className={isValidating ? 'isValidating' : ''} <strong title={`${amountAsi}`} className={feedbackClasses}>
>
{formatNumber(amountAsi || 0, 'ASI')} {formatNumber(amountAsi || 0, 'ASI')}
</strong> </strong>
<strong </p>
className={`${styles.fiat} ${isValidating ? 'isValidating' : ''}`} <p>
> <strong className={`${styles.fiat} ${feedbackClasses}`}>
{formatNumber(amountFiat || 0, 'USD')} {formatNumber(amountFiat || 0, 'USD')}
</strong> </strong>
</p>
</div> </div>
</div> </div>
) )

View File

@ -10,7 +10,7 @@ import { FormAmount } from '@/components/FormAmount'
import { getTokenBySymbol } from '@/utils' import { getTokenBySymbol } from '@/utils'
export function Buy() { export function Buy() {
const { prices, isValidating } = usePrices() const { prices, isValidating, isLoading } = usePrices()
const [amount, setAmount] = useState(100) const [amount, setAmount] = useState(100)
const [debouncedAmount] = useDebounce(amount, 500) const [debouncedAmount] = useDebounce(amount, 500)
@ -33,6 +33,7 @@ export function Buy() {
: 0 : 0
} }
isValidating={isValidating} isValidating={isValidating}
isLoading={isLoading}
/> />
<Result <Result
token={getTokenBySymbol('AGIX')} token={getTokenBySymbol('AGIX')}
@ -46,6 +47,7 @@ export function Buy() {
: 0 : 0
} }
isValidating={isValidating} isValidating={isValidating}
isLoading={isLoading}
/> />
<Result <Result
token={getTokenBySymbol('FET')} token={getTokenBySymbol('FET')}
@ -59,6 +61,7 @@ export function Buy() {
: 0 : 0
} }
isValidating={isValidating} isValidating={isValidating}
isLoading={isLoading}
/> />
</div> </div>
) )

View File

@ -5,6 +5,10 @@ import { fetcher, getTokenAddressBySymbol, getTokenBySymbol } from '@/utils'
import useSWR from 'swr' import useSWR from 'swr'
import { TokenSymbol } from '@/types' import { TokenSymbol } from '@/types'
const options = {
keepPreviousData: true // so loading UI can kick in properly
}
export function SwapResults({ export function SwapResults({
tokenSymbol, tokenSymbol,
amount amount
@ -12,30 +16,49 @@ export function SwapResults({
tokenSymbol: TokenSymbol tokenSymbol: TokenSymbol
amount: number amount: number
}) { }) {
const { prices, isValidating: isValidatingPrices } = usePrices() const {
prices,
isValidating: isValidatingPrices,
isLoading: isLoadingPrices
} = usePrices()
// -> AGIX // -> AGIX
const { data: dataSwapToAgix, isValidating: isValidatingToAgix } = useSWR( const {
data: dataSwapToAgix,
isValidating: isValidatingToAgix,
isLoading: isLoadingToAgix
} = useSWR(
`/api/quote/?tokenIn=${getTokenAddressBySymbol( `/api/quote/?tokenIn=${getTokenAddressBySymbol(
tokenSymbol tokenSymbol
)}&tokenOut=${getTokenAddressBySymbol('AGIX')}&amountIn=${amount}`, )}&tokenOut=${getTokenAddressBySymbol('AGIX')}&amountIn=${amount}`,
fetcher fetcher,
options
) )
// -> FET // -> FET
const { data: dataSwapToFet, isValidating: isValidatingToFet } = useSWR( const {
data: dataSwapToFet,
isValidating: isValidatingToFet,
isLoading: isLoadingToFet
} = useSWR(
`/api/quote/?tokenIn=${getTokenAddressBySymbol( `/api/quote/?tokenIn=${getTokenAddressBySymbol(
tokenSymbol tokenSymbol
)}&tokenOut=${getTokenAddressBySymbol('FET')}&amountIn=${amount}`, )}&tokenOut=${getTokenAddressBySymbol('FET')}&amountIn=${amount}`,
fetcher fetcher,
options
) )
// -> OCEAN // -> OCEAN
const { data: dataSwapToOcean, isValidating: isValidatingToOcean } = useSWR( const {
data: dataSwapToOcean,
isValidating: isValidatingToOcean,
isLoading: isLoadingToOcean
} = useSWR(
`/api/quote/?tokenIn=${getTokenAddressBySymbol( `/api/quote/?tokenIn=${getTokenAddressBySymbol(
tokenSymbol tokenSymbol
)}&tokenOut=${getTokenAddressBySymbol('OCEAN')}&amountIn=${amount}`, )}&tokenOut=${getTokenAddressBySymbol('OCEAN')}&amountIn=${amount}`,
fetcher fetcher,
options
) )
return ( return (
@ -66,6 +89,7 @@ export function SwapResults({
: undefined : undefined
} }
isValidating={isValidatingToOcean || isValidatingPrices} isValidating={isValidatingToOcean || isValidatingPrices}
isLoading={isLoadingToOcean || isLoadingPrices}
/> />
<Result <Result
@ -90,6 +114,7 @@ export function SwapResults({
prices.agix prices.agix
} }
isValidating={isValidatingToAgix || isValidatingPrices} isValidating={isValidatingToAgix || isValidatingPrices}
isLoading={isLoadingToAgix || isLoadingPrices}
/> />
<Result <Result
@ -110,6 +135,7 @@ export function SwapResults({
prices.asi prices.asi
} }
isValidating={isValidatingToFet || isValidatingPrices} isValidating={isValidatingToFet || isValidatingPrices}
isLoading={isLoadingToFet || isLoadingPrices}
/> />
</> </>
) )

View File

@ -1,15 +0,0 @@
.isValidating {
animation: flicker 2s infinite ease-out;
}
@keyframes flicker {
0% {
opacity: 0.1;
}
50% {
opacity: 1;
}
100% {
opacity: 0.1;
}
}

View File

@ -40,5 +40,3 @@ ul {
color-scheme: dark; color-scheme: dark;
} }
} }
@import './_animations.css';

53
styles/loading-ui.css Normal file
View File

@ -0,0 +1,53 @@
.isLoading {
background-color: rgba(0, 0, 0, 0) !important;
background-image: linear-gradient(
-45deg,
rgba(var(--foreground-rgb), 0) 0,
rgba(var(--foreground-rgb), 0.1) 50%,
rgba(var(--foreground-rgb), 0) 100%
) !important;
background-size: 800% 800% !important;
color: rgba(0, 0, 0, 0) !important;
border-color: rgba(0, 0, 0, 0) !important;
border-radius: var(--border-radius) !important;
user-select: none;
cursor: wait;
animation: loading 3s infinite ease-out !important;
}
.isLoading * {
visibility: hidden !important;
}
.isLoading:empty::after,
.isLoading *:empty::after {
content: '\00a0';
}
@keyframes loading {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
.isValidating {
animation: flicker 2s infinite ease-out;
}
@keyframes flicker {
0% {
opacity: 0.1;
}
50% {
opacity: 1;
}
100% {
opacity: 0.1;
}
}