diff --git a/components/backdrop/index.tsx b/components/backdrop/index.tsx index b0c9890..63bfae8 100644 --- a/components/backdrop/index.tsx +++ b/components/backdrop/index.tsx @@ -1,6 +1,14 @@ +import classNames from 'classnames'; import { motion } from 'framer-motion'; -const LBBackdrop = ({ onClick }: ILBBackdrop) => ( - +const LBBackdrop = ({ onClick, variant = 'primary' }: ILBBackdrop) => ( + ); export default LBBackdrop; diff --git a/components/backdrop/types.ts b/components/backdrop/types.ts index 680e756..d7d24e0 100644 --- a/components/backdrop/types.ts +++ b/components/backdrop/types.ts @@ -1,3 +1,4 @@ interface ILBBackdrop { onClick: () => void; + variant?: 'primary' | 'secondary'; } diff --git a/store/token/actions.tsx b/store/token/actions.tsx index bb7d59d..cbfccf9 100644 --- a/store/token/actions.tsx +++ b/store/token/actions.tsx @@ -27,6 +27,8 @@ import { setOneHourAnalytics, setOneMonthAnalytics, setOneWeekAnalytics, + setSearchLoading, + setSearchedTokens, } from '.'; import { CallbackProps } from '..'; import api from './api'; @@ -393,6 +395,20 @@ const useTokenActions = () => { } }; + const getSearchedTokens = async (query: string, callback?: CallbackProps) => { + try { + dispatch(setSearchLoading(true)); + const { tokens } = await api.fetchTokens(query); + + dispatch(setSearchedTokens(tokens)); + return callback?.onSuccess?.(tokens); + } catch (error: any) { + callback?.onError?.(error); + } finally { + dispatch(setSearchLoading(false)); + } + }; + useEffect(() => { _submitData(); // eslint-disable-next-line react-hooks/exhaustive-deps @@ -452,6 +468,7 @@ const useTokenActions = () => { sellTokens, getCoinPrice, getAnalytics, + getSearchedTokens, }; }; diff --git a/store/token/index.ts b/store/token/index.ts index 7900582..5082dc5 100644 --- a/store/token/index.ts +++ b/store/token/index.ts @@ -20,6 +20,8 @@ export interface TokenState { oneDayAnalytics?: Analytics; oneWeekAnalytics?: Analytics; oneMonthAnalytics?: Analytics; + searchedTokens: Token[] | undefined; + searchLoading: boolean; } const initialState: TokenState = { @@ -39,6 +41,8 @@ const initialState: TokenState = { oneWeekAnalytics: undefined, oneMonthAnalytics: undefined, loadingAnalytics: true, + searchedTokens: undefined, + searchLoading: false, }; export const tokenReducer = createSlice({ @@ -172,6 +176,18 @@ export const tokenReducer = createSlice({ state.oneWeekAnalytics = undefined; state.oneMonthAnalytics = undefined; }, + + setSearchLoading: (state, action: PayloadAction) => { + state.searchLoading = action.payload; + }, + + setSearchedTokens: (state, action: PayloadAction) => { + if (action.payload) { + state.searchedTokens = [...action.payload]; + } else { + state.searchedTokens = undefined; + } + }, }, }); @@ -195,6 +211,8 @@ export const { setUserTokensLoading, setUserTokensMeta, resetAnalytics, + setSearchLoading, + setSearchedTokens, } = tokenReducer.actions; export default tokenReducer.reducer; diff --git a/utils/helpers.ts b/utils/helpers.ts index 7ad05f3..ac9d7d4 100644 --- a/utils/helpers.ts +++ b/utils/helpers.ts @@ -1,3 +1,5 @@ +import moment from 'moment'; + export const getTokenLink = (id: number, hash?: string): { title: string; url: string } => { if (!hash) return { title: '', url: '' }; @@ -154,3 +156,20 @@ export const appearAnimation = { animate: { opacity: 1 }, exit: { opacity: 0 }, }; + +export const formatDateDifference = (date: string) => { + const startDate = moment(date); + const endDate = moment(); + + const years = endDate.diff(startDate, 'years'); + startDate.add(years, 'years'); + + const months = endDate.diff(startDate, 'months'); + startDate.add(months, 'months'); + + const days = endDate.diff(startDate, 'days'); + + const formattedString = `${years ? years + 'y ' : ''}${months ? months + 'mo ' : ''}${days ? days + 'd' : ''}`.trim(); + + return formattedString || '0d'; +}; diff --git a/views/home/index.tsx b/views/home/index.tsx index ce552f0..5fad69f 100644 --- a/views/home/index.tsx +++ b/views/home/index.tsx @@ -1,10 +1,8 @@ 'use client'; import { useEffect, useState } from 'react'; -import { AnimatePresence, motion } from 'framer-motion'; -import { debounce } from 'lodash'; +import { AnimatePresence } from 'framer-motion'; import { LBBadge, LBContainer, LBError, LBModal, LBTable, LBTradeInterface } from '@/components'; -import { SearchAltIcon } from '@/public/icons'; import useSystemFunctions from '@/hooks/useSystemFunctions'; import useTokenActions from '@/store/token/actions'; import { Token } from '@/store/token/types'; @@ -14,12 +12,12 @@ import { setMeta as setTransactionsMeta, setTransactions } from '@/store/transac import { setMeta as setHoldersMeta, setHolders } from '@/store/holder'; import { formatAmount } from '@/utils/helpers'; import { resetCastAnalytics } from '@/store/casts'; +import SearchComponent from './search'; const HomeView = () => { const { navigate, tokenState, dispatch } = useSystemFunctions(); const { getTokens } = useTokenActions(); - const [searchTerm, setSearchTerm] = useState(''); const [activeToken, setActiveToken] = useState(); const [shouldFetchMore, setShouldFetchMore] = useState(false); const [showErrorState, setShowErrorState] = useState(false); @@ -40,17 +38,8 @@ const HomeView = () => { wallet: token.token_address, })); - const debouncedSetSearchTerm = debounce((term) => { - setSearchTerm(term); - }, 300); - const showShouldFetchMore = tokens && tokens.length > 0 ? false : shouldFetchMore || (loading && !tokens); - const handleSearchChange = (event: React.ChangeEvent) => { - const { value } = event.target; - debouncedSetSearchTerm(value); - }; - const cta = (id: string) => { const token = tokens?.find((token) => token.id === id); setActiveToken(token!); @@ -75,11 +64,6 @@ const HomeView = () => { getTokens('take=20', { onError: () => setShowErrorState(true) }); }; - useEffect(() => { - // getTokens(`take=50&search=${searchTerm}`); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [setSearchTerm]); - useEffect(() => { if (!shouldFetchMore) return; @@ -118,23 +102,7 @@ const HomeView = () => { Tokens launched will be updated here in realtime - - {Boolean(tableData?.length) && ( - - - - - )} - + {Boolean(tableData?.length) && } diff --git a/views/home/search.tsx b/views/home/search.tsx new file mode 100644 index 0000000..8d46706 --- /dev/null +++ b/views/home/search.tsx @@ -0,0 +1,230 @@ +import { useState, useEffect } from 'react'; +import { debounce } from 'lodash'; +import { AnimatePresence, motion } from 'framer-motion'; +import classNames from 'classnames'; +import Image from 'next/image'; +import Link from 'next/link'; + +import useTokenActions from '@/store/token/actions'; +import { LBBackdrop } from '@/components'; +import useSystemFunctions from '@/hooks/useSystemFunctions'; +import { SearchAltIcon } from '@/public/icons'; +import { Token } from '@/store/token/types'; +import { formatAmount, formatDateDifference, formatNumber } from '@/utils/helpers'; +import useTruncateText from '@/hooks/useTruncateText'; + +const DotPulse = () => { + const dots = Array(3).fill(0); + + return ( + + {dots.map((_, index) => ( + + ))} + + ); +}; + +const SkeletonPaper = () => { + return ( + + + {[...Array(2)].map((_, index) => ( + + ))} + + + + + + + {[...Array(4)].map((_, index) => ( + + ))} + + + + + + + + + ); +}; + +const Paper = ({ token }: { token: Token }) => { + const { truncate } = useTruncateText(undefined, 4, 3); + + const { token_logo_url, token_name, price, market_cap, created_at, token_address, exchange_address } = token; + const icons = [token_logo_url, '/icons/base-primary-mobile-icon.svg']; + const age = formatDateDifference(created_at); + + const info = [ + { title: 'Price', value: `$${formatAmount(price, 7)}` }, + { title: 'Market Cap', value: `$${formatNumber(market_cap || 0)}` }, + { title: 'Age', value: age }, + ]; + + const extra = [ + { + title: 'Pair', + value: truncate(exchange_address), + }, + { + title: 'Token', + value: truncate(token_address), + }, + ]; + + return ( + + + {icons.map((icon, index) => ( + + ))} + + + + + {token_name} / Base + + + + {info.map(({ title, value }, index) => ( + + {title}: + {value} + + ))} + + + + {extra.map(({ title, value }, index) => ( + + {title}: + {value} + + ))} + + + + ); +}; + +const SearchComponent = () => { + const { getSearchedTokens } = useTokenActions(); + const { tokenState } = useSystemFunctions(); + const [searchTerm, setSearchTerm] = useState(''); + const [isOpen, setIsOpen] = useState(false); + + const { searchLoading } = tokenState; + + const showDropdown = searchTerm.length > 1 && isOpen; + const showEmptyState = !searchLoading && !Boolean(tokenState.searchedTokens?.length); + const showResults = !searchLoading && Boolean(tokenState.searchedTokens?.length); + + const debouncedSetSearchTerm = debounce((term) => { + setSearchTerm(term); + }, 300); + + const handleSearchChange = (event: React.ChangeEvent) => { + const { value } = event.target; + debouncedSetSearchTerm(value); + }; + + useEffect(() => { + if (searchTerm && searchTerm.length > 1) { + getSearchedTokens(`search=${searchTerm}`); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [searchTerm]); + + useEffect(() => { + if (isOpen) { + document.body.classList.add('overflow-hidden'); + } else { + document.body.classList.remove('overflow-hidden'); + } + + return () => { + document.body.classList.remove('overflow-hidden'); + }; + }, [isOpen]); + + return ( + + + + + + + setIsOpen(true)} + /> + + + + {searchLoading && ( + + + + )} + + + + + + {showDropdown && ( + <> + + Results + + {searchLoading && [...Array(2)].map((_, index) => )} + + {showEmptyState && ( + + + No results found for "{searchTerm}" + + + )} + + {showResults && tokenState.searchedTokens?.map((token: Token) => )} + + + setIsOpen(false)} variant="secondary" /> + > + )} + + + ); +}; + +export default SearchComponent;
Tokens launched will be updated here in realtime
+ No results found for "{searchTerm}" +