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 ( ) 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; + } +}