├── src ├── components │ ├── Tables │ │ ├── TableReceive.css │ │ ├── TableAssets.css │ │ ├── TablePayments.css │ │ ├── TableRecentTransactions.css │ │ ├── TableReceive.jsx │ │ ├── TablePayments.jsx │ │ ├── TableFiatTransfers.jsx │ │ ├── TableYourAssets.jsx │ │ ├── TableRecentTransactions.jsx │ │ └── TableAssets.jsx │ ├── TableInputs │ │ ├── TableInputSell.css │ │ ├── TableInputAddCash.jsx │ │ ├── TableInputCashout.jsx │ │ ├── TableInputBuy.jsx │ │ ├── TableInputSell.jsx │ │ ├── TableInputConvert.jsx │ │ └── TableInputSend.jsx │ ├── Main │ │ ├── Main.css │ │ └── Main.jsx │ ├── Table │ │ ├── TableCellWatch.css │ │ ├── TableRowQR.css │ │ ├── TableRowSelectAsset.css │ │ ├── TableRowInputText.css │ │ ├── TableRowQR.jsx │ │ ├── Table.jsx │ │ ├── TableCellWatch.jsx │ │ ├── TableCellCoinName.css │ │ ├── TableCellCoinName.jsx │ │ ├── TableRowAssetAddress.jsx │ │ ├── TableRowInputText.jsx │ │ ├── TableRowSelectAsset.jsx │ │ └── Table.css │ ├── Tab │ │ ├── TabContent.css │ │ ├── TabFooter.css │ │ ├── TabContent.jsx │ │ ├── TabFooter.jsx │ │ ├── Tab.css │ │ └── Tab.jsx │ ├── Inputs │ │ ├── Input.css │ │ ├── Search.jsx │ │ ├── Input.jsx │ │ ├── InputAmountDynamicWidth.css │ │ ├── Search.css │ │ ├── InputAmountContainer.css │ │ ├── InputAmountContainer.jsx │ │ └── InputAmountDynamicWidth.jsx │ ├── Content │ │ ├── Content.jsx │ │ ├── ContentRight.jsx │ │ ├── ContentCenter.jsx │ │ ├── ContentRight.css │ │ ├── ContentCenter.css │ │ └── Content.css │ ├── Auth │ │ ├── AuthLayout.jsx │ │ ├── AuthError.css │ │ ├── AuthLayout.css │ │ ├── AuthError.jsx │ │ ├── SignInForm.css │ │ ├── SignUpForm.css │ │ ├── SignInForm.jsx │ │ └── SignUpForm.jsx │ ├── Section │ │ ├── SectionTitle.css │ │ ├── Section.css │ │ ├── Section.jsx │ │ └── SectionTitle.jsx │ ├── Star │ │ ├── Star.css │ │ └── Star.jsx │ ├── Dropdown │ │ ├── Dropdown.css │ │ └── Dropdown.jsx │ ├── Dashboard │ │ ├── Dashboard.css │ │ └── Dashboard.jsx │ ├── Tooltip │ │ ├── Tooltip.jsx │ │ └── Tooltip.css │ ├── Footer │ │ ├── Footer.css │ │ └── Footer.jsx │ ├── Sidebar │ │ ├── Sidebar.css │ │ ├── Sidebar.jsx │ │ ├── SidebarNavItem.css │ │ └── SidebarNavItem.jsx │ ├── Modals │ │ ├── ModalProfile.css │ │ ├── ModalPay.jsx │ │ ├── ModalTrade.jsx │ │ ├── ModalDeposit.jsx │ │ └── ModalProfile.jsx │ ├── Logo │ │ ├── Logo.jsx │ │ └── Logo.css │ ├── Modal │ │ ├── ModalClose.css │ │ ├── ModalClose.jsx │ │ ├── Modal.jsx │ │ └── Modal.css │ ├── TabContents │ │ ├── TabContentBuyEmpty.css │ │ ├── TabContentReceive.jsx │ │ ├── TabContentSelectAsset.css │ │ ├── TabContentBuyEmpty.jsx │ │ ├── TabContentCashout.jsx │ │ ├── TabContentAddCash.jsx │ │ ├── TabContentSend.jsx │ │ ├── TabContentSell.jsx │ │ ├── TabContentConvert.jsx │ │ ├── TabContentBuy.jsx │ │ └── TabContentSelectAsset.jsx │ ├── Header │ │ ├── Header.css │ │ └── Header.jsx │ ├── MenuMobile │ │ ├── MenuMobile.css │ │ └── MenuMobile.jsx │ ├── Tabs │ │ ├── TabPay.jsx │ │ ├── TabDeposit.jsx │ │ └── TabTrade.jsx │ ├── ModalsManager │ │ └── ModalsManager.jsx │ ├── Button │ │ ├── Button.jsx │ │ └── Button.css │ ├── Charts │ │ ├── ChartPortfolio.css │ │ └── ChartPortfolio.jsx │ ├── Chart │ │ └── LineChart.jsx │ ├── Text │ │ ├── Text.css │ │ └── Text.jsx │ ├── TransactionForm │ │ └── TransactionForm.jsx │ └── index.js ├── utilities │ ├── find-asset.js │ ├── calculate-coin-balance.js │ ├── transform-dates.js │ ├── transform-sparkline.js │ ├── create-coin.js │ ├── convert-to-currency.js │ ├── add-coin.js │ ├── calculate-coin-amount.js │ ├── add-transaction.js │ ├── calculate-total-balance.js │ ├── translate-selected-timeframe.js │ ├── calculate-allocation.js │ ├── update-coin-balance.js │ ├── format-market-cap.js │ ├── fetch-coin-price-history.js │ ├── create-coin-history-request-options.js │ ├── toggle-on-watchlist.js │ ├── create-total-balance-history.js │ ├── create-coin-histories.js │ ├── update-fiat-balance.js │ ├── adapt-fetched-coins.js │ ├── create-coin-balance-history.js │ ├── create-chart-times.js │ ├── validate-transaction.js │ ├── create-transaction.js │ └── update-coins.js ├── contexts │ ├── TransactionFormContext.jsx │ ├── UserContext.jsx │ ├── AssetsContext.jsx │ ├── TransactionsContext.jsx │ ├── ModalContext.jsx │ └── SelectAssetContext.jsx ├── pages │ ├── Trade.css │ ├── index.js │ ├── ProtectedPages.jsx │ ├── Deposit.jsx │ ├── Pay.jsx │ ├── SignIn.jsx │ ├── SignUp.jsx │ ├── Assets.jsx │ └── Trade.jsx ├── hooks │ ├── usePath.js │ ├── useMediaQuery.js │ ├── useGetFiat.js │ ├── useFilter.js │ ├── useGetTransactions.js │ ├── useCombineSearchFilter.js │ ├── useAssets.js │ ├── useModal.js │ ├── useSearch.js │ ├── useBalanceHistory.js │ ├── useTransactions.js │ ├── useGetCoins.js │ ├── useSelectAsset.js │ └── useAuth.js ├── index.js ├── index.css ├── firebase-config.js ├── constants │ └── chart-options.js └── App.js ├── public ├── favicon.ico ├── logo192.png ├── logo512.png ├── robots.txt ├── manifest.json ├── index.html └── 404.html ├── coinbase-clone-preview.jpg ├── prettier.config.js ├── firebase.json ├── .gitignore ├── package.json ├── Questions.md └── README.md /src/components/Tables/TableReceive.css: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maker0101/Coinbase_Clone/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maker0101/Coinbase_Clone/HEAD/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maker0101/Coinbase_Clone/HEAD/public/logo512.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/components/TableInputs/TableInputSell.css: -------------------------------------------------------------------------------- 1 | .TableInputSell { 2 | margin-bottom: 24px; 3 | } 4 | -------------------------------------------------------------------------------- /coinbase-clone-preview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maker0101/Coinbase_Clone/HEAD/coinbase-clone-preview.jpg -------------------------------------------------------------------------------- /src/components/Tables/TableAssets.css: -------------------------------------------------------------------------------- 1 | .tableAssets__priceChart { 2 | max-width: 60px; 3 | height: 100%; 4 | } 5 | -------------------------------------------------------------------------------- /src/utilities/find-asset.js: -------------------------------------------------------------------------------- 1 | export const findAsset = (id, assets) => 2 | assets.find((asset) => asset.id === id); 3 | -------------------------------------------------------------------------------- /src/components/Main/Main.css: -------------------------------------------------------------------------------- 1 | .Main { 2 | overflow-y: scroll; 3 | display: grid; 4 | grid-template-rows: auto 1fr; 5 | } 6 | -------------------------------------------------------------------------------- /src/components/Table/TableCellWatch.css: -------------------------------------------------------------------------------- 1 | .TableCellWatch { 2 | display: flex; 3 | justify-content: flex-end; 4 | align-items: center; 5 | } 6 | -------------------------------------------------------------------------------- /src/utilities/calculate-coin-balance.js: -------------------------------------------------------------------------------- 1 | export const calculateCoinBalance = (balanceCoin, priceEUR) => 2 | balanceCoin * Number(priceEUR) || 0; 3 | -------------------------------------------------------------------------------- /src/utilities/transform-dates.js: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs'; 2 | 3 | export const transactionTime = (timestamp) => 4 | dayjs(timestamp).format('MMM D, YYYY'); 5 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | tabWidth: 2, 3 | useTabs: false, 4 | singleQuote: true, 5 | jsxSingleQuote: true, 6 | bracketSameLine: true, 7 | }; 8 | -------------------------------------------------------------------------------- /src/components/Tab/TabContent.css: -------------------------------------------------------------------------------- 1 | .TabContent { 2 | width: 100%; 3 | height: 100%; 4 | display: flex; 5 | flex-direction: column; 6 | align-items: space-between; 7 | } 8 | -------------------------------------------------------------------------------- /src/contexts/TransactionFormContext.jsx: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | 3 | const TransactionFormContext = createContext(); 4 | 5 | export { TransactionFormContext }; 6 | -------------------------------------------------------------------------------- /src/components/Inputs/Input.css: -------------------------------------------------------------------------------- 1 | .input__field { 2 | border: none; 3 | height: 40px; 4 | font-size: 16px; 5 | } 6 | 7 | .input__field:focus { 8 | outline: none; 9 | } 10 | -------------------------------------------------------------------------------- /src/components/Main/Main.jsx: -------------------------------------------------------------------------------- 1 | import './Main.css'; 2 | 3 | const Main = ({ children }) => { 4 | return
{children}
; 5 | }; 6 | 7 | export default Main; 8 | -------------------------------------------------------------------------------- /src/utilities/transform-sparkline.js: -------------------------------------------------------------------------------- 1 | export const transformSparkline = (asset) => 2 | asset.sparkline.map((price, i) => ({ 3 | price: Number(price), 4 | time: String(i), 5 | })); 6 | -------------------------------------------------------------------------------- /src/components/Content/Content.jsx: -------------------------------------------------------------------------------- 1 | import './Content.css'; 2 | 3 | const Content = ({ children }) => { 4 | return
{children}
; 5 | }; 6 | 7 | export default Content; 8 | -------------------------------------------------------------------------------- /src/components/Tab/TabFooter.css: -------------------------------------------------------------------------------- 1 | .TabFooter { 2 | display: flex; 3 | justify-content: space-between; 4 | margin-top: 24px; 5 | } 6 | 7 | .tab-footer-margin-top-none { 8 | margin-top: 0; 9 | } 10 | -------------------------------------------------------------------------------- /src/components/Tab/TabContent.jsx: -------------------------------------------------------------------------------- 1 | import './TabContent.css'; 2 | 3 | const TabContent = ({ children }) => { 4 | return
{children}
; 5 | }; 6 | 7 | export default TabContent; 8 | -------------------------------------------------------------------------------- /src/utilities/create-coin.js: -------------------------------------------------------------------------------- 1 | export const createCoin = (coin, balance = 0, onWatchlist = false) => ({ 2 | id: coin?.id, 3 | symbol: coin?.symbol, 4 | balance_coin: balance, 5 | onWatchlist: onWatchlist, 6 | }); 7 | -------------------------------------------------------------------------------- /src/components/Content/ContentRight.jsx: -------------------------------------------------------------------------------- 1 | import './ContentRight.css'; 2 | 3 | const ContentRight = ({ children }) => { 4 | return
{children}
; 5 | }; 6 | 7 | export default ContentRight; 8 | -------------------------------------------------------------------------------- /src/components/Content/ContentCenter.jsx: -------------------------------------------------------------------------------- 1 | import './ContentCenter.css'; 2 | 3 | const ContentCenter = ({ children }) => { 4 | return
{children}
; 5 | }; 6 | 7 | export default ContentCenter; 8 | -------------------------------------------------------------------------------- /src/components/Table/TableRowQR.css: -------------------------------------------------------------------------------- 1 | .TableRowQR td:hover { 2 | background: none; 3 | } 4 | 5 | .tableRowQR__iconWrapper { 6 | display: flex; 7 | justify-content: center; 8 | font-size: 240px; 9 | margin: 16px 0; 10 | } 11 | -------------------------------------------------------------------------------- /src/utilities/convert-to-currency.js: -------------------------------------------------------------------------------- 1 | export const convertToCurrency = (number) => { 2 | const result = new Intl.NumberFormat('en-US', { 3 | style: 'currency', 4 | currency: 'EUR', 5 | }).format(number); 6 | return result; 7 | }; 8 | -------------------------------------------------------------------------------- /src/components/Content/ContentRight.css: -------------------------------------------------------------------------------- 1 | .ContentRight { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | } 6 | 7 | @media (max-width: 400px) { 8 | .ContentRight { 9 | align-items: flex-start; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/pages/Trade.css: -------------------------------------------------------------------------------- 1 | .trade__searchFilterRow { 2 | display: flex; 3 | justify-content: space-between; 4 | padding: 0 32px; 5 | border-bottom: 1px solid #d8d8d8; 6 | gap: 8px; 7 | } 8 | 9 | .trade__filters { 10 | display: flex; 11 | gap: 8px; 12 | } 13 | -------------------------------------------------------------------------------- /src/components/Auth/AuthLayout.jsx: -------------------------------------------------------------------------------- 1 | import './AuthLayout.css'; 2 | 3 | const AuthLayout = ({ children }) => { 4 | return ( 5 |
6 |
{children}
7 |
8 | ); 9 | }; 10 | 11 | export default AuthLayout; 12 | -------------------------------------------------------------------------------- /src/components/Section/SectionTitle.css: -------------------------------------------------------------------------------- 1 | .SectionTitle { 2 | padding: 16px 32px; 3 | border-bottom: 1px solid #d8d8d8; 4 | } 5 | 6 | .sectionTitle-noBorderBottom { 7 | border-bottom: none; 8 | } 9 | 10 | .sectionTitle__subtitle { 11 | margin-top: 8px; 12 | } 13 | -------------------------------------------------------------------------------- /src/components/Table/TableRowSelectAsset.css: -------------------------------------------------------------------------------- 1 | .tableRowSelectAsset__cellVerticalAligned { 2 | display: flex; 3 | align-items: center; 4 | } 5 | 6 | .tableRowSelectAsset__iconAsset { 7 | margin: 3px 16px 0 0; 8 | width: 24px; 9 | color: #2151f5; 10 | } 11 | -------------------------------------------------------------------------------- /src/components/Star/Star.css: -------------------------------------------------------------------------------- 1 | .Star { 2 | display: flex; 3 | align-items: center; 4 | margin-left: 10px; 5 | font-size: 25px; 6 | cursor: pointer; 7 | } 8 | 9 | .star__filled { 10 | color: #2151f5; 11 | } 12 | 13 | .star__empty { 14 | color: #d8d8d8; 15 | } 16 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "public": "build", 4 | "ignore": ["firebase.json", "**/.*", "**/node_modules/**"], 5 | "rewrites": [ 6 | { 7 | "source": "**", 8 | "destination": "/index.html" 9 | } 10 | ] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/utilities/add-coin.js: -------------------------------------------------------------------------------- 1 | import { doc, setDoc } from 'firebase/firestore'; 2 | 3 | export const addCoin = async (db, coin) => { 4 | try { 5 | await setDoc(doc(db, 'yourCoins', coin.id), { ...coin }); 6 | } catch (err) { 7 | console.error(err); 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /src/components/Dropdown/Dropdown.css: -------------------------------------------------------------------------------- 1 | .Dropdown { 2 | height: 48px; 3 | padding-left: 8px; 4 | color: #474747; 5 | border: 1px solid #d8d8d8; 6 | border-radius: 8px; 7 | font-weight: 700; 8 | cursor: pointer; 9 | } 10 | 11 | .Dropdown:focus { 12 | outline: none; 13 | } 14 | -------------------------------------------------------------------------------- /src/components/Dashboard/Dashboard.css: -------------------------------------------------------------------------------- 1 | .Dashboard { 2 | height: 100vh; 3 | width: 100vw; 4 | overflow: hidden; 5 | display: grid; 6 | grid-template-columns: auto 1fr; 7 | } 8 | 9 | @media (max-width: 800px) { 10 | .Dashboard { 11 | grid-template-columns: 1fr; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/utilities/calculate-coin-amount.js: -------------------------------------------------------------------------------- 1 | import { findAsset } from './find-asset'; 2 | 3 | export const calculateCoinAmount = (transactionAmount, id, assets) => { 4 | const coin = findAsset(id, assets); 5 | const coinAmount = transactionAmount / coin?.price_eur; 6 | return coinAmount; 7 | }; 8 | -------------------------------------------------------------------------------- /src/components/Auth/AuthError.css: -------------------------------------------------------------------------------- 1 | .AuthError { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | background-color: #cf202f; 6 | border-radius: 8px; 7 | padding: 8px; 8 | } 9 | 10 | .AuthError > svg { 11 | color: white; 12 | margin-right: 16px; 13 | } 14 | -------------------------------------------------------------------------------- /src/components/Section/Section.css: -------------------------------------------------------------------------------- 1 | .Section { 2 | width: 100%; 3 | min-width: 320px; 4 | height: fit-content; 5 | border: 1px solid #dedfd2; 6 | border-radius: 8px; 7 | background-color: white; 8 | margin-bottom: 24px; 9 | } 10 | 11 | .section-width-s { 12 | max-width: 650px; 13 | } 14 | -------------------------------------------------------------------------------- /src/components/Tooltip/Tooltip.jsx: -------------------------------------------------------------------------------- 1 | import './Tooltip.css'; 2 | 3 | /** 4 | * Requires parent element with class 'hasTooltip' in order to display 5 | */ 6 | const Tooltip = ({ children }) => { 7 | return
{children}
; 8 | }; 9 | 10 | export default Tooltip; 11 | -------------------------------------------------------------------------------- /src/components/Auth/AuthLayout.css: -------------------------------------------------------------------------------- 1 | .AuthLayout { 2 | background-color: #2151f5; 3 | height: 100vh; 4 | width: 100vw; 5 | display: flex; 6 | justify-content: center; 7 | align-items: center; 8 | text-align: center; 9 | } 10 | 11 | .AuthLayout > div > * { 12 | margin-bottom: 16px; 13 | } 14 | -------------------------------------------------------------------------------- /src/utilities/add-transaction.js: -------------------------------------------------------------------------------- 1 | import { addDoc, collection } from 'firebase/firestore'; 2 | 3 | export const addTransaction = async (db, transaction) => { 4 | try { 5 | await addDoc(collection(db, 'transactions'), transaction); 6 | } catch (err) { 7 | console.error(err); 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /src/utilities/calculate-total-balance.js: -------------------------------------------------------------------------------- 1 | export const calculateTotalBalance = (assets) => { 2 | if (assets.length === 0) return 0; 3 | const assetBalances = assets.map((asset) => asset.balance_eur); 4 | const totalBalance = assetBalances.reduce((prev, next) => prev + next); 5 | return totalBalance; 6 | }; 7 | -------------------------------------------------------------------------------- /src/components/Footer/Footer.css: -------------------------------------------------------------------------------- 1 | .Footer { 2 | display: flex; 3 | justify-content: space-around; 4 | position: fixed; 5 | bottom: 0; 6 | width: 100%; 7 | padding: 16px 0; 8 | background-color: white; 9 | border-top: 1px solid #dedfd2; 10 | } 11 | 12 | .Footer > button { 13 | margin: 0 8px; 14 | } 15 | -------------------------------------------------------------------------------- /src/components/Table/TableRowInputText.css: -------------------------------------------------------------------------------- 1 | .TableRowInputText:hover td { 2 | background-color: white; 3 | } 4 | 5 | .tableRowInputText__cellVerticalAligned { 6 | display: flex; 7 | align-items: center; 8 | } 9 | 10 | .tableRowInputText__icon { 11 | margin: 3px 16px 0 0; 12 | color: #5b616e; 13 | } 14 | -------------------------------------------------------------------------------- /src/components/Sidebar/Sidebar.css: -------------------------------------------------------------------------------- 1 | .Sidebar { 2 | display: grid; 3 | grid-template-rows: auto 1fr; 4 | width: 236px; 5 | border-right: 1px solid #dedfd2; 6 | } 7 | 8 | .sidebar__nav { 9 | margin: 16px 8px; 10 | } 11 | 12 | @media (max-width: 1300px) { 13 | .Sidebar { 14 | width: auto; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/hooks/usePath.js: -------------------------------------------------------------------------------- 1 | import { useLocation } from 'react-router-dom'; 2 | 3 | const usePath = () => { 4 | const location = useLocation(); 5 | const path = location.pathname; 6 | const page = path[1].toUpperCase() + path.substring(2); 7 | 8 | return { path, page }; 9 | }; 10 | 11 | export default usePath; 12 | -------------------------------------------------------------------------------- /src/components/Modals/ModalProfile.css: -------------------------------------------------------------------------------- 1 | .ModalProfile__content { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | width: 100%; 6 | margin: 24px; 7 | } 8 | 9 | .ModalProfile__content > h1 { 10 | margin-bottom: 32px; 11 | } 12 | 13 | .ModalProfile__content > p { 14 | margin: 8px 0 32px 0; 15 | } 16 | -------------------------------------------------------------------------------- /src/pages/index.js: -------------------------------------------------------------------------------- 1 | import SignIn from './SignIn'; 2 | import SignUp from './SignUp'; 3 | import Assets from './Assets'; 4 | import Trade from './Trade'; 5 | import Pay from './Pay'; 6 | import Deposit from './Deposit'; 7 | import ProtectedPages from './ProtectedPages'; 8 | 9 | export { SignIn, SignUp, Assets, Trade, Pay, Deposit, ProtectedPages }; 10 | -------------------------------------------------------------------------------- /src/utilities/translate-selected-timeframe.js: -------------------------------------------------------------------------------- 1 | export const translateSelectedTimeframe = (timeFrame) => { 2 | switch (timeFrame) { 3 | case '1D': 4 | return '24h'; 5 | case '1W': 6 | return '7d'; 7 | case '1M': 8 | return '30d'; 9 | case '1Y': 10 | return '1y'; 11 | default: 12 | break; 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /src/components/Content/ContentCenter.css: -------------------------------------------------------------------------------- 1 | .ContentCenter { 2 | margin: 0 24px 0 0; 3 | display: flex; 4 | flex-direction: column; 5 | align-items: center; 6 | } 7 | 8 | @media (max-width: 1150px) { 9 | .ContentCenter { 10 | margin: 0; 11 | } 12 | } 13 | 14 | @media (max-width: 400px) { 15 | .ContentCenter { 16 | align-items: flex-start; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/components/Logo/Logo.jsx: -------------------------------------------------------------------------------- 1 | import './Logo.css'; 2 | 3 | import useMediaQuery from '../../hooks/useMediaQuery'; 4 | 5 | const Logo = () => { 6 | const isWidthMax1300 = useMediaQuery('(max-width: 1300px)'); 7 | const logoText = isWidthMax1300 ? 'C' : 'coinbase'; 8 | 9 | return
{logoText}
; 10 | }; 11 | 12 | export default Logo; 13 | -------------------------------------------------------------------------------- /src/components/Modal/ModalClose.css: -------------------------------------------------------------------------------- 1 | .ModalClose { 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | position: fixed; 6 | bottom: 24px; 7 | left: 24px; 8 | background-color: #eef0f3; 9 | border-radius: 50%; 10 | z-index: inherit; 11 | padding: 16px; 12 | cursor: pointer; 13 | } 14 | 15 | .ModalClose:hover { 16 | transform: scale(1.1); 17 | } 18 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import './index.css'; 2 | 3 | import App from './App'; 4 | import { BrowserRouter } from 'react-router-dom'; 5 | import React from 'react'; 6 | import ReactDOM from 'react-dom'; 7 | 8 | ReactDOM.render( 9 | 10 | 11 | 12 | 13 | , 14 | document.getElementById('root') 15 | ); 16 | -------------------------------------------------------------------------------- /src/utilities/calculate-allocation.js: -------------------------------------------------------------------------------- 1 | import { calculateTotalBalance } from './calculate-total-balance'; 2 | 3 | export const calculateAllocation = (allCoins, asset) => { 4 | const total = calculateTotalBalance(allCoins); 5 | const allocationDecimal = asset.balance_eur / total; 6 | const allocationPercent = Math.round(allocationDecimal * 10000) / 100; 7 | return allocationPercent; 8 | }; 9 | -------------------------------------------------------------------------------- /src/components/Section/Section.jsx: -------------------------------------------------------------------------------- 1 | import './Section.css'; 2 | 3 | import classNames from 'classnames'; 4 | 5 | const Section = ({ children, width }) => { 6 | const sectionClasses = classNames({ 7 | Section: true, 8 | 'section-width-s': width === 's', 9 | }); 10 | 11 | return
{children}
; 12 | }; 13 | 14 | export default Section; 15 | -------------------------------------------------------------------------------- /src/components/Content/Content.css: -------------------------------------------------------------------------------- 1 | .Content { 2 | display: grid; 3 | grid-template-columns: 67% 33%; 4 | overflow-y: scroll; 5 | padding: 24px 24px; 6 | background-color: #f9f9fd; 7 | } 8 | 9 | @media (max-width: 1150px) { 10 | .Content { 11 | grid-template-columns: 100%; 12 | } 13 | } 14 | 15 | @media (max-width: 800px) { 16 | .Content { 17 | padding-bottom: 64px; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/utilities/update-coin-balance.js: -------------------------------------------------------------------------------- 1 | import { doc, updateDoc } from 'firebase/firestore'; 2 | 3 | export const updateCoinBalance = async (db, coinId, newCoinBalance) => { 4 | try { 5 | const coinDoc = doc(db, 'yourCoins', coinId); 6 | 7 | await updateDoc(coinDoc, { 8 | balance_coin: newCoinBalance, 9 | }); 10 | } catch (error) { 11 | console.error(error); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /src/components/Modals/ModalPay.jsx: -------------------------------------------------------------------------------- 1 | import { Modal, ModalClose, TabPay } from '..'; 2 | 3 | import useMediaQuery from '../../hooks/useMediaQuery'; 4 | 5 | const ModalPay = () => { 6 | const isWidthMax800 = useMediaQuery('(max-width: 800px)'); 7 | 8 | return ( 9 | 10 | 11 | {isWidthMax800 && } 12 | 13 | ); 14 | }; 15 | 16 | export default ModalPay; 17 | -------------------------------------------------------------------------------- /src/components/Table/TableRowQR.jsx: -------------------------------------------------------------------------------- 1 | import './TableRowQR.css'; 2 | 3 | import { MdQrCode2 } from 'react-icons/md'; 4 | 5 | const TableRowQR = () => { 6 | return ( 7 | 8 | 9 |
10 | 11 |
12 | 13 | 14 | ); 15 | }; 16 | 17 | export default TableRowQR; 18 | -------------------------------------------------------------------------------- /src/components/Logo/Logo.css: -------------------------------------------------------------------------------- 1 | .Logo { 2 | display: flex; 3 | align-items: center; 4 | height: 35px; 5 | padding: 16px 0; 6 | margin: 0 auto 0 16px; 7 | color: #2151f5; 8 | font-weight: 700; 9 | font-size: 24px; 10 | cursor: pointer; 11 | } 12 | 13 | @media (max-width: 1300px) { 14 | .Logo { 15 | font-weight: 900; 16 | font-size: 40px; 17 | margin: 0; 18 | justify-content: center; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/components/Modals/ModalTrade.jsx: -------------------------------------------------------------------------------- 1 | import { Modal, ModalClose, TabTrade } from '..'; 2 | 3 | import useMediaQuery from '../../hooks/useMediaQuery'; 4 | 5 | const ModalTrade = () => { 6 | const isWidthMax800 = useMediaQuery('(max-width: 800px)'); 7 | 8 | return ( 9 | 10 | 11 | {isWidthMax800 && } 12 | 13 | ); 14 | }; 15 | 16 | export default ModalTrade; 17 | -------------------------------------------------------------------------------- /src/pages/ProtectedPages.jsx: -------------------------------------------------------------------------------- 1 | import { Navigate, Outlet } from 'react-router-dom'; 2 | 3 | import { UserContext } from '../contexts/UserContext'; 4 | import { useContext } from 'react'; 5 | 6 | const ProtectedPages = () => { 7 | const { user } = useContext(UserContext); 8 | const isAuth = !!user?.accessToken; 9 | 10 | return isAuth ? : ; 11 | }; 12 | 13 | export default ProtectedPages; 14 | -------------------------------------------------------------------------------- /src/components/TabContents/TabContentBuyEmpty.css: -------------------------------------------------------------------------------- 1 | .tabContentBuyEmpty { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | text-align: center; 6 | } 7 | 8 | .tabContentBuyEmpty__icon { 9 | color: #f3b74f; 10 | font-size: 90px; 11 | margin-top: 32px; 12 | } 13 | 14 | .tabContentBuyEmpty__heading { 15 | margin: 16px 0; 16 | } 17 | 18 | .tabContentBuyEmpty__content { 19 | margin-bottom: 48px; 20 | } 21 | -------------------------------------------------------------------------------- /src/components/Modals/ModalDeposit.jsx: -------------------------------------------------------------------------------- 1 | import { Modal, ModalClose, TabDeposit } from '..'; 2 | 3 | import useMediaQuery from '../../hooks/useMediaQuery'; 4 | 5 | const ModalDeposit = () => { 6 | const isWidthMax800 = useMediaQuery('(max-width: 800px)'); 7 | 8 | return ( 9 | 10 | 11 | {isWidthMax800 && } 12 | 13 | ); 14 | }; 15 | 16 | export default ModalDeposit; 17 | -------------------------------------------------------------------------------- /src/utilities/format-market-cap.js: -------------------------------------------------------------------------------- 1 | import { convertToCurrency } from './convert-to-currency'; 2 | 3 | export const formatMarketCap = (number) => { 4 | if (number >= 100000000) return `${convertToCurrency(number / 1000000000)}B`; 5 | if (number >= 100000) return `${convertToCurrency(number / 1000000)}M`; 6 | if (number >= 1000) return `${convertToCurrency(number / 1000)}K`; 7 | if (number < 1000) return `${convertToCurrency(number)}`; 8 | }; 9 | -------------------------------------------------------------------------------- /src/components/Dropdown/Dropdown.jsx: -------------------------------------------------------------------------------- 1 | import './Dropdown.css'; 2 | 3 | const Dropdown = ({ name, options, value, onChange }) => { 4 | return ( 5 | 12 | ); 13 | }; 14 | 15 | export default Dropdown; 16 | -------------------------------------------------------------------------------- /src/components/Auth/AuthError.jsx: -------------------------------------------------------------------------------- 1 | import './AuthError.css'; 2 | 3 | import { RiErrorWarningFill } from 'react-icons/ri'; 4 | import { Text } from '../index'; 5 | import useAuth from '../../hooks/useAuth'; 6 | 7 | const AuthError = () => { 8 | const { authError } = useAuth(); 9 | 10 | return ( 11 |
12 | 13 | {authError} 14 |
15 | ); 16 | }; 17 | 18 | export default AuthError; 19 | -------------------------------------------------------------------------------- /src/components/Modal/ModalClose.jsx: -------------------------------------------------------------------------------- 1 | import './ModalClose.css'; 2 | 3 | import { FaArrowLeft } from 'react-icons/fa'; 4 | import { ModalContext } from '../../contexts/ModalContext'; 5 | import { useContext } from 'react'; 6 | 7 | const ModalClose = () => { 8 | const { handleClose } = useContext(ModalContext); 9 | 10 | return ( 11 |
12 | 13 |
14 | ); 15 | }; 16 | 17 | export default ModalClose; 18 | -------------------------------------------------------------------------------- /src/components/Tooltip/Tooltip.css: -------------------------------------------------------------------------------- 1 | .hasTooltip .Tooltip { 2 | visibility: hidden; 3 | background-color: black; 4 | color: #fff; 5 | text-align: center; 6 | padding: 8px 16px; 7 | border-radius: 6px; 8 | 9 | /* Position the tooltip text - see examples below! */ 10 | position: absolute; 11 | z-index: 1; 12 | margin: 10px 0 0 50px; 13 | } 14 | 15 | /* Show the tooltip text when you mouse over the tooltip container */ 16 | .hasTooltip:hover .Tooltip { 17 | visibility: visible; 18 | } 19 | -------------------------------------------------------------------------------- /src/contexts/UserContext.jsx: -------------------------------------------------------------------------------- 1 | import { useState, createContext } from 'react'; 2 | 3 | const UserContext = createContext(); 4 | 5 | const UserProvider = ({ children }) => { 6 | const [user, setUser] = useState(null); 7 | const [authError, setAuthError] = useState(''); 8 | 9 | return ( 10 | 11 | {children} 12 | 13 | ); 14 | }; 15 | 16 | export { UserProvider, UserContext }; 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # hosting 15 | .firebase/ 16 | .firebaserc/ 17 | .firebaserc 18 | 19 | # ci 20 | .github/ 21 | 22 | 23 | # misc 24 | .DS_Store 25 | .env 26 | .env.local 27 | .env.development.local 28 | .env.test.local 29 | .env.production.local 30 | 31 | npm-debug.log* 32 | yarn-debug.log* 33 | yarn-error.log* 34 | -------------------------------------------------------------------------------- /src/contexts/AssetsContext.jsx: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | import useAssets from '../hooks/useAssets'; 3 | 4 | const AssetsContext = createContext(); 5 | 6 | const AssetsProvider = ({ children }) => { 7 | const { allCoins, yourCoins, coinsOnWatchlist, allFiat } = useAssets(); 8 | 9 | return ( 10 | 12 | {children} 13 | 14 | ); 15 | }; 16 | 17 | export { AssetsContext, AssetsProvider }; 18 | -------------------------------------------------------------------------------- /src/components/Table/Table.jsx: -------------------------------------------------------------------------------- 1 | import './Table.css'; 2 | 3 | import classNames from 'classnames'; 4 | 5 | const Table = ({ 6 | children, 7 | isInputTable = false, 8 | hasBorderBottom = true, 9 | hasSmallPadding = false, 10 | }) => { 11 | const tableClasses = classNames({ 12 | 'table-is-input-table': isInputTable, 13 | 'table-has-border-bottom': hasBorderBottom, 14 | 'table-has-small-padding': hasSmallPadding, 15 | }); 16 | 17 | return {children}
; 18 | }; 19 | 20 | export default Table; 21 | -------------------------------------------------------------------------------- /src/components/Table/TableCellWatch.jsx: -------------------------------------------------------------------------------- 1 | import './TableCellWatch.css'; 2 | 3 | import { Button, Star } from '..'; 4 | 5 | import { ModalContext } from '../../contexts/ModalContext'; 6 | import { useContext } from 'react'; 7 | 8 | const TableCellWatch = ({ coin }) => { 9 | const { handleOpen } = useContext(ModalContext); 10 | 11 | return ( 12 |
13 | 14 | 15 |
16 | ); 17 | }; 18 | 19 | export default TableCellWatch; 20 | -------------------------------------------------------------------------------- /src/components/Tables/TablePayments.css: -------------------------------------------------------------------------------- 1 | .tablePayments__cell { 2 | display: grid; 3 | grid-template-columns: fit-content(30px) 1fr; 4 | grid-template-rows: 1fr 1fr; 5 | gap: 0px 12px; 6 | grid-template-areas: 7 | 'icon name' 8 | 'icon symbol'; 9 | height: 100%; 10 | align-items: center; 11 | } 12 | 13 | .tablePayments__icon { 14 | display: flex; 15 | align-items: center; 16 | width: 24px; 17 | grid-area: icon; 18 | } 19 | 20 | .tablePayments__head { 21 | grid-area: name; 22 | } 23 | .tablePayments__body { 24 | grid-area: symbol; 25 | } 26 | -------------------------------------------------------------------------------- /src/components/Tab/TabFooter.jsx: -------------------------------------------------------------------------------- 1 | import './TabFooter.css'; 2 | 3 | import { Text } from '..'; 4 | import classNames from 'classnames'; 5 | 6 | const TabFooter = ({ textLeft, textRight, marginTopNone = false }) => { 7 | const tabFooterClasses = classNames({ 8 | TabFooter: true, 9 | 'tab-footer-margin-top-none': marginTopNone, 10 | }); 11 | 12 | return ( 13 |
14 | {textLeft} 15 | {textRight} 16 |
17 | ); 18 | }; 19 | 20 | export default TabFooter; 21 | -------------------------------------------------------------------------------- /src/components/Inputs/Search.jsx: -------------------------------------------------------------------------------- 1 | import './Search.css'; 2 | 3 | import { IoSearchOutline } from 'react-icons/io5'; 4 | 5 | const Search = ({ searchInput, handleSearch, allItems }) => { 6 | return ( 7 |
8 | handleSearch(e, allItems)} 14 | /> 15 | 16 |
17 | ); 18 | }; 19 | 20 | export default Search; 21 | -------------------------------------------------------------------------------- /src/components/Table/TableCellCoinName.css: -------------------------------------------------------------------------------- 1 | .TableCellCoinName { 2 | display: grid; 3 | grid-template-columns: fit-content(30px) 1fr; 4 | grid-template-rows: 1fr 1fr; 5 | gap: 0px 12px; 6 | grid-template-areas: 7 | 'icon name' 8 | 'icon symbol'; 9 | } 10 | 11 | .tableCellCoinName__icon { 12 | display: flex; 13 | align-items: center; 14 | width: 24px; 15 | height: 24px; 16 | margin: auto 0; 17 | grid-area: icon; 18 | } 19 | 20 | .tableCellCoinName__name { 21 | grid-area: name; 22 | } 23 | .tableCellCoinName__symbol { 24 | grid-area: symbol; 25 | } 26 | -------------------------------------------------------------------------------- /src/components/Header/Header.css: -------------------------------------------------------------------------------- 1 | .Header { 2 | display: flex; 3 | align-items: center; 4 | justify-content: space-between; 5 | height: 35px; 6 | padding: 16px 24px; 7 | border-bottom: 1px solid #dedfd2; 8 | } 9 | 10 | .header__right { 11 | display: flex; 12 | align-items: center; 13 | gap: 8px; 14 | } 15 | 16 | .header__verticalLine { 17 | border-left: 1px solid #dedfd2; 18 | width: 1px; 19 | height: 35px; 20 | margin: 0 8px; 21 | } 22 | 23 | .header__avatar { 24 | cursor: pointer; 25 | } 26 | 27 | .header__menu { 28 | font-size: 24px; 29 | cursor: pointer; 30 | } 31 | -------------------------------------------------------------------------------- /src/components/Star/Star.jsx: -------------------------------------------------------------------------------- 1 | import './Star.css'; 2 | 3 | import { HiOutlineStar, HiStar } from 'react-icons/hi'; 4 | 5 | import { db } from '../../firebase-config'; 6 | import { toggleOnWatchlist } from '../../utilities/toggle-on-watchlist'; 7 | 8 | const Star = ({ coin }) => { 9 | return ( 10 |
toggleOnWatchlist(db, coin)}> 11 | {coin?.onWatchlist ? ( 12 | 13 | ) : ( 14 | 15 | )} 16 |
17 | ); 18 | }; 19 | 20 | export default Star; 21 | -------------------------------------------------------------------------------- /src/components/Tables/TableRecentTransactions.css: -------------------------------------------------------------------------------- 1 | .tableRecentTransactions__cell { 2 | display: grid; 3 | grid-template-columns: fit-content(30px) 1fr; 4 | grid-template-rows: 1fr 1fr; 5 | gap: 0px 12px; 6 | grid-template-areas: 7 | 'icon name' 8 | 'icon symbol'; 9 | height: 100%; 10 | align-items: center; 11 | } 12 | 13 | .tableRecentTransactions__icon { 14 | display: flex; 15 | align-items: center; 16 | width: 24px; 17 | grid-area: icon; 18 | } 19 | 20 | .tableRecentTransactions__head { 21 | grid-area: name; 22 | } 23 | .tableRecentTransactions__body { 24 | grid-area: symbol; 25 | } 26 | -------------------------------------------------------------------------------- /src/components/MenuMobile/MenuMobile.css: -------------------------------------------------------------------------------- 1 | .MenuMobile { 2 | height: 100vh; 3 | width: 100vw; 4 | position: absolute; 5 | top: 0; 6 | left: 0; 7 | background-color: white; 8 | z-index: 11; 9 | } 10 | 11 | .menuMobile__navItems { 12 | height: 95%; 13 | display: flex; 14 | flex-direction: column; 15 | margin: 24px 16px; 16 | justify-content: space-between; 17 | } 18 | 19 | .menuMobile__close { 20 | display: flex; 21 | justify-content: flex-end; 22 | align-items: center; 23 | font-size: 28px; 24 | margin-bottom: 24px; 25 | } 26 | 27 | .menuMobile__close > svg { 28 | cursor: pointer; 29 | } 30 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | /* Global styles */ 2 | body { 3 | margin: 0; 4 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 5 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 6 | sans-serif; 7 | -webkit-font-smoothing: antialiased; 8 | -moz-osx-font-smoothing: grayscale; 9 | font-size: 16px; 10 | overscroll-behavior: none; 11 | } 12 | 13 | code { 14 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 15 | monospace; 16 | } 17 | 18 | a, 19 | a:hover, 20 | a:focus, 21 | a:active { 22 | text-decoration: none; 23 | color: inherit; 24 | } 25 | -------------------------------------------------------------------------------- /src/components/Tabs/TabPay.jsx: -------------------------------------------------------------------------------- 1 | import { Tab, TabContentReceive, TabContentSend } from '..'; 2 | 3 | import { SelectAssetProvider } from '../../contexts/SelectAssetContext'; 4 | 5 | const TabPay = () => { 6 | const TAB_PAY_CONTENT = [ 7 | { 8 | index: 1, 9 | name: 'Send', 10 | content: , 11 | }, 12 | { 13 | index: 2, 14 | name: 'Receive', 15 | content: , 16 | }, 17 | ]; 18 | 19 | return ( 20 | 21 | 22 | 23 | ); 24 | }; 25 | 26 | export default TabPay; 27 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Coinbase Clone", 3 | "name": "Simplified Coinbase Clone build with React and Firebase", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /src/firebase-config.js: -------------------------------------------------------------------------------- 1 | import { initializeApp } from 'firebase/app'; 2 | import { getFirestore } from 'firebase/firestore'; 3 | import { getAuth } from 'firebase/auth'; 4 | 5 | const firebaseConfig = { 6 | apiKey: 'AIzaSyASa_N9o8jH4DVb2jA2b5VG-yW7wJh7JRA', 7 | authDomain: 'coinbase-clone-0101.firebaseapp.com', 8 | projectId: 'coinbase-clone-0101', 9 | storageBucket: 'coinbase-clone-0101.appspot.com', 10 | messagingSenderId: '737129325620', 11 | appId: '1:737129325620:web:3b12cdd66bfb2956255661', 12 | }; 13 | 14 | const app = initializeApp(firebaseConfig); 15 | const db = getFirestore(app); 16 | const auth = getAuth(app); 17 | 18 | export { db, auth }; 19 | -------------------------------------------------------------------------------- /src/hooks/useMediaQuery.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | const isQueryMatching = (query) => window.matchMedia(query).matches; 4 | 5 | const useMediaQuery = (query) => { 6 | const [matches, setMatches] = useState(isQueryMatching(query)); 7 | 8 | const setQueryMatch = () => { 9 | if (isQueryMatching(query) !== matches) setMatches(isQueryMatching(query)); 10 | }; 11 | 12 | useEffect(() => { 13 | window.addEventListener('resize', setQueryMatch); 14 | return () => window.removeEventListener('resize', setQueryMatch); 15 | }, [matches, query]); 16 | 17 | return matches; 18 | }; 19 | 20 | export default useMediaQuery; 21 | -------------------------------------------------------------------------------- /src/utilities/fetch-coin-price-history.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import createCoinHistoryRequestOptions from '../utilities/create-coin-history-request-options'; 3 | 4 | const fetchCoinPriceHistory = async (coin, timeFrame) => { 5 | const requestOptions = createCoinHistoryRequestOptions(coin, timeFrame); 6 | try { 7 | const response = await axios.request(requestOptions); 8 | const priceHistoryTurned = response.data.data.history; 9 | const priceHistory = priceHistoryTurned.slice().reverse(); 10 | return priceHistory; 11 | } catch (error) { 12 | console.error(error); 13 | } 14 | }; 15 | 16 | export default fetchCoinPriceHistory; 17 | -------------------------------------------------------------------------------- /src/components/Tabs/TabDeposit.jsx: -------------------------------------------------------------------------------- 1 | import { Tab, TabContentAddCash, TabContentCashout } from '..'; 2 | 3 | import { SelectAssetProvider } from '../../contexts/SelectAssetContext'; 4 | 5 | const TabDeposit = () => { 6 | const TAB_DEPOSIT_CONTENT = [ 7 | { 8 | index: 1, 9 | name: 'Add cash', 10 | content: , 11 | }, 12 | { 13 | index: 2, 14 | name: 'Cashout', 15 | content: , 16 | }, 17 | ]; 18 | 19 | return ( 20 | 21 | 22 | 23 | ); 24 | }; 25 | 26 | export default TabDeposit; 27 | -------------------------------------------------------------------------------- /src/components/Modal/Modal.jsx: -------------------------------------------------------------------------------- 1 | import './Modal.css'; 2 | 3 | import { useContext, useRef } from 'react'; 4 | 5 | import { ModalContext } from '../../contexts/ModalContext'; 6 | 7 | const Modal = ({ children }) => { 8 | const { isOpen, handleCloseOnBgClick } = useContext(ModalContext); 9 | const modalRef = useRef(); 10 | 11 | return ( 12 | <> 13 | {isOpen && ( 14 |
handleCloseOnBgClick(e, modalRef)} 17 | ref={modalRef}> 18 |
{children}
19 |
20 | )} 21 | 22 | ); 23 | }; 24 | 25 | export default Modal; 26 | -------------------------------------------------------------------------------- /src/components/Inputs/Input.jsx: -------------------------------------------------------------------------------- 1 | import './Input.css'; 2 | 3 | const Input = ({ 4 | type, 5 | placeholder, 6 | name, 7 | required = false, 8 | autoComplete, 9 | minLength = 0, 10 | value, 11 | onChange, 12 | }) => { 13 | return ( 14 |
15 | 26 |
27 | ); 28 | }; 29 | 30 | export default Input; 31 | -------------------------------------------------------------------------------- /src/components/Modal/Modal.css: -------------------------------------------------------------------------------- 1 | .Modal { 2 | position: fixed; 3 | top: 0; 4 | left: 0; 5 | z-index: 10; 6 | width: 100%; 7 | height: 100%; 8 | display: flex; 9 | justify-content: center; 10 | align-items: center; 11 | background: #202c3acc; 12 | } 13 | 14 | .modal__content { 15 | display: flex; 16 | justify-content: center; 17 | align-items: center; 18 | z-index: 11; 19 | width: 500px; 20 | background: #fff; 21 | border-radius: 8px; 22 | } 23 | 24 | @media (max-width: 800px) { 25 | .Modal { 26 | right: 0; 27 | margin: 0px; 28 | } 29 | 30 | .modal__content { 31 | height: 100vh; 32 | width: 100vw; 33 | border-radius: 0; 34 | align-items: flex-start; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/components/Section/SectionTitle.jsx: -------------------------------------------------------------------------------- 1 | import './SectionTitle.css'; 2 | 3 | import { Text } from '..'; 4 | import classNames from 'classnames'; 5 | 6 | const SectionTitle = ({ title, subtitle, noBorderBottom }) => { 7 | const sectionTitleClasses = classNames({ 8 | SectionTitle: true, 9 | 'sectionTitle-noBorderBottom': noBorderBottom, 10 | }); 11 | 12 | return ( 13 |
14 | {title} 15 | {subtitle && ( 16 |
17 | {subtitle} 18 |
19 | )} 20 |
21 | ); 22 | }; 23 | 24 | export default SectionTitle; 25 | -------------------------------------------------------------------------------- /src/components/Auth/SignInForm.css: -------------------------------------------------------------------------------- 1 | .SignInForm { 2 | display: flex; 3 | flex-direction: column; 4 | text-align: left; 5 | background-color: white; 6 | padding: 24px; 7 | margin: 16px 0; 8 | border-radius: 8px; 9 | } 10 | 11 | .SignInForm > .Input { 12 | border: 1px solid #dedfd2; 13 | border-radius: 8px; 14 | margin-bottom: 8px; 15 | padding: 0 8px; 16 | } 17 | 18 | .SignInForm > .Input > input { 19 | width: 100%; 20 | } 21 | 22 | .SignInForm > .Button { 23 | margin: 8px 0; 24 | } 25 | 26 | .SignInForm > p { 27 | text-align: center; 28 | } 29 | 30 | .SignInForm > p > a { 31 | color: #2151f5; 32 | } 33 | 34 | .SignInForm > p > a:hover { 35 | color: #1d48d6; 36 | } 37 | -------------------------------------------------------------------------------- /src/components/Auth/SignUpForm.css: -------------------------------------------------------------------------------- 1 | .SignUpForm { 2 | display: flex; 3 | flex-direction: column; 4 | text-align: left; 5 | background-color: white; 6 | padding: 24px; 7 | margin: 16px 0; 8 | border-radius: 8px; 9 | } 10 | 11 | .SignUpForm > .Input { 12 | border: 1px solid #dedfd2; 13 | border-radius: 8px; 14 | margin-bottom: 8px; 15 | padding: 0 8px; 16 | } 17 | 18 | .SignUpForm > .Input > input { 19 | width: 100%; 20 | } 21 | 22 | .SignUpForm > .Button { 23 | margin: 8px 0; 24 | } 25 | 26 | .SignUpForm > p { 27 | text-align: center; 28 | } 29 | 30 | .SignUpForm > p > a { 31 | color: #2151f5; 32 | } 33 | 34 | .SignUpForm > p > a:hover { 35 | color: #1d48d6; 36 | } 37 | -------------------------------------------------------------------------------- /src/hooks/useGetFiat.js: -------------------------------------------------------------------------------- 1 | import { collection, onSnapshot, query } from 'firebase/firestore'; 2 | import { useEffect, useState } from 'react'; 3 | 4 | import { db } from '../firebase-config'; 5 | 6 | const useGetFiat = () => { 7 | const [fiat, setFiat] = useState([]); 8 | 9 | const fetchYourFiat = (db) => { 10 | const yourFiatQuery = query(collection(db, 'yourFiat')); 11 | 12 | return onSnapshot(yourFiatQuery, (querySnapshot) => { 13 | setFiat(querySnapshot.docs.map((doc) => ({ ...doc.data(), id: doc.id }))); 14 | }); 15 | }; 16 | 17 | useEffect(() => { 18 | fetchYourFiat(db); 19 | }, []); 20 | 21 | return { fiat }; 22 | }; 23 | 24 | export default useGetFiat; 25 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | Coinbase Clone 15 | 16 | 17 | 18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /src/components/Inputs/InputAmountDynamicWidth.css: -------------------------------------------------------------------------------- 1 | .InputAmountDynamicWidth { 2 | height: 80px; 3 | font-size: 88px; 4 | font-weight: 500; 5 | border: none; 6 | color: #2151f5; 7 | caret-color: #5b616e; 8 | } 9 | 10 | .InputAmountDynamicWidth::placeholder { 11 | font-size: 88px; 12 | font-weight: 500; 13 | } 14 | 15 | .InputAmountDynamicWidth:focus { 16 | outline: none; 17 | } 18 | 19 | /* Chrome, Safari, Edge, Opera */ 20 | .InputAmountDynamicWidth::-webkit-outer-spin-button, 21 | .InputAmountDynamicWidth::-webkit-inner-spin-button { 22 | -webkit-appearance: none; 23 | margin: 0; 24 | } 25 | 26 | /* Firefox */ 27 | .InputAmountDynamicWidth[type='number'] { 28 | -moz-appearance: textfield; 29 | } 30 | -------------------------------------------------------------------------------- /src/components/Inputs/Search.css: -------------------------------------------------------------------------------- 1 | .Search { 2 | display: flex; 3 | flex-direction: column; 4 | justify-content: center; 5 | width: 100%; 6 | } 7 | 8 | .search__bar { 9 | position: relative; 10 | height: 48px; 11 | width: 100%; 12 | padding: 0 16px 0 48px; 13 | border: 1px solid #d8d8d8; 14 | border-radius: 8px; 15 | } 16 | 17 | .search__bar:focus { 18 | outline: none; 19 | } 20 | 21 | .search__bar::placeholder { 22 | color: #808080; 23 | } 24 | 25 | .search__icon { 26 | position: relative; 27 | top: -33px; 28 | font-size: 20px; 29 | margin: 0 16px; 30 | color: #808080; 31 | } 32 | 33 | @media (min-width: 800px) { 34 | .trade__searchFilterRow > .Search { 35 | max-width: 240px; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/components/Table/TableCellCoinName.jsx: -------------------------------------------------------------------------------- 1 | import './TableCellCoinName.css'; 2 | 3 | import { Text } from '..'; 4 | 5 | const TableCellCoinName = ({ icon, name, symbol }) => { 6 | return ( 7 |
8 | {`${name} 13 |
14 | {name} 15 |
16 |
17 | 18 | {symbol} 19 | 20 |
21 |
22 | ); 23 | }; 24 | 25 | export default TableCellCoinName; 26 | -------------------------------------------------------------------------------- /src/components/TableInputs/TableInputAddCash.jsx: -------------------------------------------------------------------------------- 1 | import { Table, TableRowSelectAsset } from '..'; 2 | 3 | import { SelectAssetContext } from '../../contexts/SelectAssetContext'; 4 | import { useContext } from 'react'; 5 | 6 | const TableInputAddCash = () => { 7 | const { selectedFiat } = useContext(SelectAssetContext); 8 | 9 | return ( 10 | 11 | 12 | 18 | 19 |
20 | ); 21 | }; 22 | 23 | export default TableInputAddCash; 24 | -------------------------------------------------------------------------------- /src/components/TableInputs/TableInputCashout.jsx: -------------------------------------------------------------------------------- 1 | import { Table, TableRowSelectAsset } from '..'; 2 | 3 | import { SelectAssetContext } from '../../contexts/SelectAssetContext'; 4 | import { useContext } from 'react'; 5 | 6 | const TableInputCashout = () => { 7 | const { selectedFiat } = useContext(SelectAssetContext); 8 | 9 | return ( 10 | 11 | 12 | 18 | 19 |
20 | ); 21 | }; 22 | 23 | export default TableInputCashout; 24 | -------------------------------------------------------------------------------- /src/constants/chart-options.js: -------------------------------------------------------------------------------- 1 | export const CHART_OPTIONS = { 2 | borderColor: '#2151f5', 3 | borderWidth: 2, 4 | animation: { 5 | duration: 0, 6 | }, 7 | interaction: { 8 | intersect: false, 9 | mode: 'index', 10 | }, 11 | responsive: true, 12 | scales: { 13 | x: { 14 | display: false, 15 | grid: { 16 | display: false, 17 | }, 18 | }, 19 | y: { 20 | beginAtZero: false, 21 | display: false, 22 | }, 23 | }, 24 | elements: { 25 | point: { 26 | radius: 0, 27 | }, 28 | }, 29 | plugins: { 30 | legend: { 31 | display: false, 32 | }, 33 | tooltip: { 34 | enabled: false, 35 | }, 36 | }, 37 | }; 38 | -------------------------------------------------------------------------------- /src/utilities/create-coin-history-request-options.js: -------------------------------------------------------------------------------- 1 | import { translateSelectedTimeframe } from './translate-selected-timeframe'; 2 | 3 | const createCoinHistoryRequestOptions = (coin, timeFrame) => { 4 | const timePeriod = translateSelectedTimeframe(timeFrame); 5 | return { 6 | method: 'GET', 7 | url: `https://coinranking1.p.rapidapi.com/coin/${coin}/history`, 8 | params: { 9 | referenceCurrencyUuid: '5k-_VTxqtCEI', 10 | timePeriod: timePeriod, 11 | }, 12 | headers: { 13 | 'X-RapidAPI-Host': 'coinranking1.p.rapidapi.com', 14 | 'X-RapidAPI-Key': '3b41687c24mshc8f3e84a583efc7p133f24jsnc123081c735c', 15 | }, 16 | }; 17 | }; 18 | 19 | export default createCoinHistoryRequestOptions; 20 | -------------------------------------------------------------------------------- /src/contexts/TransactionsContext.jsx: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | import useTransactions from '../hooks/useTransactions'; 3 | 4 | const TransactionsContext = createContext(); 5 | 6 | const TransactionsProvider = ({ children }) => { 7 | const { 8 | tradeTransactions, 9 | sendTransactions, 10 | coinTransactions, 11 | fiatTransactions, 12 | } = useTransactions(); 13 | 14 | return ( 15 | 22 | {children} 23 | 24 | ); 25 | }; 26 | 27 | export { TransactionsContext, TransactionsProvider }; 28 | -------------------------------------------------------------------------------- /src/components/Tabs/TabTrade.jsx: -------------------------------------------------------------------------------- 1 | import { Tab, TabContentBuy, TabContentConvert, TabContentSell } from '..'; 2 | 3 | import { SelectAssetProvider } from '../../contexts/SelectAssetContext'; 4 | 5 | const TabTrade = () => { 6 | const TAB_TRADE_CONTENT = [ 7 | { 8 | index: 1, 9 | name: 'Buy', 10 | content: , 11 | }, 12 | { 13 | index: 2, 14 | name: 'Sell', 15 | content: , 16 | }, 17 | { 18 | index: 3, 19 | name: 'Convert', 20 | content: , 21 | }, 22 | ]; 23 | 24 | return ( 25 | 26 | 27 | 28 | ); 29 | }; 30 | 31 | export default TabTrade; 32 | -------------------------------------------------------------------------------- /src/hooks/useFilter.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | const useFilter = (allItems, defaultValue) => { 4 | const [filterResult, setFilterResult] = useState(allItems); 5 | const [filterInput, setFilterInput] = useState(defaultValue); 6 | 7 | const handleFilter = (e, allItems, query) => { 8 | const filterValue = e.target.value; 9 | setFilterInput(filterValue); 10 | 11 | const filteredAssets = allItems.filter(query); 12 | setFilterResult(filterValue !== defaultValue ? filteredAssets : allItems); 13 | }; 14 | 15 | useEffect(() => { 16 | if (filterInput === defaultValue) setFilterResult(allItems); 17 | }, [allItems]); 18 | 19 | return { filterResult, filterInput, handleFilter }; 20 | }; 21 | 22 | export default useFilter; 23 | -------------------------------------------------------------------------------- /src/components/ModalsManager/ModalsManager.jsx: -------------------------------------------------------------------------------- 1 | import { 2 | MenuMobile, 3 | ModalDeposit, 4 | ModalPay, 5 | ModalProfile, 6 | ModalTrade, 7 | } from '..'; 8 | 9 | import { ModalContext } from '../../contexts/ModalContext'; 10 | import { useContext } from 'react'; 11 | 12 | const ModalsManager = () => { 13 | const { activeModal } = useContext(ModalContext); 14 | 15 | switch (activeModal) { 16 | case 'trade': 17 | return ; 18 | case 'pay': 19 | return ; 20 | case 'deposit': 21 | return ; 22 | case 'profile': 23 | return ; 24 | case 'menuMobile': 25 | return ; 26 | default: 27 | return null; 28 | } 29 | }; 30 | 31 | export default ModalsManager; 32 | -------------------------------------------------------------------------------- /src/contexts/ModalContext.jsx: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | import useModal from '../hooks/useModal'; 3 | 4 | const ModalContext = createContext(); 5 | 6 | const ModalProvider = ({ children }) => { 7 | const { 8 | isOpen, 9 | activeModal, 10 | activeTab, 11 | setActiveTab, 12 | handleOpen, 13 | handleClose, 14 | handleCloseOnBgClick, 15 | } = useModal(); 16 | 17 | return ( 18 | 28 | {children} 29 | 30 | ); 31 | }; 32 | 33 | export { ModalContext, ModalProvider }; 34 | -------------------------------------------------------------------------------- /src/utilities/toggle-on-watchlist.js: -------------------------------------------------------------------------------- 1 | import { doc, getDoc, updateDoc } from 'firebase/firestore'; 2 | 3 | import { addCoin } from './add-coin'; 4 | import { createCoin } from './create-coin'; 5 | 6 | export const toggleOnWatchlist = async (db, coin) => { 7 | const checkIsYourCoin = async (db, coinId) => { 8 | const coin = await getDoc(doc(db, 'yourCoins', coinId)); 9 | return coin.exists(); 10 | }; 11 | const isYourCoin = await checkIsYourCoin(db, coin?.id); 12 | 13 | try { 14 | if (isYourCoin) { 15 | const coinDoc = doc(db, 'yourCoins', coin?.id); 16 | updateDoc(coinDoc, { onWatchlist: !coin?.onWatchlist }); 17 | } else { 18 | addCoin(db, createCoin(coin, 0, true)); 19 | } 20 | } catch (error) { 21 | console.error(error); 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /src/components/Inputs/InputAmountContainer.css: -------------------------------------------------------------------------------- 1 | .InputAmountContainer { 2 | display: flex; 3 | flex-direction: column; 4 | justify-content: center; 5 | margin: 48px 0 8px 0; 6 | width: 100%; 7 | } 8 | 9 | .inputAmountContainer_amount { 10 | display: flex; 11 | justify-content: center; 12 | } 13 | 14 | .inputAmountContainer_currency { 15 | font-size: 40px; 16 | font-weight: 600; 17 | color: #5b616e; 18 | } 19 | 20 | .inputAmountContainer_amountError { 21 | display: flex; 22 | justify-content: center; 23 | margin-top: 16px; 24 | min-height: 24px; 25 | } 26 | 27 | @media (max-width: 800px) { 28 | .InputAmountContainer { 29 | margin: 8% 0; 30 | } 31 | } 32 | 33 | @media (max-width: 600px) { 34 | .InputAmountContainer { 35 | margin: 16% 0; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/hooks/useGetTransactions.js: -------------------------------------------------------------------------------- 1 | import { collection, onSnapshot, query } from 'firebase/firestore'; 2 | import { useEffect, useState } from 'react'; 3 | 4 | import { db } from '../firebase-config'; 5 | 6 | const useGetTransactions = () => { 7 | const [transactions, setTransactions] = useState([]); 8 | 9 | const fetchTransactions = (db) => { 10 | const transactionQuery = query(collection(db, 'transactions')); 11 | 12 | return onSnapshot(transactionQuery, (querySnapshot) => { 13 | setTransactions( 14 | querySnapshot.docs.map((doc) => ({ ...doc.data(), id: doc.id })) 15 | ); 16 | }); 17 | }; 18 | 19 | useEffect(() => { 20 | fetchTransactions(db); 21 | }, []); 22 | 23 | return { transactions }; 24 | }; 25 | 26 | export default useGetTransactions; 27 | -------------------------------------------------------------------------------- /src/components/Table/TableRowAssetAddress.jsx: -------------------------------------------------------------------------------- 1 | import { MdOutlineContentCopy } from 'react-icons/md'; 2 | import { Text } from '..'; 3 | 4 | const TableRowAssetAddress = ({ helperText, address, icon, iconSize = 16 }) => { 5 | return ( 6 | 7 | 8 | {helperText} 9 | 10 | 11 |
12 |
15 | {icon} 16 |
17 | {address} 18 |
19 | 20 | 21 | 22 | 23 | 24 | ); 25 | }; 26 | 27 | export default TableRowAssetAddress; 28 | -------------------------------------------------------------------------------- /src/hooks/useCombineSearchFilter.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | const useCombineSearchFilter = (searchResult, filterResult, key) => { 4 | const [searchFilterResult, setSearchFilterResult] = useState(searchResult); 5 | 6 | const getSharedItems = (array1, array2, key) => { 7 | let sharedItems = []; 8 | array1.forEach((item1) => { 9 | if (array2.some((item2) => item1[key] === item2[key])) 10 | sharedItems.push(item1); 11 | }); 12 | return sharedItems; 13 | }; 14 | 15 | useEffect(() => { 16 | const sharedItems = getSharedItems(searchResult, filterResult, key); 17 | setSearchFilterResult(sharedItems); 18 | }, [searchResult, filterResult, key]); 19 | 20 | return { searchFilterResult }; 21 | }; 22 | 23 | export default useCombineSearchFilter; 24 | -------------------------------------------------------------------------------- /src/utilities/create-total-balance-history.js: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs'; 2 | 3 | const createTotalBalanceHistory = (coinHistories) => { 4 | const numCoins = coinHistories.length; 5 | if (numCoins === 0) return; 6 | 7 | const result = coinHistories[0].balanceHistoryEur.map( 8 | (balance, pointsInTime) => { 9 | let balanceSum = 0; 10 | for (let i = 0; i < numCoins; i++) { 11 | balanceSum += coinHistories[i].balanceHistoryEur[pointsInTime]; 12 | } 13 | 14 | return { 15 | balance: Number(balanceSum.toFixed(2)), 16 | timestamp: dayjs 17 | .unix(coinHistories[0]?.priceHistory[pointsInTime]?.timestamp) 18 | .format('MMM D, HH:mm'), 19 | }; 20 | } 21 | ); 22 | return result; 23 | }; 24 | 25 | export default createTotalBalanceHistory; 26 | -------------------------------------------------------------------------------- /src/components/Button/Button.jsx: -------------------------------------------------------------------------------- 1 | import './Button.css'; 2 | 3 | import classNames from 'classnames'; 4 | 5 | const Button = ({ 6 | children, 7 | color, 8 | disabled, 9 | light, 10 | size, 11 | stretch, 12 | onClick, 13 | type, 14 | }) => { 15 | const btnClasses = classNames({ 16 | Button: true, 17 | 'btn-primary': color === 'primary', 18 | 'btn-secondary': color === 'secondary', 19 | 'btn-danger': color === 'danger', 20 | 'btn-md': size === 'md', 21 | 'btn-xl': size === 'xl', 22 | 'btn-xxl': size === 'xxl', 23 | 'btn-stretch': stretch, 24 | 'btn-disabled': disabled, 25 | 'btn-light': light, 26 | }); 27 | 28 | return ( 29 | 32 | ); 33 | }; 34 | 35 | export default Button; 36 | -------------------------------------------------------------------------------- /src/hooks/useAssets.js: -------------------------------------------------------------------------------- 1 | import useGetCoins from './useGetCoins'; 2 | import useGetFiat from './useGetFiat'; 3 | 4 | const useAssets = () => { 5 | const { coins } = useGetCoins(); 6 | const { fiat } = useGetFiat(); 7 | 8 | const allCoins = coins.sort( 9 | (prev, next) => next.market_cap - prev.market_cap 10 | ); 11 | 12 | const yourCoins = coins 13 | .filter((asset) => asset.balance_coin > 0) 14 | .sort((prev, next) => next.balance_eur - prev.balance_eur); 15 | 16 | const coinsOnWatchlist = coins 17 | .filter((asset) => asset.onWatchlist) 18 | .sort((prev, next) => next.market_cap - prev.market_cap); 19 | 20 | const allFiat = fiat.sort( 21 | (prev, next) => next.balance_eur - prev.balance_eur 22 | ); 23 | 24 | return { allCoins, yourCoins, coinsOnWatchlist, allFiat }; 25 | }; 26 | 27 | export default useAssets; 28 | -------------------------------------------------------------------------------- /src/components/Inputs/InputAmountContainer.jsx: -------------------------------------------------------------------------------- 1 | import './InputAmountContainer.css'; 2 | 3 | import InputAmountDynamicWidth from './InputAmountDynamicWidth'; 4 | import { Text } from '..'; 5 | import { TransactionFormContext } from '../../contexts/TransactionFormContext'; 6 | import { useContext } from 'react'; 7 | 8 | const InputAmountContainer = () => { 9 | const { amountError } = useContext(TransactionFormContext); 10 | 11 | return ( 12 |
13 |
14 | 15 | 16 |
17 |
18 | {amountError} 19 |
20 |
21 | ); 22 | }; 23 | 24 | export default InputAmountContainer; 25 | -------------------------------------------------------------------------------- /src/components/Sidebar/Sidebar.jsx: -------------------------------------------------------------------------------- 1 | import './Sidebar.css'; 2 | 3 | import { Logo, SidebarNavItem } from '..'; 4 | import { RiBankCardFill, RiPieChartFill } from 'react-icons/ri'; 5 | 6 | import { AiFillEuroCircle } from 'react-icons/ai'; 7 | import { TiChartLine } from 'react-icons/ti'; 8 | 9 | const Sidebar = () => { 10 | return ( 11 | 24 | ); 25 | }; 26 | 27 | export default Sidebar; 28 | -------------------------------------------------------------------------------- /src/hooks/useModal.js: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | const useModal = () => { 4 | const [isOpen, setIsOpen] = useState(false); 5 | const [activeModal, setActiveModal] = useState(''); 6 | const [activeTab, setActiveTab] = useState(1); 7 | 8 | const handleOpen = (modalName, activeTab = 1) => { 9 | setActiveModal(modalName); 10 | setActiveTab(activeTab); 11 | setIsOpen(true); 12 | }; 13 | 14 | const handleClose = () => { 15 | setIsOpen(false); 16 | setActiveTab(1); 17 | setActiveModal(''); 18 | }; 19 | 20 | const handleCloseOnBgClick = (e, modalRef) => { 21 | if (modalRef.current === e.target) handleClose(); 22 | }; 23 | 24 | return { 25 | isOpen, 26 | activeModal, 27 | activeTab, 28 | setActiveTab, 29 | handleOpen, 30 | handleClose, 31 | handleCloseOnBgClick, 32 | }; 33 | }; 34 | 35 | export default useModal; 36 | -------------------------------------------------------------------------------- /src/hooks/useSearch.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | const useSearch = (allItems) => { 4 | const [searchResult, setSearchResult] = useState(allItems); 5 | const [searchInput, setSearchInput] = useState(''); 6 | 7 | const filterItemsByText = (allItems, searchTerm) => 8 | allItems.filter((item) => 9 | item.name.toLowerCase().includes(searchTerm.toLowerCase()) 10 | ); 11 | 12 | const handleSearch = (e, allItems) => { 13 | const searchTerm = e.target.value; 14 | setSearchInput(searchTerm); 15 | 16 | const filteredItems = filterItemsByText(allItems, searchTerm); 17 | setSearchResult(searchTerm ? filteredItems : allItems); 18 | }; 19 | 20 | useEffect(() => { 21 | if (!searchInput) setSearchResult(allItems); 22 | }, [allItems]); 23 | 24 | return { searchResult, searchInput, handleSearch }; 25 | }; 26 | 27 | export default useSearch; 28 | -------------------------------------------------------------------------------- /src/components/Charts/ChartPortfolio.css: -------------------------------------------------------------------------------- 1 | .chartPortfolio__header { 2 | padding: 32px; 3 | } 4 | 5 | .chartPortfolio__headerRow1 { 6 | display: flex; 7 | justify-content: space-between; 8 | margin-bottom: 8px; 9 | } 10 | 11 | .timeFrameBtn__wrapper { 12 | display: flex; 13 | align-items: center; 14 | } 15 | 16 | .timeFrameBtn { 17 | padding: 0 12px; 18 | font-size: 14px; 19 | color: #5b616e; 20 | font-weight: 600; 21 | cursor: pointer; 22 | } 23 | 24 | .timeFrameBtn__active { 25 | color: #2151f5; 26 | } 27 | 28 | .chartPortfolio__chartWrapper, 29 | .chartPortfolio__chartWrapper > canvas { 30 | max-height: 240px; 31 | } 32 | 33 | .chartPortfolio__footer { 34 | width: 100%; 35 | display: grid; 36 | grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr; 37 | text-align: center; 38 | font-size: 14px; 39 | color: #5b616e; 40 | font-weight: 600; 41 | padding: 8px 0; 42 | border-top: 1px solid #dedfd2; 43 | } 44 | -------------------------------------------------------------------------------- /src/components/TableInputs/TableInputBuy.jsx: -------------------------------------------------------------------------------- 1 | import { Table, TableRowSelectAsset } from '..'; 2 | 3 | import { SelectAssetContext } from '../../contexts/SelectAssetContext'; 4 | import { useContext } from 'react'; 5 | 6 | const TableInputBuy = () => { 7 | const { selectedCoin, selectedFiat } = useContext(SelectAssetContext); 8 | return ( 9 | 10 | 11 | 17 | 23 | 24 |
25 | ); 26 | }; 27 | 28 | export default TableInputBuy; 29 | -------------------------------------------------------------------------------- /src/pages/Deposit.jsx: -------------------------------------------------------------------------------- 1 | import { 2 | ContentCenter, 3 | ContentRight, 4 | Section, 5 | SectionTitle, 6 | TabDeposit, 7 | TableFiatTransfers, 8 | } from '../components'; 9 | 10 | import useMediaQuery from '../hooks/useMediaQuery'; 11 | 12 | const Deposit = () => { 13 | const isWidthMax1150 = useMediaQuery('(max-width: 1150px)'); 14 | const isWidthMin800 = useMediaQuery('(min-width: 800px)'); 15 | 16 | return ( 17 | <> 18 | {isWidthMin800 && ( 19 | 20 |
21 | 22 |
23 |
24 | )} 25 | 26 |
27 | 28 | 29 |
30 |
31 | 32 | ); 33 | }; 34 | 35 | export default Deposit; 36 | -------------------------------------------------------------------------------- /src/components/TableInputs/TableInputSell.jsx: -------------------------------------------------------------------------------- 1 | import { Table, TableRowSelectAsset } from '..'; 2 | 3 | import { SelectAssetContext } from '../../contexts/SelectAssetContext'; 4 | import { useContext } from 'react'; 5 | 6 | const TableInputSell = () => { 7 | const { selectedCoin, selectedFiat } = useContext(SelectAssetContext); 8 | return ( 9 | 10 | 11 | 17 | 23 | 24 |
25 | ); 26 | }; 27 | 28 | export default TableInputSell; 29 | -------------------------------------------------------------------------------- /src/components/Table/TableRowInputText.jsx: -------------------------------------------------------------------------------- 1 | import './TableRowInputText.css'; 2 | 3 | import { Input, Text } from '..'; 4 | 5 | const TableRowInputText = ({ 6 | helperText, 7 | name, 8 | inputPlaceholder, 9 | icon, 10 | iconSize, 11 | required, 12 | }) => { 13 | return ( 14 | 15 | 16 | {helperText} 17 | 18 | 19 |
20 |
23 | {icon} 24 |
25 | 30 |
31 | 32 | 33 | 34 | ); 35 | }; 36 | 37 | export default TableRowInputText; 38 | -------------------------------------------------------------------------------- /src/components/TableInputs/TableInputConvert.jsx: -------------------------------------------------------------------------------- 1 | import { Table, TableRowSelectAsset } from '..'; 2 | 3 | import { SelectAssetContext } from '../../contexts/SelectAssetContext'; 4 | import { useContext } from 'react'; 5 | 6 | const TableInputConvert = () => { 7 | const { selectedCoin, selectedCoinConvertTo } = 8 | useContext(SelectAssetContext); 9 | return ( 10 | 11 | 12 | 18 | 24 | 25 |
26 | ); 27 | }; 28 | 29 | export default TableInputConvert; 30 | -------------------------------------------------------------------------------- /src/pages/Pay.jsx: -------------------------------------------------------------------------------- 1 | import { 2 | ContentCenter, 3 | ContentRight, 4 | Section, 5 | SectionTitle, 6 | TabPay, 7 | TablePayments, 8 | } from '../components'; 9 | 10 | import useMediaQuery from '../hooks/useMediaQuery'; 11 | 12 | const Pay = () => { 13 | const isWidthMax1150 = useMediaQuery('(max-width: 1150px)'); 14 | const isWidthMin800 = useMediaQuery('(min-width: 800px)'); 15 | 16 | return ( 17 | <> 18 | {isWidthMin800 && ( 19 | 20 |
21 | 22 |
23 |
24 | )} 25 | 26 |
27 | 31 | 32 |
33 |
34 | 35 | ); 36 | }; 37 | 38 | export default Pay; 39 | -------------------------------------------------------------------------------- /src/components/Inputs/InputAmountDynamicWidth.jsx: -------------------------------------------------------------------------------- 1 | import './InputAmountDynamicWidth.css'; 2 | 3 | import { useContext, useEffect, useState } from 'react'; 4 | 5 | import { TransactionFormContext } from '../../contexts/TransactionFormContext'; 6 | 7 | const InputAmountDynamicWidth = () => { 8 | const { amount, setAmount } = useContext(TransactionFormContext); 9 | const [chars, setChars] = useState(0); 10 | const inputWidth = (chars) => (chars === 0 ? 50 : chars * 50); 11 | 12 | useEffect(() => { 13 | setChars(amount ? amount.length : 1); 14 | }, [amount]); 15 | 16 | return ( 17 | setAmount(e.target.value)} 27 | /> 28 | ); 29 | }; 30 | 31 | export default InputAmountDynamicWidth; 32 | -------------------------------------------------------------------------------- /src/utilities/create-coin-histories.js: -------------------------------------------------------------------------------- 1 | import createCoinBalanceHistory from '../utilities/create-coin-balance-history'; 2 | import fetchCoinPriceHistory from '../utilities/fetch-coin-price-history'; 3 | 4 | const createCoinHistories = async (yourCoins, timeFrame, transactions) => { 5 | const coinHistories = []; 6 | 7 | for (const coin of yourCoins) { 8 | const priceHistory = await fetchCoinPriceHistory(coin?.id, timeFrame); 9 | const balanceHistory = createCoinBalanceHistory( 10 | coin?.id, 11 | priceHistory, 12 | transactions 13 | ); 14 | 15 | const coinHistory = { 16 | id: coin?.id, 17 | name: coin?.name, 18 | priceHistory: priceHistory, 19 | coinBalanceHistory: balanceHistory, 20 | balanceHistoryEur: priceHistory.map( 21 | (time, index) => time?.price * balanceHistory[index]?.balance 22 | ), 23 | }; 24 | coinHistories.push(coinHistory); 25 | } 26 | return coinHistories; 27 | }; 28 | 29 | export default createCoinHistories; 30 | -------------------------------------------------------------------------------- /src/components/Chart/LineChart.jsx: -------------------------------------------------------------------------------- 1 | import 'chart.js/auto'; 2 | 3 | import { useEffect, useState } from 'react'; 4 | 5 | import { CHART_OPTIONS } from '../../constants/chart-options'; 6 | import { Line } from 'react-chartjs-2'; 7 | 8 | const LineChart = ({ chartData, labelsKey, datasetsKey, hasTooltip }) => { 9 | const [lineChartData, setLineChartData] = useState({ 10 | labels: chartData.map((data) => data[`${labelsKey}`]), 11 | datasets: [{ data: chartData.map((data) => data[`${datasetsKey}`]) }], 12 | }); 13 | 14 | const chartOptionsDeepCopy = JSON.parse(JSON.stringify(CHART_OPTIONS)); 15 | chartOptionsDeepCopy.plugins.tooltip.enabled = hasTooltip; 16 | 17 | useEffect(() => { 18 | setLineChartData({ 19 | labels: chartData.map((data) => data[`${labelsKey}`]), 20 | datasets: [{ data: chartData.map((data) => data[`${datasetsKey}`]) }], 21 | }); 22 | }, [chartData]); 23 | 24 | return ; 25 | }; 26 | 27 | export default LineChart; 28 | -------------------------------------------------------------------------------- /src/contexts/SelectAssetContext.jsx: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | import useSelectAsset from '../hooks/useSelectAsset'; 3 | 4 | const SelectAssetContext = createContext(); 5 | 6 | const SelectAssetProvider = ({ children }) => { 7 | const { 8 | isSelectAssetOpen, 9 | lastSelectAssetType, 10 | selectedCoin, 11 | selectedCoinConvertTo, 12 | selectedFiat, 13 | toggleIsSelectAssetOpen, 14 | handleSelectAssetOpenClick, 15 | handleSelectAsset, 16 | checkIsSelected, 17 | } = useSelectAsset(); 18 | 19 | return ( 20 | 32 | {children} 33 | 34 | ); 35 | }; 36 | 37 | export { SelectAssetContext, SelectAssetProvider }; 38 | -------------------------------------------------------------------------------- /src/components/TabContents/TabContentReceive.jsx: -------------------------------------------------------------------------------- 1 | import { TabContent, TabContentSelectAsset, TabFooter, TableReceive } from '..'; 2 | 3 | import { SelectAssetContext } from '../../contexts/SelectAssetContext'; 4 | import { convertToCurrency } from '../../utilities/convert-to-currency'; 5 | import { useContext } from 'react'; 6 | 7 | const TabContentReceive = (props) => { 8 | const { isSelectAssetOpen, selectedCoin } = useContext(SelectAssetContext); 9 | return ( 10 | <> 11 | {isSelectAssetOpen ? ( 12 | 13 | ) : ( 14 | 15 | 16 | 23 | 24 | )} 25 | 26 | ); 27 | }; 28 | 29 | export default TabContentReceive; 30 | -------------------------------------------------------------------------------- /src/components/Tab/Tab.css: -------------------------------------------------------------------------------- 1 | .Tab { 2 | display: flex; 3 | flex-direction: column; 4 | width: 100%; 5 | height: 100%; 6 | } 7 | 8 | .tabBtn__wrapper { 9 | display: flex; 10 | } 11 | 12 | .tabBtn { 13 | position: relative; 14 | padding: 24px 16px; 15 | text-align: center; 16 | font-weight: 600; 17 | width: 100%; 18 | cursor: pointer; 19 | border: none; 20 | border-bottom: 1px solid #dedfd2; 21 | } 22 | .tabBtn:not(:last-child) { 23 | border-right: 1px solid #dedfd2; 24 | } 25 | 26 | .tabBtn__active { 27 | border-bottom: 1px solid transparent; 28 | background-color: white; 29 | border-radius: 8px 8px 0 0; 30 | color: #2151f5; 31 | } 32 | 33 | .tabBtn__active::before { 34 | content: ''; 35 | display: block; 36 | position: absolute; 37 | top: -1px; 38 | left: 50%; 39 | transform: translateX(-50%); 40 | width: 100%; 41 | border-radius: 8px 8px 0 0; 42 | height: 3px; 43 | background: #2151f5; 44 | } 45 | 46 | .tabContent { 47 | padding: 20px; 48 | display: none; 49 | height: 100%; 50 | } 51 | 52 | .tabContent__active { 53 | display: block; 54 | } 55 | -------------------------------------------------------------------------------- /src/components/Modals/ModalProfile.jsx: -------------------------------------------------------------------------------- 1 | import './ModalProfile.css'; 2 | 3 | import { Button, Modal, ModalClose, Text } from '..'; 4 | 5 | import Avvvatars from 'avvvatars-react'; 6 | import { UserContext } from '../../contexts/UserContext'; 7 | import useAuth from '../../hooks/useAuth'; 8 | import { useContext } from 'react'; 9 | import useMediaQuery from '../../hooks/useMediaQuery'; 10 | 11 | const ModalProfile = () => { 12 | const isWidthMax800 = useMediaQuery('(max-width: 800px)'); 13 | const { user } = useContext(UserContext); 14 | const { signout } = useAuth(); 15 | 16 | return ( 17 | 18 |
19 | Profile 20 | 21 | {user?.email || 'Guest'} 22 | 25 | {isWidthMax800 && } 26 |
27 |
28 | ); 29 | }; 30 | 31 | export default ModalProfile; 32 | -------------------------------------------------------------------------------- /src/components/TabContents/TabContentSelectAsset.css: -------------------------------------------------------------------------------- 1 | .tabContent__titleGrid { 2 | display: grid; 3 | align-items: center; 4 | grid-template-columns: 1fr 5fr 1fr; 5 | height: 32px; 6 | margin-bottom: 24px; 7 | text-align: center; 8 | } 9 | 10 | .tabContent__titleGrid > svg { 11 | cursor: pointer; 12 | } 13 | 14 | .tabContent__isSelectedAsset { 15 | background-color: #eef0f3; 16 | } 17 | 18 | .tabContent__selectAssetCell { 19 | display: flex; 20 | justify-content: flex-end; 21 | align-items: center; 22 | gap: 8px; 23 | } 24 | 25 | .tabContent__selectAssetCell svg { 26 | color: #2151f5; 27 | margin-left: 8px; 28 | } 29 | 30 | .tabContent__selectAssetBalance { 31 | display: flex; 32 | flex-direction: column; 33 | } 34 | 35 | .tabContent__selectAssetTableBody { 36 | max-height: 410px; 37 | overflow: scroll; 38 | } 39 | 40 | .tabContent__selectAssetBody { 41 | display: block; 42 | width: 100%; 43 | max-height: 390px; 44 | overflow: auto; 45 | } 46 | 47 | .tabContent__selectAssetBody tr { 48 | display: table; 49 | width: 100%; 50 | } 51 | -------------------------------------------------------------------------------- /src/components/TabContents/TabContentBuyEmpty.jsx: -------------------------------------------------------------------------------- 1 | import './TabContentBuyEmpty.css'; 2 | 3 | import { Button, TabContent, Text } from '..'; 4 | 5 | import { HiExclamationCircle } from 'react-icons/hi'; 6 | import { useNavigate } from 'react-router-dom'; 7 | 8 | const TabContentBuyEmpty = () => { 9 | const navigate = useNavigate(); 10 | 11 | return ( 12 | 13 |
14 | 15 |
16 | Deposit required 17 |
18 |
19 | 20 | You'll need to deposit money into your fiat wallet before you can 21 | buy any assets. 22 | 23 |
24 | 27 |
28 |
29 | ); 30 | }; 31 | 32 | export default TabContentBuyEmpty; 33 | -------------------------------------------------------------------------------- /src/utilities/update-fiat-balance.js: -------------------------------------------------------------------------------- 1 | import { doc, updateDoc } from 'firebase/firestore'; 2 | 3 | export const updateFiatBalance = async (db, transaction, fiatAssets) => { 4 | try { 5 | let fiatBalanceChange; 6 | switch (transaction.type) { 7 | case 'depositFiat': 8 | case 'sellCoin': 9 | fiatBalanceChange = transaction.fiat.amount; 10 | break; 11 | case 'cashoutFiat': 12 | case 'buyCoin': 13 | fiatBalanceChange = -transaction.fiat.amount; 14 | break; 15 | case 'sendCoin': 16 | case 'convertCoin': 17 | return; 18 | default: 19 | console.error('Unknown transaction type'); 20 | return; 21 | } 22 | 23 | const fiatId = fiatAssets[0].id; 24 | const prevFiatBalance = fiatAssets[0].balance_eur; 25 | const newFiatBalance = prevFiatBalance + fiatBalanceChange; 26 | 27 | const fiatDoc = doc(db, 'yourFiat', fiatId); 28 | 29 | await updateDoc(fiatDoc, { 30 | balance_eur: newFiatBalance, 31 | }); 32 | } catch (err) { 33 | console.error(err); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /src/components/Sidebar/SidebarNavItem.css: -------------------------------------------------------------------------------- 1 | .SidebarNavItem { 2 | display: grid; 3 | align-content: center; 4 | grid-template-columns: 30px auto; 5 | height: 48px; 6 | padding-left: 8px; 7 | margin-bottom: 24px; 8 | border-radius: 8px; 9 | } 10 | 11 | .SidebarNavItem:hover { 12 | background-color: #eef0f3; 13 | } 14 | 15 | .SidebarNavItem:hover svg, 16 | .sidebarNavItem-active svg { 17 | color: #2151f5; 18 | } 19 | 20 | .sidebarNavItem__circle { 21 | display: flex; 22 | justify-content: center; 23 | align-items: center; 24 | width: 32px; 25 | height: 32px; 26 | border-radius: 50%; 27 | background-color: #eef0f3; 28 | } 29 | 30 | .sidebarNavItem__icon { 31 | display: flex; 32 | justify-content: center; 33 | align-items: center; 34 | height: 16px; 35 | width: 16px; 36 | } 37 | 38 | .sidebarNavItem__text { 39 | display: flex; 40 | align-items: center; 41 | margin-left: 16px; 42 | font-weight: 600; 43 | } 44 | 45 | @media (max-width: 1300px) { 46 | .SidebarNavItem { 47 | padding-right: 8px; 48 | } 49 | 50 | .Sidebar .sidebarNavItem__text { 51 | display: none; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/components/Tables/TableReceive.jsx: -------------------------------------------------------------------------------- 1 | import './TableReceive.css'; 2 | 3 | import { 4 | Table, 5 | TableRowAssetAddress, 6 | TableRowQR, 7 | TableRowSelectAsset, 8 | } from '..'; 9 | 10 | import { FaAddressCard } from 'react-icons/fa'; 11 | import { SelectAssetContext } from '../../contexts/SelectAssetContext'; 12 | import { useContext } from 'react'; 13 | 14 | const TableReceive = () => { 15 | const { selectedCoin } = useContext(SelectAssetContext); 16 | return ( 17 |
18 | 19 | 20 | 21 | 27 | } 31 | /> 32 | 33 |
34 |
35 | ); 36 | }; 37 | 38 | export default TableReceive; 39 | -------------------------------------------------------------------------------- /src/components/Text/Text.css: -------------------------------------------------------------------------------- 1 | .Text { 2 | margin: 0; 3 | } 4 | 5 | .text-h1 { 6 | font-size: 22px; 7 | font-weight: 600; 8 | } 9 | 10 | .text-h2 { 11 | font-size: 22px; 12 | font-weight: 500; 13 | } 14 | 15 | .text-h3 { 16 | font-size: 18px; 17 | font-weight: 600; 18 | } 19 | 20 | .text-xxl { 21 | font-size: 32px; 22 | } 23 | 24 | .text-xl { 25 | font-size: 22px; 26 | } 27 | 28 | .text-l { 29 | font-size: 18px; 30 | } 31 | 32 | .text-m { 33 | font-size: 16px; 34 | } 35 | 36 | .text-s { 37 | font-size: 14px; 38 | } 39 | 40 | .text-xs { 41 | font-size: 12px; 42 | } 43 | 44 | .text-700 { 45 | font-weight: 700; 46 | } 47 | 48 | .text-600 { 49 | font-weight: 600; 50 | } 51 | 52 | .text-500 { 53 | font-weight: 500; 54 | } 55 | 56 | .text-400 { 57 | font-weight: 400; 58 | } 59 | 60 | .text-white { 61 | color: white; 62 | } 63 | 64 | .text-grey { 65 | color: #5b616e; 66 | } 67 | 68 | .text-blue { 69 | color: #2151f5; 70 | } 71 | 72 | .text-red { 73 | color: #cf202f; 74 | } 75 | 76 | .text-green { 77 | color: #098551; 78 | } 79 | 80 | .text-uppercase { 81 | text-transform: uppercase; 82 | } 83 | -------------------------------------------------------------------------------- /src/utilities/adapt-fetched-coins.js: -------------------------------------------------------------------------------- 1 | import { calculateCoinBalance } from '../utilities/calculate-coin-balance'; 2 | import { findAsset } from '../utilities/find-asset'; 3 | import { transformSparkline } from '../utilities/transform-sparkline'; 4 | 5 | export const adaptFetchedCoins = (fetchedCoins, yourCoins) => { 6 | const adaptedCoins = fetchedCoins.map((coin) => { 7 | const coinFound = findAsset(coin?.uuid, yourCoins); 8 | const coinBalance = coinFound?.balance_coin || 0; 9 | const onWatchList = coinFound?.onWatchlist || false; 10 | 11 | return { 12 | id: coin.uuid, 13 | name: coin.name, 14 | symbol: coin.symbol, 15 | isFiat: false, 16 | icon: coin.iconUrl, 17 | color: coin.color, 18 | balance_eur: calculateCoinBalance(coinBalance, coin.price), 19 | balance_coin: coinBalance, 20 | price_eur: Number(coin.price), 21 | price_change24h: Number(coin.change), 22 | market_cap: Number(coin.marketCap), 23 | onWatchlist: onWatchList, 24 | sparkline: transformSparkline(coin), 25 | }; 26 | }); 27 | return adaptedCoins; 28 | }; 29 | -------------------------------------------------------------------------------- /src/utilities/create-coin-balance-history.js: -------------------------------------------------------------------------------- 1 | const createCoinBalanceHistory = (coinId, priceHistory, transactions) => { 2 | const coinBalanceHistory = priceHistory.map((data) => { 3 | const filteredTransactions = transactions 4 | .filter((t) => coinId === t?.coin?.id || t?.coinConvertTo?.id) 5 | .filter((t) => data.timestamp * 1000 > t.timestamp); 6 | 7 | const balance = filteredTransactions.reduce((accum, curr) => { 8 | switch (curr.type) { 9 | case 'buyCoin': 10 | return accum + curr.coin?.amount; 11 | case 'sellCoin': 12 | case 'sendCoin': 13 | return accum - curr.coin?.amount; 14 | case 'convertCoin': 15 | if (coinId === curr.coin?.id) return accum - curr.coin?.amount; 16 | if (coinId === curr.coinConvertTo?.id) 17 | return accum + curr.coinConvertTo?.amount; 18 | default: 19 | return accum; 20 | } 21 | }, 0); 22 | 23 | return { 24 | timestamp: data?.timestamp, 25 | balance: balance, 26 | }; 27 | }); 28 | return coinBalanceHistory; 29 | }; 30 | 31 | export default createCoinBalanceHistory; 32 | -------------------------------------------------------------------------------- /src/components/Dashboard/Dashboard.jsx: -------------------------------------------------------------------------------- 1 | import './Dashboard.css'; 2 | 3 | import { Content, Footer, Header, Main, ModalsManager, Sidebar } from '..'; 4 | 5 | import { AssetsProvider } from '../../contexts/AssetsContext'; 6 | import { ModalProvider } from '../../contexts/ModalContext'; 7 | import { Outlet } from 'react-router-dom'; 8 | import { TransactionsProvider } from '../../contexts/TransactionsContext'; 9 | import useMediaQuery from '../../hooks/useMediaQuery'; 10 | 11 | const Dashboard = () => { 12 | const isWidthMin800 = useMediaQuery('(min-width: 800px)'); 13 | 14 | return ( 15 |
16 | 17 | 18 | 19 | {isWidthMin800 && } 20 |
21 |
22 | 23 | 24 | 25 | {!isWidthMin800 &&
} 26 | 27 |
28 |
29 |
30 |
31 |
32 | ); 33 | }; 34 | 35 | export default Dashboard; 36 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import { 2 | Assets, 3 | Deposit, 4 | Pay, 5 | ProtectedPages, 6 | SignIn, 7 | SignUp, 8 | Trade, 9 | } from './pages'; 10 | import { Navigate, Route, Routes } from 'react-router-dom'; 11 | 12 | import { Dashboard } from './components'; 13 | import { UserProvider } from './contexts/UserContext'; 14 | 15 | function App() { 16 | return ( 17 |
18 | 19 | 20 | } /> 21 | } /> 22 | } /> 23 | }> 24 | }> 25 | } /> 26 | } /> 27 | } /> 28 | } /> 29 | } /> 30 | 31 | 32 | 33 | 34 |
35 | ); 36 | } 37 | 38 | export default App; 39 | -------------------------------------------------------------------------------- /src/components/TabContents/TabContentCashout.jsx: -------------------------------------------------------------------------------- 1 | import { 2 | Button, 3 | InputAmountContainer, 4 | TabContent, 5 | TabContentSelectAsset, 6 | TabFooter, 7 | TableInputCashout, 8 | TransactionForm, 9 | } from '..'; 10 | 11 | import { SelectAssetContext } from '../../contexts/SelectAssetContext'; 12 | import { convertToCurrency } from '../../utilities/convert-to-currency'; 13 | import { useContext } from 'react'; 14 | 15 | const TabContentCashout = () => { 16 | const { isSelectAssetOpen, selectedFiat } = useContext(SelectAssetContext); 17 | 18 | return ( 19 | <> 20 | {isSelectAssetOpen ? ( 21 | 22 | ) : ( 23 | 24 | 25 | 26 | 27 | 28 | 29 | 33 | 34 | )} 35 | 36 | ); 37 | }; 38 | 39 | export default TabContentCashout; 40 | -------------------------------------------------------------------------------- /src/hooks/useBalanceHistory.js: -------------------------------------------------------------------------------- 1 | import { useContext, useEffect, useState } from 'react'; 2 | 3 | import { AssetsContext } from '../contexts/AssetsContext'; 4 | import { TransactionsContext } from '../contexts/TransactionsContext'; 5 | import createCoinHistories from '../utilities/create-coin-histories'; 6 | import createTotalBalanceHistory from '../utilities/create-total-balance-history'; 7 | 8 | const useBalanceHistory = (activeTimeFrame) => { 9 | const { yourCoins } = useContext(AssetsContext); 10 | const [balanceHistory, setBalanceHistory] = useState([]); 11 | const { coinTransactions } = useContext(TransactionsContext); 12 | 13 | const handleGetCoinPriceHistories = async (coins, time, transactions) => { 14 | const coinHistories = await createCoinHistories(coins, time, transactions); 15 | const totalHistory = await createTotalBalanceHistory(coinHistories); 16 | setBalanceHistory(totalHistory); 17 | }; 18 | 19 | useEffect(() => { 20 | if (yourCoins?.length) 21 | handleGetCoinPriceHistories(yourCoins, activeTimeFrame, coinTransactions); 22 | }, [yourCoins, activeTimeFrame]); 23 | 24 | return balanceHistory; 25 | }; 26 | 27 | export default useBalanceHistory; 28 | -------------------------------------------------------------------------------- /src/utilities/create-chart-times.js: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs'; 2 | 3 | export const createChartTimes = () => { 4 | const times = { 5 | '1D': [], 6 | '1W': [], 7 | '1M': [], 8 | '1Y': [], 9 | }; 10 | 11 | let stepSize = 24 / 6; 12 | let offset = 2; 13 | for (let i = 24 - stepSize; i >= 0; i -= stepSize) { 14 | const time = dayjs() 15 | .subtract(i + offset, 'hour') 16 | .format('H:mm A'); 17 | times['1D'].push(time); 18 | } 19 | 20 | stepSize = 1; 21 | offset = 16; 22 | for (let i = 6 - stepSize; i >= 0; i -= stepSize) { 23 | const time = dayjs() 24 | .subtract(i * 24 + offset, 'hour') 25 | .format('MMM DD'); 26 | times['1W'].push(time); 27 | } 28 | 29 | stepSize = 30 / 6; 30 | offset = 2; 31 | for (let i = 30 - stepSize; i >= 0; i -= stepSize) { 32 | const time = dayjs() 33 | .subtract(i + offset, 'day') 34 | .format('MMM DD'); 35 | times['1M'].push(time); 36 | } 37 | 38 | stepSize = 12 / 6; 39 | for (let i = 12 - stepSize; i >= 0; i -= stepSize) { 40 | const time = dayjs().subtract(i, 'month').format('MMM YYYY'); 41 | times['1Y'].push(time); 42 | } 43 | 44 | return times; 45 | }; 46 | -------------------------------------------------------------------------------- /src/pages/SignIn.jsx: -------------------------------------------------------------------------------- 1 | import { AuthError, AuthLayout, Button, SignInForm, Text } from '../components'; 2 | import { useContext, useEffect } from 'react'; 3 | 4 | import { UserContext } from '../contexts/UserContext'; 5 | import { auth } from '../firebase-config'; 6 | import { onAuthStateChanged } from 'firebase/auth'; 7 | import useAuth from '../hooks/useAuth'; 8 | import { useNavigate } from 'react-router-dom'; 9 | 10 | const SignIn = () => { 11 | const { user, setUser } = useContext(UserContext); 12 | const { handleSignIn, authError } = useAuth(); 13 | const navigate = useNavigate(); 14 | 15 | useEffect(() => { 16 | if (user) navigate('/assets'); 17 | }, [user]); 18 | 19 | onAuthStateChanged(auth, (currentUser) => setUser(currentUser)); 20 | 21 | return ( 22 | 23 | 24 | Sign in to Coinbase Clone 25 | 26 | 29 | OR 30 | 31 | {authError && } 32 | 33 | ); 34 | }; 35 | 36 | export default SignIn; 37 | -------------------------------------------------------------------------------- /src/pages/SignUp.jsx: -------------------------------------------------------------------------------- 1 | import { AuthError, AuthLayout, Button, SignUpForm, Text } from '../components'; 2 | import { useContext, useEffect } from 'react'; 3 | 4 | import { UserContext } from '../contexts/UserContext'; 5 | import { auth } from '../firebase-config'; 6 | import { onAuthStateChanged } from 'firebase/auth'; 7 | import useAuth from '../hooks/useAuth'; 8 | import { useNavigate } from 'react-router-dom'; 9 | 10 | const SignUp = () => { 11 | const { user, setUser } = useContext(UserContext); 12 | const { handleSignUp, authError } = useAuth(); 13 | const navigate = useNavigate(); 14 | 15 | useEffect(() => { 16 | if (user) navigate('/assets'); 17 | }, [user]); 18 | 19 | onAuthStateChanged(auth, (currentUser) => setUser(currentUser)); 20 | 21 | return ( 22 | 23 | 24 | Sign up for Coinbase Clone 25 | 26 | 29 | OR 30 | 31 | {authError && } 32 | 33 | ); 34 | }; 35 | 36 | export default SignUp; 37 | -------------------------------------------------------------------------------- /src/components/Table/TableRowSelectAsset.jsx: -------------------------------------------------------------------------------- 1 | import './TableRowSelectAsset.css'; 2 | 3 | import { MdOutlineArrowForwardIos } from 'react-icons/md'; 4 | import { SelectAssetContext } from '../../contexts/SelectAssetContext'; 5 | import { Text } from '..'; 6 | import { useContext } from 'react'; 7 | 8 | const TableRowSelectAsset = ({ 9 | helperText, 10 | assetName, 11 | assetIcon, 12 | selectAssetType, 13 | }) => { 14 | const { handleSelectAssetOpenClick } = useContext(SelectAssetContext); 15 | return ( 16 | handleSelectAssetOpenClick(e)}> 19 | 20 | {helperText} 21 | 22 | 23 |
24 | {`${assetName} 29 | {assetName} 30 |
31 | 32 | 33 |
34 | 35 |
36 | 37 | 38 | ); 39 | }; 40 | 41 | export default TableRowSelectAsset; 42 | -------------------------------------------------------------------------------- /src/components/TabContents/TabContentAddCash.jsx: -------------------------------------------------------------------------------- 1 | import { 2 | Button, 3 | InputAmountContainer, 4 | TabContent, 5 | TabContentSelectAsset, 6 | TabFooter, 7 | TableInputAddCash, 8 | TransactionForm, 9 | } from '..'; 10 | 11 | import { SelectAssetContext } from '../../contexts/SelectAssetContext'; 12 | import { convertToCurrency } from '../../utilities/convert-to-currency'; 13 | import { useContext } from 'react'; 14 | 15 | const TabContentAddCash = () => { 16 | const { isSelectAssetOpen, selectedFiat } = useContext(SelectAssetContext); 17 | 18 | return ( 19 | <> 20 | {isSelectAssetOpen ? ( 21 | 22 | ) : ( 23 | 24 | 25 | 26 | 27 | 30 | 31 | 35 | 36 | )} 37 | 38 | ); 39 | }; 40 | 41 | export default TabContentAddCash; 42 | -------------------------------------------------------------------------------- /src/components/TableInputs/TableInputSend.jsx: -------------------------------------------------------------------------------- 1 | import { Table, TableRowInputText, TableRowSelectAsset } from '..'; 2 | 3 | import { FaWallet } from 'react-icons/fa'; 4 | import { MdEdit } from 'react-icons/md'; 5 | import { SelectAssetContext } from '../../contexts/SelectAssetContext'; 6 | import { useContext } from 'react'; 7 | 8 | const TableInputSend = () => { 9 | const { selectedCoin } = useContext(SelectAssetContext); 10 | return ( 11 | 12 | 13 | 19 | } 24 | iconSize={16} 25 | required={true} 26 | /> 27 | } 32 | iconSize={21} 33 | /> 34 | 35 |
36 | ); 37 | }; 38 | 39 | export default TableInputSend; 40 | -------------------------------------------------------------------------------- /src/pages/Assets.jsx: -------------------------------------------------------------------------------- 1 | import { 2 | ChartPortfolio, 3 | ContentCenter, 4 | ContentRight, 5 | Section, 6 | SectionTitle, 7 | TabTrade, 8 | TableAssets, 9 | TableYourAssets, 10 | } from '../components'; 11 | 12 | import { AssetsContext } from '../contexts/AssetsContext'; 13 | import { useContext } from 'react'; 14 | import useMediaQuery from '../hooks/useMediaQuery'; 15 | 16 | const Assets = () => { 17 | const isWidthMin1150 = useMediaQuery('(min-width: 1150px)'); 18 | const { yourCoins, coinsOnWatchlist } = useContext(AssetsContext); 19 | 20 | return ( 21 | <> 22 | 23 |
24 | 25 |
26 |
27 | 28 | 29 |
30 |
31 | 32 | 33 |
34 |
35 | {isWidthMin1150 && ( 36 | 37 |
38 | 39 |
40 |
41 | )} 42 | 43 | ); 44 | }; 45 | 46 | export default Assets; 47 | -------------------------------------------------------------------------------- /src/hooks/useTransactions.js: -------------------------------------------------------------------------------- 1 | import useGetTransactions from './useGetTransactions'; 2 | 3 | const useTransactions = () => { 4 | const { transactions } = useGetTransactions(); 5 | 6 | const tradeTransactions = transactions 7 | .filter( 8 | (t) => 9 | t.type === 'buyCoin' || 10 | t.type === 'sellCoin' || 11 | t.type === 'convertCoin' 12 | ) 13 | .sort((prev, next) => next.timestamp - prev.timestamp); 14 | 15 | const sendTransactions = transactions 16 | .filter((t) => t.type === 'sendCoin') 17 | .sort((prev, next) => next.timestamp - prev.timestamp); 18 | 19 | const coinTransactions = transactions 20 | .filter( 21 | (t) => 22 | t.type === 'buyCoin' || 23 | t.type === 'sellCoin' || 24 | t.type === 'convertCoin' || 25 | t.type === 'sendCoin' 26 | ) 27 | .sort((prev, next) => next.timestamp - prev.timestamp); 28 | 29 | const fiatTransactions = transactions 30 | .filter((t) => t.type === 'depositFiat' || t.type === 'cashoutFiat') 31 | .sort((prev, next) => next.timestamp - prev.timestamp); 32 | 33 | return { 34 | tradeTransactions, 35 | sendTransactions, 36 | coinTransactions, 37 | fiatTransactions, 38 | }; 39 | }; 40 | 41 | export default useTransactions; 42 | -------------------------------------------------------------------------------- /src/components/Tab/Tab.jsx: -------------------------------------------------------------------------------- 1 | import './Tab.css'; 2 | 3 | import { ModalContext } from '../../contexts/ModalContext'; 4 | import classNames from 'classnames'; 5 | import { useContext } from 'react'; 6 | 7 | const Tab = ({ data }) => { 8 | const { activeTab, setActiveTab } = useContext(ModalContext); 9 | 10 | return ( 11 |
12 |
13 | {data.map((tab) => { 14 | const tabBtnClasses = classNames({ 15 | tabBtn: true, 16 | tabBtn__active: activeTab === tab.index, 17 | }); 18 | 19 | return ( 20 |
setActiveTab(tab.index)}> 24 | {tab.name} 25 |
26 | ); 27 | })} 28 |
29 | {data.map((tab) => { 30 | const tabContent = classNames({ 31 | tabContent: true, 32 | tabContent__active: activeTab === tab.index, 33 | }); 34 | 35 | return ( 36 |
37 |
{tab.content}
38 |
39 | ); 40 | })} 41 |
42 | ); 43 | }; 44 | 45 | export default Tab; 46 | -------------------------------------------------------------------------------- /src/components/Text/Text.jsx: -------------------------------------------------------------------------------- 1 | import './Text.css'; 2 | 3 | import classNames from 'classnames'; 4 | 5 | const Text = ({ children, h1, h2, h3, size, weight, color, uppercase }) => { 6 | const textClasses = classNames({ 7 | Text: true, 8 | 'text-h1': h1, 9 | 'text-h2': h2, 10 | 'text-h3': h3, 11 | 'text-xxl': size === 'xxl', 12 | 'text-xl': size === 'xl', 13 | 'text-l': size === 'l', 14 | 'text-m': size === 'm', 15 | 'text-s': size === 's', 16 | 'text-xs': size === 'xs', 17 | 'text-700': weight === '700', 18 | 'text-600': weight === '600', 19 | 'text-500': weight === '500', 20 | 'text-400': weight === '400', 21 | 'text-black': color === 'black', 22 | 'text-white': color === 'white', 23 | 'text-grey': color === 'grey', 24 | 'text-blue': color === 'blue', 25 | 'text-red': color === 'red', 26 | 'text-green': color === 'green', 27 | 'text-uppercase': uppercase, 28 | }); 29 | 30 | return ( 31 | <> 32 | {h1 &&

{children}

} 33 | {h2 &&

{children}

} 34 | {h3 &&

{children}

} 35 | {!(h1 || h2 || h3) &&

{children}

} 36 | 37 | ); 38 | }; 39 | 40 | export default Text; 41 | -------------------------------------------------------------------------------- /src/components/TabContents/TabContentSend.jsx: -------------------------------------------------------------------------------- 1 | import { 2 | Button, 3 | InputAmountContainer, 4 | TabContent, 5 | TabContentSelectAsset, 6 | TabFooter, 7 | TableInputSend, 8 | TransactionForm, 9 | } from '..'; 10 | 11 | import { SelectAssetContext } from '../../contexts/SelectAssetContext'; 12 | import { convertToCurrency } from '../../utilities/convert-to-currency'; 13 | import { useContext } from 'react'; 14 | 15 | const TabContentSend = () => { 16 | const { isSelectAssetOpen, selectedCoin } = useContext(SelectAssetContext); 17 | return ( 18 | <> 19 | {isSelectAssetOpen ? ( 20 | 21 | ) : ( 22 | 23 | 24 | 25 | 26 | 27 | 28 | 34 | 35 | )} 36 | 37 | ); 38 | }; 39 | 40 | export default TabContentSend; 41 | -------------------------------------------------------------------------------- /src/components/TabContents/TabContentSell.jsx: -------------------------------------------------------------------------------- 1 | import { 2 | Button, 3 | InputAmountContainer, 4 | TabContent, 5 | TabContentSelectAsset, 6 | TabFooter, 7 | TableInputSell, 8 | TransactionForm, 9 | } from '..'; 10 | 11 | import { SelectAssetContext } from '../../contexts/SelectAssetContext'; 12 | import { convertToCurrency } from '../../utilities/convert-to-currency'; 13 | import { useContext } from 'react'; 14 | 15 | const TabContentSell = () => { 16 | const { isSelectAssetOpen, selectedCoin } = useContext(SelectAssetContext); 17 | 18 | return ( 19 | <> 20 | {isSelectAssetOpen ? ( 21 | 22 | ) : ( 23 | 24 | 25 | 26 | 27 | 28 | 29 | 35 | 36 | )} 37 | 38 | ); 39 | }; 40 | 41 | export default TabContentSell; 42 | -------------------------------------------------------------------------------- /src/components/Sidebar/SidebarNavItem.jsx: -------------------------------------------------------------------------------- 1 | import './SidebarNavItem.css'; 2 | 3 | import { Link } from 'react-router-dom'; 4 | import { ModalContext } from '../../contexts/ModalContext'; 5 | import { Tooltip } from '..'; 6 | import classNames from 'classnames'; 7 | import { useContext } from 'react'; 8 | import useMediaQuery from '../../hooks/useMediaQuery'; 9 | import usePath from '../../hooks/usePath'; 10 | 11 | const SidebarNavItem = ({ to, icon, text }) => { 12 | const { activeModal, handleClose } = useContext(ModalContext); 13 | const isWidthMax1300 = useMediaQuery('(max-width: 1300px)'); 14 | const { page } = usePath(); 15 | const inMobileMenu = activeModal === 'menuMobile'; 16 | 17 | const sidebarNavItemClasses = classNames({ 18 | SidebarNavItem: true, 19 | hasTooltip: !inMobileMenu && isWidthMax1300, 20 | 'sidebarNavItem-active': page === text, 21 | }); 22 | 23 | return ( 24 | 25 |
26 |
{icon}
27 |
28 |
{text}
29 | {!inMobileMenu && isWidthMax1300 && {text}} 30 | 31 | ); 32 | }; 33 | 34 | export default SidebarNavItem; 35 | -------------------------------------------------------------------------------- /src/components/TabContents/TabContentConvert.jsx: -------------------------------------------------------------------------------- 1 | import { 2 | Button, 3 | InputAmountContainer, 4 | TabContent, 5 | TabContentSelectAsset, 6 | TabFooter, 7 | TableInputConvert, 8 | TransactionForm, 9 | } from '..'; 10 | 11 | import { SelectAssetContext } from '../../contexts/SelectAssetContext'; 12 | import { convertToCurrency } from '../../utilities/convert-to-currency'; 13 | import { useContext } from 'react'; 14 | 15 | const TabContentConvert = () => { 16 | const { isSelectAssetOpen, selectedCoin } = useContext(SelectAssetContext); 17 | return ( 18 | <> 19 | {isSelectAssetOpen ? ( 20 | 21 | ) : ( 22 | 23 | 24 | 25 | 26 | 27 | 28 | 34 | 35 | )} 36 | 37 | ); 38 | }; 39 | 40 | export default TabContentConvert; 41 | -------------------------------------------------------------------------------- /src/components/Footer/Footer.jsx: -------------------------------------------------------------------------------- 1 | import './Footer.css'; 2 | 3 | import { Button } from '..'; 4 | import { ModalContext } from '../../contexts/ModalContext'; 5 | import { useContext } from 'react'; 6 | import usePath from '../../hooks/usePath'; 7 | 8 | const Footer = () => { 9 | const { page } = usePath(); 10 | const { handleOpen } = useContext(ModalContext); 11 | 12 | return ( 13 |
14 | {['Assets', 'Trade'].includes(page) && ( 15 | <> 16 | 19 | 22 | 23 | )} 24 | {page === 'Pay' && ( 25 | <> 26 | 29 | 32 | 33 | )} 34 | {page === 'Deposit' && ( 35 | <> 36 | 39 | 42 | 43 | )} 44 |
45 | ); 46 | }; 47 | 48 | export default Footer; 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "coinbase-clone", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.16.2", 7 | "@testing-library/react": "^12.1.3", 8 | "@testing-library/user-event": "^13.5.0", 9 | "avvvatars-react": "^0.4.2", 10 | "axios": "^0.26.1", 11 | "chart.js": "^3.7.1", 12 | "classnames": "^2.3.1", 13 | "dayjs": "^1.11.0", 14 | "firebase": "^9.6.8", 15 | "normalize.css": "^8.0.1", 16 | "react": "^17.0.2", 17 | "react-chartjs-2": "^4.0.1", 18 | "react-dom": "^17.0.2", 19 | "react-icons": "^4.3.1", 20 | "react-router-dom": "^6.2.1", 21 | "react-scripts": "5.0.0", 22 | "web-vitals": "^2.1.4" 23 | }, 24 | "scripts": { 25 | "start": "react-scripts start", 26 | "build": "react-scripts build", 27 | "test": "react-scripts test", 28 | "eject": "react-scripts eject", 29 | "pretty": "prettier --write \"./**/*.{js,jsx,json}\"" 30 | }, 31 | "eslintConfig": { 32 | "extends": [ 33 | "react-app", 34 | "react-app/jest" 35 | ] 36 | }, 37 | "browserslist": { 38 | "production": [ 39 | ">0.2%", 40 | "not dead", 41 | "not op_mini all" 42 | ], 43 | "development": [ 44 | "last 1 chrome version", 45 | "last 1 firefox version", 46 | "last 1 safari version" 47 | ] 48 | }, 49 | "devDependencies": { 50 | "prettier": "2.5.1" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/components/Tables/TablePayments.jsx: -------------------------------------------------------------------------------- 1 | import './TablePayments.css'; 2 | 3 | import { Table, Text } from '..'; 4 | 5 | import { TransactionsContext } from '../../contexts/TransactionsContext'; 6 | import { transactionTime } from '../../utilities/transform-dates'; 7 | import { useContext } from 'react'; 8 | 9 | const TablePayments = () => { 10 | const { sendTransactions } = useContext(TransactionsContext); 11 | 12 | return ( 13 | 14 | 15 | {sendTransactions.slice(0, 5).map((t) => ( 16 | 17 | 36 | 37 | ))} 38 | 39 |
18 |
19 | {`${t?.coin?.name} 24 |
25 | {`Sent ${t?.coin?.amount?.toFixed(6)} ${ 26 | t?.coin?.symbol 27 | }`} 28 |
29 |
30 | 31 | {`To ${t?.address} on ${transactionTime(t?.timestamp)}`} 32 | 33 |
34 |
35 |
40 | ); 41 | }; 42 | 43 | export default TablePayments; 44 | -------------------------------------------------------------------------------- /src/components/Tables/TableFiatTransfers.jsx: -------------------------------------------------------------------------------- 1 | import './TablePayments.css'; 2 | 3 | import { Table, Text } from '..'; 4 | 5 | import { TransactionsContext } from '../../contexts/TransactionsContext'; 6 | import { transactionTime } from '../../utilities/transform-dates'; 7 | import { useContext } from 'react'; 8 | 9 | const TableFiatTransfers = () => { 10 | const { fiatTransactions } = useContext(TransactionsContext); 11 | 12 | return ( 13 | 14 | 15 | {fiatTransactions.slice(0, 5).map((t) => ( 16 | 17 | 36 | 37 | ))} 38 | 39 |
18 |
19 | {`${t?.fiat?.name} 24 |
25 | {`${ 26 | t?.type === 'depositFiat' ? 'Deposited' : 'Withdrew' 27 | } ${t?.fiat?.amount} ${t?.fiat?.symbol}`} 28 |
29 |
30 | 31 | {`On ${transactionTime(t?.timestamp)}`} 32 | 33 |
34 |
35 |
40 | ); 41 | }; 42 | 43 | export default TableFiatTransfers; 44 | -------------------------------------------------------------------------------- /src/components/Button/Button.css: -------------------------------------------------------------------------------- 1 | .Button { 2 | padding: 5px; 3 | border: 1px solid #2151f5; 4 | padding: 11px 16px; 5 | border-radius: 4px; 6 | font-size: 15px; 7 | font-weight: 600; 8 | background-color: #2151f5; 9 | color: white; 10 | cursor: pointer; 11 | } 12 | 13 | .Button:hover { 14 | background-color: #1d48d6; 15 | } 16 | 17 | .btn-secondary { 18 | background-color: white; 19 | color: black; 20 | border: 1px solid #d8d8d8; 21 | } 22 | 23 | .btn-secondary:hover { 24 | background-color: rgb(241, 241, 241); 25 | } 26 | 27 | .btn-danger { 28 | background-color: #cf202f; 29 | color: white; 30 | border: 1px solid #cf202f; 31 | } 32 | 33 | .btn-danger:hover { 34 | background-color: rgb(233, 66, 66); 35 | } 36 | 37 | .btn-xl { 38 | padding: 22px; 39 | width: 100%; 40 | } 41 | 42 | .btn-xxl { 43 | padding: 32px; 44 | width: 100%; 45 | } 46 | 47 | .btn-stretch { 48 | width: 100%; 49 | } 50 | 51 | .btn-disabled { 52 | cursor: not-allowed; 53 | } 54 | 55 | .btn-light { 56 | border: none; 57 | background-color: white; 58 | } 59 | 60 | .btn-light.btn-primary { 61 | color: #2151f5; 62 | } 63 | 64 | .btn-light.btn-primary:hover { 65 | color: #1d48d6; 66 | background-color: white; 67 | } 68 | 69 | .btn-light.btn-secondary { 70 | color: black; 71 | } 72 | 73 | .btn-light.btn-secondary:hover { 74 | color: black; 75 | background-color: white; 76 | } 77 | 78 | .btn-light.btn-danger { 79 | color: #cf202f; 80 | } 81 | 82 | .btn-light.btn-danger:hover { 83 | color: #cf202f; 84 | background-color: white; 85 | } 86 | -------------------------------------------------------------------------------- /src/components/TabContents/TabContentBuy.jsx: -------------------------------------------------------------------------------- 1 | import { 2 | Button, 3 | InputAmountContainer, 4 | TabContent, 5 | TabContentBuyEmpty, 6 | TabContentSelectAsset, 7 | TabFooter, 8 | TableInputBuy, 9 | TransactionForm, 10 | } from '..'; 11 | 12 | import { AssetsContext } from '../../contexts/AssetsContext'; 13 | import { SelectAssetContext } from '../../contexts/SelectAssetContext'; 14 | import { calculateTotalBalance } from '../../utilities/calculate-total-balance'; 15 | import { convertToCurrency } from '../../utilities/convert-to-currency'; 16 | import { useContext } from 'react'; 17 | 18 | const TabContentBuy = () => { 19 | const { isSelectAssetOpen, selectedCoin, selectedFiat } = 20 | useContext(SelectAssetContext); 21 | const { allFiat } = useContext(AssetsContext); 22 | const hasNoFiat = Boolean(!calculateTotalBalance(allFiat)); 23 | 24 | if (hasNoFiat) { 25 | return ; 26 | } else if (isSelectAssetOpen) { 27 | return ; 28 | } else { 29 | return ( 30 | 31 | 32 | 33 | 34 | 35 | 36 | 40 | 41 | ); 42 | } 43 | }; 44 | 45 | export default TabContentBuy; 46 | -------------------------------------------------------------------------------- /src/utilities/validate-transaction.js: -------------------------------------------------------------------------------- 1 | import { findAsset } from './find-asset'; 2 | 3 | const validateSufficientFiatBalance = (transaction, fiatAssets) => { 4 | const fiat = findAsset(transaction?.fiat?.id, fiatAssets); 5 | const fiatBalance = fiat?.balance_eur; 6 | 7 | if (fiat && fiatBalance >= transaction?.fiat?.amount) { 8 | return { 9 | isValid: true, 10 | error: '', 11 | }; 12 | } else { 13 | return { 14 | isValid: false, 15 | error: `Non-sufficient ${transaction?.fiat?.name} funds`, 16 | }; 17 | } 18 | }; 19 | 20 | const validateSufficientCoinBalance = (transaction, coinAssets) => { 21 | const coin = findAsset(transaction?.coin?.id, coinAssets); 22 | const coinBalance = coin?.balance_coin; 23 | 24 | if (coin && coinBalance >= transaction.coin.amount) { 25 | return { 26 | isValid: true, 27 | error: '', 28 | }; 29 | } else { 30 | return { 31 | isValid: false, 32 | error: `Non-sufficient ${transaction.coin.name} funds`, 33 | }; 34 | } 35 | }; 36 | 37 | export const validateTransaction = (transaction, fiatAssets, coinAssets) => { 38 | switch (transaction.type) { 39 | case 'buyCoin': 40 | case 'cashoutFiat': 41 | return validateSufficientFiatBalance(transaction, fiatAssets); 42 | case 'sellCoin': 43 | case 'convertCoin': 44 | case 'sendCoin': 45 | return validateSufficientCoinBalance(transaction, coinAssets); 46 | case 'depositFiat': 47 | return { 48 | isValid: true, 49 | error: '', 50 | }; 51 | default: 52 | console.error('Unknown transaction type'); 53 | break; 54 | } 55 | }; 56 | -------------------------------------------------------------------------------- /src/components/MenuMobile/MenuMobile.jsx: -------------------------------------------------------------------------------- 1 | import './MenuMobile.css'; 2 | 3 | import { Button, SidebarNavItem } from '..'; 4 | import { RiBankCardFill, RiCloseLine, RiPieChartFill } from 'react-icons/ri'; 5 | 6 | import { AiFillEuroCircle } from 'react-icons/ai'; 7 | import { ModalContext } from '../../contexts/ModalContext'; 8 | import { TiChartLine } from 'react-icons/ti'; 9 | import useAuth from '../../hooks/useAuth'; 10 | import { useContext } from 'react'; 11 | 12 | const MenuMobile = () => { 13 | const { isOpen, handleClose } = useContext(ModalContext); 14 | const { signout } = useAuth(); 15 | 16 | return ( 17 | <> 18 | {isOpen && ( 19 |
20 |
21 |
22 |
23 | 24 |
25 | } 29 | /> 30 | } /> 31 | } /> 32 | } 36 | /> 37 |
38 | 41 |
42 |
43 | )} 44 | 45 | ); 46 | }; 47 | 48 | export default MenuMobile; 49 | -------------------------------------------------------------------------------- /src/components/Table/Table.css: -------------------------------------------------------------------------------- 1 | .Table, 2 | table { 3 | width: 100%; 4 | overflow-x: scroll; 5 | } 6 | 7 | table { 8 | border-spacing: 0; 9 | width: 100%; 10 | } 11 | 12 | th, 13 | td { 14 | padding: 8px; 15 | text-align: left; 16 | padding: 14px 16px; 17 | } 18 | 19 | tr { 20 | cursor: pointer; 21 | } 22 | 23 | tbody tr:hover > td { 24 | background: #f8f8f8; 25 | } 26 | 27 | tbody .tr-no-hover-bg tr:hover > td { 28 | background: none; 29 | } 30 | 31 | tbody tr:first-child > td:first-child { 32 | border-top-left-radius: 8px; 33 | } 34 | tbody tr:first-child > td:last-child { 35 | border-top-right-radius: 8px; 36 | } 37 | tbody tr:last-child > td:first-child { 38 | border-bottom-left-radius: 8px; 39 | } 40 | tbody tr:last-child > td:last-child { 41 | border-bottom-right-radius: 8px; 42 | } 43 | 44 | th:last-child, 45 | td:last-child { 46 | text-align: right; 47 | padding-right: 32px; 48 | } 49 | 50 | th:first-child, 51 | td:first-child { 52 | padding-left: 32px; 53 | text-align: left; 54 | } 55 | 56 | tr:last-child > td { 57 | border-bottom: none; 58 | } 59 | 60 | th { 61 | color: #5b616e; 62 | font-weight: 400; 63 | font-size: 14px; 64 | } 65 | 66 | .table-is-input-table { 67 | border: 1px solid #dedfd2; 68 | border-radius: 8px; 69 | margin-bottom: 24px; 70 | } 71 | 72 | .table-is-input-table > tbody > tr { 73 | height: 65px; 74 | cursor: pointer; 75 | } 76 | 77 | .table-has-border-bottom th, 78 | td { 79 | border-bottom: 1px solid #d8d8d8; 80 | } 81 | 82 | .table-has-small-padding th:last-child, 83 | .table-has-small-padding td:last-child { 84 | padding-right: 16px; 85 | } 86 | 87 | .table-has-small-padding th:first-child, 88 | .table-has-small-padding td:first-child { 89 | padding-left: 16px; 90 | } 91 | -------------------------------------------------------------------------------- /src/components/Header/Header.jsx: -------------------------------------------------------------------------------- 1 | import './Header.css'; 2 | 3 | import { Button, Logo, Text } from '..'; 4 | 5 | import Avvvatars from 'avvvatars-react'; 6 | import { IoMenuSharp } from 'react-icons/io5'; 7 | import { ModalContext } from '../../contexts/ModalContext'; 8 | import { UserContext } from '../../contexts/UserContext'; 9 | import { useContext } from 'react'; 10 | import useMediaQuery from '../../hooks/useMediaQuery'; 11 | import usePath from '../../hooks/usePath'; 12 | 13 | const Header = () => { 14 | let isWidthMin800 = useMediaQuery('(min-width: 800px)'); 15 | const { page } = usePath(); 16 | const { handleOpen } = useContext(ModalContext); 17 | const { user } = useContext(UserContext); 18 | 19 | return ( 20 |
21 | {!isWidthMin800 && } 22 | {page} 23 | {isWidthMin800 && ( 24 |
25 | 28 | 31 |
32 |
handleOpen('profile')}> 33 | 34 |
35 |
36 | )} 37 | {!isWidthMin800 && ( 38 |
39 | handleOpen('menuMobile')} 42 | /> 43 |
44 | )} 45 |
46 | ); 47 | }; 48 | 49 | export default Header; 50 | -------------------------------------------------------------------------------- /src/components/Auth/SignInForm.jsx: -------------------------------------------------------------------------------- 1 | import './SignInForm.css'; 2 | 3 | import { Button, Input, Text } from '../index'; 4 | import { useContext, useEffect } from 'react'; 5 | 6 | import { UserContext } from '../../contexts/UserContext'; 7 | import { auth } from '../../firebase-config'; 8 | import { onAuthStateChanged } from 'firebase/auth'; 9 | import useAuth from '../../hooks/useAuth'; 10 | import { useNavigate } from 'react-router-dom'; 11 | 12 | const SignInForm = () => { 13 | const { user, setUser } = useContext(UserContext); 14 | const { 15 | handleSignIn, 16 | signInEmail, 17 | setSignInEmail, 18 | signInPassword, 19 | setSignInPassword, 20 | } = useAuth(); 21 | const navigate = useNavigate(); 22 | 23 | useEffect(() => { 24 | if (user) navigate('/assets'); 25 | }, [user]); 26 | 27 | onAuthStateChanged(auth, (currentUser) => setUser(currentUser)); 28 | 29 | return ( 30 |
handleSignIn('email', e)}> 31 | setSignInEmail(e.target.value)} 39 | /> 40 | setSignInPassword(e.target.value)} 48 | /> 49 | 50 | 51 | Don't have an account yet? Sign up 52 | 53 |
54 | ); 55 | }; 56 | 57 | export default SignInForm; 58 | -------------------------------------------------------------------------------- /src/components/Auth/SignUpForm.jsx: -------------------------------------------------------------------------------- 1 | import './SignUpForm.css'; 2 | 3 | import { Button, Input, Text } from '../index'; 4 | import { useContext, useEffect } from 'react'; 5 | 6 | import { UserContext } from '../../contexts/UserContext'; 7 | import { auth } from '../../firebase-config'; 8 | import { onAuthStateChanged } from 'firebase/auth'; 9 | import useAuth from '../../hooks/useAuth'; 10 | import { useNavigate } from 'react-router-dom'; 11 | 12 | const SignUpForm = () => { 13 | const { user, setUser } = useContext(UserContext); 14 | const { 15 | signUpEmail, 16 | setSignUpEmail, 17 | signUpPassword, 18 | setSignUpPassword, 19 | handleSignUp, 20 | } = useAuth(); 21 | const navigate = useNavigate(); 22 | 23 | useEffect(() => { 24 | if (user) navigate('/assets'); 25 | }, [user]); 26 | 27 | onAuthStateChanged(auth, (currentUser) => setUser(currentUser)); 28 | 29 | return ( 30 |
handleSignUp('email', e)}> 31 | setSignUpEmail(e.target.value)} 39 | /> 40 | setSignUpPassword(e.target.value)} 49 | /> 50 | 51 | 52 | Already signed up? Go to log in 53 | 54 |
55 | ); 56 | }; 57 | 58 | export default SignUpForm; 59 | -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Page Not Found 7 | 8 | 23 | 24 | 25 |
26 |

404

27 |

Page Not Found

28 |

The specified file was not found on this website. Please check the URL for mistakes and try again.

29 |

Why am I seeing this?

30 |

This page was generated by the Firebase Command-Line Interface. To modify it, edit the 404.html file in your project's configured public directory.

31 |
32 | 33 | 34 | -------------------------------------------------------------------------------- /src/components/Tables/TableYourAssets.jsx: -------------------------------------------------------------------------------- 1 | import { Table, TableCellCoinName, Text } from '..'; 2 | 3 | import { calculateAllocation } from '../../utilities/calculate-allocation'; 4 | import { convertToCurrency } from '../../utilities/convert-to-currency'; 5 | import useMediaQuery from '../../hooks/useMediaQuery'; 6 | 7 | const TableYourAssets = ({ assets }) => { 8 | const isWidthMin800 = useMediaQuery('(min-width: 800px)'); 9 | 10 | return ( 11 | 12 | {isWidthMin800 && ( 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | )} 22 | 23 | {assets.map((asset) => ( 24 | 25 | 32 | 38 | {isWidthMin800 && ( 39 | 47 | )} 48 | {isWidthMin800 && ( 49 | 52 | )} 53 | 54 | ))} 55 | 56 |
NameBallancePriceAllocation
26 | 31 | 33 | {convertToCurrency(asset.balance_eur)} 34 | 35 | {`${asset?.balance_coin?.toFixed(6)} ${asset?.symbol}`} 36 | 37 | 40 | {convertToCurrency(asset?.price_eur)} 41 | 44 | {asset?.price_change24h}% 45 | 46 | 50 | {calculateAllocation(assets, asset)}% 51 |
57 | ); 58 | }; 59 | 60 | export default TableYourAssets; 61 | -------------------------------------------------------------------------------- /src/hooks/useGetCoins.js: -------------------------------------------------------------------------------- 1 | import { collection, onSnapshot, query } from 'firebase/firestore'; 2 | import { useEffect, useState } from 'react'; 3 | 4 | import { adaptFetchedCoins } from '../utilities/adapt-fetched-coins'; 5 | import axios from 'axios'; 6 | import { db } from '../firebase-config'; 7 | 8 | const REQUEST_OPTIONS = { 9 | method: 'GET', 10 | url: 'https://coinranking1.p.rapidapi.com/coins', 11 | params: { 12 | referenceCurrencyUuid: '5k-_VTxqtCEI', 13 | timePeriod: '24h', 14 | tiers: '1,2', 15 | orderBy: 'marketCap', 16 | orderDirection: 'desc', 17 | limit: '100', 18 | offset: '0', 19 | }, 20 | headers: { 21 | 'x-rapidapi-host': process.env.REACT_APP_X_RAPIDAPI_HOST, 22 | 'x-rapidapi-key': process.env.REACT_APP_X_RAPIDAPI_KEY, 23 | }, 24 | }; 25 | 26 | const useGetCoins = () => { 27 | const [coins, setCoins] = useState([]); 28 | const [yourCoins, setYourCoins] = useState([]); 29 | 30 | const fetchCoins = async (options) => { 31 | try { 32 | const response = await axios.request(options); 33 | const coins = await response.data.data.coins; 34 | return coins; 35 | } catch (error) { 36 | console.error(error); 37 | } 38 | }; 39 | 40 | const fetchYourCoins = (db) => { 41 | const yourCoinsQuery = query(collection(db, 'yourCoins')); 42 | 43 | return onSnapshot(yourCoinsQuery, (querySnapshot) => { 44 | setYourCoins( 45 | querySnapshot.docs.map((doc) => ({ ...doc.data(), id: doc.id })) 46 | ); 47 | }); 48 | }; 49 | 50 | const handleGetAllCoins = async (requestOptions, yourCoins) => { 51 | const fetchedCoins = await fetchCoins(requestOptions); 52 | const adaptedCoins = adaptFetchedCoins(fetchedCoins, yourCoins); 53 | setCoins(adaptedCoins); 54 | }; 55 | 56 | useEffect(() => { 57 | fetchYourCoins(db); 58 | }, []); 59 | 60 | useEffect(() => { 61 | handleGetAllCoins(REQUEST_OPTIONS, yourCoins); 62 | }, [yourCoins]); 63 | 64 | return { coins }; 65 | }; 66 | 67 | export default useGetCoins; 68 | -------------------------------------------------------------------------------- /src/utilities/create-transaction.js: -------------------------------------------------------------------------------- 1 | import { calculateCoinAmount } from './calculate-coin-amount'; 2 | 3 | export const createTransaction = (e, type, selectedAssets, allCoins) => { 4 | const transaction = { 5 | type: type, 6 | timestamp: Date.now(), 7 | note: e.target.note?.value || '', 8 | address: e.target.address?.value || '', 9 | }; 10 | 11 | const fiat = { 12 | id: selectedAssets?.fiat?.id, 13 | symbol: selectedAssets?.fiat?.symbol, 14 | name: selectedAssets?.fiat?.name, 15 | icon: selectedAssets?.fiat?.icon, 16 | amount: Number(e.target.amount?.value) || 0, 17 | }; 18 | 19 | const coin = { 20 | id: selectedAssets?.coin?.id, 21 | symbol: selectedAssets?.coin?.symbol, 22 | name: selectedAssets?.coin?.name, 23 | icon: selectedAssets?.coin?.icon, 24 | amount: calculateCoinAmount( 25 | fiat?.amount, 26 | selectedAssets?.coin?.id, 27 | allCoins 28 | ), 29 | }; 30 | 31 | const coinConvertTo = { 32 | id: selectedAssets?.coinConvertTo?.id, 33 | symbol: selectedAssets?.coinConvertTo?.symbol, 34 | name: selectedAssets?.coinConvertTo?.name, 35 | icon: selectedAssets?.coinConvertTo?.icon, 36 | amount: calculateCoinAmount( 37 | fiat?.amount, 38 | selectedAssets?.coinConvertTo?.id, 39 | allCoins 40 | ), 41 | }; 42 | 43 | switch (type) { 44 | case 'depositFiat': 45 | case 'cashoutFiat': 46 | return { 47 | ...transaction, 48 | fiat: fiat, 49 | }; 50 | case 'buyCoin': 51 | case 'sellCoin': 52 | return { 53 | ...transaction, 54 | fiat: fiat, 55 | coin: coin, 56 | }; 57 | case 'sendCoin': 58 | return { 59 | ...transaction, 60 | coin: coin, 61 | }; 62 | case 'convertCoin': 63 | return { 64 | ...transaction, 65 | coin: coin, 66 | coinConvertTo: coinConvertTo, 67 | }; 68 | default: 69 | console.error('Unsupported transaction type'); 70 | break; 71 | } 72 | }; 73 | -------------------------------------------------------------------------------- /src/components/Tables/TableRecentTransactions.jsx: -------------------------------------------------------------------------------- 1 | import './TableRecentTransactions.css'; 2 | 3 | import { Table, Text } from '..'; 4 | 5 | import { TransactionsContext } from '../../contexts/TransactionsContext'; 6 | import { transactionTime } from '../../utilities/transform-dates'; 7 | import { useContext } from 'react'; 8 | 9 | const TableRecentTransactions = () => { 10 | const { tradeTransactions } = useContext(TransactionsContext); 11 | 12 | const createHeadText = (transaction) => { 13 | const assetName = transaction?.coin?.name; 14 | let action; 15 | if (transaction?.type === 'buyCoin') action = 'Bought'; 16 | if (transaction?.type === 'sellCoin') action = 'Sold'; 17 | if (transaction?.type === 'convertCoin') action = 'Converted'; 18 | 19 | return `${action} ${assetName}`; 20 | }; 21 | 22 | const createBodyText = (transaction) => { 23 | const symbol = transaction?.coin?.symbol; 24 | const amount = transaction?.coin?.amount?.toFixed(6); 25 | const time = transactionTime(transaction?.timestamp); 26 | 27 | return `${amount} ${symbol} on ${time}`; 28 | }; 29 | 30 | return ( 31 | 32 | 33 | {tradeTransactions.slice(0, 5).map((t) => ( 34 | 35 | 52 | 53 | ))} 54 | 55 |
36 |
37 | {`${t?.coin?.name} 42 |
43 | {createHeadText(t)} 44 |
45 |
46 | 47 | {createBodyText(t)} 48 | 49 |
50 |
51 |
56 | ); 57 | }; 58 | 59 | export default TableRecentTransactions; 60 | -------------------------------------------------------------------------------- /src/components/TransactionForm/TransactionForm.jsx: -------------------------------------------------------------------------------- 1 | import { useContext, useState } from 'react'; 2 | 3 | import { AssetsContext } from '../../contexts/AssetsContext'; 4 | import { SelectAssetContext } from '../../contexts/SelectAssetContext'; 5 | import { TransactionFormContext } from '../../contexts/TransactionFormContext'; 6 | import { addTransaction } from '../../utilities/add-transaction'; 7 | import { createTransaction } from '../../utilities/create-transaction'; 8 | import { db } from '../../firebase-config'; 9 | import { updateCoins } from '../../utilities/update-coins'; 10 | import { updateFiatBalance } from '../../utilities/update-fiat-balance'; 11 | import { validateTransaction } from '../../utilities/validate-transaction'; 12 | 13 | const TransactionForm = ({ children, type }) => { 14 | const { allCoins, yourCoins, allFiat } = useContext(AssetsContext); 15 | const { selectedCoin, selectedCoinConvertTo, selectedFiat } = 16 | useContext(SelectAssetContext); 17 | const [amount, setAmount] = useState(0); 18 | const [amountError, setAmountError] = useState(''); 19 | 20 | const selectedAssets = { 21 | fiat: selectedFiat, 22 | coin: selectedCoin, 23 | coinConvertTo: selectedCoinConvertTo, 24 | }; 25 | 26 | const handleSubmit = (e, type, selectedAssets) => { 27 | e.preventDefault(); 28 | const transaction = createTransaction(e, type, selectedAssets, allCoins); 29 | const result = validateTransaction(transaction, allFiat, yourCoins); 30 | 31 | if (result?.isValid) { 32 | addTransaction(db, transaction); 33 | updateFiatBalance(db, transaction, allFiat); 34 | updateCoins(db, transaction, yourCoins); 35 | setAmount(0); 36 | } else { 37 | setAmountError(result.error); 38 | } 39 | }; 40 | 41 | return ( 42 | 43 |
handleSubmit(e, type, selectedAssets)} 45 | onChange={() => setAmountError('')}> 46 | {children} 47 |
48 |
49 | ); 50 | }; 51 | 52 | export default TransactionForm; 53 | -------------------------------------------------------------------------------- /src/utilities/update-coins.js: -------------------------------------------------------------------------------- 1 | import { addCoin } from './add-coin'; 2 | import { createCoin } from './create-coin'; 3 | import { findAsset } from './find-asset'; 4 | import { updateCoinBalance } from './update-coin-balance'; 5 | 6 | export const updateCoins = (db, transaction, coinAssets) => { 7 | try { 8 | const coin = findAsset(transaction?.coin?.id, coinAssets); 9 | let newCoinBalance; 10 | 11 | switch (transaction.type) { 12 | case 'buyCoin': 13 | newCoinBalance = coin?.balance_coin + transaction?.coin?.amount; 14 | if (coin) { 15 | updateCoinBalance(db, coin?.id, newCoinBalance); 16 | } else { 17 | const newCoin = createCoin( 18 | transaction?.coin, 19 | transaction?.coin?.amount 20 | ); 21 | addCoin(db, newCoin); 22 | } 23 | break; 24 | 25 | case 'sendCoin': 26 | case 'sellCoin': 27 | newCoinBalance = coin?.balance_coin - transaction?.coin?.amount; 28 | updateCoinBalance(db, coin.id, newCoinBalance); 29 | break; 30 | 31 | case 'convertCoin': 32 | const coinConvertTo = findAsset( 33 | transaction?.coinConvertTo?.id, 34 | coinAssets 35 | ); 36 | 37 | newCoinBalance = coin?.balance_coin - transaction?.coin?.amount; 38 | const newCoinConvertToBalance = 39 | coinConvertTo?.balance_coin + transaction?.coinConvertTo?.amount; 40 | 41 | if (coinConvertTo) { 42 | updateCoinBalance(db, coin.id, newCoinBalance); 43 | updateCoinBalance(db, coinConvertTo?.id, newCoinConvertToBalance); 44 | } else { 45 | updateCoinBalance(db, coin.id, newCoinBalance); 46 | const newCoin = createCoin( 47 | transaction?.coinConvertTo, 48 | transaction?.coinConvertTo?.amount 49 | ); 50 | addCoin(db, newCoin); 51 | } 52 | break; 53 | case 'cashoutFiat': 54 | case 'depositFiat': 55 | return; 56 | default: 57 | console.error('Unknown transaction type'); 58 | return; 59 | } 60 | } catch (err) { 61 | console.error(err); 62 | } 63 | }; 64 | -------------------------------------------------------------------------------- /src/hooks/useSelectAsset.js: -------------------------------------------------------------------------------- 1 | import { useContext, useEffect, useState } from 'react'; 2 | 3 | import { AssetsContext } from '../contexts/AssetsContext'; 4 | 5 | const useSelectAsset = () => { 6 | const { allCoins, allFiat } = useContext(AssetsContext); 7 | const [isSelectAssetOpen, setIsSelectAssetOpen] = useState(false); 8 | const [lastSelectAssetType, setLastSelectAssetType] = useState('coin'); 9 | const [selectedFiat, setSelectedFiat] = useState(allFiat[0]); 10 | const [selectedCoin, setSelectedCoin] = useState(allCoins[0]); 11 | const [selectedCoinConvertTo, setSelectedCoinConvertTo] = useState( 12 | allCoins[1] 13 | ); 14 | 15 | const setSelectAssetType = (e) => { 16 | if (e?.target?.matches('.selectFiat, .selectFiat *')) 17 | setLastSelectAssetType('fiat'); 18 | if (e?.target?.matches('.selectCoin, .selectCoin *')) 19 | setLastSelectAssetType('coin'); 20 | if (e?.target?.matches('.selectCoinConvertTo, .selectCoinConvertTo *')) 21 | setLastSelectAssetType('coinConvertTo'); 22 | }; 23 | 24 | const toggleIsSelectAssetOpen = () => 25 | setIsSelectAssetOpen(() => !isSelectAssetOpen); 26 | 27 | const handleSelectAssetOpenClick = (e) => { 28 | setSelectAssetType(e); 29 | toggleIsSelectAssetOpen(); 30 | }; 31 | 32 | const handleSelectAsset = (asset, lastSelectAssetType) => { 33 | if (asset.isFiat) setSelectedFiat(asset); 34 | if (!asset.isFiat && lastSelectAssetType === 'coin') setSelectedCoin(asset); 35 | if (!asset.isFiat && lastSelectAssetType === 'coinConvertTo') 36 | setSelectedCoinConvertTo(asset); 37 | toggleIsSelectAssetOpen(); 38 | }; 39 | 40 | const checkIsSelected = (asset, assetSelected1 = {}, assetSelected2 = {}) => 41 | assetSelected1?.symbol === asset.symbol || 42 | assetSelected2?.symbol === asset.symbol; 43 | 44 | useEffect(() => { 45 | setSelectedCoin(allCoins[0]); 46 | setSelectedCoinConvertTo(allCoins[1]); 47 | setSelectedFiat(allFiat[0]); 48 | }, [allCoins, allFiat]); 49 | 50 | return { 51 | isSelectAssetOpen, 52 | lastSelectAssetType, 53 | selectedCoin, 54 | selectedCoinConvertTo, 55 | selectedFiat, 56 | toggleIsSelectAssetOpen, 57 | handleSelectAssetOpenClick, 58 | handleSelectAsset, 59 | checkIsSelected, 60 | }; 61 | }; 62 | 63 | export default useSelectAsset; 64 | -------------------------------------------------------------------------------- /src/components/Tables/TableAssets.jsx: -------------------------------------------------------------------------------- 1 | import './TableAssets.css'; 2 | 3 | import { LineChart, Table, TableCellCoinName, TableCellWatch, Text } from '..'; 4 | 5 | import { convertToCurrency } from '../../utilities/convert-to-currency'; 6 | import { formatMarketCap } from '../../utilities/format-market-cap'; 7 | import useMediaQuery from '../../hooks/useMediaQuery'; 8 | 9 | const TableAssets = ({ assets }) => { 10 | const isWidthMin800 = useMediaQuery('(min-width: 800px)'); 11 | const isWidthMin400 = useMediaQuery('(min-width: 400px)'); 12 | 13 | return ( 14 | 15 | {isWidthMin800 && ( 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | )} 27 | 28 | {assets.map((asset) => ( 29 | 30 | 37 | 38 | {isWidthMin400 && ( 39 | 49 | )} 50 | 58 | {isWidthMin800 && ( 59 | <> 60 | 65 | 68 | 71 | 72 | )} 73 | 74 | ))} 75 | 76 |
NamePast 24hPriceChangeMarket capWatch
31 | 36 | 40 |
41 | 47 |
48 |
51 | {convertToCurrency(asset?.price_eur)} 52 | {!isWidthMin800 && ( 53 | 54 | {asset?.price_change24h}% 55 | 56 | )} 57 | 61 | 62 | {asset?.price_change24h}% 63 | 64 | 66 | {formatMarketCap(asset?.market_cap)} 67 | 69 | 70 |
77 | ); 78 | }; 79 | 80 | export default TableAssets; 81 | -------------------------------------------------------------------------------- /src/pages/Trade.jsx: -------------------------------------------------------------------------------- 1 | import './Trade.css'; 2 | 3 | import { 4 | ContentCenter, 5 | ContentRight, 6 | Dropdown, 7 | Search, 8 | Section, 9 | SectionTitle, 10 | TabTrade, 11 | TableAssets, 12 | TableRecentTransactions, 13 | } from '../components'; 14 | 15 | import { AssetsContext } from '../contexts/AssetsContext'; 16 | import useCombineSearchFilter from '../hooks/useCombineSearchFilter'; 17 | import { useContext } from 'react'; 18 | import useFilter from '../hooks/useFilter'; 19 | import useMediaQuery from '../hooks/useMediaQuery'; 20 | import useSearch from '../hooks/useSearch'; 21 | 22 | const ASSET_OPTIONS = ['All assets', 'Watchlist']; 23 | 24 | const Trade = () => { 25 | const isWidthMin1150 = useMediaQuery('(min-width: 1150px)'); 26 | const isWidthMin800 = useMediaQuery('(min-width: 800px)'); 27 | const { allCoins } = useContext(AssetsContext); 28 | 29 | const { searchResult, searchInput, handleSearch } = useSearch(allCoins); 30 | const { filterResult, filterInput, handleFilter } = useFilter( 31 | allCoins, 32 | 'All assets' 33 | ); 34 | const { searchFilterResult } = useCombineSearchFilter( 35 | searchResult, 36 | filterResult, 37 | 'symbol' 38 | ); 39 | 40 | const assetsInTable = 41 | searchInput || filterInput !== 'All assets' ? searchFilterResult : allCoins; 42 | 43 | const watchlistQuery = (asset) => asset.onWatchlist; 44 | 45 | return ( 46 | <> 47 | 48 |
49 | 50 |
51 | 56 | {isWidthMin800 && ( 57 |
58 | handleFilter(e, allCoins, watchlistQuery)} 63 | /> 64 |
65 | )} 66 |
67 | 68 |
69 |
70 | 71 | {isWidthMin1150 && ( 72 |
73 | 74 |
75 | )} 76 |
77 | 78 | 79 |
80 |
81 | 82 | ); 83 | }; 84 | 85 | export default Trade; 86 | -------------------------------------------------------------------------------- /src/hooks/useAuth.js: -------------------------------------------------------------------------------- 1 | import { 2 | GoogleAuthProvider, 3 | createUserWithEmailAndPassword, 4 | getAuth, 5 | onAuthStateChanged, 6 | signInAnonymously, 7 | signInWithEmailAndPassword, 8 | signOut, 9 | } from 'firebase/auth'; 10 | import { useContext, useState } from 'react'; 11 | 12 | import { UserContext } from '../contexts/UserContext'; 13 | import { auth } from '../firebase-config'; 14 | 15 | const useAuth = () => { 16 | const { setUser, authError, setAuthError } = useContext(UserContext); 17 | const [signInEmail, setSignInEmail] = useState(''); 18 | const [signInPassword, setSignInPassword] = useState(''); 19 | const [signUpEmail, setSignUpEmail] = useState(''); 20 | const [signUpPassword, setSignUpPassword] = useState(''); 21 | 22 | const handleSignIn = async (signInMethod, e = {}) => { 23 | const provider = new GoogleAuthProvider(); 24 | const auth = getAuth(); 25 | 26 | try { 27 | switch (signInMethod) { 28 | case 'email': 29 | e.preventDefault(); 30 | await signInWithEmailAndPassword(auth, signInEmail, signInPassword); 31 | setAuthError(''); 32 | break; 33 | 34 | case 'guest': 35 | await signInAnonymously(auth); 36 | setAuthError(''); 37 | break; 38 | 39 | default: 40 | return; 41 | } 42 | } catch (error) { 43 | setAuthError(error?.code); 44 | console.error(error?.code); 45 | } 46 | }; 47 | 48 | const handleSignUp = async (signUpMethod, event = {}) => { 49 | const provider = new GoogleAuthProvider(); 50 | const auth = getAuth(); 51 | 52 | try { 53 | switch (signUpMethod) { 54 | case 'email': 55 | event.preventDefault(); 56 | await createUserWithEmailAndPassword( 57 | auth, 58 | signUpEmail, 59 | signUpPassword 60 | ); 61 | setAuthError(''); 62 | break; 63 | 64 | case 'guest': 65 | await signInAnonymously(auth); 66 | setAuthError(''); 67 | break; 68 | 69 | default: 70 | return; 71 | } 72 | } catch (error) { 73 | setAuthError(error?.code); 74 | console.error(error?.code); 75 | } 76 | }; 77 | 78 | const signout = () => signOut(auth); 79 | 80 | onAuthStateChanged(auth, (currentUser) => setUser(currentUser)); 81 | 82 | return { 83 | signInEmail, 84 | setSignInEmail, 85 | signInPassword, 86 | setSignInPassword, 87 | signUpEmail, 88 | setSignUpEmail, 89 | signUpPassword, 90 | setSignUpPassword, 91 | authError, 92 | setAuthError, 93 | handleSignIn, 94 | handleSignUp, 95 | signout, 96 | }; 97 | }; 98 | 99 | export default useAuth; 100 | -------------------------------------------------------------------------------- /src/components/Charts/ChartPortfolio.jsx: -------------------------------------------------------------------------------- 1 | import './ChartPortfolio.css'; 2 | 3 | import { Dropdown, LineChart, Text } from '..'; 4 | 5 | import { calculateTotalBalance } from '../../utilities/calculate-total-balance'; 6 | import classNames from 'classnames'; 7 | import { convertToCurrency } from '../../utilities/convert-to-currency'; 8 | import { createChartTimes } from '../../utilities/create-chart-times'; 9 | import useBalanceHistory from '../../hooks/useBalanceHistory'; 10 | import useMediaQuery from '../../hooks/useMediaQuery'; 11 | import { useState } from 'react'; 12 | 13 | const TIMEFRAME_OPTIONS = ['1D', '1W', '1M', '1Y']; 14 | 15 | const ChartPortfolio = ({ assets }) => { 16 | const isWidthMax600 = useMediaQuery('(max-width: 600px)'); 17 | const [activeTimeFrame, setActiveTimeFrame] = useState('1M'); 18 | const portfolioBalance = convertToCurrency(calculateTotalBalance(assets)); 19 | const chartTimes = createChartTimes(); 20 | const balanceHistory = useBalanceHistory(activeTimeFrame); 21 | 22 | return ( 23 |
24 |
25 |
26 | 27 | Portfolio balance 28 | 29 |
30 | {isWidthMax600 && ( 31 | setActiveTimeFrame(e.target.value)} 36 | /> 37 | )} 38 | {!isWidthMax600 && 39 | TIMEFRAME_OPTIONS.map((time) => { 40 | const timeFrameBtnClasses = classNames({ 41 | timeFrameBtn: true, 42 | timeFrameBtn__active: activeTimeFrame === time, 43 | }); 44 | 45 | return ( 46 |
setActiveTimeFrame(time)}> 50 | {time} 51 |
52 | ); 53 | })} 54 |
55 |
56 | 57 | {portfolioBalance} 58 | 59 |
60 |
61 | 67 |
68 |
69 | {chartTimes[activeTimeFrame].map((date) => ( 70 |
71 | {date} 72 |
73 | ))} 74 |
75 |
76 | ); 77 | }; 78 | 79 | export default ChartPortfolio; 80 | -------------------------------------------------------------------------------- /src/components/TabContents/TabContentSelectAsset.jsx: -------------------------------------------------------------------------------- 1 | import './TabContentSelectAsset.css'; 2 | 3 | import { FaArrowLeft, FaCheck } from 'react-icons/fa'; 4 | import { Search, TabContent, Table, TableCellCoinName, Text } from '..'; 5 | 6 | import { AssetsContext } from '../../contexts/AssetsContext'; 7 | import { SelectAssetContext } from '../../contexts/SelectAssetContext'; 8 | import { convertToCurrency } from '../../utilities/convert-to-currency'; 9 | import { useContext } from 'react'; 10 | import useSearch from '../../hooks/useSearch'; 11 | 12 | const TabContentSelectAsset = () => { 13 | const { 14 | toggleIsSelectAssetOpen, 15 | selectedCoin, 16 | selectedFiat, 17 | lastSelectAssetType, 18 | handleSelectAsset, 19 | checkIsSelected, 20 | } = useContext(SelectAssetContext); 21 | const { allCoins, allFiat } = useContext(AssetsContext); 22 | const assets = lastSelectAssetType === 'fiat' ? allFiat : allCoins; 23 | 24 | const { searchResult, searchInput, handleSearch } = useSearch(assets); 25 | 26 | return ( 27 | 28 |
29 | 30 | Select Asset 31 |
32 | 38 | 39 | 40 | {searchResult.map((asset) => ( 41 | handleSelectAsset(asset, lastSelectAssetType)}> 49 | 56 | 73 | 74 | ))} 75 | 76 |
50 | 55 | 57 |
58 | {lastSelectAssetType === 'fiat' ? ( 59 | {convertToCurrency(asset?.balance_eur)} 60 | ) : ( 61 |
62 | {`${asset?.balance_coin} ${asset?.symbol}`} 63 | 64 | {convertToCurrency(asset?.balance_eur)} 65 | 66 |
67 | )} 68 | {checkIsSelected(asset, selectedCoin, selectedFiat) && ( 69 | 70 | )} 71 |
72 |
77 |
78 | ); 79 | }; 80 | 81 | export default TabContentSelectAsset; 82 | -------------------------------------------------------------------------------- /Questions.md: -------------------------------------------------------------------------------- 1 | # Questions 2 | 3 | ## Do I need to change my 'yourCoin' ids to support multiple users? How to structure data? 4 | 5 | Situation: 6 | 7 | - In Firestore, the documents in the 'yourCoins' collection get their id's from the Coinranking API 8 | - e.g. When I fetch Bitcoin prices from Coinranking, I get a Bitcoin Id (e.g. 123) and I take that Id and use it in Firebase to store the Bitcoin object (including the coin balance the user currently holds) 9 | - This worked well (and made my code simple) until I tried adding multi-user support 10 | - Now e.g. 2 users can hold Bitcoin and thus I would need 2 Bitcoin objects with unique ids 11 | - While it would be possible to do that, my code would become more complicated, e.g. when I try to exchange information between Coinranking and Firestore 12 | 13 | Questions: 14 | 15 | - Do I need to refactor my Firestore Ids / Id generation process or is there another way? 16 | - Would you just change the ids to make them unique or change the data structure as awhole? 17 | - Would you recommend to calculate everything from a single source of truth (e.g. transactions collection), even if it makes some of the code more complicated? (e.g. I would need to recalculate the current Bitcoin coin balance each time from transactions instead getting it directly from 'yourCoins' collection) 18 | 19 | ## Memory leak error (again) 20 | 21 | Situation: 22 | 23 | - When I navigate from the /assets page to the /trade page and then e.g. Buy coins there (in the right side Tab), I'm consistently getting multiple memory errors: 24 | 25 | `Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.`` 26 | 27 | - The errors point to elements from the /assets page as source (e.g. `at ChartPortfolio`, `at Assets`) 28 | 29 | Question: 30 | 31 | - Could you please help me understand and fix this issue? I know you already helped me fix it once in the Todoist clone with some sort of cleanup/unsubscription. But I'm unable to do it on my own now. 32 | - It seems to be so common that I would like to learn how to fix it myself in the future. 33 | 34 | ## 'Violation' terminal warnings on /trade page 35 | 36 | Situation: 37 | 38 | - when loading the /trade page I get 2 terminal warnings: 39 | [Violation] Forced reflow while executing JavaScript took 63ms. 40 | [Violation] 'setTimeout' handler took 50ms. 41 | 42 | - In addition, when clicking the Star icon, I also get the foloowing warning: 43 | [Violation] 'setTimeout' handler took 50ms. 44 | 45 | Question: 46 | 47 | - First question: Should I worry about it at the moment and work on a fix? (I've mostly avoided studying about React/Javascript performance as I had the feeling I wanted to learn the basics first) 48 | - Is it easy for you to pinpoint the reason what's causing it? If yes, what is it? Does it have todo with data fetching taking time? 49 | 50 | ## Terminal warning "React Hook useEffect has a missing dependency: functions 51 | 52 | Situation: 53 | 54 | - while developing I sometimes got the terminal waring `React Hook useEffect has a missing dependency` and then the name of a function I'm using in my useEffect hook 55 | - the warnings seems to have gone away (not sure why) but were so common for a while that I would like to understand how to handle them in the future 56 | - I get the abstract idea of useEffect wanting all parameters that can change in it's dependecy array? 57 | - What I don't get is why it sometimes wants functions in the dependency array (not the function arguments). 58 | - And if I tried to add the function as a dependency, I got another error that it was an invalid dependency or something? 59 | 60 | e.g. 61 | ./hooks/useGetCoins.js#L59 62 | ./hooks/useMediaQuery.js#L20 63 | 64 | Question: 65 | 66 | - Do you know this type of warning and how do you handle it? 67 | 68 | ## Should I use optional chaining (?.) by default? 69 | 70 | Situation: 71 | 72 | - While implementing data fetching and the business logic I've run multiple times into hard errors because I tried to access an object property that whas not yet fully fetched 73 | - To fix the issues I've again and again used optional chaining (?.), so much so that I'm asking myself now if I should opt for it by default now when accessing any object property in the future 74 | - It would add more code, but I would be at least on the safe side then 75 | 76 | Question: 77 | 78 | - What do you think? How are you dealing with it? 79 | -------------------------------------------------------------------------------- /src/components/index.js: -------------------------------------------------------------------------------- 1 | import AuthError from './Auth/AuthError'; 2 | import AuthLayout from './Auth/AuthLayout'; 3 | import Button from './Button/Button'; 4 | import ChartPortfolio from './Charts/ChartPortfolio'; 5 | import Content from './Content/Content'; 6 | import ContentCenter from './Content/ContentCenter'; 7 | import ContentRight from './Content/ContentRight'; 8 | import Dashboard from './Dashboard/Dashboard'; 9 | import Dropdown from './Dropdown/Dropdown'; 10 | import Footer from './Footer/Footer'; 11 | import Header from './Header/Header'; 12 | import Input from './Inputs/Input'; 13 | import InputAmountContainer from './Inputs/InputAmountContainer'; 14 | import LineChart from './Chart/LineChart'; 15 | import Logo from './Logo/Logo'; 16 | import Main from './Main/Main'; 17 | import MenuMobile from './MenuMobile/MenuMobile'; 18 | import Modal from './Modal/Modal'; 19 | import ModalClose from './Modal/ModalClose'; 20 | import ModalDeposit from './Modals/ModalDeposit'; 21 | import ModalPay from './Modals/ModalPay'; 22 | import ModalProfile from './Modals/ModalProfile'; 23 | import ModalTrade from './Modals/ModalTrade'; 24 | import ModalsManager from './ModalsManager/ModalsManager'; 25 | import Search from './Inputs/Search'; 26 | import Section from './Section/Section'; 27 | import SectionTitle from './Section/SectionTitle'; 28 | import Sidebar from './Sidebar/Sidebar'; 29 | import SidebarNavItem from './Sidebar/SidebarNavItem'; 30 | import SignInForm from './Auth/SignInForm'; 31 | import SignUpForm from './Auth/SignUpForm'; 32 | import Star from './Star/Star'; 33 | import Tab from './Tab/Tab'; 34 | import TabContent from './Tab/TabContent'; 35 | import TabContentAddCash from './TabContents/TabContentAddCash'; 36 | import TabContentBuy from './TabContents/TabContentBuy'; 37 | import TabContentBuyEmpty from './TabContents/TabContentBuyEmpty'; 38 | import TabContentCashout from './TabContents/TabContentCashout'; 39 | import TabContentConvert from './TabContents/TabContentConvert'; 40 | import TabContentReceive from './TabContents/TabContentReceive'; 41 | import TabContentSelectAsset from './TabContents/TabContentSelectAsset'; 42 | import TabContentSell from './TabContents/TabContentSell'; 43 | import TabContentSend from './TabContents/TabContentSend'; 44 | import TabDeposit from './Tabs/TabDeposit'; 45 | import TabFooter from './Tab/TabFooter'; 46 | import TabPay from './Tabs/TabPay'; 47 | import TabTrade from './Tabs/TabTrade'; 48 | import Table from './Table/Table'; 49 | import TableAssets from './Tables/TableAssets'; 50 | import TableCellCoinName from './Table/TableCellCoinName'; 51 | import TableCellWatch from './Table/TableCellWatch'; 52 | import TableFiatTransfers from './Tables/TableFiatTransfers'; 53 | import TableInputAddCash from './TableInputs/TableInputAddCash'; 54 | import TableInputBuy from './TableInputs/TableInputBuy'; 55 | import TableInputCashout from './TableInputs/TableInputCashout'; 56 | import TableInputConvert from './TableInputs/TableInputConvert'; 57 | import TableInputSell from './TableInputs/TableInputSell'; 58 | import TableInputSend from './TableInputs/TableInputSend'; 59 | import TablePayments from './Tables/TablePayments'; 60 | import TableReceive from './Tables/TableReceive'; 61 | import TableRecentTransactions from './Tables/TableRecentTransactions'; 62 | import TableRowAssetAddress from './Table/TableRowAssetAddress'; 63 | import TableRowInputText from './Table/TableRowInputText'; 64 | import TableRowQR from './Table/TableRowQR'; 65 | import TableRowSelectAsset from './Table/TableRowSelectAsset'; 66 | import TableYourAssets from './Tables/TableYourAssets'; 67 | import Text from './Text/Text'; 68 | import Tooltip from './Tooltip/Tooltip'; 69 | import TransactionForm from './TransactionForm/TransactionForm'; 70 | 71 | export { 72 | AuthLayout, 73 | SignUpForm, 74 | SignInForm, 75 | AuthError, 76 | Dashboard, 77 | Main, 78 | Sidebar, 79 | SidebarNavItem, 80 | Header, 81 | Content, 82 | ContentCenter, 83 | ContentRight, 84 | Section, 85 | SectionTitle, 86 | Button, 87 | Text, 88 | Table, 89 | TableCellCoinName, 90 | TableCellWatch, 91 | TableRowSelectAsset, 92 | TableRowAssetAddress, 93 | TableRowInputText, 94 | TableRowQR, 95 | TableYourAssets, 96 | TableAssets, 97 | TableRecentTransactions, 98 | TablePayments, 99 | TableReceive, 100 | TableFiatTransfers, 101 | TableInputSend, 102 | TableInputBuy, 103 | TableInputSell, 104 | TableInputConvert, 105 | TableInputAddCash, 106 | TableInputCashout, 107 | Star, 108 | Input, 109 | InputAmountContainer, 110 | Search, 111 | Dropdown, 112 | Tab, 113 | TabFooter, 114 | TabTrade, 115 | TabPay, 116 | TabDeposit, 117 | TabContentBuy, 118 | TabContent, 119 | TabContentSell, 120 | TabContentConvert, 121 | TabContentBuyEmpty, 122 | TabContentSend, 123 | TabContentReceive, 124 | TabContentAddCash, 125 | TabContentCashout, 126 | TabContentSelectAsset, 127 | ChartPortfolio, 128 | LineChart, 129 | ModalsManager, 130 | Modal, 131 | ModalClose, 132 | ModalPay, 133 | ModalTrade, 134 | ModalDeposit, 135 | ModalProfile, 136 | MenuMobile, 137 | Tooltip, 138 | Logo, 139 | Footer, 140 | TransactionForm, 141 | }; 142 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Simplified Coinbase Clone

2 |

Build with React, JavaScript, and Firebase. Real-time data from Coinranking API.

3 | 4 | ![Coinbase Clone Screenshot](coinbase-clone-preview.jpg) 5 | [Go to live app](https://coinbase-clone.com/) 6 | 7 | ## What is this and who is it for ⭐ 8 | 9 | This is a simplified Coinbase clone built with React, Firebase, and the Coinranking API. The app lets you check prices, price histories, and market cap of popular cryptocurrencies. Furthermore, you can simulate trading crypto assets, sending crypto to friends as well as depositing and withdrawing fiat currencies. 10 | 11 | After building a [Todoist Clone as my first React project](https://github.com/maker0101/Todoist_Clone) I was looking for a second React training project where I could: 12 | 13 | - Continue to practice building reusable components and hooks. 14 | - Work with external data APIs for data fetching 15 | - Build my first Charts :-) 16 | 17 | This project focuses on the Frontend. You are very welcome to have a look at the code. 18 | As this project helped me tremendously on my journey to become an awesome Frontend Developer, I hope it provides some value for you too 🤓. 19 | 20 | ## Features and Technologies 21 | 22 | - Written in **modern React** and **JavaScript** 23 | - **Simple** vanilla React **state management** (without Redux, Recoil, or similar) 24 | - **Coinbase UI** (as of March 2022) 25 | - **Firebase** as backend (for authentication and database) 26 | - Crypto data fetched using [Coinranks awesome API](https://developers.coinranking.com/api) via [RapidAPI](https://rapidapi.com/) 27 | - Charts created with [Chart.js](https://www.chartjs.org/) 28 | 29 | ## Getting started 🛠 30 | 31 | - Signup for a [RapidAPI account](https://rapidapi.com/), set up an app, and Subscribe to the [Coinranking API](https://rapidapi.com/Coinranking/api/coinranking1/) 32 | - Signup for a [Firebase account](https://firebase.google.com/) if you don't have it already, create a new project, then set up a web app. 33 | - Inside Firebase, set up Firestore and create three collections `transactions`, `yourCoins`, `yourFiat`. 34 | - Inside Firebase, setup Firebase authentication and enable Email/Password, Anonymous, and Google SignIn. 35 | - `git clone https://github.com/maker0101/Coinbase_Clone` 36 | - Create an empty `.env.local` file in the root directory, copy `.env.local.example` contents into it, and fill `XXX` placeholders with your RapidAPI and Firebase project credentials. 37 | - `npm install` 38 | - `npm start` 39 | - The app should now be running on `http://localhost:3000/`. 40 | 41 | ## Shortcomings 42 | 43 | I'm aware of the following shortcomings listed below: 44 | 45 | ### Performance 🚀 46 | 47 | - The current app is not performance optimized 48 | - Currently, there is no data caching of responses implemented, resulting in considerable layout shifts 49 | 50 | ### Security 🔒 51 | 52 | - All validations of transactions are currently taking place on the client. For a production app, transaction validations should be implemented in the backend. 53 | - Separating production and development environments is recommended. 54 | 55 | ### Styling 🎨 56 | 57 | - To make the app more maintainable, repeating properties like colors, breakpoints, and sizes should be stored and used as custom CSS properties. 58 | - The current app implementation performs styling mostly via classnames. But in situations where truly dynamic CSS properties are needed, some inline styling is used. While this is working, switching to a unified styling approach (e.g. with 'styled-components') is recommended. 59 | - App uses BEM classname convention to prevent classname collisions. While working, adopting e.g. 'styled-components' would eliminate the need for BEM, because of automatic unique class name generation 60 | 61 | ### Data management 🗄 62 | 63 | - Currently, all users share the same data. This is a major shortcoming and I will work on fixing it soon. 64 | 65 | ### Accessibility ♿ 66 | 67 | No extra efforts have been put into making this project more accessible. For a production-ready app, adding aria roles and keyboard support would go a long way toward making the app more accessible. 68 | 69 | ### Testing 🧪 70 | 71 | - Testing is currently missing. This is a major shortcoming and I will work on fixing it soon. 72 | 73 | ## Author: Max Breitsprecher 74 | 75 | - Website: [maxbreitsprecher.com](https://www.maxbreitsprecher.com/) 76 | - Email: max.breitsp@gmail.com 77 | - Github profile: [github.com/maker0101](https://github.com/maker0101) 78 | 79 | If you have any questions or feedback, feel free to say hi. 👋 80 | 81 | ## License 82 | 83 | [MIT](https://opensource.org/licenses/MIT) 84 | 85 | ## Shout out 86 | 87 | - This great [YouTube video from JavaScript Mastery](https://youtu.be/9DDX3US3kss) helped me discover the Coinranking API and the Rapid API service. 88 | - Again, I would like to thank my awesome code tutor [Esen](https://github.com/snqb) for his invaluable feedback, code reviews, and words of encouragement. 89 | 90 | ## Disclaimer 91 | 92 | This project is entirely for educational purposes. It is in no way connected to Coinbase, the app or Coinbase, the company. I'm just a fan of their app and figured it would be a fun, educational challenge to recreate a simplified web-app version of it. 93 | --------------------------------------------------------------------------------