diff --git a/app/layout.tsx b/app/layout.tsx
index b3fc6c7..4f526ac 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -1,6 +1,7 @@
import type { Metadata } from 'next'
import { Hanken_Grotesk } from 'next/font/google'
import '@/styles/globals.css'
+import '@/styles/loading-ui.css'
const hankenGrotesk = Hanken_Grotesk({
subsets: ['latin'],
diff --git a/components/CalculationBaseOutput/CalculationBase.tsx b/components/CalculationBaseOutput/CalculationBase.tsx
index 7187bcc..f0d4cc7 100644
--- a/components/CalculationBaseOutput/CalculationBase.tsx
+++ b/components/CalculationBaseOutput/CalculationBase.tsx
@@ -6,34 +6,48 @@ import { usePrices } from '@/hooks'
import { Label } from '@/components/Label'
export function CalculationBase() {
- const { prices, isValidating } = usePrices()
+ const { prices, isValidating, isLoading } = usePrices()
+
+ const feedbackClasses = isLoading
+ ? 'isLoading'
+ : isValidating
+ ? 'isValidating'
+ : ''
return (
-
1 ASI
- = ${prices.asi}
+
+ = ${prices.asi}
+
-
1 Fet = {ratioFetToAsi} ASI
- = ${prices.fet}
+
+ = ${prices.fet}
+
-
1 OCEAN = {ratioOceanToAsi} ASI
- = ${prices.ocean}
+
+ = ${prices.ocean}
+
-
1 AGIX = {ratioAgixToAsi} ASI
- = ${prices.agix}
+
+ = ${prices.agix}
+
)
diff --git a/components/FormAmount/FormAmount.module.css b/components/FormAmount/FormAmount.module.css
index f635f5c..ea5457d 100644
--- a/components/FormAmount/FormAmount.module.css
+++ b/components/FormAmount/FormAmount.module.css
@@ -3,4 +3,5 @@
border: 1px solid rgba(var(--foreground-rgb), 0.15);
border-radius: var(--border-radius);
overflow: hidden;
+ margin: 0 0.25rem;
}
diff --git a/components/FormAmount/Inputs/InputAmount.module.css b/components/FormAmount/Inputs/InputAmount.module.css
index e920d9c..5b68656 100644
--- a/components/FormAmount/Inputs/InputAmount.module.css
+++ b/components/FormAmount/Inputs/InputAmount.module.css
@@ -3,6 +3,7 @@
width: 60px;
padding: 0 0.2rem;
text-align: center;
+ border-right: 1px solid rgba(var(--foreground-rgb), 0.15);
}
.input:hover {
diff --git a/components/FormAmount/Inputs/InputToken.module.css b/components/FormAmount/Inputs/InputToken.module.css
index beee7f0..74cda6e 100644
--- a/components/FormAmount/Inputs/InputToken.module.css
+++ b/components/FormAmount/Inputs/InputToken.module.css
@@ -1,7 +1,26 @@
.select {
display: inline-block;
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 {
@@ -10,7 +29,8 @@
.icon {
position: absolute;
- right: 0.175rem;
+ right: 0.1rem;
+ width: 1em;
height: 100%;
z-index: -1;
}
diff --git a/components/ResultRow/ResultRow.module.css b/components/ResultRow/ResultRow.module.css
index 36d3450..5ef116b 100644
--- a/components/ResultRow/ResultRow.module.css
+++ b/components/ResultRow/ResultRow.module.css
@@ -11,7 +11,7 @@
.resultLine {
display: inline-grid;
- grid-template-columns: 24px 2fr 3fr;
+ grid-template-columns: 24px 40% 40%;
gap: 0.5rem;
align-items: center;
width: 100%;
diff --git a/components/ResultRow/ResultRow.tsx b/components/ResultRow/ResultRow.tsx
index c30b75d..11d5e79 100644
--- a/components/ResultRow/ResultRow.tsx
+++ b/components/ResultRow/ResultRow.tsx
@@ -11,6 +11,7 @@ type Props = {
amountFiat: number
amountOriginalFiat?: number
isValidating: boolean
+ isLoading: boolean
}
export function Result({
@@ -19,36 +20,48 @@ export function Result({
amountAsi,
amountFiat,
amountOriginalFiat,
- isValidating
+ isValidating,
+ isLoading
}: Props) {
+ const feedbackClasses = isLoading
+ ? 'isLoading'
+ : isValidating
+ ? 'isValidating'
+ : ''
+
return (
-
- {formatNumber(amount || 0, token?.symbol || '')}
-
+
+
+ {formatNumber(amount || 0, token?.symbol || '')}
+
+
{amountOriginalFiat ? (
-
- {formatNumber(amountOriginalFiat || 0, 'USD')}
-
+
+
+ {formatNumber(amountOriginalFiat || 0, 'USD')}
+
+
) : null}
+
-
- {formatNumber(amountAsi || 0, 'ASI')}
-
-
- {formatNumber(amountFiat || 0, 'USD')}
-
+
+
+
+ {formatNumber(amountAsi || 0, 'ASI')}
+
+
+
+
+ {formatNumber(amountFiat || 0, 'USD')}
+
+
)
diff --git a/components/Strategies/Buy.tsx b/components/Strategies/Buy.tsx
index 6af278f..50d63db 100644
--- a/components/Strategies/Buy.tsx
+++ b/components/Strategies/Buy.tsx
@@ -10,7 +10,7 @@ import { FormAmount } from '@/components/FormAmount'
import { getTokenBySymbol } from '@/utils'
export function Buy() {
- const { prices, isValidating } = usePrices()
+ const { prices, isValidating, isLoading } = usePrices()
const [amount, setAmount] = useState(100)
const [debouncedAmount] = useDebounce(amount, 500)
@@ -33,6 +33,7 @@ export function Buy() {
: 0
}
isValidating={isValidating}
+ isLoading={isLoading}
/>
)
diff --git a/components/Strategies/Swap/Results.tsx b/components/Strategies/Swap/Results.tsx
index 8988b1a..7c90ca4 100644
--- a/components/Strategies/Swap/Results.tsx
+++ b/components/Strategies/Swap/Results.tsx
@@ -5,6 +5,10 @@ import { fetcher, getTokenAddressBySymbol, getTokenBySymbol } from '@/utils'
import useSWR from 'swr'
import { TokenSymbol } from '@/types'
+const options = {
+ keepPreviousData: true // so loading UI can kick in properly
+}
+
export function SwapResults({
tokenSymbol,
amount
@@ -12,30 +16,49 @@ export function SwapResults({
tokenSymbol: TokenSymbol
amount: number
}) {
- const { prices, isValidating: isValidatingPrices } = usePrices()
+ const {
+ prices,
+ isValidating: isValidatingPrices,
+ isLoading: isLoadingPrices
+ } = usePrices()
// -> AGIX
- const { data: dataSwapToAgix, isValidating: isValidatingToAgix } = useSWR(
+ const {
+ data: dataSwapToAgix,
+ isValidating: isValidatingToAgix,
+ isLoading: isLoadingToAgix
+ } = useSWR(
`/api/quote/?tokenIn=${getTokenAddressBySymbol(
tokenSymbol
)}&tokenOut=${getTokenAddressBySymbol('AGIX')}&amountIn=${amount}`,
- fetcher
+ fetcher,
+ options
)
// -> FET
- const { data: dataSwapToFet, isValidating: isValidatingToFet } = useSWR(
+ const {
+ data: dataSwapToFet,
+ isValidating: isValidatingToFet,
+ isLoading: isLoadingToFet
+ } = useSWR(
`/api/quote/?tokenIn=${getTokenAddressBySymbol(
tokenSymbol
)}&tokenOut=${getTokenAddressBySymbol('FET')}&amountIn=${amount}`,
- fetcher
+ fetcher,
+ options
)
// -> OCEAN
- const { data: dataSwapToOcean, isValidating: isValidatingToOcean } = useSWR(
+ const {
+ data: dataSwapToOcean,
+ isValidating: isValidatingToOcean,
+ isLoading: isLoadingToOcean
+ } = useSWR(
`/api/quote/?tokenIn=${getTokenAddressBySymbol(
tokenSymbol
)}&tokenOut=${getTokenAddressBySymbol('OCEAN')}&amountIn=${amount}`,
- fetcher
+ fetcher,
+ options
)
return (
@@ -66,6 +89,7 @@ export function SwapResults({
: undefined
}
isValidating={isValidatingToOcean || isValidatingPrices}
+ isLoading={isLoadingToOcean || isLoadingPrices}
/>
>
)
diff --git a/styles/_animations.css b/styles/_animations.css
deleted file mode 100644
index a3ed8bd..0000000
--- a/styles/_animations.css
+++ /dev/null
@@ -1,15 +0,0 @@
-.isValidating {
- animation: flicker 2s infinite ease-out;
-}
-
-@keyframes flicker {
- 0% {
- opacity: 0.1;
- }
- 50% {
- opacity: 1;
- }
- 100% {
- opacity: 0.1;
- }
-}
diff --git a/styles/globals.css b/styles/globals.css
index 70975b5..e5dabc1 100644
--- a/styles/globals.css
+++ b/styles/globals.css
@@ -40,5 +40,3 @@ ul {
color-scheme: dark;
}
}
-
-@import './_animations.css';
diff --git a/styles/loading-ui.css b/styles/loading-ui.css
new file mode 100644
index 0000000..743bc17
--- /dev/null
+++ b/styles/loading-ui.css
@@ -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;
+ }
+}