├── .eslintrc ├── .gitignore ├── .sequelizerc ├── README.md ├── api ├── authApi.ts ├── index.ts ├── marketApi.ts └── userApi.ts ├── components ├── Banner │ ├── Banner.module.scss │ └── index.tsx ├── BarChart │ └── index.tsx ├── Button │ ├── Button.module.scss │ └── index.tsx ├── BuySellCard │ ├── BuySell │ │ ├── BuySell.module.scss │ │ └── index.tsx │ ├── BuySellCard.module.scss │ └── index.tsx ├── Card │ ├── Card.module.scss │ └── index.tsx ├── ChartIntervalProvider │ ├── ChartIntervalProvider.module.scss │ └── index.tsx ├── ContentLayout │ ├── ContentLayout.module.scss │ └── index.tsx ├── CopyButton │ └── index.tsx ├── EducationCard │ ├── EducationCard.module.scss │ └── index.tsx ├── LandingLayout │ ├── LandingHeader │ │ ├── LandingHeader.module.scss │ │ └── index.tsx │ ├── LandingLayout.module.scss │ └── index.tsx ├── Layout │ ├── Layout.module.scss │ └── index.tsx ├── MarketChart │ ├── CustomBrushChart │ │ └── index.tsx │ ├── CustomChart │ │ └── index.tsx │ ├── CustomTooltip │ │ └── index.tsx │ ├── IntervalSelector │ │ ├── IntervalSelector.module.scss │ │ └── index.tsx │ ├── MarketChart.module.scss │ └── index.tsx ├── MarketTable │ ├── MarketTable.module.scss │ ├── SortableTable │ │ ├── SortableTable.module.scss │ │ └── index.tsx │ └── index.tsx ├── MarketTransactions │ ├── MarketTransactions.module.scss │ └── index.tsx ├── Navbar │ ├── NavItem │ │ ├── NavItem.module.scss │ │ └── index.tsx │ ├── Navbar.module.scss │ ├── UserDialog │ │ ├── UserDialog.module.scss │ │ └── index.tsx │ └── index.tsx ├── Pagination │ ├── Pagination.module.scss │ └── index.tsx ├── Paper │ ├── Paper.module.scss │ └── index.tsx ├── PieChart │ ├── PieChart.module.scss │ └── index.tsx ├── PortfolioBalanceCard │ ├── PortfolioBalanceCard.module.scss │ └── index.tsx ├── Preloader │ ├── Preloader.module.scss │ └── index.tsx ├── PriceChangeField │ ├── PriceChangeField.module.scss │ └── index.tsx ├── PriceStatistics │ ├── PriceStatistics.module.scss │ ├── PriceStatisticsGroup │ │ ├── PriceStatisticsGroup.module.scss │ │ └── index.tsx │ ├── PriceStatisticsItem │ │ ├── PriceStatisticsItem.module.scss │ │ └── index.tsx │ └── index.tsx ├── RecentTransactions │ ├── RecentTransaction.module.scss │ └── index.tsx ├── TextField │ ├── TextField.module.scss │ └── index.tsx ├── TransactionHistory │ ├── TransactionHistory.module.scss │ └── index.tsx ├── Typography │ ├── Typography.module.scss │ └── index.tsx ├── Watchlist │ ├── Watchlist.module.scss │ └── index.tsx └── WatchlistButton │ ├── WatchlistButton.module.scss │ └── index.tsx ├── db ├── config │ └── config.js ├── migrations │ ├── 20210713145001-create-user.js │ ├── 20210713145652-create-currency.js │ ├── 20210713150012-create-asset.js │ ├── 20210713154145-create-transaction.js │ ├── 20210713154345-add-asset-associate.js │ ├── 20210716143023-create-historical-data.js │ ├── 20210716143446-create-balance.js │ ├── 20210716143529-create-pnl.js │ ├── 20210716147521-add-historical-data-associate.js │ ├── 20210731114944-create-watch.js │ └── 20210731115521-add-watch-associate.js └── models │ ├── asset.js │ ├── balance.js │ ├── currency.js │ ├── historicalData.js │ ├── index.js │ ├── pnl.js │ ├── transaction.js │ ├── user.js │ └── watch.js ├── hooks ├── useControlBuySell.ts ├── useControlInput.ts ├── useDebounce.ts ├── useDidMount.ts ├── useFullscreenStatus.ts ├── useMediaQuery.ts └── useSortableData.ts ├── middlewares └── passport.ts ├── next-env.d.ts ├── next.config.js ├── package.json ├── pages ├── _app.tsx ├── api │ ├── admin │ │ └── update.ts │ ├── auth │ │ └── [...slug].ts │ ├── hello.ts │ └── user │ │ ├── assets │ │ ├── [...slug].ts │ │ └── index.ts │ │ ├── portfolio │ │ ├── historical │ │ │ ├── balanceData.ts │ │ │ └── pnlData.ts │ │ ├── index.ts │ │ └── transactionHistory.ts │ │ └── watchlist │ │ ├── [currencyId].ts │ │ └── index.ts ├── education │ └── index.tsx ├── home │ ├── Home.module.scss │ └── index.tsx ├── index.module.scss ├── index.tsx ├── login │ ├── Login.module.scss │ └── index.tsx ├── logout │ ├── Logout.module.scss │ └── index.tsx ├── market │ ├── Market.module.scss │ ├── [currencyId].tsx │ └── index.tsx ├── portfolio │ ├── Portfolio.module.scss │ ├── index.tsx │ └── transactionHistory.tsx ├── referral │ ├── Referral.module.scss │ └── index.tsx ├── register │ ├── Register.module.scss │ └── index.tsx └── verification │ ├── Verification.module.scss │ ├── [hash].tsx │ └── index.tsx ├── public ├── favicon.ico ├── static │ ├── back.svg │ ├── bought.svg │ ├── btc.png │ ├── check.svg │ ├── close.svg │ ├── coinGecko.png │ ├── crypto.png │ ├── cryptocurrencies.svg │ ├── cryptocurrency.svg │ ├── education.svg │ ├── filledStar.svg │ ├── fullscreen.svg │ ├── fullscreenExit.svg │ ├── github.svg │ ├── home.svg │ ├── lineChart.svg │ ├── linkedin.svg │ ├── loading.svg │ ├── loadingMini.svg │ ├── logo.png │ ├── logout.svg │ ├── market.svg │ ├── menu.svg │ ├── portfolio.png │ ├── portfolio.svg │ ├── referral.svg │ ├── search.svg │ ├── security.svg │ ├── star.svg │ ├── swap.svg │ ├── toll.svg │ ├── unverified.svg │ ├── usd.svg │ ├── user.svg │ ├── verification.svg │ └── verified.svg └── vercel.svg ├── store ├── index.ts ├── selectors.ts └── slices │ ├── assetsSlice.ts │ ├── types.ts │ ├── userSlice.ts │ └── watchlistSlice.ts ├── styles ├── globals.scss └── theme.scss ├── tsconfig.json ├── utils ├── apiRoutes │ ├── getAssets.ts │ ├── getAssetsMarketData.ts │ ├── getPaginatedData.ts │ ├── getPagination.ts │ ├── getTransactionHistory.ts │ ├── routeHandler.ts │ └── updateUsdtAsset.ts ├── checkAuth.ts ├── createJwtToken.ts ├── createPagination.ts ├── formatDollar.ts ├── formatPercent.ts ├── generateHash.ts ├── isDevMode.ts └── roundDec.ts └── yarn.lock /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next", "next/core-web-vitals"] 3 | } 4 | -------------------------------------------------------------------------------- /.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 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env 29 | .env.local 30 | .env.development.local 31 | .env.test.local 32 | .env.production.local 33 | 34 | # vercel 35 | .vercel 36 | 37 | /db/seeders/ 38 | #/db/seeders/* 39 | #!/db/seeders/20210713150632-currency.js -------------------------------------------------------------------------------- /.sequelizerc: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | module.exports = { 3 | 'config': path.resolve('db/config', 'config.js'), 4 | 'models-path': path.resolve('db', 'models'), 5 | 'seeders-path': path.resolve('db', 'seeders'), 6 | 'migrations-path': path.resolve('db', 'migrations') 7 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TryCrypto 2 | 3 | An application that simulates the work of a cryptocurrency exchange. You can try to work with cryptocurrency: buy, sell, watch the dynamics of prices, view your transaction history 4 | 5 | ![TC Preview](https://i.ibb.co/Xsg4CRM/try-crypto-herokuapp-com.png) 6 | 7 | ## Features 8 | 9 | - SSR 10 | - Data provided by CoinGecko API 11 | - Responsive design 12 | - Portfolio with daily PNL 13 | - Transaction history 14 | - Email verification 15 | 16 | ## Technologies 17 | 18 | The application is written in TypeScript. The main feature is Server Side Rendering, which is made using React framework Next JS. The backend is made using the Route API provided by Next JS 19 | 20 | Technologies used: 21 | - React, React-Hook-Form 22 | - Redux-Toolkit 23 | - Next JS, Route API, SWR, Next-Connect 24 | - Sass, Css Modules 25 | - Victory (for charts) 26 | - Sequelize, Passport, JWT, Nodemailer 27 | 28 | The application has a MySQL database with ORM Sequelize. Entity relationship diagram of database: 29 | 30 | ![TC DB](https://i.ibb.co/0X2yMsS/Try-Crypto-DB.png) 31 | 32 | -------------------------------------------------------------------------------- /api/authApi.ts: -------------------------------------------------------------------------------- 1 | import { AxiosInstance } from 'axios'; 2 | import { User } from 'store/slices/types'; 3 | 4 | export type AuthPayload = { 5 | email: string; 6 | password: string; 7 | ref: string | undefined; 8 | }; 9 | 10 | export const authApi = (instance: AxiosInstance) => { 11 | return { 12 | register: ({ email, password, ref }: AuthPayload): Promise => { 13 | return instance.post(`/auth/register${ref ? '?ref=' + ref : ''}`, { email, password }); 14 | }, 15 | login: (payload: AuthPayload): Promise => { 16 | return instance.post(`/auth/login`, payload).then(({ data }) => data.data); 17 | }, 18 | getMe: (): Promise => { 19 | return instance.get(`/auth/me`).then(({ data }) => data.data); 20 | }, 21 | sendEmail: (): Promise => { 22 | return instance.get(`/auth/sendEmail`); 23 | }, 24 | verify: (hash: string): Promise => { 25 | return instance.patch(`/auth/verify/${hash}`).then(({ data }) => data.data); 26 | }, 27 | getNumberOfReferrals: (): Promise => { 28 | return instance.get(`/auth/referrals`).then(({ data }) => data.data); 29 | }, 30 | }; 31 | }; 32 | -------------------------------------------------------------------------------- /api/index.ts: -------------------------------------------------------------------------------- 1 | import Cookies from 'js-cookie'; 2 | import { authApi } from './authApi'; 3 | import axios from 'axios'; 4 | import { userApi } from './userApi'; 5 | import { isDevMode } from 'utils/isDevMode'; 6 | 7 | type ApiReturnType = ReturnType & ReturnType; 8 | 9 | export const Api = (token?: string): ApiReturnType => { 10 | if (!token) token = Cookies.get('token'); 11 | 12 | const instance = axios.create({ 13 | //baseURL: 'http://localhost:3000/api', 14 | baseURL: isDevMode() ? 'http://localhost:3000/api' : 'https://try-crypto.herokuapp.com/api', 15 | headers: { 16 | Authorization: 'Bearer ' + token || Cookies.get('token'), 17 | }, 18 | }); 19 | 20 | return [userApi, authApi].reduce((prev, f) => ({ ...prev, ...f(instance) }), {} as ApiReturnType); 21 | }; 22 | -------------------------------------------------------------------------------- /api/marketApi.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | const instance = axios.create({ 4 | baseURL: 'https://api.coingecko.com/api/v3/', 5 | }); 6 | 7 | export const fetcher = (...urls: string[]) => { 8 | const f = (u: string) => instance.get(u).then((r) => r.data); 9 | if (urls.length > 1) return Promise.all(urls.map(f)); 10 | 11 | return f(urls[0]); 12 | }; 13 | 14 | export type TableCoin = { 15 | ordNum: number; 16 | id: string; 17 | symbol: string; 18 | name: string; 19 | image: string; 20 | current_price: number; 21 | price_change_percentage_24h: number; 22 | price_change_percentage_7d_in_currency: number; 23 | market_cap: number; 24 | sparkline_in_7d: { price: number[] }; 25 | }; 26 | 27 | export type TableConfig = { 28 | direction: Direction; 29 | key: Key; 30 | }; 31 | 32 | export type Direction = 'asc' | 'desc'; 33 | export type Key = 34 | | 'ordNum' 35 | | 'name' 36 | | 'current_price' 37 | | 'price_change_percentage_24h' 38 | | 'price_change_percentage_7d_in_currency' 39 | | 'market_cap'; 40 | 41 | export type ListCoin = Pick; 42 | 43 | export const MarketApi = { 44 | getTableDataUrl: (page: number = 1, ids?: string[]) => { 45 | return () => 46 | `coins/markets?vs_currency=usd${ 47 | ids ? `&ids=${ids}` : `` 48 | }&order=market_cap_desc&per_page=50&page=${page}&sparkline=true&price_change_percentage=7d`; 49 | }, 50 | getCoinsListUrl: () => `coins/list?include_platform=false`, 51 | getCurrencyDataUrl: (currencyId: string) => 52 | `coins/${currencyId}?localization=false&tickers=false&market_data=true&community_data=false&developer_data=false&sparkline=true`, 53 | getMarketChartUrl: (currencyId: string, interval: string) => 54 | `coins/${currencyId}/market_chart?vs_currency=usd&days=${interval}`, 55 | }; 56 | -------------------------------------------------------------------------------- /api/userApi.ts: -------------------------------------------------------------------------------- 1 | import { AxiosInstance } from 'axios'; 2 | import { 3 | PaginatedTransactions, 4 | Asset, 5 | Transaction, 6 | UserPortfolio, 7 | WatchlistCurrency, 8 | HistoricalDataItem, 9 | } from 'store/slices/types'; 10 | 11 | interface UserPortfolioWithAssets extends UserPortfolio { 12 | assets: Asset[]; 13 | } 14 | export interface CreateTransactionPayload extends Omit { 15 | assetId: number | null; 16 | } 17 | export interface FetchAssetTransactionsPayload extends PaginationPayload { 18 | currencyId: string; 19 | assetId: string; 20 | } 21 | export type PaginationPayload = { 22 | size?: number; 23 | page?: number; 24 | }; 25 | 26 | export const userApi = (instance: AxiosInstance) => { 27 | return { 28 | getUserPortfolio: (): Promise => { 29 | return instance.get('/user/portfolio').then(({ data }) => data.data); 30 | }, 31 | getUserTransactionHistory: ({ 32 | size = 15, 33 | page = 0, 34 | }: PaginationPayload): Promise => { 35 | return instance 36 | .get(`/user/portfolio/transactionHistory?size=${size}&page=${page}`) 37 | .then(({ data }) => data.data); 38 | }, 39 | getUserAssets: (): Promise<{ assets: Asset[]; balance: number }> => { 40 | return instance.get('/user/assets').then(({ data }) => data.data); 41 | }, 42 | getUserAsset: (currencyId: string): Promise => { 43 | return instance.get(`/user/assets/${currencyId}`).then(({ data }) => data.data); 44 | }, 45 | getAssetTransactions: ({ 46 | currencyId, 47 | assetId, 48 | size = 7, 49 | page = 1, 50 | }: FetchAssetTransactionsPayload): Promise<{ 51 | currencyId: string; 52 | transactions: PaginatedTransactions; 53 | }> => { 54 | return instance 55 | .get(`/user/assets/${currencyId}/transactions?assetId=${assetId}&size=${size}&page=${page}`) 56 | .then(({ data }) => data.data); 57 | }, 58 | createTransaction: ( 59 | currencyId: string, 60 | payload: CreateTransactionPayload 61 | ): Promise => { 62 | return instance.post(`/user/assets/${currencyId}`, payload).then(({ data }) => data.data); 63 | }, 64 | getUserWatchlist: (): Promise => { 65 | return instance.get('/user/watchlist').then(({ data }) => data.data); 66 | }, 67 | getUserWatchlistCurrency: (currencyId: string): Promise => { 68 | return instance.get(`/user/watchlist/${currencyId}`).then(({ data }) => data.data); 69 | }, 70 | createUserWatchlistCurrency: (currencyId: string): Promise => { 71 | return instance.post(`/user/watchlist/${currencyId}`).then(({ data }) => data.data); 72 | }, 73 | deleteUserWatchlistCurrency: (currencyId: string): Promise => { 74 | return instance.delete(`/user/watchlist/${currencyId}`).then(() => currencyId); 75 | }, 76 | getHistoricalBalanceData: (interval: number): Promise => { 77 | return instance 78 | .get(`/user/portfolio/historical/balanceData?interval=${interval}`) 79 | .then(({ data }) => data.data); 80 | }, 81 | getHistoricalPnlData: (interval: number): Promise => { 82 | return instance 83 | .get(`/user/portfolio/historical/pnlData?interval=${interval}`) 84 | .then(({ data }) => data.data); 85 | }, 86 | }; 87 | }; 88 | -------------------------------------------------------------------------------- /components/Banner/Banner.module.scss: -------------------------------------------------------------------------------- 1 | @import 'styles/theme.scss'; 2 | 3 | .container { 4 | padding: 16px; 5 | } 6 | .textContainer { 7 | margin: 0 0 20px; 8 | } 9 | .button { 10 | @media all and (max-width: 768px) { 11 | width: 100%; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /components/Banner/index.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from 'components/Button'; 2 | import { Paper } from 'components/Paper'; 3 | import { Typography } from 'components/Typography'; 4 | import Link from 'next/link'; 5 | import React from 'react'; 6 | import styles from './Banner.module.scss'; 7 | 8 | type PropsType = { 9 | title: string; 10 | text: string; 11 | button: string; 12 | href: string; 13 | }; 14 | 15 | export const Banner: React.FC = ({ title, text, button, href }) => { 16 | return ( 17 | 18 |
19 |
20 | {title} 21 | 22 | {text} 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 |
31 |
32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /components/Button/Button.module.scss: -------------------------------------------------------------------------------- 1 | @import 'styles/theme.scss'; 2 | 3 | .button { 4 | //color: white; 5 | color: $background; 6 | font-size: 20px; 7 | font-weight: 500; 8 | border-radius: 8px; 9 | cursor: pointer; 10 | transition: 0.2s; 11 | outline: none; 12 | border: 0; 13 | padding: 8px 16px; 14 | } 15 | .primary { 16 | background-color: $primary; 17 | &:hover { 18 | background-color: #f19a30; 19 | } 20 | } 21 | .secondary { 22 | background-color: $secondary; 23 | color: white !important; 24 | &:hover { 25 | background-color: #35373d; 26 | } 27 | } 28 | .disabled { 29 | cursor: default; 30 | background-color: $gray !important; 31 | &:hover { 32 | background-color: $gray; 33 | } 34 | } 35 | .fullWidth { 36 | padding: 16px; 37 | width: 100%; 38 | } 39 | .isLoading { 40 | display: flex; 41 | align-items: center; 42 | justify-content: center; 43 | height: 100%; 44 | margin: -4.5px; 45 | } 46 | -------------------------------------------------------------------------------- /components/Button/index.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image'; 2 | import React from 'react'; 3 | import styles from './Button.module.scss'; 4 | import loadingIcon from 'public/static/loadingMini.svg'; 5 | import clsx from 'clsx'; 6 | 7 | type PropsType = { 8 | children: string | React.ReactNode; 9 | color?: 'primary' | 'secondary'; 10 | className?: string; 11 | fullWidth?: boolean; 12 | disabled?: boolean; 13 | isLoading?: boolean; 14 | type?: 'button' | 'submit' | 'reset'; 15 | onClick?: ((event: React.MouseEvent) => void) | undefined; 16 | }; 17 | export const Button: React.FC = React.memo(function Button({ 18 | children, 19 | color = 'primary', 20 | className, 21 | fullWidth, 22 | disabled, 23 | isLoading, 24 | type, 25 | onClick, 26 | }) { 27 | if (isLoading) disabled = true; 28 | return ( 29 | 49 | ); 50 | }); 51 | -------------------------------------------------------------------------------- /components/BuySellCard/BuySellCard.module.scss: -------------------------------------------------------------------------------- 1 | @import 'styles/theme.scss'; 2 | 3 | .container { 4 | padding: 16px; 5 | 6 | .actionsContainer { 7 | display: flex; 8 | align-items: center; 9 | background-color: $secondary; 10 | border-radius: 6px; 11 | margin-bottom: 16px; 12 | .action { 13 | padding: 10px 16px; 14 | background-color: $secondary; 15 | width: 100%; 16 | font-size: 20px; 17 | font-weight: 500; 18 | text-align: center; 19 | cursor: pointer; 20 | } 21 | .actionBuy { 22 | border-bottom-left-radius: 6px; 23 | border-top-left-radius: 6px; 24 | } 25 | .actionSell { 26 | border-bottom-right-radius: 6px; 27 | border-top-right-radius: 6px; 28 | } 29 | .actionBuyActive { 30 | background-color: $green; 31 | } 32 | .actionSellActive { 33 | background-color: $red; 34 | } 35 | .actionDivider { 36 | height: 46.5px; 37 | width: 46px; 38 | padding: 16px; 39 | background-color: $secondary; 40 | position: relative; 41 | .actionDividerOuter { 42 | position: absolute; 43 | top: 0px; 44 | left: 0px; 45 | width: 46px; 46 | height: 46.5px; 47 | overflow: hidden; 48 | } 49 | .actionDividerInnerBuy { 50 | transform: rotate(45deg); 51 | background-color: $green; 52 | width: 70px; 53 | height: 50px; 54 | top: -10px; 55 | left: -40px; 56 | position: relative; 57 | border-radius: 6px; 58 | } 59 | .actionDividerInnerSell { 60 | transform: rotate(135deg); 61 | background-color: $red; 62 | width: 70px; 63 | height: 50px; 64 | top: -10px; 65 | left: 8px; 66 | position: relative; 67 | border-radius: 6px; 68 | } 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /components/BuySellCard/index.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import { Paper } from 'components/Paper'; 3 | import React from 'react'; 4 | import { Currency } from 'store/slices/types'; 5 | import { BuySell } from './BuySell'; 6 | import styles from './BuySellCard.module.scss'; 7 | 8 | type PropsType = { 9 | currency: Currency; 10 | currentPrice: number; 11 | }; 12 | 13 | export type BuySellType = 'buy' | 'sell'; 14 | 15 | export const BuySellCard: React.FC = React.memo(function BuySellCard({ 16 | currency, 17 | currentPrice, 18 | }) { 19 | const [action, setAction] = React.useState('buy'); 20 | const handleChangeAction = (newAction: BuySellType) => { 21 | setAction(newAction); 22 | }; 23 | 24 | return ( 25 | 26 |
27 |
handleChangeAction('buy')} 29 | className={clsx( 30 | styles.action, 31 | styles.actionBuy, 32 | action === 'buy' && styles.actionBuyActive 33 | )} 34 | > 35 | BUY 36 |
37 |
38 |
39 |
44 |
45 |
46 |
handleChangeAction('sell')} 48 | className={clsx( 49 | styles.action, 50 | styles.actionSell, 51 | action === 'sell' && styles.actionSellActive 52 | )} 53 | > 54 | SELL 55 |
56 |
57 | 58 | 59 |
60 | ); 61 | }); 62 | -------------------------------------------------------------------------------- /components/Card/Card.module.scss: -------------------------------------------------------------------------------- 1 | @import 'styles/theme.scss'; 2 | 3 | .container { 4 | background-color: $backgroundGray; 5 | border-radius: 12px; 6 | margin-bottom: 24px; 7 | width: 100%; 8 | display: flex; 9 | flex-direction: column; 10 | justify-content: space-between; 11 | @media all and (max-width: 768px) { 12 | margin-bottom: 16px; 13 | } 14 | } 15 | .transparent { 16 | background-color: transparent; 17 | } 18 | 19 | .title { 20 | padding: 16px 16px 8px; 21 | //border-bottom: 1px solid $gray; 22 | } 23 | .transparentTitle { 24 | padding: 0px; 25 | padding-bottom: 4px; 26 | } 27 | 28 | .withPadding { 29 | padding: 16px; 30 | padding-top: 0px; 31 | } 32 | -------------------------------------------------------------------------------- /components/Card/index.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import { Typography } from 'components/Typography'; 3 | import React from 'react'; 4 | import styles from './Card.module.scss'; 5 | 6 | type PropsType = { 7 | children: React.ReactNode; 8 | title: any; //string | Element; 9 | button?: { 10 | name: string; 11 | href: string; 12 | }; 13 | className?: string; 14 | withPadding?: boolean; 15 | transparent?: boolean; 16 | }; 17 | 18 | export const Card: React.FC = React.memo(function Card({ 19 | children, 20 | title, 21 | withPadding, 22 | transparent, 23 | className, 24 | }) { 25 | //const Title = title; 26 | // 27 | 28 | return ( 29 | <div className={clsx(className, styles.container, transparent && styles.transparent)}> 30 | <div> 31 | <div className={clsx(styles.title, transparent && styles.transparentTitle)}> 32 | {typeof title === 'string' ? <Typography variant="title">{title}</Typography> : title} 33 | </div> 34 | <div className={clsx(withPadding && styles.withPadding)}>{children}</div> 35 | </div> 36 | </div> 37 | ); 38 | }); 39 | -------------------------------------------------------------------------------- /components/ChartIntervalProvider/ChartIntervalProvider.module.scss: -------------------------------------------------------------------------------- 1 | @import 'styles/theme.scss'; 2 | 3 | .cardTitle { 4 | display: flex; 5 | justify-content: space-between; 6 | .cardTitleIntervals { 7 | display: flex; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /components/ChartIntervalProvider/index.tsx: -------------------------------------------------------------------------------- 1 | import { Card } from 'components/Card'; 2 | import { IntervalSelector } from 'components/MarketChart/IntervalSelector'; 3 | import { Typography } from 'components/Typography'; 4 | import { parseISO, subDays } from 'date-fns'; 5 | import React, { useState } from 'react'; 6 | import { useDispatch } from 'react-redux'; 7 | import { HistoricalDataItem } from 'store/slices/types'; 8 | import styles from './ChartIntervalProvider.module.scss'; 9 | 10 | type PropsType = { 11 | data: any[]; 12 | Chart: React.FC<any>; 13 | title: string; 14 | withAxies?: boolean; 15 | handleFetch: (interval: number) => void; 16 | }; 17 | 18 | export const ChartIntervalProvider: React.FC<PropsType> = React.memo( 19 | function ChartIntervalProvider({ data, title, Chart, withAxies, handleFetch }) { 20 | const [interval, setInterval] = useState(intervals[1].value); 21 | const dispatch = useDispatch(); 22 | 23 | const handleChangeInterval = (newInt: number) => { 24 | setInterval(newInt); 25 | dispatch(handleFetch(newInt)); 26 | }; 27 | 28 | const formattedData = data.map((item) => { 29 | if (withAxies) 30 | return { 31 | x: new Date(item.date).getTime(), 32 | y: item.usdValue, 33 | }; 34 | return [new Date(item.date).getTime(), item.usdValue]; 35 | //return item; 36 | }); 37 | 38 | const placeholderItems: any[] = []; 39 | 40 | const placeholderItemsLength = interval - data.length; 41 | const endDate = data.length 42 | ? parseISO(data[data.length - 1].date) 43 | : parseISO(new Date().toISOString()); 44 | 45 | for (let i = 0; i < placeholderItemsLength; i++) { 46 | const sub = subDays(endDate, i + 1).getTime(); 47 | if (withAxies) placeholderItems.push({ x: sub, y: 0 }); 48 | else placeholderItems.push([sub, 0]); 49 | } 50 | 51 | const fullData = [...placeholderItems.reverse(), ...formattedData]; 52 | 53 | return ( 54 | <Card 55 | title={<CardTitle title={title} interval={interval} setInterval={handleChangeInterval} />} 56 | > 57 | <Chart data={fullData} dataInterval={`${interval}`} /> 58 | </Card> 59 | ); 60 | } 61 | ); 62 | 63 | type CardTitlePropsType = { 64 | title: string; 65 | interval: number; 66 | setInterval: (newInt: number) => void; 67 | }; 68 | 69 | const CardTitle: React.FC<CardTitlePropsType> = React.memo(function CardTitle({ 70 | title, 71 | interval, 72 | setInterval, 73 | }) { 74 | return ( 75 | <div className={styles.cardTitle}> 76 | <Typography variant="title">{title}</Typography> 77 | <div className={styles.cardTitleIntervals}> 78 | {intervals.map((int) => ( 79 | <IntervalSelector 80 | key={int.value} 81 | name={int.name} 82 | isActive={int.value === interval} 83 | handleSelectChart={() => setInterval(int.value)} 84 | /> 85 | ))} 86 | </div> 87 | </div> 88 | ); 89 | }); 90 | 91 | const intervals = [ 92 | { 93 | name: '7D', 94 | value: 7, 95 | }, 96 | { 97 | name: '30D', 98 | value: 30, 99 | }, 100 | { 101 | name: '90D', 102 | value: 90, 103 | }, 104 | // { 105 | // name: '180D', 106 | // value: 180, 107 | // }, 108 | ]; 109 | -------------------------------------------------------------------------------- /components/ContentLayout/ContentLayout.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | width: 100%; 4 | gap: 24px; 5 | & > * { 6 | //first-child 7 | width: 100%; 8 | } 9 | & > * + * { 10 | //last-child 11 | max-width: 380px; 12 | } 13 | @media all and (max-width: 1024px) { 14 | flex-direction: column; 15 | gap: 0px; 16 | 17 | & > * + * { 18 | //last-child 19 | max-width: 2000px; 20 | width: 100%; 21 | display: flex; 22 | gap: 24px; 23 | } 24 | } 25 | } 26 | .halfs { 27 | width: 100%; 28 | @media all and (max-width: 1024px) { 29 | & > * { 30 | min-width: 100%; 31 | } 32 | } 33 | & > * { 34 | //first-child 35 | max-width: 50%; 36 | } 37 | // & > * + * { 38 | // //last-child 39 | // // width: 50%; 40 | // } 41 | } 42 | -------------------------------------------------------------------------------- /components/ContentLayout/index.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import React from 'react'; 3 | import styles from './ContentLayout.module.scss'; 4 | 5 | type PropsType = { 6 | children: React.ReactNode; 7 | type?: 'default' | 'halfs'; 8 | }; 9 | 10 | export const ContentLayout: React.FC<PropsType> = React.memo(function ContentLayout({ 11 | children, 12 | type = 'default', 13 | }) { 14 | return <div className={clsx(styles.container, type === 'halfs' && styles.halfs)}>{children}</div>; 15 | }); 16 | -------------------------------------------------------------------------------- /components/CopyButton/index.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from 'components/Button'; 2 | import React, { useState } from 'react'; 3 | 4 | type PropsType = { 5 | refCode: string; 6 | className?: string; 7 | fullWidth?: boolean; 8 | }; 9 | 10 | export const CopyButton: React.FC<PropsType> = React.memo(function CopyButton({ 11 | refCode, 12 | className, 13 | fullWidth, 14 | }) { 15 | const [copied, setCopied] = useState(false); 16 | const referralLink = `localhost:3000/register?ref=${refCode}`; 17 | const handleCopy = () => { 18 | navigator.clipboard.writeText(referralLink); 19 | setCopied(true); 20 | setTimeout(() => setCopied(false), 1000); 21 | }; 22 | 23 | return ( 24 | <Button onClick={handleCopy} fullWidth={fullWidth} className={className}> 25 | {!copied ? 'Copy' : 'Copied'} 26 | </Button> 27 | ); 28 | }); 29 | -------------------------------------------------------------------------------- /components/EducationCard/EducationCard.module.scss: -------------------------------------------------------------------------------- 1 | @import 'styles/theme.scss'; 2 | 3 | .education { 4 | display: flex; 5 | flex-direction: column; 6 | padding: 16px; 7 | margin-bottom: 0px; 8 | .educationCurrency { 9 | display: flex; 10 | align-items: center; 11 | margin-bottom: 10px; 12 | .educationCurrencyName { 13 | margin: 0px 8px; 14 | } 15 | } 16 | .educationBtn { 17 | align-self: flex-end; 18 | margin-top: 16px; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /components/EducationCard/index.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from 'components/Button'; 2 | import { Paper } from 'components/Paper'; 3 | import { Typography } from 'components/Typography'; 4 | import Image from 'next/image'; 5 | import React from 'react'; 6 | import styles from './EducationCard.module.scss'; 7 | import btcIcon from 'public/static/btc.png'; 8 | 9 | type PropsTypes = {}; 10 | 11 | export const EducationCard: React.FC<PropsTypes> = React.memo(function EducationCard() { 12 | return ( 13 | <Paper className={styles.education}> 14 | <div className={styles.educationCurrency}> 15 | <Image layout="fixed" src={btcIcon} alt={`btc icon`} width={48} height={48} /> 16 | <Typography className={styles.educationCurrencyName} fs="fs-22" fw="fw-500"> 17 | Bitcoin 18 | </Typography> 19 | <Typography fs="fs-22" fw="fw-500" color="gray"> 20 | BTC 21 | </Typography> 22 | </div> 23 | <Typography variant="regularText" color="gray"> 24 | A decentralized digital currency, without a central bank or single administrator, that can 25 | be sent from user to user on the peer-to-peer bitcoin network without the need for 26 | intermediaries 27 | </Typography> 28 | <Button fullWidth className={styles.educationBtn}> 29 | Learn more 30 | </Button> 31 | </Paper> 32 | ); 33 | }); 34 | -------------------------------------------------------------------------------- /components/LandingLayout/LandingHeader/LandingHeader.module.scss: -------------------------------------------------------------------------------- 1 | @import 'styles/theme.scss'; 2 | 3 | .container { 4 | height: 64px; 5 | background-color: $backgroundGray; 6 | //max-width: 1220px; 7 | width: 100%; 8 | padding: 0 20px; 9 | display: flex; 10 | align-items: center; 11 | justify-content: space-between; 12 | .leftSide { 13 | display: flex; 14 | align-items: center; 15 | flex-direction: row; 16 | .logoContainer { 17 | display: flex; 18 | align-items: center; 19 | margin-right: 30px; 20 | span { 21 | font-size: 26px; 22 | font-weight: 700; 23 | color: $primary; 24 | margin-left: 10px; 25 | margin-bottom: 6px; 26 | @media all and (max-width: 768px) { 27 | display: none; 28 | } 29 | } 30 | } 31 | } 32 | .navItem { 33 | font-size: 18px; 34 | font-weight: 500; 35 | margin-right: 16px; 36 | &:hover { 37 | color: $primary; 38 | cursor: pointer; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /components/LandingLayout/LandingHeader/index.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from 'components/Button'; 2 | import Image from 'next/image'; 3 | import Link from 'next/link'; 4 | import logoIcon from 'public/static/logo.png'; 5 | import React from 'react'; 6 | import styles from './LandingHeader.module.scss'; 7 | 8 | export const LandingHeader = React.memo(function LandingHeader() { 9 | return ( 10 | <div className={styles.container}> 11 | <div className={styles.leftSide}> 12 | <Link href="/"> 13 | <a className={styles.logoContainer}> 14 | <Image src={logoIcon} alt="Logo icon" width={42} height={38} /> 15 | <span>TryCrypto</span> 16 | </a> 17 | </Link> 18 | </div> 19 | <div> 20 | <Link href="/login"> 21 | <a className={styles.navItem}>Log In</a> 22 | </Link> 23 | <Link href="/register"> 24 | <a> 25 | <Button>Register</Button> 26 | </a> 27 | </Link> 28 | </div> 29 | </div> 30 | ); 31 | }); 32 | -------------------------------------------------------------------------------- /components/LandingLayout/LandingLayout.module.scss: -------------------------------------------------------------------------------- 1 | @import 'styles/theme.scss'; 2 | 3 | .container { 4 | width: 100%; 5 | display: flex; 6 | flex-direction: column; 7 | align-items: center; 8 | .content { 9 | //background-color: royalblue; 10 | //max-width: 1180px; 11 | width: 100%; 12 | //padding: 0 20px; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /components/LandingLayout/index.tsx: -------------------------------------------------------------------------------- 1 | import { Navbar } from 'components/Navbar'; 2 | import { useRouter } from 'next/router'; 3 | import React from 'react'; 4 | import { LandingHeader } from './LandingHeader'; 5 | import styles from './LandingLayout.module.scss'; 6 | 7 | type PropsType = { 8 | children: React.ReactNode; 9 | }; 10 | 11 | export const LandingLayout: React.FC<PropsType> = React.memo(function LandingLayout({ children }) { 12 | return ( 13 | <div className={styles.container}> 14 | <LandingHeader /> 15 | <div className={styles.content}>{children}</div> 16 | </div> 17 | ); 18 | }); 19 | -------------------------------------------------------------------------------- /components/Layout/Layout.module.scss: -------------------------------------------------------------------------------- 1 | @import 'styles/theme.scss'; 2 | 3 | .container { 4 | //display: flex; 5 | min-width: 100%; 6 | min-height: 100vh; 7 | position: relative; 8 | } 9 | .content { 10 | width: 100%; 11 | } 12 | .children { 13 | max-width: 1450px; 14 | width: 100%; 15 | gap: 24px; 16 | margin-bottom: 24px; 17 | } 18 | .footer { 19 | position: absolute; 20 | bottom: 0; 21 | width: calc(100% - 215px); 22 | height: 24px; 23 | left: 215px; 24 | display: flex; 25 | justify-content: space-between; 26 | padding: 0px 24px; 27 | background-color: $backgroundGray; 28 | @media all and (max-width: 1280px) { 29 | width: calc(100% - 82px); 30 | left: 82px; 31 | } 32 | @media all and (max-width: 768px) { 33 | left: 0px; 34 | width: 100%; 35 | padding: 0px 16px; 36 | 37 | .footerName { 38 | display: none; 39 | } 40 | } 41 | div { 42 | a { 43 | color: $primary; 44 | &:hover { 45 | text-decoration: underline; 46 | } 47 | } 48 | 49 | span { 50 | color: $gray; 51 | font-weight: 400; 52 | } 53 | } 54 | .footerDataProvider { 55 | @media all and (max-width: 340px) { 56 | display: none; 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /components/Layout/index.tsx: -------------------------------------------------------------------------------- 1 | import { Navbar } from 'components/Navbar'; 2 | import { useRouter } from 'next/router'; 3 | import React from 'react'; 4 | import styles from './Layout.module.scss'; 5 | 6 | type PropsType = { 7 | children: React.ReactNode; 8 | }; 9 | 10 | export const Layout: React.FC<PropsType> = React.memo(function Layout({ children }) { 11 | return ( 12 | <div className={styles.container}> 13 | <Navbar> 14 | <div className={styles.children}>{children}</div> 15 | </Navbar> 16 | <div className={styles.footer}> 17 | <div> 18 | <a href="https://github.com/VHarastei" rel="noreferrer" target="_blank"> 19 | VHarastei 20 | </a> 21 | <span> © 2021 </span> 22 | <span className={styles.footerName}> TryCrypto</span> 23 | </div> 24 | <div className={styles.footerDataProvider}> 25 | <span>Data from </span> 26 | <a href="https://www.coingecko.com" rel="noreferrer" target="_blank"> 27 | CoinGecko 28 | </a> 29 | </div> 30 | </div> 31 | </div> 32 | ); 33 | }); 34 | -------------------------------------------------------------------------------- /components/MarketChart/CustomBrushChart/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { VictoryBrushContainer, VictoryChart, VictoryLine } from 'victory'; 3 | import { ChartArray } from '..'; 4 | 5 | type PropsType = { 6 | data: ChartArray[] | undefined; 7 | }; 8 | 9 | export const CustomBrushChart: React.FC<PropsType> = ({ data }) => { 10 | const [domain, setDomain] = React.useState(); 11 | 12 | if (!data) return <div>nema</div>; 13 | return ( 14 | <VictoryChart 15 | width={1000} 16 | height={100} 17 | //padding={{ left: 0, top: 0, bottom: 0, right: 0 }} 18 | domainPadding={{ y: 5 }} 19 | containerComponent={ 20 | <VictoryBrushContainer 21 | allowDraw 22 | allowResize 23 | 24 | //style={{ cursor: 'crosshair' }} 25 | /> 26 | } 27 | > 28 | <VictoryLine 29 | style={{ 30 | data: { 31 | stroke: '#f3aa4e', 32 | strokeWidth: 2, 33 | // margin: 0, 34 | // padding: 0, 35 | }, 36 | }} 37 | data={data.map((item) => ({ 38 | x: item[0], 39 | y: item[1], 40 | }))} 41 | /> 42 | {/* <VictoryAxis 43 | orientation="bottom" 44 | style={{ 45 | axis: { 46 | stroke: '#7b7f82', 47 | strokeWidth: 1, 48 | }, 49 | tickLabels: { 50 | fontSize: 16, 51 | fill: '#7b7f82', 52 | }, 53 | }} 54 | tickFormat={(x) => format(x, 'y')} 55 | /> */} 56 | </VictoryChart> 57 | ); 58 | }; 59 | -------------------------------------------------------------------------------- /components/MarketChart/CustomChart/index.tsx: -------------------------------------------------------------------------------- 1 | import { format } from 'date-fns'; 2 | import React from 'react'; 3 | import { VictoryAxis, VictoryChart, VictoryLine, VictoryVoronoiContainer } from 'victory'; 4 | import { ChartArray } from '..'; 5 | import { CustomTooltip } from '../CustomTooltip'; 6 | 7 | type PropsType = { 8 | data: ChartArray[]; 9 | dataInterval: string; 10 | }; 11 | 12 | export const CustomChart: React.FC<PropsType> = React.memo(function CustomChart({ 13 | data, 14 | dataInterval, 15 | }) { 16 | return ( 17 | <VictoryChart 18 | animate={{ duration: 300, onLoad: { duration: 300 } }} 19 | width={1000} 20 | height={400} 21 | padding={{ left: 0, top: 76, bottom: 32, right: 0 }} 22 | domainPadding={{ y: 5 }} 23 | containerComponent={ 24 | <VictoryVoronoiContainer 25 | labels={() => ` `} 26 | style={{ 27 | cursor: 'crosshair', 28 | zIndex: 99, 29 | //borderBottom: '1.5px solid #7b7f8297' 30 | }} 31 | portalZIndex={99} 32 | labelComponent={<CustomTooltip />} 33 | /> 34 | } 35 | > 36 | <VictoryLine 37 | style={{ 38 | data: { 39 | stroke: '#f3aa4e', 40 | strokeWidth: 2, 41 | margin: 0, 42 | padding: 0, 43 | }, 44 | }} 45 | data={data.map((item) => ({ 46 | x: item[0], 47 | y: item[1], 48 | }))} 49 | /> 50 | <VictoryAxis 51 | orientation="bottom" 52 | style={{ 53 | axis: { 54 | stroke: '#7b7f82', 55 | strokeWidth: 1, 56 | }, 57 | tickLabels: { 58 | fontSize: 16, 59 | fill: '#7b7f82', 60 | }, 61 | }} 62 | tickFormat={(x) => { 63 | if (+dataInterval <= 1) { 64 | return format(x, 'p'); 65 | } 66 | if (dataInterval === 'max') { 67 | return format(x, 'MMM y'); 68 | } 69 | return format(x, 'MMM d'); 70 | }} 71 | /> 72 | </VictoryChart> 73 | ); 74 | }); 75 | -------------------------------------------------------------------------------- /components/MarketChart/CustomTooltip/index.tsx: -------------------------------------------------------------------------------- 1 | import { format } from 'date-fns'; 2 | import React from 'react'; 3 | import { formatDollar } from 'utils/formatDollar'; 4 | 5 | export const CustomTooltip = React.memo(function CustomTooltip(props: any) { 6 | const { x, y, datum } = props; 7 | return ( 8 | <foreignObject 9 | style={{ pointerEvents: 'none', position: 'relative' }} 10 | x={x >= 950 ? x - 185 : x - 85} 11 | y={y - 75} 12 | width="300" 13 | height="100%" 14 | > 15 | <div 16 | style={{ 17 | display: 'flex', 18 | alignItems: 'center', 19 | flexDirection: 'column', 20 | position: 'relative', 21 | height: `auto`, 22 | width: 170, 23 | }} 24 | > 25 | <div 26 | style={{ 27 | display: 'flex', 28 | alignItems: 'center', 29 | flexDirection: 'column', 30 | height: `auto`, 31 | width: '100%', 32 | //padding: `4px`, 33 | background: 'white', 34 | color: 'black', 35 | borderRadius: '4px', 36 | position: 'absolute', 37 | left: x <= 85 ? 0 : 'auto', 38 | marginLeft: x <= 85 ? 100 : 0, 39 | // right: x >= 950 ? 0 : 'auto', 40 | // marginRight: x >= 950 ? 100 : 0, 41 | zIndex: 1001, 42 | }} 43 | > 44 | <div style={{ fontSize: 20, fontWeight: 700 }}> 45 | {formatDollar( 46 | datum.y === 0 47 | ? 0 48 | : datum.y < 0.0000000001 49 | ? 0.0000000001 50 | : datum.y < 1 51 | ? datum.y 52 | : datum.y.toFixed(2), 53 | datum.y < 0.0001 ? 2 : datum.y > 1000000000 ? 10 : 7 54 | )} 55 | </div> 56 | <div style={{ fontSize: 12, fontWeight: 700, color: '#7b7f82' }}> 57 | {format(datum.x, 'MMM d p')} 58 | </div> 59 | </div> 60 | <div 61 | style={{ 62 | position: 'absolute', 63 | backgroundColor: '#f3aa4e', 64 | top: 67, 65 | left: x >= 950 ? 177 : '50', 66 | height: 16, 67 | width: 16, 68 | borderRadius: 100, 69 | border: '4px solid #212528', 70 | }} 71 | ></div> 72 | </div> 73 | </foreignObject> 74 | ); 75 | }); 76 | -------------------------------------------------------------------------------- /components/MarketChart/IntervalSelector/IntervalSelector.module.scss: -------------------------------------------------------------------------------- 1 | @import 'styles/theme.scss'; 2 | 3 | .container { 4 | font-size: 14px; 5 | font-weight: 500; 6 | padding: 2px 2px 2px 0px; 7 | margin: 0px 5px; 8 | color: $gray; 9 | cursor: pointer; 10 | &:hover { 11 | color: white; 12 | } 13 | } 14 | .active { 15 | color: $primary; 16 | &:hover { 17 | color: $primary; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /components/MarketChart/IntervalSelector/index.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import React from 'react'; 3 | import styles from './IntervalSelector.module.scss'; 4 | 5 | type PropsType = { 6 | name: string; 7 | isActive: boolean; 8 | handleSelectChart: () => void; 9 | }; 10 | 11 | export const IntervalSelector: React.FC<PropsType> = React.memo(function IntervalSelector({ 12 | name, 13 | isActive, 14 | handleSelectChart, 15 | }) { 16 | return ( 17 | <div className={clsx(styles.container, isActive && styles.active)} onClick={handleSelectChart}> 18 | {name} 19 | </div> 20 | ); 21 | }); 22 | -------------------------------------------------------------------------------- /components/MarketChart/MarketChart.module.scss: -------------------------------------------------------------------------------- 1 | @import 'styles/theme.scss'; 2 | 3 | @media all and (max-width: 768px) { 4 | .chartSelectorTypes { 5 | display: none !important; 6 | } 7 | .resizeBtn { 8 | display: block !important; 9 | } 10 | .resizeContainer { 11 | justify-content: space-between !important; 12 | } 13 | } 14 | 15 | .header { 16 | display: flex; 17 | padding: 16px; 18 | padding-bottom: 6px; 19 | //align-items: center; 20 | justify-content: space-between; 21 | flex-direction: column; 22 | .chartSelectors { 23 | display: flex; 24 | justify-content: space-between; 25 | 26 | .chartSelectorContainer { 27 | display: flex; 28 | white-space: nowrap; 29 | :last-of-type { 30 | padding-right: 0px; 31 | } 32 | } 33 | .resizeContainer { 34 | width: 100%; 35 | display: flex; 36 | align-items: center; 37 | justify-content: flex-end; 38 | .resizeBtn { 39 | display: none; 40 | margin: 0px; 41 | height: 28px; 42 | margin: -4px; 43 | } 44 | } 45 | } 46 | .chartPriceContainer { 47 | display: flex; 48 | .chartPrice { 49 | font-size: 38px; 50 | font-weight: 400; 51 | } 52 | .chartPricePercent { 53 | padding-top: 2px; 54 | margin-left: 6px; 55 | } 56 | } 57 | } 58 | 59 | .preloaderContainer { 60 | width: 100%; 61 | height: 465px; 62 | } 63 | 64 | .fullscreenChart { 65 | z-index: 1001; 66 | top: 0px; 67 | left: 0px; 68 | position: absolute; 69 | background-color: $backgroundGray; 70 | width: 100%; 71 | min-height: 100vh; 72 | border-radius: 0px; 73 | //min-height: -webkit-fill-available; 74 | .fullscreenData { 75 | position: absolute; 76 | bottom: 0px; 77 | } 78 | } 79 | 80 | .display { 81 | display: block !important; 82 | } 83 | .hide { 84 | display: none !important; 85 | } 86 | .justify { 87 | justify-content: space-between !important; 88 | } 89 | -------------------------------------------------------------------------------- /components/MarketTable/MarketTable.module.scss: -------------------------------------------------------------------------------- 1 | @import 'styles/theme.scss'; 2 | 3 | -------------------------------------------------------------------------------- /components/MarketTable/SortableTable/SortableTable.module.scss: -------------------------------------------------------------------------------- 1 | @import 'styles/theme.scss'; 2 | 3 | .container { 4 | overflow-x: auto; 5 | } 6 | 7 | .table { 8 | border: 0; 9 | border-spacing: 0; 10 | border-collapse: collapse; 11 | width: 100%; 12 | text-align: center; 13 | td, 14 | th { 15 | padding: 8px; 16 | } 17 | th { 18 | list-style: none; 19 | font-size: 16px; 20 | font-weight: 400; 21 | color: $gray; 22 | user-select: none; 23 | .tableHeaderItem { 24 | display: flex; 25 | align-items: center; 26 | div { 27 | cursor: pointer; 28 | } 29 | div:hover { 30 | color: $primary; 31 | } 32 | } 33 | } 34 | .sort { 35 | width: 10px; 36 | height: 5px; 37 | margin-left: 6px; 38 | } 39 | .desc { 40 | border-top: solid 5px $primary; 41 | border-left: solid 5px transparent; 42 | border-right: solid 5px transparent; 43 | } 44 | .asc { 45 | border-bottom: solid 5px $primary; 46 | border-left: solid 5px transparent; 47 | border-right: solid 5px transparent; 48 | } 49 | .watch { 50 | padding-left: 16px; 51 | } 52 | .sparkline { 53 | justify-content: center; 54 | padding-right: 16px; 55 | } 56 | } 57 | 58 | .tableRowContainer { 59 | border-top: 1px solid $gray; 60 | &:hover { 61 | background-color: $secondary; 62 | } 63 | li { 64 | list-style: none; 65 | align-items: center; 66 | font-size: 16px; 67 | font-weight: 400; 68 | } 69 | .watch { 70 | text-align: left; 71 | } 72 | .ordNum { 73 | text-align: left; 74 | } 75 | .name { 76 | text-align: left; 77 | display: flex; 78 | align-items: center; 79 | font-size: 18px; 80 | font-weight: 400; 81 | a { 82 | min-width: 100%; 83 | width: 100%; 84 | } 85 | p { 86 | margin: 0 10px 0px 16px; 87 | } 88 | span { 89 | @media all and (max-width: 1024px) { 90 | display: none; 91 | } 92 | color: $gray; 93 | } 94 | } 95 | .price { 96 | text-align: left; 97 | } 98 | .change { 99 | text-align: left; 100 | } 101 | .marketCap { 102 | text-align: left; 103 | } 104 | .sparkline { 105 | min-width: 150px; 106 | height: 30px; 107 | text-align: center; 108 | padding-right: 16px; 109 | } 110 | } 111 | .foundQuantity { 112 | width: 100%; 113 | padding: 16px; 114 | border-top: 1px solid $gray; 115 | } 116 | -------------------------------------------------------------------------------- /components/MarketTable/index.tsx: -------------------------------------------------------------------------------- 1 | import { fetcher, MarketApi, TableCoin } from 'api/marketApi'; 2 | import { Pagination } from 'components/Pagination'; 3 | import React, { useState } from 'react'; 4 | import useSWR from 'swr'; 5 | import { SortableTable } from './SortableTable'; 6 | 7 | type PropsType = { 8 | data: TableCoin[]; 9 | currentPage: number; 10 | }; 11 | 12 | export const MarketTable: React.FC<PropsType> = React.memo((props) => { 13 | const [currentPage, setCurrentPage] = useState(props.currentPage); 14 | const { data } = useSWR(MarketApi.getTableDataUrl(currentPage), fetcher, { 15 | initialData: currentPage === props.currentPage ? props.data : [], 16 | refreshInterval: 30000, 17 | }); 18 | return ( 19 | <div> 20 | <SortableTable data={data} currentPage={currentPage} /> 21 | {data.length && ( 22 | <Pagination 23 | currentPage={currentPage} 24 | itemsPerPage={50} 25 | numberOfItems={6120} 26 | setCurrentPage={setCurrentPage} 27 | navHref="/market" 28 | /> 29 | )} 30 | </div> 31 | ); 32 | }); 33 | -------------------------------------------------------------------------------- /components/MarketTransactions/MarketTransactions.module.scss: -------------------------------------------------------------------------------- 1 | @import 'styles/theme.scss'; 2 | 3 | .rTTitle { 4 | display: flex; 5 | //align-items: center; 6 | justify-content: space-between; 7 | @media all and (max-width: 768px) { 8 | flex-direction: column; 9 | } 10 | } 11 | .errorContainer { 12 | padding: 16px; 13 | } 14 | .btnContainer { 15 | display: flex; 16 | align-items: center; 17 | justify-content: center; 18 | padding: 9px; 19 | border-top: 1px solid $gray; 20 | } 21 | -------------------------------------------------------------------------------- /components/Navbar/NavItem/NavItem.module.scss: -------------------------------------------------------------------------------- 1 | @import 'styles/theme.scss'; 2 | 3 | .navItem { 4 | display: flex; 5 | align-items: center; 6 | margin-bottom: 16px; 7 | color: $gray; 8 | padding: 6px 12px; 9 | width: 187px; 10 | @media all and (max-width: 1280px) { 11 | width: 54px; 12 | span { 13 | display: none; 14 | } 15 | } 16 | @media all and (max-width: 768px) { 17 | width: 100%; 18 | span { 19 | display: inline; 20 | } 21 | } 22 | &:hover { 23 | color: white; 24 | cursor: pointer; 25 | background-color: $secondary; 26 | border-radius: 8px; 27 | } 28 | span { 29 | font-size: 22px; 30 | font-weight: 500; 31 | margin-left: 12px; 32 | } 33 | } 34 | .navItemActive { 35 | color: white; 36 | } 37 | -------------------------------------------------------------------------------- /components/Navbar/NavItem/index.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image'; 2 | import Link from 'next/link'; 3 | import React from 'react'; 4 | import styles from './NavItem.module.scss'; 5 | 6 | type PropsType = { 7 | icon: StaticImageData; 8 | href: string; 9 | active: string; 10 | }; 11 | 12 | export const NavItem: React.FC<PropsType> = React.memo(function NavItem({ icon, href, active }) { 13 | return ( 14 | <Link href={`/${href}`}> 15 | <a className={styles.navItem}> 16 | <Image layout="fixed" src={icon} alt={`${icon} icon`} width={30} height={30} /> 17 | <span className={active === href ? styles.navItemActive : ''}> 18 | {href[0].toUpperCase() + href.slice(1)} 19 | </span> 20 | </a> 21 | </Link> 22 | ); 23 | }); 24 | -------------------------------------------------------------------------------- /components/Navbar/Navbar.module.scss: -------------------------------------------------------------------------------- 1 | @import 'styles/theme.scss'; 2 | 3 | @media all and (max-width: 768px) { 4 | .logoContainer { 5 | background-color: $background !important; 6 | span { 7 | display: inline !important; 8 | } 9 | } 10 | .activePage { 11 | display: none !important; 12 | } 13 | .menuIcon { 14 | display: inline !important; 15 | } 16 | .headerBar { 17 | padding: 20px 16px !important; 18 | } 19 | .logoContainer { 20 | padding: 20px 16px !important; 21 | } 22 | } 23 | 24 | .container { 25 | flex-direction: column; 26 | width: 100%; 27 | } 28 | 29 | .header { 30 | display: flex; 31 | align-items: center; 32 | position: sticky; 33 | top: 0px; 34 | z-index: 101; 35 | background-color: $backgroundGray; 36 | 37 | .logoContainer { 38 | display: flex; 39 | flex-grow: 0; 40 | flex-shrink: 0; 41 | flex-basis: 215px; 42 | height: 80px; 43 | justify-content: space-between; 44 | align-items: center; 45 | padding: 20px 20px; 46 | width: 100%; 47 | @media all and (max-width: 1280px) { 48 | flex-basis: 82px; 49 | span { 50 | display: none; 51 | } 52 | } 53 | span { 54 | font-size: 26px; 55 | font-weight: 700; 56 | color: $primary; 57 | margin-left: 10px; 58 | @media all and (max-width: 320px) { 59 | display: none !important; 60 | } 61 | } 62 | } 63 | .headerBar { 64 | display: flex; 65 | width: 100%; 66 | align-items: center; 67 | padding: 20px 24px; 68 | background-color: $background; 69 | height: 80px; 70 | justify-content: space-between; 71 | .activePage { 72 | cursor: pointer; 73 | font-size: 24px; 74 | font-weight: 500; 75 | display: flex; 76 | align-items: center; 77 | span { 78 | margin-left: 6px; 79 | } 80 | } 81 | .dialogs { 82 | width: 100%; 83 | display: flex; 84 | align-items: center; 85 | justify-content: flex-end; 86 | .menuIcon { 87 | display: none; 88 | margin-top: 4px; 89 | margin-left: 16px; 90 | } 91 | } 92 | } 93 | } 94 | 95 | .content { 96 | display: flex; 97 | .navContainer { 98 | @media all and (max-width: 768px) { 99 | display: none; 100 | } 101 | position: sticky; 102 | top: 80px; 103 | height: calc(100vh - 80px); 104 | padding: 0px 14px; 105 | background-color: $backgroundGray; 106 | } 107 | .navMobile { 108 | display: block; 109 | scroll-snap-stop: none; 110 | z-index: 1001; 111 | position: fixed; 112 | top: 0px; 113 | min-height: 100vh; 114 | min-height: -webkit-fill-available; 115 | width: 100%; 116 | padding: 0px 16px; 117 | .closeIcon { 118 | text-align: right; 119 | margin: 26px 1px; 120 | margin-bottom: 12px; 121 | } 122 | } 123 | .childrenContainer { 124 | padding: 0px 24px; 125 | width: 100%; 126 | display: flex; 127 | justify-content: center; 128 | @media all and (max-width: 1280px) { 129 | width: calc(100% - 82px); 130 | } 131 | @media all and (max-width: 768px) { 132 | width: 100%; 133 | padding: 0px 16px; 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /components/Navbar/UserDialog/UserDialog.module.scss: -------------------------------------------------------------------------------- 1 | @import 'styles/theme.scss'; 2 | 3 | @media all and (max-width: 768px) { 4 | .icon { 5 | width: 28px !important; 6 | height: 28px !important; 7 | } 8 | .dropDown { 9 | width: 100% !important; 10 | } 11 | .dropDownMenu { 12 | width: calc(100% - 32px) !important; 13 | right: 16px !important; 14 | } 15 | } 16 | 17 | .icon { 18 | display: flex; 19 | align-items: center; 20 | cursor: pointer; 21 | } 22 | .dropDown { 23 | position: absolute; 24 | top: 0px; 25 | left: 0px; 26 | background-color: rgba(0, 0, 0, 0.5); 27 | width: calc(100% + 24px); 28 | height: 100vh; 29 | } 30 | .dropDownMenu { 31 | width: 270px; 32 | position: absolute; 33 | top: 66px; 34 | right: 50px; 35 | background-color: $backgroundGray; 36 | border-radius: 12px; 37 | .title { 38 | font-size: 22px; 39 | font-weight: 500; 40 | padding: 16px; 41 | padding-bottom: 10px; 42 | } 43 | .subTitleContainer { 44 | display: flex; 45 | align-items: center; 46 | justify-content: space-between; 47 | padding-left: 16px; 48 | padding-bottom: 16px; 49 | .subTitle { 50 | display: flex; 51 | font-size: 16px; 52 | font-weight: 500; 53 | color: $gray; 54 | div { 55 | margin-left: 6px; 56 | } 57 | } 58 | .verification { 59 | display: flex; 60 | align-items: center; 61 | border-bottom-left-radius: 16px; 62 | border-top-left-radius: 16px; 63 | padding: 3px 5px; 64 | } 65 | .verified { 66 | background-color: rgba(2, 192, 118, 0.3); 67 | } 68 | .unVerified { 69 | background-color: rgba(248, 73, 96, 0.3); 70 | } 71 | } 72 | .dropDownMenuItem { 73 | padding: 10px 16px; 74 | display: flex; 75 | align-items: center; 76 | span { 77 | color: $gray; 78 | font-size: 17px; 79 | font-weight: 500; 80 | margin-left: 10px; 81 | } 82 | &:hover { 83 | color: white; 84 | cursor: pointer; 85 | background-color: $secondary; 86 | span { 87 | color: white; 88 | } 89 | } 90 | } 91 | .dropDownMenuLogout { 92 | border-top: 1px solid $secondary; 93 | border-bottom-left-radius: 12px; 94 | border-bottom-right-radius: 12px; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /components/Navbar/UserDialog/index.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import { Typography } from 'components/Typography'; 3 | import Image from 'next/image'; 4 | import Link from 'next/link'; 5 | import logoutIcon from 'public/static/logout.svg'; 6 | import referralIcon from 'public/static/referral.svg'; 7 | import unVerifiedIcon from 'public/static/unverified.svg'; 8 | import userIcon from 'public/static/user.svg'; 9 | import verificationIcon from 'public/static/verification.svg'; 10 | import verifiedIcon from 'public/static/verified.svg'; 11 | import React, { useState } from 'react'; 12 | import { useSelector } from 'react-redux'; 13 | import { RemoveScrollBar } from 'react-remove-scroll-bar'; 14 | import { selectUser } from 'store/selectors'; 15 | import styles from './UserDialog.module.scss'; 16 | 17 | export const UserDialog = React.memo(function UserDialog() { 18 | const [display, setDisplay] = useState(false); 19 | const user = useSelector(selectUser); 20 | if (!user) return null; 21 | 22 | const arr = user.email.split('@'); 23 | const email = `${arr[0].slice(0, 2)}***@${arr[1]}`; 24 | 25 | return ( 26 | <div className={styles.container}> 27 | <div className={styles.icon} onClick={() => setDisplay(true)}> 28 | <Image src={userIcon} alt="User icon" width={40} height={40} /> 29 | </div> 30 | {display && ( 31 | <div className={styles.dropDown} onClick={() => setDisplay(false)}> 32 | <RemoveScrollBar /> 33 | <div className={styles.dropDownMenu} onClick={(e) => e.stopPropagation()}> 34 | <div className={styles.title}>{email}</div> 35 | <div className={styles.subTitleContainer}> 36 | <div className={styles.subTitle}> 37 | UID: <Typography color="white">{user.id}</Typography> 38 | </div> 39 | <div 40 | className={clsx( 41 | styles.verification, 42 | user.verified ? styles.verified : styles.unVerified 43 | )} 44 | > 45 | <Image 46 | src={user.verified ? verifiedIcon : unVerifiedIcon} 47 | alt="verification icon" 48 | width={22} 49 | height={22} 50 | /> 51 | <Typography fw={'fw-500'} fs="fs-14" color={user.verified ? 'green' : 'red'}> 52 | {user.verified ? 'Verified' : 'Unverified'} 53 | </Typography> 54 | </div> 55 | </div> 56 | <Link href="/verification"> 57 | <a className={styles.dropDownMenuItem}> 58 | <Image src={verificationIcon} alt="verificationIcon" width={30} height={30} /> 59 | <span>Verification</span> 60 | </a> 61 | </Link> 62 | <Link href="/referral"> 63 | <a className={styles.dropDownMenuItem}> 64 | <Image src={referralIcon} alt="referralIcon" width={30} height={30} /> 65 | <span>Referral</span> 66 | </a> 67 | </Link> 68 | <Link href="/logout"> 69 | <a className={clsx(styles.dropDownMenuItem, styles.dropDownMenuLogout)}> 70 | <Image src={logoutIcon} alt="logoutIcon" width={30} height={30} /> 71 | <span>Log Out</span> 72 | </a> 73 | </Link> 74 | </div> 75 | </div> 76 | )} 77 | </div> 78 | ); 79 | }); 80 | -------------------------------------------------------------------------------- /components/Navbar/index.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image'; 2 | import { useRouter } from 'next/router'; 3 | import educationIcon from 'public/static/education.svg'; 4 | import homeIcon from 'public/static/home.svg'; 5 | import logoIcon from 'public/static/logo.png'; 6 | import marketIcon from 'public/static/market.svg'; 7 | import portfolioIcon from 'public/static/portfolio.svg'; 8 | import menuIcon from 'public/static/menu.svg'; 9 | import backIcon from 'public/static/back.svg'; 10 | import closeIcon from 'public/static/close.svg'; 11 | import React from 'react'; 12 | import styles from './Navbar.module.scss'; 13 | import { NavItem } from './NavItem'; 14 | import Link from 'next/link'; 15 | import { UserDialog } from './UserDialog'; 16 | import { useState } from 'react'; 17 | import clsx from 'clsx'; 18 | import { RemoveScrollBar } from 'react-remove-scroll-bar'; 19 | 20 | type PropsType = { 21 | children: React.ReactNode; 22 | }; 23 | 24 | export const Navbar: React.FC<PropsType> = React.memo(function Navbar({ children }) { 25 | const [displayNav, setDisplayNav] = useState(false); 26 | 27 | const router = useRouter(); 28 | const path = router.pathname.split('/'); 29 | const active = path[1]; 30 | 31 | return ( 32 | <div className={styles.container}> 33 | <header className={styles.header}> 34 | <Link href="/home"> 35 | <a className={styles.logoContainer}> 36 | <Image src={logoIcon} layout="fixed" alt="Logo icon" width={42} height={38} /> 37 | <span>TryCrypto</span> 38 | </a> 39 | </Link> 40 | <div className={styles.headerBar}> 41 | <Link href={`/${active}`}> 42 | <a className={styles.activePage}> 43 | {path[2] && <Image src={backIcon} alt="Back icon" width={24} height={24} />} 44 | <span>{active[0].toUpperCase() + active.slice(1)}</span> 45 | </a> 46 | </Link> 47 | <div className={styles.dialogs}> 48 | <UserDialog /> 49 | <div className={styles.menuIcon} onClick={() => setDisplayNav(true)}> 50 | <Image src={menuIcon} layout="fixed" alt="menu icon" width={28} height={28} /> 51 | </div> 52 | </div> 53 | </div> 54 | </header> 55 | 56 | <div className={styles.content}> 57 | <nav className={clsx(styles.navContainer, displayNav && styles.navMobile)}> 58 | {displayNav && ( 59 | <div className={styles.closeIcon} onClick={() => setDisplayNav(false)}> 60 | <RemoveScrollBar /> 61 | <Image src={closeIcon} layout="fixed" alt="Logo icon" width={28} height={28} /> 62 | </div> 63 | )} 64 | <NavItem icon={homeIcon} href="home" active={active} /> 65 | <NavItem icon={educationIcon} href="education" active={active} /> 66 | <NavItem icon={portfolioIcon} href="portfolio" active={active} /> 67 | <NavItem icon={marketIcon} href="market" active={active} /> 68 | </nav> 69 | <div className={styles.childrenContainer}>{children}</div> 70 | </div> 71 | </div> 72 | ); 73 | }); 74 | -------------------------------------------------------------------------------- /components/Pagination/Pagination.module.scss: -------------------------------------------------------------------------------- 1 | @import 'styles/theme.scss'; 2 | 3 | .container { 4 | display: flex; 5 | align-items: center; 6 | justify-content: center; 7 | position: relative; 8 | border-top: 1px solid $gray; 9 | .pagination { 10 | margin: 16px 0px; 11 | display: flex; 12 | align-items: center; 13 | .paginationArrow { 14 | cursor: pointer; 15 | display: flex; 16 | align-items: center; 17 | margin: 0px 6px; 18 | user-select: none; 19 | } 20 | .disabledArrow { 21 | pointer-events: none; 22 | } 23 | .rotate { 24 | transform: rotate(180deg); 25 | } 26 | .paginationBtn { 27 | cursor: pointer; 28 | border-radius: 4px; 29 | height: 32px; 30 | min-width: 32px; 31 | display: flex; 32 | align-items: center; 33 | justify-content: center; 34 | margin: 0px 2px; 35 | padding: 0px 6px; 36 | font-size: 16px; 37 | font-weight: 600; 38 | user-select: none; 39 | &:hover { 40 | background-color: $secondary; 41 | } 42 | } 43 | .active { 44 | background-color: $primary; 45 | } 46 | .active:hover { 47 | background-color: $primary; 48 | } 49 | } 50 | .showing { 51 | @media all and (max-width: 768px) { 52 | display: none; 53 | } 54 | position: absolute; 55 | left: 16px; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /components/Pagination/index.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import Image from 'next/image'; 3 | import Link from 'next/link'; 4 | import arrowIcon from 'public/static/back.svg'; 5 | import React from 'react'; 6 | import { createPagination } from 'utils/createPagination'; 7 | import styles from './Pagination.module.scss'; 8 | 9 | type PropsType = { 10 | currentPage: number; 11 | numberOfItems: number; 12 | itemsPerPage: number; 13 | navHref: string; 14 | setCurrentPage: (newPage: number) => void; 15 | }; 16 | 17 | export const Pagination: React.FC<PropsType> = React.memo( 18 | ({ currentPage, numberOfItems, itemsPerPage, navHref, setCurrentPage }) => { 19 | const { pagination, showing } = createPagination({ 20 | numberOfItems, 21 | itemsPerPage, 22 | numberOfButtons: 5, 23 | currentPage, 24 | }); 25 | 26 | const handleChangePage = (newPage: number) => () => { 27 | setCurrentPage(newPage); 28 | window.scrollTo(0, 0); 29 | }; 30 | 31 | return ( 32 | <div className={styles.container}> 33 | <div className={styles.showing}>{`Showing ${showing.items} out of ${showing.total}`}</div> 34 | <div className={styles.pagination}> 35 | <Link 36 | href={`${navHref}${pagination[0] === currentPage ? '' : `?page=${currentPage - 1}`}`} 37 | > 38 | <a 39 | onClick={handleChangePage(currentPage - 1)} 40 | className={clsx( 41 | styles.paginationArrow, 42 | pagination[0] === currentPage && styles.disabledArrow 43 | )} 44 | > 45 | <Image src={arrowIcon} alt="Arrow icon" width={14} height={14} /> 46 | </a> 47 | </Link> 48 | 49 | {pagination.map((page) => { 50 | return ( 51 | <Link key={page} href={`${navHref}${page === 1 ? '' : `?page=${page}`}`}> 52 | <a> 53 | <div 54 | color="secondary" 55 | onClick={handleChangePage(page)} 56 | className={clsx(styles.paginationBtn, page === currentPage && styles.active)} 57 | > 58 | {page} 59 | </div> 60 | </a> 61 | </Link> 62 | ); 63 | })} 64 | 65 | <Link 66 | href={`${navHref}${ 67 | [...pagination].reverse()[0] === currentPage ? '' : `?page=${currentPage + 1}` 68 | }`} 69 | > 70 | <a 71 | onClick={handleChangePage(currentPage + 1)} 72 | className={clsx( 73 | styles.paginationArrow, 74 | styles.rotate, 75 | [...pagination].reverse()[0] === currentPage && styles.disabledArrow 76 | )} 77 | > 78 | <Image src={arrowIcon} alt="Arrow icon" width={14} height={14} /> 79 | </a> 80 | </Link> 81 | </div> 82 | </div> 83 | ); 84 | } 85 | ); 86 | -------------------------------------------------------------------------------- /components/Paper/Paper.module.scss: -------------------------------------------------------------------------------- 1 | @import 'styles/theme.scss'; 2 | 3 | .container { 4 | background-color: $backgroundGray; 5 | border-radius: 12px; 6 | margin-bottom: 24px; 7 | width: 100%; 8 | @media all and (max-width: 768px) { 9 | margin-bottom: 16px; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /components/Paper/index.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import React from 'react'; 3 | import styles from './Paper.module.scss'; 4 | 5 | type PropsType = { 6 | children: React.ReactNode; 7 | className?: string; 8 | }; 9 | 10 | export const Paper: React.FC<PropsType> = React.memo(function Paper({ children, className }) { 11 | return <div className={clsx(className, styles.container)}>{children}</div>; 12 | }); 13 | -------------------------------------------------------------------------------- /components/PieChart/PieChart.module.scss: -------------------------------------------------------------------------------- 1 | @import 'styles/theme.scss'; 2 | 3 | .container { 4 | display: flex; 5 | width: 100%; 6 | justify-content: space-between; 7 | .chart { 8 | height: 220px; 9 | width: 220px; 10 | } 11 | .chartItemsContainer { 12 | margin-left: 16px; 13 | .chartItem { 14 | display: flex; 15 | align-items: center; 16 | justify-content: space-between; 17 | margin: 6px 0px; 18 | padding: 2px 8px; 19 | border-radius: 8px; 20 | &:hover { 21 | background-color: $secondary; 22 | cursor: pointer; 23 | } 24 | .chartItemName { 25 | display: flex; 26 | align-items: center; 27 | margin-right: 16px; 28 | .chartItemColor { 29 | height: 8px; 30 | width: 8px; 31 | margin-right: 10px; 32 | } 33 | .bg_f3aa4e { 34 | background-color: $primary; 35 | } 36 | .bg_6076ff { 37 | background-color: #6076ff; 38 | } 39 | .bg_omato { 40 | background-color: tomato; 41 | } 42 | .bg_82bb47 { 43 | background-color: #82bb47; 44 | } 45 | .bg_gray { 46 | background-color: $gray; 47 | } 48 | } 49 | } 50 | } 51 | } 52 | 53 | @media all and (max-width: 768px) { 54 | .chart { 55 | display: flex; 56 | align-items: center; 57 | } 58 | .chartShimmer { 59 | height: 160px !important; 60 | width: 160px !important; 61 | .chartShimmerInner { 62 | height: 130px !important; 63 | width: 130px !important; 64 | } 65 | } 66 | .chartItemValueShimmer { 67 | display: none; 68 | } 69 | .chartItemValue { 70 | display: none; 71 | } 72 | } 73 | 74 | .chartShimmer { 75 | height: 220px; 76 | width: 220px; 77 | background-color: $secondary; 78 | border-radius: 100%; 79 | display: flex; 80 | align-items: center; 81 | justify-content: center; 82 | .chartShimmerInner { 83 | height: 180px; 84 | width: 180px; 85 | background-color: $backgroundGray; 86 | border-radius: 100%; 87 | } 88 | } 89 | 90 | .chartItemNameShimmer { 91 | width: 70px; 92 | height: 24px; 93 | background-color: $secondary; 94 | border-radius: 6px; 95 | margin-right: 16px; 96 | } 97 | .chartItemValueShimmer { 98 | width: 50px; 99 | height: 24px; 100 | background-color: $secondary; 101 | border-radius: 6px; 102 | } 103 | -------------------------------------------------------------------------------- /components/PortfolioBalanceCard/PortfolioBalanceCard.module.scss: -------------------------------------------------------------------------------- 1 | @import 'styles/theme.scss'; 2 | 3 | .portfolioBalanceContainer { 4 | display: flex; 5 | justify-content: space-between; 6 | @media all and (max-width: 768px) { 7 | flex-direction: column; 8 | .portfolioBalance { 9 | margin-bottom: 10px; 10 | } 11 | } 12 | .portfolioBalance { 13 | width: 100%; 14 | } 15 | .portfolioBalanceShimmer { 16 | width: 100px; 17 | height: 25px; 18 | background-color: $secondary; 19 | border-radius: 6px; 20 | margin-top: 4px; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /components/PortfolioBalanceCard/index.tsx: -------------------------------------------------------------------------------- 1 | import { Typography } from 'components/Typography'; 2 | import { formatPercent } from 'utils/formatPercent'; 3 | import Link from 'next/link'; 4 | import React from 'react'; 5 | import { VictoryContainer, VictoryPie } from 'victory'; 6 | import styles from './PortfolioBalanceCard.module.scss'; 7 | import { formatDollar } from 'utils/formatDollar'; 8 | import { Card } from 'components/Card'; 9 | import { PieChart, PieChartPreloader } from 'components/PieChart'; 10 | import { Asset } from 'store/slices/types'; 11 | import clsx from 'clsx'; 12 | 13 | type PropsType = { 14 | balance: number; 15 | assets: Asset[]; 16 | }; 17 | 18 | export const PortfolioBalanceCard: React.FC<PropsType> = React.memo(function PortfolioBalanceCard({ 19 | balance, 20 | assets, 21 | }) { 22 | return ( 23 | <Card title="Portfolio balance" withPadding> 24 | <div className={styles.portfolioBalanceContainer}> 25 | <div className={styles.portfolioBalance}> 26 | <Typography variant="regularText" color="gray"> 27 | Account balance: 28 | </Typography> 29 | {balance === 0 ? ( 30 | <div className={clsx(styles.portfolioBalanceShimmer, styles.shimmer)}></div> 31 | ) : ( 32 | <Typography fw="fw-500" fs="fs-24"> 33 | {formatDollar(balance, 20)} 34 | </Typography> 35 | )} 36 | </div> 37 | {!!assets.length ? <PieChart data={assets} /> : <PieChartPreloader />} 38 | </div> 39 | </Card> 40 | ); 41 | }); 42 | -------------------------------------------------------------------------------- /components/Preloader/Preloader.module.scss: -------------------------------------------------------------------------------- 1 | @import 'styles/theme.scss'; 2 | 3 | .preloader { 4 | display: flex; 5 | align-items: center; 6 | justify-content: center; 7 | width: 100%; 8 | height: 100%; 9 | } 10 | -------------------------------------------------------------------------------- /components/Preloader/index.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image'; 2 | import React from 'react'; 3 | import styles from './Preloader.module.scss'; 4 | import loadingIcon from 'public/static/loading.svg'; 5 | 6 | export const Preloader = () => { 7 | return ( 8 | <div className={styles.preloader}> 9 | <Image src={loadingIcon} layout="fixed" alt="Search icon" width={72} height={72} /> 10 | </div> 11 | ); 12 | }; 13 | -------------------------------------------------------------------------------- /components/PriceChangeField/PriceChangeField.module.scss: -------------------------------------------------------------------------------- 1 | @import 'styles/theme.scss'; 2 | 3 | .green { 4 | color: $green; 5 | display: flex; 6 | align-items: center; 7 | //justify-content: flex-end; 8 | span { 9 | margin-right: 3px; 10 | width: 10px; 11 | height: 5px; 12 | border-bottom: solid 5px $green; 13 | border-left: solid 5px transparent; 14 | border-right: solid 5px transparent; 15 | } 16 | } 17 | .red { 18 | color: $red; 19 | display: flex; 20 | align-items: center; 21 | //justify-content: flex-end; 22 | span { 23 | margin-right: 3px; 24 | width: 10px; 25 | height: 5px; 26 | border-top: solid 5px $red; 27 | border-left: solid 5px transparent; 28 | border-right: solid 5px transparent; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /components/PriceChangeField/index.tsx: -------------------------------------------------------------------------------- 1 | import { Typography } from 'components/Typography'; 2 | import { formatPercent } from 'utils/formatPercent'; 3 | import React from 'react'; 4 | import styles from './PriceChangeField.module.scss'; 5 | 6 | type PropsType = { 7 | value: number; 8 | fw?: 'fw-300' | 'fw-400' | 'fw-500' | 'fw-600' | 'fw-700' | 'fw-800'; 9 | fs?: 'fs-12' | 'fs-14' | 'fs-16' | 'fs-18' | 'fs-20' | 'fs-22' | 'fs-24'; 10 | }; 11 | 12 | export const PriceChangeField: React.FC<PropsType> = React.memo(function PriceChangeField({ 13 | value, 14 | fw, 15 | fs, 16 | }) { 17 | return ( 18 | <div className={value > 0 ? styles.green : styles.red}> 19 | <span></span> 20 | <Typography fw={fw} fs={fs}> 21 | {formatPercent(Math.abs(value))} 22 | </Typography> 23 | </div> 24 | ); 25 | }); 26 | -------------------------------------------------------------------------------- /components/PriceStatistics/PriceStatistics.module.scss: -------------------------------------------------------------------------------- 1 | @import 'styles/theme.scss'; 2 | 3 | .showBtn { 4 | padding: 0; 5 | padding: 16px; 6 | width: 100%; 7 | } 8 | -------------------------------------------------------------------------------- /components/PriceStatistics/PriceStatisticsGroup/PriceStatisticsGroup.module.scss: -------------------------------------------------------------------------------- 1 | @import 'styles/theme.scss'; 2 | 3 | .container { 4 | margin-bottom: 32px; 5 | .title { 6 | font-size: 14px; 7 | font-weight: 500; 8 | margin-bottom: 6px; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /components/PriceStatistics/PriceStatisticsGroup/index.tsx: -------------------------------------------------------------------------------- 1 | import { Typography } from 'components/Typography'; 2 | import React from 'react'; 3 | import styles from './PriceStatisticsGroup.module.scss'; 4 | 5 | type PropsType = { 6 | title: string; 7 | children: React.ReactNode; 8 | }; 9 | 10 | export const PriceStatisticsGroup: React.FC<PropsType> = React.memo(function PriceStatisticsGroup({ 11 | title, 12 | children, 13 | }) { 14 | return ( 15 | <div className={styles.container}> 16 | <div className={styles.title}>{title}</div> 17 | {children} 18 | </div> 19 | ); 20 | }); 21 | -------------------------------------------------------------------------------- /components/PriceStatistics/PriceStatisticsItem/PriceStatisticsItem.module.scss: -------------------------------------------------------------------------------- 1 | @import 'styles/theme.scss'; 2 | 3 | .container { 4 | display: flex; 5 | align-items: center; 6 | justify-content: space-between; 7 | padding-bottom: 6px; 8 | margin-bottom: 12px; 9 | border-bottom: 1px solid $gray; 10 | &:last-of-type { 11 | border-bottom: 0; 12 | } 13 | div { 14 | font-weight: 500; 15 | } 16 | .col { 17 | text-align: right; 18 | &:first-of-type { 19 | text-align: left; 20 | } 21 | } 22 | } 23 | .subTitle { 24 | font-size: 14px; 25 | font-weight: 500; 26 | color: $gray; 27 | margin-top: 2px; 28 | } 29 | .subValue { 30 | font-size: 14px; 31 | margin-top: 2px; 32 | &:first-of-type { 33 | margin-top: 0; 34 | font-size: 18px; 35 | } 36 | } 37 | 38 | .green { 39 | color: $green; 40 | display: flex; 41 | align-items: center; 42 | justify-content: flex-end; 43 | span { 44 | margin-right: 3px; 45 | width: 10px; 46 | height: 5px; 47 | border-bottom: solid 5px $green; 48 | border-left: solid 5px transparent; 49 | border-right: solid 5px transparent; 50 | } 51 | } 52 | .red { 53 | color: $red; 54 | display: flex; 55 | align-items: center; 56 | justify-content: flex-end; 57 | span { 58 | margin-right: 3px; 59 | width: 10px; 60 | height: 5px; 61 | border-top: solid 5px $red; 62 | border-left: solid 5px transparent; 63 | border-right: solid 5px transparent; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /components/PriceStatistics/PriceStatisticsItem/index.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import { Typography } from 'components/Typography'; 3 | import React from 'react'; 4 | import styles from './PriceStatisticsItem.module.scss'; 5 | 6 | type PropsType = { 7 | title: string; 8 | value?: string; 9 | subTitle?: string; 10 | subValue?: string; 11 | }; 12 | 13 | export const PriceStatisticsItem: React.FC<PropsType> = React.memo(function PriceStatisticsItem({ 14 | title, 15 | value, 16 | subTitle, 17 | subValue, 18 | }) { 19 | return ( 20 | <div className={styles.container}> 21 | <div className={styles.col}> 22 | <Typography variant="regularText" color="gray"> 23 | {title} 24 | </Typography> 25 | {subTitle && ( 26 | <div className={styles.subTitle}> 27 | <span></span> 28 | {subTitle} 29 | </div> 30 | )} 31 | </div> 32 | <div className={styles.col}> 33 | {value && ( 34 | <Typography variant="mediumText"> 35 | {value === '∞' || value.includes('#') 36 | ? value 37 | : !!!parseFloat(value.slice(value.includes('-') ? 2 : value.includes('$') ? 1 : 0)) 38 | ? '--' 39 | : value} 40 | </Typography> 41 | )} 42 | {subValue && ( 43 | <div 44 | className={clsx( 45 | styles.subValue, 46 | subValue && !!!parseFloat(subValue) 47 | ? '' 48 | : parseFloat(subValue) > 0 49 | ? styles.green 50 | : styles.red 51 | )} 52 | > 53 | <span></span> 54 | {!!!parseFloat(subValue) ? '--' : subValue} 55 | </div> 56 | )} 57 | </div> 58 | </div> 59 | ); 60 | }); 61 | -------------------------------------------------------------------------------- /components/RecentTransactions/RecentTransaction.module.scss: -------------------------------------------------------------------------------- 1 | @import 'styles/theme.scss'; 2 | 3 | .rTTitle { 4 | display: flex; 5 | align-items: center; 6 | justify-content: space-between; 7 | } 8 | .transactionContainer { 9 | border-top: 1px solid $gray; 10 | display: flex; 11 | align-items: center; 12 | justify-content: space-between; 13 | padding: 10px 16px; 14 | margin-top: -1px; 15 | .nameContainer { 16 | display: flex; 17 | align-items: center; 18 | gap: 20px; 19 | 20 | .date { 21 | text-align: center; 22 | } 23 | .description { 24 | @media all and (max-width: 550px) { 25 | display: none; 26 | } 27 | } 28 | .actionType { 29 | display: flex; 30 | align-items: center; 31 | justify-content: center; 32 | border-radius: 100%; 33 | border: 2px solid $primary; 34 | color: $primary; 35 | font-size: 13px; 36 | font-weight: 500; 37 | padding: 6px; 38 | line-height: 40px; 39 | width: 40px; 40 | height: 40px; 41 | } 42 | } 43 | .priceContainer { 44 | text-align: end; 45 | } 46 | } 47 | 48 | .btnContainer { 49 | display: flex; 50 | align-items: center; 51 | justify-content: center; 52 | padding: 9px; 53 | border-top: 1px solid $gray; 54 | } 55 | 56 | .actionTypeShimmer { 57 | background-color: $secondary; 58 | height: 45.33px; 59 | width: 45.33px; 60 | border-radius: 100%; 61 | } 62 | .contentShimmer { 63 | width: 100%; 64 | height: 45.33px; 65 | background-color: $secondary; 66 | border-radius: 6px; 67 | margin-left: 10px; 68 | } 69 | .withPadding { 70 | padding: 16px; 71 | padding-top: 0px; 72 | } 73 | -------------------------------------------------------------------------------- /components/RecentTransactions/index.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import { Button } from 'components/Button'; 3 | import { Card } from 'components/Card'; 4 | import { Typography } from 'components/Typography'; 5 | import { format, parseISO } from 'date-fns'; 6 | import Link from 'next/link'; 7 | import React from 'react'; 8 | import { Transaction } from 'store/slices/types'; 9 | import styles from './RecentTransaction.module.scss'; 10 | 11 | type PropsType = { 12 | withPadding?: boolean; 13 | transactions?: Transaction[]; 14 | }; 15 | 16 | export const RecentTransactions: React.FC<PropsType> = React.memo(function RecentTransactions({ 17 | transactions, 18 | withPadding, 19 | }) { 20 | return ( 21 | <Card title={'Recent transactions'} withPadding={withPadding}> 22 | <div> 23 | {transactions && transactions.length ? ( 24 | <div> 25 | {transactions.map((txn) => { 26 | return <TransactionItem key={txn.id} {...txn} />; 27 | })} 28 | <Link href="/portfolio/transactionHistory"> 29 | <a className={styles.btnContainer}> 30 | <Button color="secondary">See transaction history</Button> 31 | </a> 32 | </Link> 33 | </div> 34 | ) : ( 35 | <div className={styles.withPadding}> 36 | <Typography variant="regularText"> 37 | You don`t have any transactions. Buy some crypto to see your recent transactions. 38 | </Typography> 39 | </div> 40 | )} 41 | </div> 42 | </Card> 43 | ); 44 | }); 45 | 46 | export const TransactionItem: React.FC<Transaction> = React.memo(function TransactionItem({ 47 | date, 48 | type, 49 | source, 50 | amount, 51 | usdValue, 52 | asset, 53 | }) { 54 | const parsedDate = parseISO(date); 55 | const month = format(parsedDate, 'MMM').toUpperCase(); 56 | const day = format(parsedDate, 'd'); 57 | 58 | return ( 59 | <div className={styles.transactionContainer}> 60 | <div className={styles.nameContainer}> 61 | <div className={styles.actionType}> 62 | {type === 'buy' ? 'Buy' : type === 'sell' ? 'Sell' : 'Rec'} 63 | </div> 64 | <div className={styles.date}> 65 | <Typography variant="thinText">{month}</Typography> 66 | <Typography variant="regularText" color="gray"> 67 | {day} 68 | </Typography> 69 | </div> 70 | <div className={styles.description}> 71 | <Typography variant="regularText">{`${ 72 | type === 'buy' ? 'Bought' : type === 'receive' ? 'Received' : 'Sold' 73 | } ${asset.currency.name}`}</Typography> 74 | <Typography variant="thinText" color="gray"> 75 | {`${type === 'buy' || type === 'receive' ? 'From' : 'To'} ${source}`} 76 | </Typography> 77 | </div> 78 | </div> 79 | <div className={styles.priceContainer}> 80 | <Typography variant="regularText">{`${ 81 | type === 'sell' ? '-' : '+' 82 | }${amount} ${asset.currency.symbol.toUpperCase()}`}</Typography> 83 | <Typography variant="thinText" color="gray"> 84 | {`${type === 'buy' ? '-' : '+'}$${usdValue} `} 85 | </Typography> 86 | </div> 87 | </div> 88 | ); 89 | }); 90 | 91 | export const TransactionItemPreloader = React.memo(function TransactionItemPreloader() { 92 | return ( 93 | <div className={styles.transactionContainer}> 94 | <div className={clsx(styles.actionTypeShimmer, styles.shimmer)}></div> 95 | <div className={clsx(styles.contentShimmer, styles.shimmer)}></div> 96 | </div> 97 | ); 98 | }); 99 | -------------------------------------------------------------------------------- /components/TextField/TextField.module.scss: -------------------------------------------------------------------------------- 1 | @import 'styles/theme.scss'; 2 | 3 | .container { 4 | margin-bottom: 12px; 5 | input { 6 | width: 100%; 7 | height: 100%; 8 | background-color: transparent; 9 | box-shadow: none; 10 | overflow: hidden; 11 | outline: none; 12 | color: white; 13 | font-size: 16px; 14 | border-radius: 6px; 15 | border: 1px solid $gray; 16 | padding: 12px 14px; 17 | margin: 6px 0px; 18 | &:hover { 19 | border-color: $primary; 20 | } 21 | &:focus { 22 | border-color: $primary; 23 | } 24 | &:-webkit-autofill, 25 | &:-webkit-autofill:hover, 26 | &:-webkit-autofill:focus, 27 | &:-webkit-autofill:active { 28 | -webkit-transition-delay: 9999s; 29 | transition-delay: 9999s; 30 | } 31 | } 32 | .error { 33 | border-color: $red !important; 34 | } 35 | } 36 | .title { 37 | font-size: 32px; 38 | font-weight: 600; 39 | } 40 | -------------------------------------------------------------------------------- /components/TextField/index.tsx: -------------------------------------------------------------------------------- 1 | import { Typography } from 'components/Typography'; 2 | import { formatPercent } from 'utils/formatPercent'; 3 | import React from 'react'; 4 | import styles from './TextField.module.scss'; 5 | 6 | type PropsType = { 7 | type: 'text' | 'password'; 8 | name: string; 9 | error?: string; 10 | register: any; 11 | }; 12 | 13 | export const TextField: React.FC<PropsType> = React.memo(function TextField({ 14 | type, 15 | name, 16 | error, 17 | register, 18 | }) { 19 | return ( 20 | <div className={styles.container}> 21 | <Typography fs="fs-14" fw="fw-500"> 22 | {name} 23 | </Typography> 24 | <input type={type} className={error && styles.error} {...register} /> 25 | {error && ( 26 | <Typography fs="fs-14" fw="fw-500" color="red"> 27 | {error} 28 | </Typography> 29 | )} 30 | </div> 31 | ); 32 | }); 33 | -------------------------------------------------------------------------------- /components/TransactionHistory/TransactionHistory.module.scss: -------------------------------------------------------------------------------- 1 | @import 'styles/theme.scss'; 2 | 3 | .table { 4 | border: 0; 5 | border-spacing: 0; 6 | border-collapse: initial; 7 | width: 100%; 8 | text-align: center; 9 | .tableHeader { 10 | display: flex; 11 | justify-content: space-between; 12 | padding: 12px 24px; 13 | margin: 0; 14 | border-top: 1px solid $gray; 15 | li { 16 | list-style: none; 17 | text-align: left; 18 | flex-basis: 14%; 19 | } 20 | } 21 | } 22 | .tableDate { 23 | flex-basis: 18% !important; 24 | margin-right: 6px; 25 | } 26 | .tableSource { 27 | flex-basis: 8% !important; 28 | } 29 | 30 | .tableRowContainer { 31 | display: flex; 32 | justify-content: space-between; 33 | padding: 16px 24px; 34 | border-top: 1px solid $gray; 35 | margin: 0; 36 | 37 | &:hover { 38 | background-color: $secondary; 39 | } 40 | li { 41 | font-weight: 400; 42 | font-size: 18px; 43 | list-style: none; 44 | text-align: left; 45 | flex-basis: 14%; 46 | } 47 | .tableAsset { 48 | :hover { 49 | cursor: pointer; 50 | color: $primary; 51 | text-decoration: underline; 52 | } 53 | } 54 | } 55 | .list { 56 | padding: 0px 16px; 57 | margin: 0; 58 | :first-child { 59 | border: none !important; 60 | padding-top: 0 !important; 61 | } 62 | .li { 63 | display: flex; 64 | flex-direction: column; 65 | width: 100%; 66 | padding: 8px 0px; 67 | border-top: 1px solid $gray; 68 | .liRow { 69 | display: flex; 70 | justify-content: space-between; 71 | } 72 | .liRowDivider { 73 | height: 6px; 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /components/Typography/Typography.module.scss: -------------------------------------------------------------------------------- 1 | @import 'styles/theme.scss'; 2 | 3 | .title { 4 | font-size: 22px; 5 | font-weight: 500; 6 | margin-bottom: 6px; 7 | } 8 | .thinText { 9 | font-size: 16px; 10 | font-weight: 400; 11 | } 12 | .regularText { 13 | font-size: 18px; 14 | font-weight: 400; 15 | } 16 | .mediumText { 17 | font-size: 18px; 18 | font-weight: 500; 19 | } 20 | .white { 21 | color: white; 22 | } 23 | .gray { 24 | color: $gray; 25 | } 26 | .green { 27 | color: $green; 28 | } 29 | .red { 30 | color: $red; 31 | } 32 | @for $i from 3 through 8 { 33 | .fw-#{$i * 100} { 34 | font-weight: #{$i * 100}; 35 | } 36 | } 37 | @for $i from 1 through 7 { 38 | .fs-#{$i * 2 + 10} { 39 | font-size: #{$i * 2 + 10}px; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /components/Typography/index.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import React from 'react'; 3 | import styles from './Typography.module.scss'; 4 | 5 | type PropsType = { 6 | children: string | string[] | number; 7 | variant?: 'title' | 'regularText' | 'mediumText' | 'thinText'; 8 | color?: 'gray' | 'white' | 'green' | 'red'; 9 | fw?: 'fw-300' | 'fw-400' | 'fw-500' | 'fw-600' | 'fw-700' | 'fw-800'; 10 | fs?: 'fs-12' | 'fs-14' | 'fs-16' | 'fs-18' | 'fs-20' | 'fs-22' | 'fs-24'; 11 | className?: string; 12 | }; 13 | 14 | export const Typography: React.FC<PropsType> = React.memo(function Typography({ 15 | children, 16 | variant, 17 | color, 18 | fw, 19 | fs, 20 | className, 21 | }) { 22 | return ( 23 | <div 24 | className={clsx( 25 | className, 26 | variant && styles[variant], 27 | color && styles[color], 28 | fw && styles[fw], 29 | fs && styles[fs] 30 | )} 31 | > 32 | {children} 33 | </div> 34 | ); 35 | }); 36 | -------------------------------------------------------------------------------- /components/Watchlist/Watchlist.module.scss: -------------------------------------------------------------------------------- 1 | @import 'styles/theme.scss'; 2 | 3 | @media all and (max-width: 768px) { 4 | .container { 5 | row-gap: 16px !important; 6 | } 7 | .miniChartsContainer { 8 | gap: 16px !important; 9 | } 10 | } 11 | 12 | .container { 13 | display: flex; 14 | flex-direction: column; 15 | row-gap: 24px; 16 | } 17 | 18 | .miniChartsContainer { 19 | display: flex; 20 | gap: 24px; 21 | // flex-wrap: wrap; 22 | // justify-content: space-around; 23 | @media all and (max-width: 1024px) { 24 | flex-direction: column; 25 | column-gap: 24px; 26 | } 27 | } 28 | .miniChart { 29 | //transition: 0.3s; 30 | //min-width: 250px; 31 | width: 100%; 32 | height: 100%; 33 | padding-bottom: 0px; 34 | flex-shrink: 2; 35 | //flex-basis: 100%; 36 | position: relative; 37 | cursor: pointer; 38 | background-color: $backgroundGray; 39 | border-radius: 12px; 40 | .miniChartDesc { 41 | padding: 16px; 42 | .miniChartInfo { 43 | display: flex; 44 | align-items: center; 45 | justify-content: space-between; 46 | 47 | .miniChartInfoName { 48 | margin: 0px 6px; 49 | } 50 | margin-bottom: 6px; 51 | div { 52 | display: flex; 53 | align-items: center; 54 | } 55 | } 56 | } 57 | } 58 | .miniChartButton { 59 | position: absolute; 60 | left: 50%; 61 | top: 50%; 62 | transform: translate(-50%, -50%); 63 | opacity: 0; 64 | transition: 0.2s ease; 65 | } 66 | .display { 67 | opacity: 1; 68 | } 69 | .discoverMore { 70 | display: flex; 71 | align-items: center; 72 | justify-content: center; 73 | border: none; 74 | min-width: 250px; 75 | padding: 16px 0px; 76 | flex-shrink: 2; 77 | flex-basis: 100%; 78 | position: relative; 79 | background-color: $backgroundGray; 80 | border-radius: 12px; 81 | } 82 | .discoverMoreWide { 83 | //margin-top: 24px; 84 | padding: 16px; 85 | } 86 | .preloaderContainer { 87 | width: 100%; 88 | height: 237px; 89 | } 90 | //shimmer 91 | 92 | .miniChartInfoImageNameCon { 93 | width: 50%; 94 | display: flex; 95 | 96 | .miniChartInfoImg { 97 | min-width: 30px; 98 | height: 30px; 99 | border-radius: 100%; 100 | background-color: $secondary; 101 | } 102 | .miniChartInfoCurrName { 103 | margin-left: 10px; 104 | max-width: 70px; 105 | width: 100%; 106 | 107 | height: 22px; 108 | background-color: $secondary; 109 | border-radius: 6px; 110 | } 111 | } 112 | // @media all and (max-width: 1600px) { 113 | // .miniChartInfoCurrName { 114 | // width: 30px !important; 115 | // } 116 | // .miniChartInfoPrice { 117 | // width: 50px !important; 118 | // } 119 | // } 120 | .miniChartInfoPrice { 121 | margin-left: 10px; 122 | max-width: 100px; 123 | width: 50%; 124 | height: 24px; 125 | background-color: $secondary; 126 | border-radius: 6px; 127 | } 128 | .miniChartInfoDate { 129 | margin-top: 4px; 130 | width: 50px; 131 | height: 20px; 132 | background-color: $secondary; 133 | border-radius: 6px; 134 | } 135 | .miniChartInfoPriceChange { 136 | margin-top: 4px; 137 | width: 50px; 138 | height: 20px; 139 | background-color: $secondary; 140 | border-radius: 6px; 141 | } 142 | .miniChartSparkline { 143 | background-color: $secondary; 144 | border-bottom-left-radius: 12px; 145 | border-bottom-right-radius: 12px; 146 | } 147 | 148 | .emptyWatchlistContainer { 149 | display: flex; 150 | flex-direction: column; 151 | align-items: center; 152 | justify-content: center; 153 | width: 100%; 154 | height: 200px; 155 | background-color: $backgroundGray; 156 | border-radius: 12px; 157 | .emptyWatchlistText { 158 | margin-top: 10px; 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /components/WatchlistButton/WatchlistButton.module.scss: -------------------------------------------------------------------------------- 1 | @import 'styles/theme.scss'; 2 | .container { 3 | height: 36px; 4 | width: 36px; 5 | position: relative; 6 | cursor: pointer; 7 | } 8 | .container::before { 9 | position: absolute; 10 | bottom: 2px; 11 | left: 6px; 12 | font-size: 28px; 13 | color: $gray; 14 | :hover { 15 | color: $primary; 16 | } 17 | //fill: $primary; 18 | content: '☆'; 19 | } 20 | .container:hover::before { 21 | color: $primary; 22 | } 23 | .isWatchlisted::before { 24 | content: '★'; 25 | color: $primary; 26 | } 27 | .outlined { 28 | padding: 18px; 29 | margin-left: 10px; 30 | background-color: $secondary; 31 | border-radius: 8px; 32 | } 33 | -------------------------------------------------------------------------------- /components/WatchlistButton/index.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import React, { useState } from 'react'; 3 | import { useDispatch, useSelector } from 'react-redux'; 4 | import { selectIsWatclistedCurrency } from 'store/selectors'; 5 | import { 6 | fetchCreateWatchlistCurrency, 7 | fetchDeleteWatchlistCurrency, 8 | } from 'store/slices/watchlistSlice'; 9 | import styles from './WatchlistButton.module.scss'; 10 | 11 | type PropsTypes = { 12 | currencyId: string; 13 | outlined?: boolean; 14 | //isWatchlisted?: boolean; 15 | }; 16 | 17 | export const WatchlistButton: React.FC<PropsTypes> = React.memo(function WatchlistButton({ 18 | currencyId, 19 | outlined, 20 | }) { 21 | const isWatchlisted = useSelector(selectIsWatclistedCurrency(currencyId)); 22 | const dispatch = useDispatch(); 23 | 24 | const handleIsWatchlisted = () => { 25 | if (!isWatchlisted) { 26 | dispatch(fetchCreateWatchlistCurrency(currencyId)); 27 | } else dispatch(fetchDeleteWatchlistCurrency(currencyId)); 28 | }; 29 | 30 | return ( 31 | <div 32 | onClick={handleIsWatchlisted} 33 | className={clsx( 34 | styles.container, 35 | outlined && styles.outlined, 36 | isWatchlisted && styles.isWatchlisted 37 | )} 38 | ></div> 39 | ); 40 | }); 41 | -------------------------------------------------------------------------------- /db/config/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = { 3 | development: { 4 | username: 'root', 5 | password: 'password', 6 | database: 'try-crypto', 7 | host: '127.0.0.1', 8 | dialect: 'mysql', 9 | logging: false, 10 | }, 11 | test: { 12 | username: process.env.DB_USER, 13 | password: process.env.DB_PASS, 14 | database: process.env.DB_NAME, 15 | host: process.env.DB_HOST, 16 | dialect: 'mysql', 17 | logging: false, 18 | }, 19 | production: { 20 | use_env_variable: 'CLEARDB_DATABASE_URL', 21 | dialect: 'mysql', 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /db/migrations/20210713145001-create-user.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { v4: uuidv4 } = require('uuid'); 3 | 4 | module.exports = { 5 | up: async (queryInterface, Sequelize) => { 6 | await queryInterface.createTable('Users', { 7 | id: { 8 | allowNull: false, 9 | autoIncrement: true, 10 | primaryKey: true, 11 | type: Sequelize.INTEGER, 12 | }, 13 | password: { 14 | type: Sequelize.STRING, 15 | allowNull: false, 16 | }, 17 | email: { 18 | type: Sequelize.STRING, 19 | allowNull: false, 20 | }, 21 | verifyHash: { 22 | type: Sequelize.STRING, 23 | allowNull: false, 24 | }, 25 | verified: { 26 | type: Sequelize.BOOLEAN, 27 | allowNull: false, 28 | defaultValue: false, 29 | }, 30 | invitedBy: { 31 | type: Sequelize.STRING, 32 | }, 33 | referralLink: { 34 | allowNull: false, 35 | type: Sequelize.STRING, 36 | }, 37 | 38 | createdAt: { 39 | allowNull: false, 40 | type: Sequelize.DATE, 41 | }, 42 | updatedAt: { 43 | allowNull: false, 44 | type: Sequelize.DATE, 45 | }, 46 | }); 47 | }, 48 | 49 | down: async (queryInterface, Sequelize) => { 50 | await queryInterface.dropTable('Users'); 51 | }, 52 | }; 53 | -------------------------------------------------------------------------------- /db/migrations/20210713145652-create-currency.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = { 3 | up: async (queryInterface, Sequelize) => { 4 | await queryInterface.createTable('Currencies', { 5 | // id: { 6 | // allowNull: false, 7 | // autoIncrement: true, 8 | // primaryKey: true, 9 | // type: Sequelize.INTEGER 10 | // }, 11 | id: { 12 | allowNull: false, 13 | primaryKey: true, 14 | type: Sequelize.STRING, 15 | }, 16 | name: { 17 | type: Sequelize.STRING, 18 | }, 19 | symbol: { 20 | type: Sequelize.STRING, 21 | }, 22 | }); 23 | }, 24 | down: async (queryInterface, Sequelize) => { 25 | await queryInterface.dropTable('Currencies'); 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /db/migrations/20210713150012-create-asset.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = { 3 | up: async (queryInterface, Sequelize) => { 4 | await queryInterface.createTable('Assets', { 5 | id: { 6 | allowNull: false, 7 | autoIncrement: true, 8 | primaryKey: true, 9 | type: Sequelize.INTEGER, 10 | }, 11 | amount: { 12 | type: Sequelize.DOUBLE, 13 | }, 14 | createdAt: { 15 | allowNull: false, 16 | type: Sequelize.DATE, 17 | }, 18 | updatedAt: { 19 | allowNull: false, 20 | type: Sequelize.DATE, 21 | }, 22 | }); 23 | }, 24 | down: async (queryInterface, Sequelize) => { 25 | await queryInterface.dropTable('Assets'); 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /db/migrations/20210713154145-create-transaction.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = { 3 | up: async (queryInterface, Sequelize) => { 4 | await queryInterface.createTable('Transactions', { 5 | id: { 6 | allowNull: false, 7 | autoIncrement: true, 8 | primaryKey: true, 9 | type: Sequelize.INTEGER, 10 | }, 11 | date: { 12 | type: Sequelize.DATE, 13 | }, 14 | source: { 15 | type: Sequelize.STRING, 16 | }, 17 | type: { 18 | type: Sequelize.STRING, 19 | }, 20 | usdValue: { 21 | type: Sequelize.DOUBLE, 22 | }, 23 | amount: { 24 | type: Sequelize.DOUBLE, 25 | }, 26 | total: { 27 | type: Sequelize.DOUBLE, 28 | }, 29 | }); 30 | }, 31 | down: async (queryInterface, Sequelize) => { 32 | await queryInterface.dropTable('Transactions'); 33 | }, 34 | }; 35 | -------------------------------------------------------------------------------- /db/migrations/20210713154345-add-asset-associate.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = { 3 | up: (queryInterface, Sequelize) => { 4 | return queryInterface 5 | .addColumn( 6 | 'assets', // name of Source model 7 | 'userId', // name of the key we're adding 8 | { 9 | type: Sequelize.INTEGER, 10 | references: { 11 | model: 'Users', // name of Target model 12 | key: 'id', // key in Target model that we're referencing 13 | }, 14 | onUpdate: 'CASCADE', 15 | onDelete: 'SET NULL', 16 | } 17 | ) 18 | .then(() => { 19 | return queryInterface.addColumn( 20 | 'assets', // name of Source model 21 | 'currencyId', // name of the key we're adding 22 | { 23 | type: Sequelize.STRING, 24 | references: { 25 | model: 'Currencies', // name of Target model 26 | key: 'id', // key in Target model that we're referencing 27 | }, 28 | onUpdate: 'CASCADE', 29 | onDelete: 'SET NULL', 30 | } 31 | ); 32 | }) 33 | .then(() => { 34 | return queryInterface.addColumn( 35 | 'transactions', // name of Source model 36 | 'assetId', // name of the key we're adding 37 | { 38 | type: Sequelize.INTEGER, 39 | references: { 40 | model: 'assets', // name of Target model 41 | key: 'id', // key in Target model that we're referencing 42 | }, 43 | onUpdate: 'CASCADE', 44 | onDelete: 'SET NULL', 45 | } 46 | ); 47 | }); 48 | }, 49 | down: (queryInterface, Sequelize) => { 50 | return queryInterface 51 | .removeColumn( 52 | 'assets', // name of Source model 53 | 'userId' // key we want to remove 54 | ) 55 | .then(() => { 56 | queryInterface.removeColumn( 57 | 'assets', // name of Source model 58 | 'currencyId' // key we want to remove 59 | ); 60 | }) 61 | .then(() => { 62 | queryInterface.removeColumn( 63 | 'transactions', // name of Source model 64 | 'assetId' // key we want to remove 65 | ); 66 | }); 67 | }, 68 | }; 69 | -------------------------------------------------------------------------------- /db/migrations/20210716143023-create-historical-data.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = { 3 | up: async (queryInterface, Sequelize) => { 4 | await queryInterface.createTable('HistoricalData', { 5 | id: { 6 | allowNull: false, 7 | autoIncrement: true, 8 | primaryKey: true, 9 | type: Sequelize.INTEGER, 10 | }, 11 | }); 12 | }, 13 | down: async (queryInterface, Sequelize) => { 14 | await queryInterface.dropTable('HistoricalData'); 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /db/migrations/20210716143446-create-balance.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = { 3 | up: async (queryInterface, Sequelize) => { 4 | await queryInterface.createTable('Balances', { 5 | id: { 6 | allowNull: false, 7 | autoIncrement: true, 8 | primaryKey: true, 9 | type: Sequelize.INTEGER, 10 | }, 11 | date: { 12 | type: Sequelize.DATE, 13 | allowNull: false, 14 | }, 15 | usdValue: { 16 | type: Sequelize.DOUBLE, 17 | }, 18 | }); 19 | }, 20 | down: async (queryInterface, Sequelize) => { 21 | await queryInterface.dropTable('Balances'); 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /db/migrations/20210716143529-create-pnl.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = { 3 | up: async (queryInterface, Sequelize) => { 4 | await queryInterface.createTable('PNLs', { 5 | id: { 6 | allowNull: false, 7 | autoIncrement: true, 8 | primaryKey: true, 9 | type: Sequelize.INTEGER, 10 | }, 11 | date: { 12 | type: Sequelize.DATE, 13 | allowNull: false, 14 | }, 15 | usdValue: { 16 | type: Sequelize.DOUBLE, 17 | }, 18 | }); 19 | }, 20 | down: async (queryInterface, Sequelize) => { 21 | await queryInterface.dropTable('PNLs'); 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /db/migrations/20210716147521-add-historical-data-associate.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = { 3 | up: (queryInterface, Sequelize) => { 4 | return queryInterface 5 | .addColumn( 6 | 'HistoricalData', // name of Source model 7 | 'userId', // name of the key we're adding 8 | { 9 | type: Sequelize.INTEGER, 10 | references: { 11 | model: 'Users', // name of Target model 12 | key: 'id', // key in Target model that we're referencing 13 | }, 14 | onUpdate: 'CASCADE', 15 | onDelete: 'SET NULL', 16 | } 17 | ) 18 | .then(() => { 19 | return queryInterface.addColumn( 20 | 'Balances', // name of Source model 21 | 'historicalDataId', // name of the key we're adding 22 | { 23 | type: Sequelize.INTEGER, 24 | references: { 25 | model: 'HistoricalData', // name of Target model 26 | key: 'id', // key in Target model that we're referencing 27 | }, 28 | onUpdate: 'CASCADE', 29 | onDelete: 'SET NULL', 30 | } 31 | ); 32 | }) 33 | .then(() => { 34 | return queryInterface.addColumn( 35 | 'PNLs', // name of Source model 36 | 'historicalDataId', // name of the key we're adding 37 | { 38 | type: Sequelize.INTEGER, 39 | references: { 40 | model: 'HistoricalData', // name of Target model 41 | key: 'id', // key in Target model that we're referencing 42 | }, 43 | onUpdate: 'CASCADE', 44 | onDelete: 'SET NULL', 45 | } 46 | ); 47 | }); 48 | }, 49 | down: (queryInterface, Sequelize) => { 50 | return queryInterface 51 | .removeColumn( 52 | 'HistoricalData', // name of Source model 53 | 'userId' // key we want to remove 54 | ) 55 | .then(() => { 56 | queryInterface.removeColumn( 57 | 'Balances', // name of Source model 58 | 'historicalDataId' // key we want to remove 59 | ); 60 | }) 61 | .then(() => { 62 | queryInterface.removeColumn( 63 | 'PNLs', // name of Source model 64 | 'historicalDataId' // key we want to remove 65 | ); 66 | }); 67 | }, 68 | }; 69 | -------------------------------------------------------------------------------- /db/migrations/20210731114944-create-watch.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = { 3 | up: async (queryInterface, Sequelize) => { 4 | await queryInterface.createTable('Watches', { 5 | id: { 6 | allowNull: false, 7 | autoIncrement: true, 8 | primaryKey: true, 9 | type: Sequelize.INTEGER, 10 | }, 11 | createdAt: { 12 | allowNull: false, 13 | type: Sequelize.DATE, 14 | }, 15 | }); 16 | }, 17 | down: async (queryInterface, Sequelize) => { 18 | await queryInterface.dropTable('Watches'); 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /db/migrations/20210731115521-add-watch-associate.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = { 3 | up: (queryInterface, Sequelize) => { 4 | return queryInterface 5 | .addColumn( 6 | 'watches', // name of Source model 7 | 'userId', // name of the key we're adding 8 | { 9 | type: Sequelize.INTEGER, 10 | references: { 11 | model: 'Users', // name of Target model 12 | key: 'id', // key in Target model that we're referencing 13 | }, 14 | onUpdate: 'CASCADE', 15 | onDelete: 'SET NULL', 16 | } 17 | ) 18 | .then(() => { 19 | return queryInterface.addColumn( 20 | 'watches', // name of Source model 21 | 'currencyId', // name of the key we're adding 22 | { 23 | type: Sequelize.STRING, 24 | references: { 25 | model: 'Currencies', // name of Target model 26 | key: 'id', // key in Target model that we're referencing 27 | }, 28 | onUpdate: 'CASCADE', 29 | onDelete: 'SET NULL', 30 | } 31 | ); 32 | }); 33 | }, 34 | down: (queryInterface, Sequelize) => { 35 | return queryInterface 36 | .removeColumn( 37 | 'watches', // name of Source model 38 | 'userId' // key we want to remove 39 | ) 40 | .then(() => { 41 | queryInterface.removeColumn( 42 | 'watches', // name of Source model 43 | 'currencyId' // key we want to remove 44 | ); 45 | }); 46 | }, 47 | }; 48 | -------------------------------------------------------------------------------- /db/models/asset.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = (sequelize, DataTypes) => { 4 | const Asset = sequelize.define('Asset', { 5 | amount: DataTypes.DOUBLE, 6 | }); 7 | 8 | Asset.associate = (models) => { 9 | Asset.belongsTo(models.User, { as: 'assets', foreignKey: 'userId' }); 10 | Asset.belongsTo(models.Currency, { as: 'currency', foreignKey: 'currencyId' }); 11 | Asset.hasMany(models.Transaction, { as: 'transactions', foreignKey: 'assetId' }); 12 | }; 13 | 14 | return Asset; 15 | }; 16 | -------------------------------------------------------------------------------- /db/models/balance.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = (sequelize, DataTypes) => { 4 | const Balance = sequelize.define( 5 | 'Balance', 6 | { 7 | date: { 8 | type: DataTypes.DATE, 9 | allowNull: false, 10 | }, 11 | usdValue: { 12 | type: DataTypes.DOUBLE, 13 | }, 14 | }, 15 | { 16 | timestamps: false, 17 | } 18 | ); 19 | 20 | Balance.associate = (models) => { 21 | Balance.belongsTo(models.HistoricalData, { as: 'balance', foreignKey: 'historicalDataId' }); 22 | }; 23 | 24 | return Balance; 25 | }; 26 | -------------------------------------------------------------------------------- /db/models/currency.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = (sequelize, DataTypes) => { 4 | const Currency = sequelize.define( 5 | 'Currency', 6 | { 7 | id: { 8 | allowNull: false, 9 | primaryKey: true, 10 | type: DataTypes.STRING, 11 | }, 12 | name: DataTypes.STRING, 13 | symbol: DataTypes.STRING, 14 | }, 15 | { 16 | timestamps: false, 17 | } 18 | ); 19 | 20 | Currency.associate = (models) => { 21 | Currency.hasMany(models.Asset, { as: 'currency', foreignKey: 'currencyId' }); 22 | }; 23 | 24 | return Currency; 25 | }; 26 | -------------------------------------------------------------------------------- /db/models/historicalData.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = (sequelize, DataTypes) => { 4 | const HistoricalData = sequelize.define('HistoricalData', {}, { timestamps: false }); 5 | 6 | HistoricalData.associate = (models) => { 7 | HistoricalData.belongsTo(models.User, { as: 'historicalData', foreignKey: 'userId' }); 8 | HistoricalData.hasMany(models.Balance, { as: 'balance', foreignKey: 'historicalDataId' }); 9 | HistoricalData.hasMany(models.PNL, { as: 'PNL', foreignKey: 'historicalDataId' }); 10 | }; 11 | 12 | return HistoricalData; 13 | }; 14 | -------------------------------------------------------------------------------- /db/models/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const Sequelize = require('sequelize'); 6 | const basename = path.basename(__filename); 7 | const env = process.env.NODE_ENV || 'development'; 8 | const config = require(__dirname + '/../config/config.js')[env]; 9 | const db = {}; 10 | 11 | /* Custom handler for reading current working directory */ 12 | const models = process.cwd() + '/db/models/' || __dirname; 13 | 14 | let sequelize; 15 | if (config.use_env_variable) { 16 | sequelize = new Sequelize(process.env[config.use_env_variable], config); 17 | } else { 18 | sequelize = new Sequelize(config.database, config.username, config.password, config); 19 | } 20 | /* fs.readdirSync(__dirname) */ 21 | fs.readdirSync(models) 22 | .filter((file) => { 23 | return file.indexOf('.') !== 0 && file !== basename && file.slice(-3) === '.js'; 24 | }) 25 | .forEach((file) => { 26 | if (file === 'index.js') return; 27 | const model = require(`db/models/${file}`)(sequelize, Sequelize.DataTypes); 28 | 29 | db[model.name] = model; 30 | }); 31 | 32 | Object.keys(db).forEach((modelName) => { 33 | if (db[modelName].associate) { 34 | db[modelName].associate(db); 35 | } 36 | }); 37 | 38 | db.sequelize = sequelize; 39 | db.Sequelize = Sequelize; 40 | 41 | module.exports = db; 42 | -------------------------------------------------------------------------------- /db/models/pnl.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = (sequelize, DataTypes) => { 4 | const PNL = sequelize.define( 5 | 'PNL', 6 | { 7 | date: { 8 | type: DataTypes.DATE, 9 | allowNull: false, 10 | }, 11 | usdValue: { 12 | type: DataTypes.DOUBLE, 13 | }, 14 | }, 15 | { 16 | timestamps: false, 17 | } 18 | ); 19 | 20 | PNL.associate = (models) => { 21 | PNL.belongsTo(models.HistoricalData, { as: 'PNL', foreignKey: 'historicalDataId' }); 22 | }; 23 | 24 | return PNL; 25 | }; 26 | -------------------------------------------------------------------------------- /db/models/transaction.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = (sequelize, DataTypes) => { 4 | const Transaction = sequelize.define( 5 | 'Transaction', 6 | { 7 | date: DataTypes.DATE, 8 | source: DataTypes.STRING, 9 | type: DataTypes.STRING, 10 | usdValue: DataTypes.DOUBLE, 11 | amount: DataTypes.DOUBLE, 12 | total: DataTypes.DOUBLE, 13 | }, 14 | { 15 | timestamps: false, 16 | } 17 | ); 18 | 19 | Transaction.associate = (models) => { 20 | Transaction.belongsTo(models.Asset, { 21 | as: 'transactions', 22 | foreignKey: 'assetId', 23 | }); 24 | }; 25 | 26 | return Transaction; 27 | }; 28 | -------------------------------------------------------------------------------- /db/models/user.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { v4 } = require('uuid'); 3 | 4 | module.exports = (sequelize, DataTypes) => { 5 | const User = sequelize.define('User', { 6 | password: { 7 | type: DataTypes.STRING, 8 | allowNull: false, 9 | }, 10 | email: { 11 | type: DataTypes.STRING, 12 | allowNull: false, 13 | }, 14 | verifyHash: { 15 | type: DataTypes.STRING, 16 | allowNull: false, 17 | }, 18 | verified: { 19 | type: DataTypes.BOOLEAN, 20 | allowNull: false, 21 | defaultValue: false, 22 | }, 23 | invitedBy: { 24 | type: DataTypes.STRING, 25 | }, 26 | referralLink: { 27 | allowNull: false, 28 | type: DataTypes.STRING, 29 | defaultValue: Math.random().toString(36).substring(2), 30 | }, 31 | }); 32 | // User.beforeSave((user, _) => { 33 | // return (user.referralLink = Math.random().toString(36).substring(2)); 34 | // }); 35 | 36 | User.associate = (models) => { 37 | User.hasMany(models.Asset, { as: 'assets', foreignKey: 'userId' }); 38 | User.hasOne(models.HistoricalData, { as: 'historicalData', foreignKey: 'userId' }); 39 | }; 40 | 41 | return User; 42 | }; 43 | -------------------------------------------------------------------------------- /db/models/watch.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = (sequelize, DataTypes) => { 4 | const Watch = sequelize.define('Watch', {}, { updatedAt: false }); 5 | 6 | Watch.associate = (models) => { 7 | Watch.belongsTo(models.User, { as: 'watchlist', foreignKey: 'userId' }); 8 | Watch.belongsTo(models.Currency, { as: 'currency', foreignKey: 'currencyId' }); 9 | }; 10 | 11 | return Watch; 12 | }; 13 | -------------------------------------------------------------------------------- /hooks/useControlBuySell.ts: -------------------------------------------------------------------------------- 1 | import { roundDec } from './../utils/roundDec'; 2 | import { BuySellType } from 'components/BuySellCard'; 3 | import { useEffect } from 'react'; 4 | import { useControlInput } from './useControlInput'; 5 | 6 | export const useControlBuySell = (action: BuySellType, currentPrice: number) => { 7 | const precision = currentPrice > 0.01 ? { amount: 6, total: 2 } : { amount: 0, total: 8 }; 8 | const { value: amount, onChange: onChangeAmount } = useControlInput(20, precision.amount); 9 | const { value: total, onChange: onChangeTotal } = useControlInput(20, precision.total); 10 | 11 | const handleSetAmount = (val: string) => { 12 | onChangeAmount(val); 13 | const newTotal = +val * currentPrice; 14 | onChangeTotal(newTotal > 0 ? `${roundDec(newTotal, precision.total)}` : ''); 15 | }; 16 | const handleSetTotal = (val: string) => { 17 | onChangeTotal(val); 18 | const newAmount = +val / currentPrice; 19 | onChangeAmount(newAmount > 0 ? `${roundDec(newAmount, precision.amount)}` : ''); 20 | }; 21 | 22 | const handleClear = () => { 23 | handleSetAmount(''); 24 | }; 25 | 26 | useEffect(() => handleClear, [action]); 27 | 28 | return { 29 | amount, 30 | total, 31 | handleSetAmount, 32 | handleSetTotal, 33 | handleClear, 34 | precision, 35 | }; 36 | }; 37 | -------------------------------------------------------------------------------- /hooks/useControlInput.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const useControlInput = (intPrecision: number, decPrecision: number) => { 4 | const [value, setValue] = React.useState<string>(''); 5 | 6 | return { 7 | value, 8 | onChange: (val: string) => { 9 | if (val[val.length - 1] === ' ' || isNaN(+val)) return; 10 | 11 | const arr = val.split('.'); 12 | if (arr[0].length <= intPrecision) { 13 | if (arr.length > 1) { 14 | if (arr[1].length <= decPrecision) { 15 | setValue(val); 16 | } 17 | } else setValue(val); 18 | } 19 | }, 20 | }; 21 | }; 22 | -------------------------------------------------------------------------------- /hooks/useDebounce.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | 3 | export default function useDebounce(value: any, delay: number) { 4 | const [debouncedValue, setDebouncedValue] = useState(value); 5 | 6 | useEffect(() => { 7 | const handler = setTimeout(() => { 8 | setDebouncedValue(value); 9 | }, delay); 10 | 11 | return () => { 12 | clearTimeout(handler); 13 | }; 14 | }, [value, delay]); 15 | 16 | return debouncedValue; 17 | } 18 | -------------------------------------------------------------------------------- /hooks/useDidMount.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | 3 | export const useDidMount = () => { 4 | const mountRef = useRef(true); 5 | 6 | useEffect(() => { 7 | mountRef.current = false; 8 | }, []); 9 | return mountRef.current; 10 | }; 11 | -------------------------------------------------------------------------------- /hooks/useFullscreenStatus.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const useFullscreenStatus = (elRef: React.RefObject<HTMLDivElement>) => { 4 | const [isFullscreen, setIsFullscreen] = React.useState(document.fullscreenElement != null); 5 | 6 | const setFullscreen = () => { 7 | if (elRef.current == null) return; 8 | elRef.current 9 | .requestFullscreen() 10 | .then(() => { 11 | setIsFullscreen(document.fullscreenElement != null); 12 | }) 13 | .catch(() => { 14 | setIsFullscreen(false); 15 | }); 16 | }; 17 | 18 | React.useLayoutEffect(() => { 19 | document.onfullscreenchange = () => setIsFullscreen(document.fullscreenElement != null); 20 | return () => (document.onfullscreenchange = null) as unknown as undefined; 21 | }); 22 | 23 | return { isFullscreen, setIsFullscreen: setFullscreen }; 24 | }; 25 | -------------------------------------------------------------------------------- /hooks/useMediaQuery.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | 3 | export const useMediaQuery = (query: string) => { 4 | if (typeof window === 'undefined' || typeof window.matchMedia === 'undefined') return null; 5 | 6 | const [matches, setMatches] = useState<boolean | null>(null); 7 | 8 | useEffect(() => { 9 | const media = window.matchMedia(query); 10 | if (media.matches !== matches) { 11 | setMatches(media.matches); 12 | } 13 | const listener = () => { 14 | setMatches(media.matches); 15 | }; 16 | media.addEventListener('change', listener); 17 | return () => media.removeEventListener('change', listener); 18 | }, [matches, query]); 19 | 20 | return matches; 21 | }; 22 | -------------------------------------------------------------------------------- /hooks/useSortableData.ts: -------------------------------------------------------------------------------- 1 | import { Direction, Key, TableCoin, TableConfig } from 'api/marketApi'; 2 | import React from 'react'; 3 | 4 | type useSortableDataType = { 5 | data: TableCoin[]; 6 | config: TableConfig; 7 | }; 8 | 9 | export const useSortableData = ({ data, config }: useSortableDataType) => { 10 | const [sortConfig, setSortConfig] = React.useState(config); 11 | const [items, setItems] = React.useState(data); 12 | 13 | const sortedItems = React.useMemo(() => { 14 | let sortableItems = [...items]; 15 | sortableItems.sort((a: TableCoin, b: TableCoin) => { 16 | if (a[sortConfig.key] < b[sortConfig.key]) { 17 | return sortConfig.direction === 'asc' ? -1 : 1; 18 | } 19 | if (a[sortConfig.key] > b[sortConfig.key]) { 20 | return sortConfig.direction === 'asc' ? 1 : -1; 21 | } 22 | return 0; 23 | }); 24 | 25 | return sortableItems; 26 | }, [items, sortConfig]); 27 | 28 | const requestSort = (key: Key) => { 29 | let direction: Direction = 'asc'; 30 | if (sortConfig && sortConfig.key === key && sortConfig.direction === 'asc') { 31 | direction = 'desc'; 32 | } 33 | setSortConfig({ key, direction }); 34 | }; 35 | 36 | return { items: sortedItems, setItems, requestSort, sortConfig }; 37 | }; 38 | -------------------------------------------------------------------------------- /middlewares/passport.ts: -------------------------------------------------------------------------------- 1 | import passport from 'passport'; 2 | import { ExtractJwt, Strategy as JWTStrategy } from 'passport-jwt'; 3 | import { Strategy as LocalStrategy } from 'passport-local'; 4 | import { createJwtToken } from 'utils/createJwtToken'; 5 | import { generateMD5 } from 'utils/generateHash'; 6 | const db = require('db/models'); 7 | 8 | passport.use( 9 | new LocalStrategy( 10 | { 11 | usernameField: 'email', 12 | passwordField: 'password', 13 | }, 14 | async (email, password, done) => { 15 | try { 16 | const user = await db.User.findOne({ where: { email } }); 17 | if (!user) return done(null, false, { message: 'Incorrect email.' }); 18 | 19 | if (user.password === generateMD5(password + process.env.SECRET_KEY)) { 20 | const { id, email, verified, referralLink } = user; 21 | return done(null, { id, email, verified, referralLink, token: createJwtToken(user) }); 22 | } else { 23 | return done(null, false, { message: 'Incorrect password.' }); 24 | } 25 | } catch (err) { 26 | done(err, false); 27 | } 28 | } 29 | ) 30 | ); 31 | 32 | passport.use( 33 | new JWTStrategy( 34 | { 35 | secretOrKey: process.env.JWT_SECRET_KEY || 'qwerty', 36 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), 37 | }, 38 | async (payload, done) => { 39 | try { 40 | const user = await db.User.findByPk(payload.data.id); 41 | if (user) { 42 | const { id, email, verified, referralLink } = user; 43 | return done(null, { id, email, verified, referralLink }); 44 | } 45 | done(null, false); 46 | } catch (err) { 47 | done(err, false); 48 | } 49 | } 50 | ) 51 | ); 52 | passport.serializeUser((user: any, done) => { 53 | done(null, user.id); 54 | }); 55 | 56 | passport.deserializeUser(async (id, done) => { 57 | const user = await db.User.findByPk(id); 58 | done(null, user); 59 | }); 60 | 61 | export default passport; 62 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// <reference types="next" /> 2 | /// <reference types="next/types/global" /> 3 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | reactStrictMode: true, 3 | env: { 4 | CLEARDB_DATABASE_URL: process.env.CLEARDB_DATABASE_URL, 5 | SECRET_KEY: process.env.SECRET_KEY, 6 | JWT_SECRET_KEY: process.env.JWT_SECRET_KEY, 7 | ADMIN_SECRET_KEY: process.env.ADMIN_SECRET_KEY, 8 | GMAIL_USER: process.env.GMAIL_USER, 9 | GMAIL_PASS: process.env.GMAIL_PASS, 10 | }, 11 | images: { 12 | domains: ['assets.coingecko.com'], 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "try-crypto", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@hookform/resolvers": "^2.7.1", 13 | "@reduxjs/toolkit": "^1.6.1", 14 | "axios": "^0.21.1", 15 | "bcryptjs": "^2.4.3", 16 | "clsx": "^1.1.1", 17 | "crypto": "^1.0.1", 18 | "date-fns": "^2.22.1", 19 | "js-cookie": "^2.2.1", 20 | "jsonwebtoken": "^8.5.1", 21 | "mysql2": "^2.2.5", 22 | "next": "11.0.0", 23 | "next-connect": "^0.10.1", 24 | "next-redux-wrapper": "^7.0.2", 25 | "nodemailer": "^6.6.3", 26 | "nookies": "^2.5.2", 27 | "nprogress": "^0.2.0", 28 | "passport": "^0.4.1", 29 | "passport-jwt": "^4.0.0", 30 | "passport-local": "^1.0.0", 31 | "react": "17.0.2", 32 | "react-dom": "17.0.2", 33 | "react-hook-form": "^7.12.2", 34 | "react-redux": "^7.2.4", 35 | "react-remove-scroll-bar": "^2.2.0", 36 | "sass": "^1.35.1", 37 | "sequelize": "^6.6.5", 38 | "sequelize-cli": "^6.2.0", 39 | "swr": "^0.5.6", 40 | "uuid": "^8.3.2", 41 | "victory": "^35.9.0", 42 | "yup": "^0.32.9" 43 | }, 44 | "devDependencies": { 45 | "@types/date-fns": "^2.6.0", 46 | "@types/js-cookie": "^2.2.7", 47 | "@types/nodemailer": "^6.4.4", 48 | "@types/nprogress": "^0.2.0", 49 | "@types/passport": "^1.0.7", 50 | "@types/passport-jwt": "^3.0.6", 51 | "@types/passport-local": "^1.0.34", 52 | "@types/react": "17.0.11", 53 | "eslint": "7.28.0", 54 | "eslint-config-next": "11.0.0", 55 | "typescript": "4.3.2" 56 | }, 57 | "description": "This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).", 58 | "main": "next.config.js", 59 | "author": "", 60 | "license": "ISC" 61 | } 62 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import '../styles/globals.scss'; 2 | import type { AppContext, AppProps } from 'next/app'; 3 | import NProgress from 'nprogress'; 4 | import 'nprogress/nprogress.css'; 5 | import Router from 'next/router'; 6 | import { wrapper } from '../store'; 7 | import App from 'next/app'; 8 | import React from 'react'; 9 | import Head from 'next/head'; 10 | 11 | Router.events.on('routeChangeStart', () => NProgress.start()); 12 | Router.events.on('routeChangeComplete', () => NProgress.done()); 13 | Router.events.on('routeChangeError', () => NProgress.done()); 14 | 15 | class MyApp extends App { 16 | static async getServer({ Component, ctx }: AppContext) { 17 | const pageProps = Component.getInitialProps ? await Component.getInitialProps(ctx) : {}; 18 | return { pageProps }; 19 | } 20 | render() { 21 | const { Component, pageProps } = this.props; 22 | return ( 23 | <> 24 | <Head> 25 | <title>TryCrypto 26 | 27 | 28 | 29 | 30 | ); 31 | } 32 | } 33 | 34 | export default wrapper.withRedux(MyApp); 35 | -------------------------------------------------------------------------------- /pages/api/admin/update.ts: -------------------------------------------------------------------------------- 1 | import { addDays } from 'date-fns'; 2 | import type { NextApiRequest, NextApiResponse } from 'next'; 3 | import nextConnect from 'next-connect'; 4 | import { getAssetsMarketData } from 'utils/apiRoutes/getAssetsMarketData'; 5 | 6 | const db = require('db/models/index'); 7 | 8 | const handler = nextConnect().get(async (req: NextApiRequest, res: NextApiResponse) => { 9 | try { 10 | // const isAuth = req.headers.authorization === process.env.ADMIN_SECRET_KEY; 11 | // if (!isAuth) return res.status(403).send('unauthorized'); 12 | // turned off because easycron can`t set headers with free plan 13 | 14 | const users = await db.User.findAll({ 15 | attributes: ['id'], 16 | include: [ 17 | { 18 | model: db.Asset, 19 | as: 'assets', 20 | attributes: { exclude: ['userId', 'currencyId'] }, 21 | include: [{ model: db.Currency, as: 'currency' }], 22 | }, 23 | ], 24 | }); 25 | 26 | const balances = await Promise.all( 27 | users.map(async (user: any) => { 28 | const historicalData = await db.HistoricalData.findOne({ 29 | where: { userId: user.id }, 30 | }); 31 | const historicalDataId = historicalData.id; 32 | 33 | const { assets, balance } = await getAssetsMarketData(user.assets); 34 | 35 | const yesterdayBalance = await db.Balance.findOne({ 36 | where: { historicalDataId: historicalData.id }, 37 | order: [['date', 'DESC']], 38 | }); 39 | 40 | //const date = addDays(new Date(), 1); 41 | const date = new Date().toISOString(); 42 | await db.Balance.create({ 43 | date, //new Date().toISOString(), 44 | usdValue: balance, 45 | historicalDataId, 46 | }); 47 | await db.PNL.create({ 48 | date, //new Date().toISOString(), 49 | usdValue: yesterdayBalance ? +(balance - yesterdayBalance.usdValue).toFixed(2) : 0, 50 | historicalDataId, 51 | }); 52 | console.log('UPDATED'); 53 | 54 | // return { 55 | // date, //new Date().toISOString(), 56 | // usdValue: balance, 57 | // historicalDataId, 58 | // }; 59 | }) 60 | ); 61 | 62 | res.statusCode = 200; 63 | res.json({ 64 | status: 'success', 65 | data: balances, 66 | }); 67 | } catch (err) { 68 | console.log(err); 69 | res.statusCode = 500; 70 | res.json({ 71 | status: 'error', 72 | data: err, 73 | }); 74 | } 75 | }); 76 | 77 | export default handler; 78 | -------------------------------------------------------------------------------- /pages/api/hello.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from 'next' 3 | 4 | type Data = { 5 | name: string 6 | } 7 | 8 | export default function handler( 9 | req: NextApiRequest, 10 | res: NextApiResponse 11 | ) { 12 | res.status(200).json({ name: 'John Doe' }) 13 | } 14 | -------------------------------------------------------------------------------- /pages/api/user/assets/index.ts: -------------------------------------------------------------------------------- 1 | import passport from 'middlewares/passport'; 2 | import type { NextApiResponse } from 'next'; 3 | import nextConnect from 'next-connect'; 4 | import { NextApiReqWithUser } from 'pages/api/auth/[...slug]'; 5 | import { getAssetsMarketData } from 'utils/apiRoutes/getAssetsMarketData'; 6 | 7 | const db = require('db/models/index'); 8 | 9 | const handler = nextConnect().get( 10 | passport.authenticate('jwt', { session: false }), 11 | async (req: NextApiReqWithUser, res: NextApiResponse) => { 12 | try { 13 | const userId = req.user.id; 14 | 15 | const dbAssets = await db.Asset.findAll({ 16 | where: { userId }, 17 | attributes: { exclude: ['userId', 'currencyId'] }, 18 | include: [{ model: db.Currency, as: 'currency' }], 19 | }); 20 | 21 | let { assets, balance } = await getAssetsMarketData(dbAssets); 22 | 23 | if (!Array.isArray(assets)) assets = [assets]; //TODO: fix this 24 | 25 | res.status(200).json({ 26 | status: 'success', 27 | data: { 28 | balance, 29 | assets, 30 | }, 31 | }); 32 | } catch (err) { 33 | console.log(err); 34 | res.status(500).json({ 35 | status: 'error', 36 | data: err, 37 | }); 38 | } 39 | } 40 | ); 41 | 42 | export default handler; 43 | -------------------------------------------------------------------------------- /pages/api/user/portfolio/historical/balanceData.ts: -------------------------------------------------------------------------------- 1 | import passport from 'middlewares/passport'; 2 | import type { NextApiResponse } from 'next'; 3 | import nextConnect from 'next-connect'; 4 | import { NextApiReqWithUser } from './../../../auth/[...slug]'; 5 | const db = require('db/models/index'); 6 | 7 | const handler = nextConnect().get( 8 | passport.authenticate('jwt', { session: false }), 9 | async (req: NextApiReqWithUser, res: NextApiResponse) => { 10 | try { 11 | const userId = req.user.id; 12 | const { interval } = req.query as { interval: string }; 13 | const data = await db.HistoricalData.findOne({ 14 | where: { userId }, 15 | attributes: ['id'], 16 | include: [ 17 | { 18 | model: db.Balance, 19 | as: 'balance', 20 | attributes: { exclude: ['id', 'historicalDataId'] }, 21 | order: [['date', 'DESC']], 22 | limit: +interval, 23 | }, 24 | ], 25 | }); 26 | 27 | res.statusCode = 200; 28 | res.json({ 29 | status: 'success', 30 | data: data.balance, 31 | }); 32 | } catch (err) { 33 | console.log(err); 34 | res.statusCode = 500; 35 | res.json({ 36 | status: 'error', 37 | data: err, 38 | }); 39 | } 40 | } 41 | ); 42 | 43 | export default handler; 44 | -------------------------------------------------------------------------------- /pages/api/user/portfolio/historical/pnlData.ts: -------------------------------------------------------------------------------- 1 | import passport from 'middlewares/passport'; 2 | import type { NextApiResponse } from 'next'; 3 | import nextConnect from 'next-connect'; 4 | import { NextApiReqWithUser } from './../../../auth/[...slug]'; 5 | const db = require('db/models/index'); 6 | 7 | const handler = nextConnect().get( passport.authenticate('jwt', { session: false }), 8 | async (req: NextApiReqWithUser, res: NextApiResponse) => { 9 | try { 10 | const userId = req.user.id; 11 | 12 | const { interval } = req.query as { interval: string }; 13 | const data = await db.HistoricalData.findOne({ 14 | where: { userId }, 15 | attributes: ['id'], 16 | include: [ 17 | { 18 | model: db.PNL, 19 | as: 'PNL', 20 | attributes: { exclude: ['id', 'historicalDataId'] }, 21 | order: [['date', 'DESC']], 22 | limit: +interval, 23 | }, 24 | ], 25 | }); 26 | 27 | res.statusCode = 200; 28 | res.json({ 29 | status: 'success', 30 | data: data.PNL, 31 | }); 32 | } catch (err) { 33 | console.log(err); 34 | res.statusCode = 500; 35 | res.json({ 36 | status: 'error', 37 | data: err, 38 | }); 39 | } 40 | }); 41 | 42 | export default handler; 43 | -------------------------------------------------------------------------------- /pages/api/user/portfolio/index.ts: -------------------------------------------------------------------------------- 1 | import passport from 'middlewares/passport'; 2 | import type { NextApiResponse } from 'next'; 3 | import nextConnect from 'next-connect'; 4 | import { getAssetsMarketData } from 'utils/apiRoutes/getAssetsMarketData'; 5 | import { getTransactionHistory } from 'utils/apiRoutes/getTransactionHistory'; 6 | import { NextApiReqWithUser } from './../../auth/[...slug]'; 7 | 8 | const db = require('db/models/index'); 9 | 10 | const handler = nextConnect().get( 11 | passport.authenticate('jwt', { session: false }), 12 | async (req: NextApiReqWithUser, res: NextApiResponse) => { 13 | try { 14 | const userId = req.user.id; 15 | const data = await db.User.findOne({ 16 | where: { id: userId }, 17 | attributes: [], 18 | include: [ 19 | { 20 | model: db.Asset, 21 | as: 'assets', 22 | attributes: { exclude: ['userId', 'currencyId'] }, 23 | include: [{ model: db.Currency, as: 'currency' }], 24 | }, 25 | ], 26 | }); 27 | 28 | const historicalData = await db.HistoricalData.findOne({ 29 | where: { userId }, 30 | attributes: ['id'], 31 | include: [ 32 | { 33 | model: db.Balance, 34 | as: 'balance', 35 | attributes: { exclude: ['id', 'historicalDataId'] }, 36 | order: [['date', 'DESC']], 37 | limit: 30, 38 | }, 39 | { 40 | model: db.PNL, 41 | as: 'PNL', 42 | attributes: { exclude: ['id', 'historicalDataId'] }, 43 | order: [['date', 'DESC']], 44 | limit: 30, 45 | }, 46 | ], 47 | }); 48 | 49 | const { assets, balance } = await getAssetsMarketData(data.assets); 50 | const recentTransactions = await getTransactionHistory(userId); 51 | 52 | const calcChange = (a: number, b: number) => +(((b - a) / a) * 100).toFixed(2); 53 | let yesterdaysPNL, thirtydaysPNL; 54 | if ( 55 | historicalData.balance.length && 56 | historicalData.PNL.length && 57 | historicalData.balance.length !== 1 58 | ) { 59 | yesterdaysPNL = { 60 | usdValue: historicalData.PNL[0].usdValue, 61 | usdValueChangePercetage: calcChange( 62 | historicalData.balance[1].usdValue, 63 | historicalData.balance[0].usdValue 64 | ), 65 | }; 66 | 67 | thirtydaysPNL = { 68 | usdValue: +( 69 | historicalData.balance[0].usdValue - 70 | historicalData.balance[historicalData.balance.length - 1].usdValue 71 | ).toFixed(2), 72 | usdValueChangePercetage: calcChange( 73 | historicalData.balance[historicalData.balance.length - 1].usdValue, 74 | historicalData.balance[0].usdValue 75 | ), 76 | }; 77 | } else { 78 | yesterdaysPNL = { usdValue: 0, usdValueChangePercetage: 0 }; 79 | thirtydaysPNL = { usdValue: 0, usdValueChangePercetage: 0 }; 80 | } 81 | 82 | res.status(200).json({ 83 | status: 'success', 84 | data: { 85 | balance, 86 | assets, 87 | recentTransactions: recentTransactions.items, 88 | yesterdaysPNL, //: { usdValue: 10, usdValueChangePercetage: 2 }, 89 | thirtydaysPNL, //: { usdValue: 15, usdValueChangePercetage: 5 }, 90 | historicalData, 91 | }, 92 | }); 93 | } catch (err) { 94 | console.log(err); 95 | res.statusCode = 500; 96 | res.json({ 97 | status: 'error', 98 | data: err, 99 | }); 100 | } 101 | } 102 | ); 103 | 104 | export default handler; 105 | -------------------------------------------------------------------------------- /pages/api/user/portfolio/transactionHistory.ts: -------------------------------------------------------------------------------- 1 | import passport from 'middlewares/passport'; 2 | import type { NextApiResponse } from 'next'; 3 | import nextConnect from 'next-connect'; 4 | import { getTransactionHistory } from 'utils/apiRoutes/getTransactionHistory'; 5 | import { NextApiReqWithUser } from './../../auth/[...slug]'; 6 | 7 | const db = require('db/models/index'); 8 | 9 | const handler = nextConnect().get( 10 | passport.authenticate('jwt', { session: false }), 11 | async (req: NextApiReqWithUser, res: NextApiResponse) => { 12 | try { 13 | const userId = req.user.id; 14 | const { size, page } = req.query as { size: string; page: string }; 15 | 16 | const data = await getTransactionHistory(userId, +size, +page); 17 | res.statusCode = 200; 18 | res.json({ 19 | status: 'success', 20 | data, 21 | }); 22 | } catch (err) { 23 | console.log(err); 24 | res.statusCode = 500; 25 | res.json({ 26 | status: 'error', 27 | data: err, 28 | }); 29 | } 30 | } 31 | ); 32 | 33 | export default handler; 34 | -------------------------------------------------------------------------------- /pages/api/user/watchlist/[currencyId].ts: -------------------------------------------------------------------------------- 1 | import passport from 'middlewares/passport'; 2 | import type { NextApiRequest, NextApiResponse } from 'next'; 3 | import nextConnect from 'next-connect'; 4 | import { NextApiReqWithUser } from 'pages/api/auth/[...slug]'; 5 | 6 | const db = require('db/models/index'); 7 | 8 | const handler = nextConnect() 9 | .use(passport.authenticate('jwt', { session: false })) 10 | .get(async (req: NextApiReqWithUser, res: NextApiResponse) => { 11 | try { 12 | const userId = req.user.id; 13 | 14 | const { currencyId } = req.query as { currencyId: string }; 15 | 16 | const watchlistCurrency = await db.Watch.findOne({ 17 | where: { userId, currencyId }, 18 | }); 19 | 20 | res.status(200).json({ 21 | status: 'success', 22 | data: watchlistCurrency, 23 | }); 24 | } catch (err) { 25 | console.log(err); 26 | res.status(500).json({ 27 | status: 'error', 28 | data: err, 29 | }); 30 | } 31 | }) 32 | .post(async (req: NextApiReqWithUser, res: NextApiResponse) => { 33 | try { 34 | const userId = req.user.id; 35 | const { currencyId } = req.query as { currencyId: string }; 36 | 37 | const newWatchlistCurrency = await db.Watch.create({ 38 | userId, 39 | currencyId, 40 | }); 41 | 42 | res.status(201).json({ 43 | status: 'success', 44 | data: newWatchlistCurrency, 45 | }); 46 | } catch (err) { 47 | console.log(err); 48 | res.status(500).json({ 49 | status: 'error', 50 | data: err, 51 | }); 52 | } 53 | }) 54 | .delete(async (req: NextApiReqWithUser, res: NextApiResponse) => { 55 | try { 56 | const userId = req.user.id; 57 | const { currencyId } = req.query as { currencyId: string }; 58 | 59 | await db.Watch.destroy({ where: { userId, currencyId } }); 60 | 61 | res.status(200).json({ 62 | status: 'success', 63 | data: {}, 64 | }); 65 | } catch (err) { 66 | console.log(err); 67 | res.status(500).json({ 68 | status: 'error', 69 | data: err, 70 | }); 71 | } 72 | }); 73 | 74 | export default handler; 75 | -------------------------------------------------------------------------------- /pages/api/user/watchlist/index.ts: -------------------------------------------------------------------------------- 1 | import passport from 'middlewares/passport'; 2 | import type { NextApiRequest, NextApiResponse } from 'next'; 3 | import nextConnect from 'next-connect'; 4 | import { NextApiReqWithUser } from 'pages/api/auth/[...slug]'; 5 | import { User } from 'store/slices/types'; 6 | import { routeHandler } from 'utils/apiRoutes/routeHandler'; 7 | 8 | const db = require('db/models/index'); 9 | 10 | const handler = nextConnect() 11 | .use(passport.authenticate('jwt', { session: false })) 12 | .get(async (req: NextApiReqWithUser, res: NextApiResponse) => 13 | routeHandler(req, res, async () => { 14 | const userId = req.user.id; 15 | 16 | const dbWatchlist = await db.Watch.findAll({ 17 | where: { userId }, 18 | attributes: { exclude: ['userId'] }, 19 | }); 20 | 21 | res.status(200).json({ 22 | status: 'success', 23 | data: dbWatchlist, 24 | }); 25 | }) 26 | ); 27 | // .get(async (req: NextApiReqWithUser, res: NextApiResponse) => { 28 | // try { 29 | // const userId = req.user.id; 30 | 31 | // const dbWatchlist = await db.Watch.findAll({ 32 | // where: { userId }, 33 | // attributes: { exclude: ['userId'] }, 34 | // }); 35 | 36 | // res.status(200).json({ 37 | // status: 'success', 38 | // data: dbWatchlist, 39 | // }); 40 | // } catch (err) { 41 | // console.log(err); 42 | // res.status(500).json({ 43 | // status: 'error', 44 | // data: err, 45 | // }); 46 | // } 47 | // }); 48 | 49 | export default handler; 50 | -------------------------------------------------------------------------------- /pages/education/index.tsx: -------------------------------------------------------------------------------- 1 | import { Layout } from 'components/Layout'; 2 | import React from 'react'; 3 | 4 | export default function Education() { 5 | return ( 6 |
7 | Coming soon 8 |
9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /pages/home/Home.module.scss: -------------------------------------------------------------------------------- 1 | @import 'styles/theme.scss'; 2 | 3 | .inviteUrl { 4 | font-size: 18px; 5 | border: 1px solid $gray; 6 | border-radius: 8px; 7 | padding: 16px; 8 | margin: 16px 0px; 9 | word-break: break-all; 10 | } 11 | .educationContainer { 12 | display: flex; 13 | gap: 24px; 14 | @media all and (max-width: 768px) { 15 | flex-direction: column; 16 | column-gap: 24px; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /pages/home/index.tsx: -------------------------------------------------------------------------------- 1 | import { Api } from 'api'; 2 | import { Banner } from 'components/Banner'; 3 | import { Button } from 'components/Button'; 4 | import { Card } from 'components/Card'; 5 | import { ContentLayout } from 'components/ContentLayout'; 6 | import { CopyButton } from 'components/CopyButton'; 7 | import { EducationCard } from 'components/EducationCard'; 8 | import { Layout } from 'components/Layout'; 9 | import { Paper } from 'components/Paper'; 10 | import { PortfolioBalanceCard } from 'components/PortfolioBalanceCard'; 11 | import { Typography } from 'components/Typography'; 12 | import { Watchlist } from 'components/Watchlist'; 13 | import Image from 'next/image'; 14 | import btcIcon from 'public/static/btc.png'; 15 | import React, { useEffect } from 'react'; 16 | import { useDispatch, useSelector } from 'react-redux'; 17 | import { wrapper } from 'store'; 18 | import { selectUser, selectUserAssets, selectUserPortfolio } from 'store/selectors'; 19 | import { fetchUserAssets } from 'store/slices/assetsSlice'; 20 | import { setUserWatchlist } from 'store/slices/watchlistSlice'; 21 | import { checkAuth } from 'utils/checkAuth'; 22 | import { isDevMode } from 'utils/isDevMode'; 23 | import styles from './Home.module.scss'; 24 | 25 | export default function Home() { 26 | const portfolio = useSelector(selectUserPortfolio); 27 | const assets = useSelector(selectUserAssets); 28 | const user = useSelector(selectUser); 29 | const dispatch = useDispatch(); 30 | useEffect(() => { 31 | dispatch(fetchUserAssets()); 32 | }, [dispatch]); 33 | 34 | return ( 35 | 36 | 42 | 43 | 49 | 50 | 51 | {user && ( 52 | 53 | 54 | Invite a friend and you will both receive $50 in USDT, when they successfully verify 55 | email address 56 | 57 |
58 | {`${isDevMode() ? 'localhost:3000' : 'try-crypto.herokuapp.com'}/register?ref=${ 59 | user.referralLink 60 | }`.slice(0, 35) + '...'} 61 |
62 | 63 |
64 | )} 65 |
66 | 67 |
68 | 69 | 70 | 71 |
72 |
73 |
74 | ); 75 | } 76 | 77 | export const getServerSideProps = wrapper.getServerSideProps((store) => async ({ req, res }) => { 78 | try { 79 | const isRedirect = await checkAuth(store, req.cookies.token); 80 | if (isRedirect) return isRedirect; 81 | 82 | const watchlist = await Api(req.cookies.token).getUserWatchlist(); 83 | store.dispatch(setUserWatchlist(watchlist)); 84 | return { 85 | props: {}, 86 | }; 87 | } catch (err) { 88 | console.log(err); 89 | return { 90 | props: {}, 91 | }; 92 | } 93 | }); 94 | -------------------------------------------------------------------------------- /pages/login/Login.module.scss: -------------------------------------------------------------------------------- 1 | @import 'styles/theme.scss'; 2 | 3 | .container { 4 | display: flex; 5 | flex-direction: column; 6 | align-items: center; 7 | .form { 8 | background-color: $backgroundGray; 9 | padding: 20px; 10 | border-radius: 12px; 11 | display: flex; 12 | flex-direction: column; 13 | max-width: 400px; 14 | width: 100%; 15 | margin-top: 30px; 16 | margin-bottom: 24px; 17 | } 18 | .title { 19 | font-size: 32px; 20 | font-weight: 600; 21 | margin-top: 30px; 22 | } 23 | .link { 24 | color: $primary; 25 | &:hover { 26 | text-decoration: underline; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /pages/login/index.tsx: -------------------------------------------------------------------------------- 1 | import { LandingLayout } from 'components/LandingLayout'; 2 | import React from 'react'; 3 | import { useForm } from 'react-hook-form'; 4 | import { yupResolver } from '@hookform/resolvers/yup'; 5 | import * as Yup from 'yup'; 6 | import styles from './Login.module.scss'; 7 | import { TextField } from 'components/TextField'; 8 | import { Button } from 'components/Button'; 9 | import { useDispatch, useSelector } from 'react-redux'; 10 | import { AuthPayload } from 'api/authApi'; 11 | import { fetchLogin, fetchRegister, setUserLoadingState } from 'store/slices/userSlice'; 12 | import { selectUser, selectUserLoadingState } from 'store/selectors'; 13 | import { LoadingState } from 'store/slices/types'; 14 | import { useEffect } from 'react'; 15 | import { useRouter } from 'next/router'; 16 | import Cookies from 'js-cookie'; 17 | import Link from 'next/link'; 18 | 19 | const schema = Yup.object().shape({ 20 | email: Yup.string().email('Invalid email').required('Required'), 21 | password: Yup.string().min(6, 'Too Short!').max(50, 'Too Long!').required('Required'), 22 | }); 23 | 24 | export default function Login() { 25 | const dispatch = useDispatch(); 26 | const loadingState = useSelector(selectUserLoadingState); 27 | 28 | const { 29 | register, 30 | handleSubmit, 31 | formState: { errors }, 32 | setError, 33 | } = useForm({ 34 | resolver: yupResolver(schema), 35 | }); 36 | 37 | const onSubmit = (data: AuthPayload) => { 38 | dispatch(fetchLogin(data)); 39 | }; 40 | 41 | const router = useRouter(); 42 | const user = useSelector(selectUser); 43 | 44 | useEffect(() => { 45 | if (loadingState === LoadingState.ERROR) 46 | setError('password', { type: 'manual', message: 'Wrong email or password' }); 47 | if (loadingState === LoadingState.LOADED && user) { 48 | Cookies.remove('token'); 49 | Cookies.set('token', user.token); 50 | router.push('/home'); 51 | } 52 | 53 | return () => { 54 | dispatch(setUserLoadingState(LoadingState.NEVER)); 55 | }; 56 | 57 | // eslint-disable-next-line react-hooks/exhaustive-deps 58 | }, [loadingState]); 59 | 60 | return ( 61 | 62 |
63 |
TryCrypto Account Login
64 |
65 | 71 | 72 | 78 | 85 | 86 | 87 | Don`t have an account? Register now 88 | 89 |
90 |
91 | ); 92 | } 93 | -------------------------------------------------------------------------------- /pages/logout/Logout.module.scss: -------------------------------------------------------------------------------- 1 | @import 'styles/theme.scss'; 2 | 3 | .container { 4 | width: 100%; 5 | display: flex; 6 | align-items: center; 7 | justify-content: center; 8 | flex-direction: column; 9 | margin-top: 100px; 10 | .title { 11 | font-size: 48px; 12 | font-weight: 500; 13 | margin-bottom: 10px; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /pages/logout/index.tsx: -------------------------------------------------------------------------------- 1 | import { LandingLayout } from 'components/LandingLayout'; 2 | import { Typography } from 'components/Typography'; 3 | import Cookies from 'js-cookie'; 4 | import { useRouter } from 'next/router'; 5 | import React from 'react'; 6 | import { useEffect } from 'react'; 7 | import { wrapper } from 'store'; 8 | import { setUserData } from 'store/slices/userSlice'; 9 | import styles from './Logout.module.scss'; 10 | 11 | export default function Logout() { 12 | const router = useRouter(); 13 | useEffect(() => { 14 | Cookies.remove('token'); 15 | router.push('/'); 16 | }, [router]); 17 | 18 | return ( 19 | 20 |
21 |
Logout...
22 | 23 | Please wait a few seconds 24 | 25 |
26 |
27 | ); 28 | } 29 | 30 | export const getServerSideProps = wrapper.getServerSideProps((store) => async ({ req, res }) => { 31 | store.dispatch(setUserData(null)); 32 | return { 33 | props: {}, 34 | }; 35 | }); 36 | -------------------------------------------------------------------------------- /pages/market/Market.module.scss: -------------------------------------------------------------------------------- 1 | @import 'styles/theme.scss'; 2 | 3 | @media all and (max-width: 768px) { 4 | .header { 5 | flex-direction: column; 6 | margin-bottom: 16px !important; 7 | } 8 | .menu { 9 | margin-top: 10px; 10 | 11 | width: 100%; 12 | .tab { 13 | width: 50%; 14 | text-align: center; 15 | } 16 | } 17 | } 18 | .header { 19 | display: flex; 20 | justify-content: space-between; 21 | margin-bottom: 30px; 22 | margin-top: 6px; 23 | .nameContainer { 24 | display: flex; 25 | align-items: center; 26 | 27 | h1 { 28 | margin: 0; 29 | font-weight: 500; 30 | color: $gray; 31 | } 32 | .name { 33 | margin: 0px 8px 0px 16px; 34 | color: white; 35 | } 36 | } 37 | .watchlistBtn { 38 | display: flex; 39 | align-items: center; 40 | padding: 6px; 41 | margin-left: 10px; 42 | } 43 | } 44 | 45 | .menu { 46 | display: flex; 47 | 48 | .tab { 49 | cursor: pointer; 50 | color: $gray; 51 | border-top: 2.5px solid transparent; 52 | padding: 2px 24px; 53 | padding-top: 8px; 54 | font-weight: 400; 55 | font-size: 24px; 56 | transition: 0.2s ease-out; 57 | } 58 | .active { 59 | border-color: $primary; 60 | color: $primary; 61 | } 62 | } 63 | .aboutCurrency { 64 | a { 65 | color: $primary; 66 | } 67 | a:hover { 68 | text-decoration: underline; 69 | } 70 | } 71 | .preloaderContainer { 72 | width: 100%; 73 | height: 400px; 74 | } 75 | 76 | .searchBar { 77 | display: flex; 78 | align-items: center; 79 | padding: 16px; 80 | border-bottom: 1px solid $gray; 81 | .searchBarField { 82 | border: 1px solid $gray; 83 | border-radius: 12px; 84 | padding: 8px 12px; 85 | color: $gray; 86 | display: flex; 87 | align-items: center; 88 | max-width: 350px; 89 | width: 100%; 90 | input { 91 | width: 80%; 92 | box-shadow: none; 93 | appearance: none; 94 | background-color: $backgroundGray; 95 | color: white; 96 | border: 0; 97 | outline: 0; 98 | font-size: 16px; 99 | margin-left: 8px; 100 | height: 20px; 101 | line-height: 20px; 102 | } 103 | .searchBarFieldRemove { 104 | cursor: pointer; 105 | } 106 | } 107 | } 108 | 109 | .notFound { 110 | display: flex; 111 | align-items: center; 112 | justify-content: center; 113 | flex-direction: column; 114 | height: 200px; 115 | } 116 | -------------------------------------------------------------------------------- /pages/portfolio/Portfolio.module.scss: -------------------------------------------------------------------------------- 1 | @import 'styles/theme.scss'; 2 | 3 | @media all and (max-width: 768px) { 4 | .header { 5 | flex-direction: column; 6 | } 7 | .headerPnls { 8 | margin-top: 20px; 9 | flex-direction: column; 10 | } 11 | } 12 | 13 | .header { 14 | display: flex; 15 | justify-content: space-between; 16 | padding: 16px; 17 | .headerItem { 18 | margin-right: 170px; 19 | @media all and (max-width: 1024px) { 20 | margin-right: 50px; 21 | } 22 | 23 | .headerItemValueContainer { 24 | margin-top: 6px; 25 | display: flex; 26 | align-items: flex-start; 27 | .headerItemValue { 28 | font-size: 28px; 29 | font-weight: 500; 30 | margin-right: 6px; 31 | } 32 | } 33 | } 34 | .headerPnls { 35 | display: flex; 36 | } 37 | } 38 | 39 | .assetsTable { 40 | .assetsTableHeader { 41 | padding: 8px 16px; 42 | display: flex; 43 | align-items: center; 44 | justify-content: space-between; 45 | border-top: 1px solid $gray; 46 | } 47 | .assetsTableAsset { 48 | flex-basis: 50%; 49 | display: flex; 50 | align-items: center; 51 | .assetsTableAssetName { 52 | margin: 0px 10px; 53 | } 54 | .assetsTableAssetSymbol { 55 | @media all and (max-width: 550px) { 56 | display: none; 57 | } 58 | } 59 | } 60 | .assetsTableAmount { 61 | flex-basis: 25%; 62 | text-align: end; 63 | } 64 | 65 | .assetsTablePrice { 66 | flex-basis: 25%; 67 | text-align: end; 68 | } 69 | } 70 | .tableRowContainer { 71 | cursor: pointer; 72 | padding: 12px 16px; 73 | display: flex; 74 | justify-content: space-between; 75 | align-items: center; 76 | border-top: 1px solid $gray; 77 | &:hover { 78 | background-color: $secondary; 79 | } 80 | } 81 | .allocationContainer { 82 | display: flex; 83 | flex-direction: column; 84 | gap: 0px; 85 | } 86 | 87 | .preloader { 88 | height: 72px; 89 | } 90 | -------------------------------------------------------------------------------- /pages/portfolio/transactionHistory.tsx: -------------------------------------------------------------------------------- 1 | import { Api } from 'api'; 2 | import { Card } from 'components/Card'; 3 | import { Layout } from 'components/Layout'; 4 | import { Pagination } from 'components/Pagination'; 5 | import { Preloader } from 'components/Preloader'; 6 | import { TransactionHistory } from 'components/TransactionHistory'; 7 | import { useMediaQuery } from 'hooks/useMediaQuery'; 8 | import React, { useState } from 'react'; 9 | import { useSelector } from 'react-redux'; 10 | import { wrapper } from 'store'; 11 | import { selectUserTransactionHistory } from 'store/selectors'; 12 | import { setUserTransactionHistory } from 'store/slices/userSlice'; 13 | import { checkAuth } from 'utils/checkAuth'; 14 | import styles from './Portfolio.module.scss'; 15 | 16 | export default function TransactionHistoryPage() { 17 | const data = useSelector(selectUserTransactionHistory); 18 | const [currentPage, setCurrentPage] = useState(data.currentPage + 1); 19 | const isMobile = useMediaQuery('(max-width: 768px)'); 20 | return ( 21 | 22 | 23 | {isMobile === null ? ( 24 |
25 | 26 |
27 | ) : ( 28 |
29 | 30 | {data.totalItems > 15 && ( 31 | 38 | )} 39 |
40 | )} 41 |
42 |
43 | ); 44 | } 45 | 46 | export const getServerSideProps = wrapper.getServerSideProps( 47 | (store) => 48 | async ({ req, res, query }) => { 49 | try { 50 | const isRedirect = await checkAuth(store, req.cookies.token); 51 | if (isRedirect) return isRedirect; 52 | 53 | let currentPage = 0; 54 | let currentSize = 15; 55 | const { size, page } = query; 56 | if (size) currentSize = +size; 57 | if (page) currentPage = +page - 1; 58 | 59 | const data = await Api(req.cookies.token).getUserTransactionHistory({ 60 | size: currentSize, 61 | page: currentPage, 62 | }); 63 | store.dispatch(setUserTransactionHistory(data)); 64 | 65 | return { 66 | props: {}, 67 | }; 68 | } catch (err) { 69 | console.log(err); 70 | return { 71 | props: {}, 72 | }; 73 | } 74 | } 75 | ); 76 | -------------------------------------------------------------------------------- /pages/referral/Referral.module.scss: -------------------------------------------------------------------------------- 1 | @import 'styles/theme.scss'; 2 | 3 | .container { 4 | width: 100%; 5 | display: flex; 6 | flex-direction: column; 7 | align-items: center; 8 | justify-content: center; 9 | .content { 10 | display: flex; 11 | flex-direction: column; 12 | justify-content: space-between; 13 | max-width: 700px; 14 | width: 100%; 15 | padding: 16px; 16 | .title { 17 | font-size: 32px; 18 | font-weight: 500; 19 | } 20 | .subTitle { 21 | margin-top: 10px; 22 | margin-bottom: 16px; 23 | } 24 | .link { 25 | display: flex; 26 | @media all and (max-width: 768px) { 27 | flex-direction: column; 28 | .button { 29 | margin-top: 16px; 30 | width: 100% !important; 31 | } 32 | } 33 | span { 34 | font-size: 18px; 35 | border: 1px solid $gray; 36 | border-radius: 8px; 37 | padding: 16px; 38 | margin-right: 16px; 39 | width: 100%; 40 | overflow: auto; 41 | height: 58px; 42 | word-break: break-all; 43 | } 44 | .button { 45 | padding: 8px; 46 | width: 140px; 47 | } 48 | } 49 | } 50 | } 51 | .statTitle { 52 | display: flex; 53 | align-items: center; 54 | justify-content: center; 55 | font-size: 26px; 56 | font-weight: 500; 57 | margin-bottom: 10px; 58 | } 59 | .statContainer { 60 | display: flex; 61 | align-items: center; 62 | justify-content: space-around; 63 | .statItem { 64 | display: flex; 65 | flex-direction: column; 66 | align-items: center; 67 | .statItemTitle { 68 | font-size: 28px; 69 | font-weight: 500; 70 | margin-bottom: 6px; 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /pages/referral/index.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from 'components/Button'; 2 | import { Layout } from 'components/Layout'; 3 | import { Paper } from 'components/Paper'; 4 | import { Typography } from 'components/Typography'; 5 | import React from 'react'; 6 | import styles from './Referral.module.scss'; 7 | import securityIcon from 'public/static/security.svg'; 8 | import Image from 'next/image'; 9 | import { useState } from 'react'; 10 | import { selectUser } from 'store/selectors'; 11 | import { useSelector } from 'react-redux'; 12 | import { wrapper } from 'store'; 13 | import { checkAuth } from 'utils/checkAuth'; 14 | import { Api } from 'api'; 15 | import { CopyButton } from 'components/CopyButton'; 16 | import { isDevMode } from 'utils/isDevMode'; 17 | 18 | type PropsType = { 19 | numberOfReferrals: number; 20 | }; 21 | 22 | export default function Referral({ numberOfReferrals }: PropsType) { 23 | const user = useSelector(selectUser); 24 | 25 | return ( 26 | 27 |
28 | 29 |
Get $50 for Every Friend
30 | 31 | The more the merrier! Get another $50 every time someone uses your link and verify email 32 | address 33 | 34 | {user && ( 35 |
36 | {`${isDevMode() ? 'localhost:3000' : 'try-crypto.herokuapp.com'}/register?ref=${ 37 | user.referralLink 38 | }`} 39 | 40 |
41 | )} 42 |
43 | 44 |
Statistics
45 |
46 |
47 |
{(numberOfReferrals * 50).toFixed(2)}
48 | 49 | USDT Earned 50 | 51 |
52 |
53 |
{numberOfReferrals}
54 | 55 | Friends Referred 56 | 57 |
58 |
59 |
60 |
61 |
62 | ); 63 | } 64 | 65 | export const getServerSideProps = wrapper.getServerSideProps((store) => async ({ req, res }) => { 66 | try { 67 | const token = req.cookies.token; 68 | const isRedirect = await checkAuth(store, token); 69 | if (isRedirect) return isRedirect; 70 | 71 | const numberOfReferrals = await Api(token).getNumberOfReferrals(); 72 | return { props: { numberOfReferrals } }; 73 | } catch (err) { 74 | console.log(err); 75 | return { props: { numberOfReferrals: 0 } }; 76 | } 77 | }); 78 | -------------------------------------------------------------------------------- /pages/register/Register.module.scss: -------------------------------------------------------------------------------- 1 | @import 'styles/theme.scss'; 2 | 3 | .container { 4 | display: flex; 5 | flex-direction: column; 6 | align-items: center; 7 | .form { 8 | background-color: $backgroundGray; 9 | padding: 20px; 10 | border-radius: 12px; 11 | display: flex; 12 | flex-direction: column; 13 | max-width: 400px; 14 | width: 100%; 15 | margin-top: 30px; 16 | margin-bottom: 24px; 17 | } 18 | .title { 19 | font-size: 32px; 20 | font-weight: 600; 21 | margin-top: 30px; 22 | } 23 | .link { 24 | color: $primary; 25 | &:hover { 26 | text-decoration: underline; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /pages/register/index.tsx: -------------------------------------------------------------------------------- 1 | import { LandingLayout } from 'components/LandingLayout'; 2 | import React from 'react'; 3 | import { useForm } from 'react-hook-form'; 4 | import { yupResolver } from '@hookform/resolvers/yup'; 5 | import * as Yup from 'yup'; 6 | import styles from './Register.module.scss'; 7 | import { TextField } from 'components/TextField'; 8 | import { Button } from 'components/Button'; 9 | import { useDispatch, useSelector } from 'react-redux'; 10 | import { AuthPayload } from 'api/authApi'; 11 | import { fetchRegister, setUserLoadingState } from 'store/slices/userSlice'; 12 | import { selectUserLoadingState } from 'store/selectors'; 13 | import { LoadingState } from 'store/slices/types'; 14 | import { useEffect } from 'react'; 15 | import { useRouter } from 'next/router'; 16 | import Link from 'next/link'; 17 | 18 | const schema = Yup.object().shape({ 19 | email: Yup.string().email('Invalid email adress').required('Required'), 20 | password: Yup.string().min(6, 'Too Short!').max(50, 'Too Long!').required('Required'), 21 | passwordConfirmation: Yup.string() 22 | .oneOf([Yup.ref('password'), null], 'Passwords must match') 23 | .required('Required'), 24 | }); 25 | 26 | export default function Register() { 27 | const router = useRouter(); 28 | const { ref } = router.query as { ref: string }; 29 | 30 | const dispatch = useDispatch(); 31 | const loadingState = useSelector(selectUserLoadingState); 32 | 33 | interface RegisterForm extends AuthPayload { 34 | passwordConfirmation: string; 35 | } 36 | 37 | const { 38 | register, 39 | handleSubmit, 40 | formState: { errors }, 41 | setError, 42 | } = useForm({ 43 | resolver: yupResolver(schema), 44 | }); 45 | 46 | const onSubmit = (data: RegisterForm) => { 47 | const payload = { 48 | email: data.email, 49 | password: data.password, 50 | ref, 51 | }; 52 | 53 | dispatch(fetchRegister(payload as AuthPayload)); 54 | }; 55 | 56 | useEffect(() => { 57 | if (loadingState === LoadingState.ERROR) 58 | setError('email', { type: 'manual', message: 'Email already in use' }); 59 | if (loadingState === LoadingState.LOADED) router.push('/login'); 60 | 61 | return () => { 62 | dispatch(setUserLoadingState(LoadingState.NEVER)); 63 | }; 64 | // eslint-disable-next-line react-hooks/exhaustive-deps 65 | }, [loadingState]); 66 | 67 | return ( 68 | 69 |
70 |
Create account
71 | 72 |
73 | 79 | 85 | 91 | 92 | 99 | 100 | 101 | Already registered? Log In 102 | 103 |
104 |
105 | ); 106 | } 107 | -------------------------------------------------------------------------------- /pages/verification/Verification.module.scss: -------------------------------------------------------------------------------- 1 | @import 'styles/theme.scss'; 2 | 3 | .container { 4 | width: 100%; 5 | display: flex; 6 | align-items: center; 7 | justify-content: center; 8 | .content { 9 | display: flex; 10 | justify-content: space-between; 11 | align-items: center; 12 | max-width: 600px; 13 | width: 100%; 14 | padding: 16px; 15 | } 16 | .title { 17 | font-size: 32px; 18 | font-weight: 500; 19 | } 20 | .subTitle { 21 | margin-top: 10px; 22 | margin-bottom: 16px; 23 | } 24 | .button { 25 | padding: 8px 48px; 26 | } 27 | .hashContent { 28 | margin-top: 100px; 29 | display: flex; 30 | flex-direction: column; 31 | align-items: center; 32 | justify-content: center; 33 | max-width: 350px; 34 | width: 100%; 35 | padding: 16px; 36 | } 37 | } 38 | @media all and (max-width: 768px) { 39 | .icon { 40 | display: none; 41 | } 42 | .button { 43 | width: 100%; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /pages/verification/[hash].tsx: -------------------------------------------------------------------------------- 1 | import { Button } from 'components/Button'; 2 | import { LandingLayout } from 'components/LandingLayout'; 3 | import { Paper } from 'components/Paper'; 4 | import { Typography } from 'components/Typography'; 5 | import Image from 'next/image'; 6 | import Link from 'next/link'; 7 | import { useRouter } from 'next/router'; 8 | import checkIcon from 'public/static/check.svg'; 9 | import React, { useEffect } from 'react'; 10 | import { useDispatch } from 'react-redux'; 11 | import { fetchVerify } from 'store/slices/userSlice'; 12 | import styles from './Verification.module.scss'; 13 | 14 | export default function HashVerification() { 15 | const router = useRouter(); 16 | const { hash } = router.query as { hash: string }; 17 | 18 | const dispatch = useDispatch(); 19 | useEffect(() => { 20 | dispatch(fetchVerify(hash)); 21 | }, [dispatch, router, hash]); 22 | 23 | return ( 24 | 25 |
26 | 27 | check icon 28 |
Thank you
29 | 30 | You have verified your email 31 | 32 | 33 | 34 | 35 | 36 | 37 |
38 |
39 |
40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /pages/verification/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './Verification.module.scss'; 3 | import { Layout } from 'components/Layout'; 4 | import { Typography } from 'components/Typography'; 5 | import { Button } from 'components/Button'; 6 | import securityIcon from 'public/static/security.svg'; 7 | import Image from 'next/image'; 8 | import { useSelector } from 'react-redux'; 9 | import { selectUser } from 'store/selectors'; 10 | import { wrapper } from 'store'; 11 | import { checkAuth } from 'utils/checkAuth'; 12 | import { Paper } from 'components/Paper'; 13 | import { Api } from 'api'; 14 | import Link from 'next/link'; 15 | import { useState } from 'react'; 16 | 17 | export default function Verification() { 18 | const user = useSelector(selectUser); 19 | const [counter, setCounter] = useState(0); 20 | 21 | const handleSendEmail = () => { 22 | Api().sendEmail(); 23 | setCounter(60); 24 | }; 25 | 26 | React.useEffect(() => { 27 | counter > 0 && setTimeout(() => setCounter(counter - 1), 1000); 28 | }, [counter]); 29 | 30 | return ( 31 | 32 |
33 | 34 | {!user?.verified ? ( 35 |
36 |
Verify your email address
37 | 38 | {`Receive $50 by verifying, we will send to you email: ${user?.email} verication message`} 39 | 40 | 43 |
44 | ) : ( 45 |
46 |
Thank you
47 | 48 | You had already verified your email address, we send to you $50 in USDT check you 49 | portfolio 50 | 51 | 52 | 53 | 54 | 55 | 56 |
57 | )} 58 |
59 | 60 | security icon 61 |
62 |
63 |
64 |
65 | ); 66 | } 67 | 68 | export const getServerSideProps = wrapper.getServerSideProps((store) => async ({ req, res }) => { 69 | try { 70 | const isRedirect = await checkAuth(store, req.cookies.token); 71 | if (isRedirect) return isRedirect; 72 | 73 | return { 74 | props: {}, 75 | }; 76 | } catch (err) { 77 | console.log(err); 78 | return { 79 | props: {}, 80 | }; 81 | } 82 | }); 83 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VHarastei/TryCrypto/827a688146265844c4426ed1bc85a01cb5ec104d/public/favicon.ico -------------------------------------------------------------------------------- /public/static/back.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/static/bought.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /public/static/btc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VHarastei/TryCrypto/827a688146265844c4426ed1bc85a01cb5ec104d/public/static/btc.png -------------------------------------------------------------------------------- /public/static/check.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/static/close.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/static/coinGecko.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VHarastei/TryCrypto/827a688146265844c4426ed1bc85a01cb5ec104d/public/static/coinGecko.png -------------------------------------------------------------------------------- /public/static/crypto.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VHarastei/TryCrypto/827a688146265844c4426ed1bc85a01cb5ec104d/public/static/crypto.png -------------------------------------------------------------------------------- /public/static/cryptocurrencies.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/static/cryptocurrency.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/static/education.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/static/filledStar.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/static/fullscreen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/static/fullscreenExit.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/static/github.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /public/static/home.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/static/lineChart.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/static/linkedin.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/static/loading.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /public/static/loadingMini.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /public/static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VHarastei/TryCrypto/827a688146265844c4426ed1bc85a01cb5ec104d/public/static/logo.png -------------------------------------------------------------------------------- /public/static/logout.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/static/market.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/static/menu.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/static/portfolio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VHarastei/TryCrypto/827a688146265844c4426ed1bc85a01cb5ec104d/public/static/portfolio.png -------------------------------------------------------------------------------- /public/static/portfolio.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/static/referral.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/static/search.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/static/security.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/static/star.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/static/swap.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/static/toll.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/static/unverified.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/static/usd.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/static/user.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /public/static/verification.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/static/verified.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /store/index.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers, configureStore, Store } from '@reduxjs/toolkit'; 2 | import { createWrapper } from 'next-redux-wrapper'; 3 | import { useDispatch } from 'react-redux'; 4 | import { assetsReducer, AssetsSliceState } from './slices/assetsSlice'; 5 | import { userReducer, UserSliceState } from './slices/userSlice'; 6 | import { watchlistReducer, WatchlistSliceState } from './slices/watchlistSlice'; 7 | 8 | export type RootState = { 9 | user: UserSliceState; 10 | assets: AssetsSliceState; 11 | watchlist: WatchlistSliceState; 12 | }; 13 | 14 | export const rootReducer = combineReducers({ 15 | user: userReducer, 16 | assets: assetsReducer, 17 | watchlist: watchlistReducer, 18 | }); 19 | 20 | export const makeStore = (): Store => 21 | configureStore({ 22 | reducer: rootReducer, 23 | }); 24 | 25 | const store = configureStore({ 26 | reducer: rootReducer, 27 | }); 28 | 29 | export type AppDispatch = typeof store.dispatch; 30 | export const useAppDispatch = () => useDispatch(); 31 | 32 | export const wrapper = createWrapper>(makeStore); 33 | //{ debug: true } 34 | -------------------------------------------------------------------------------- /store/selectors.ts: -------------------------------------------------------------------------------- 1 | import { createSelector } from '@reduxjs/toolkit'; 2 | import { RootState } from 'store'; 3 | import { Asset, LoadingState, WatchlistCurrency } from './slices/types'; 4 | type Selector = (state: RootState) => S; 5 | 6 | export const selectUserLoadingState = (state: RootState) => state.user.loadingState; 7 | export const selectUser = (state: RootState) => state.user.data; 8 | 9 | export const selectUserPortfolio = (state: RootState) => state.user.portfolio; 10 | export const selectUserTransactionHistory = (state: RootState) => 11 | state.user.portfolio.transactionHistory; 12 | 13 | export const selectUserAssets = (state: RootState) => state.assets.items; 14 | export const selectUserAsset = (currencyId: string): Selector => 15 | createSelector( 16 | [(state: RootState) => state.assets.items.find((a) => a.currency.id === currencyId)], 17 | (asset) => asset 18 | ); 19 | export const selectUserAssetsIsLoading = (state: RootState) => 20 | state.assets.loadingState === LoadingState.LOADING; 21 | export const selectAssetsTransactionIsLoading = (state: RootState) => 22 | state.assets.transactionLoadingState === LoadingState.LOADING; 23 | 24 | export const selectUserWatchlist = (state: RootState) => state.watchlist.items; 25 | export const selectIsWatclistedCurrency = ( 26 | currencyId: string 27 | ): Selector => 28 | createSelector( 29 | [(state: RootState) => state.watchlist.items.some((a) => a.currencyId === currencyId)], 30 | (asset) => asset 31 | ); 32 | -------------------------------------------------------------------------------- /store/slices/types.ts: -------------------------------------------------------------------------------- 1 | export enum LoadingState { 2 | LOADED = 'LOADED', 3 | LOADING = 'LOADING', 4 | ERROR = 'ERROR', 5 | NEVER = 'NEVER', 6 | } 7 | 8 | export interface Currency { 9 | id: string; 10 | symbol: string; 11 | name: string; 12 | image: string; 13 | } 14 | 15 | export type Asset = { 16 | id: number; 17 | amount: number; 18 | usdValue: number; 19 | usdValuePercentage: number; 20 | currencyPrice: number; 21 | currency: Currency; 22 | transactions: PaginatedTransactions; 23 | }; 24 | 25 | export type Transaction = { 26 | id: number; 27 | date: string; 28 | source: 'market' | 'education' | 'bonuses'; 29 | type: 'buy' | 'sell' | 'receive'; 30 | usdValue: number; 31 | amount: number; 32 | total: number; 33 | asset: Pick; 34 | }; 35 | 36 | export type PaginatedTransactions = { 37 | totalItems: number; 38 | totalPages: number; 39 | currentPage: number; 40 | items: Transaction[]; 41 | loadingState: LoadingState; 42 | }; 43 | 44 | export type HistoricalDataItem = { 45 | date: string; 46 | usdValue: number; 47 | }; 48 | 49 | export type HistoricalData = { 50 | balance: HistoricalDataItem[]; 51 | PNL: HistoricalDataItem[]; 52 | }; 53 | 54 | export type PNL = { 55 | usdValue: number; 56 | usdValueChangePercetage: number; 57 | }; 58 | 59 | export type UserPortfolio = { 60 | balance: number; 61 | recentTransactions: Transaction[]; 62 | yesterdaysPNL: PNL; 63 | thirtydaysPNL: PNL; 64 | historicalData: HistoricalData; 65 | transactionHistory: PaginatedTransactions; 66 | }; 67 | export type WatchlistCurrency = { 68 | id: string; 69 | currencyId: string; 70 | createdAt: string; 71 | }; 72 | 73 | export type User = { 74 | id: string; 75 | email: string; 76 | token: string; 77 | verified: boolean; 78 | referralLink: string; 79 | }; 80 | -------------------------------------------------------------------------------- /styles/globals.scss: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: 'Roboto', -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, 6 | Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 7 | color: white; 8 | background-color: #181a1e; 9 | } 10 | 11 | a { 12 | color: inherit; 13 | text-decoration: none; 14 | } 15 | 16 | * { 17 | box-sizing: border-box; 18 | } 19 | -------------------------------------------------------------------------------- /styles/theme.scss: -------------------------------------------------------------------------------- 1 | $primary: #f3aa4e; 2 | $secondary: #3b3e44; 3 | $backgroundGray: #212528; 4 | $gray: #7b7f82; 5 | $green: #02c076; 6 | $red: #f84960; 7 | $background: #181a1e; 8 | 9 | .shimmer { 10 | animation: shimmer 3s; 11 | animation-iteration-count: infinite; 12 | background: linear-gradient( 13 | to right, 14 | $secondary 0%, 15 | #4f5355 10%, 16 | #5f6264 20%, 17 | #4f5355 30%, 18 | $secondary 40%, 19 | $secondary 80% 20 | ); 21 | 22 | background-size: 800px 100%; 23 | } 24 | @keyframes shimmer { 25 | from { 26 | background-position: -400px 0; 27 | } 28 | to { 29 | background-position: 400px 0; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "baseUrl": "." 17 | }, 18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /utils/apiRoutes/getAssets.ts: -------------------------------------------------------------------------------- 1 | import { fetcher, MarketApi } from 'api/marketApi'; 2 | import { DbAsset } from './getAssetsMarketData'; 3 | import { getPaginatedData } from './getPaginatedData'; 4 | import { getPagination } from './getPagination'; 5 | const db = require('db/models/index'); 6 | 7 | export const getAssets = async (userId: string, currencyIds: string[], page = 0, size = 7) => { 8 | const assets: any[] = await db.Asset.findAll({ 9 | where: { userId, currencyId: currencyIds }, 10 | attributes: ['id', 'amount'], 11 | //order: [[{ model: db.Transaction, as: 'transactions' }, 'date', 'DESC']], 12 | //limit: 7, 13 | //subQuery: false, 14 | include: [ 15 | { 16 | model: db.Currency, 17 | as: 'currency', 18 | }, 19 | // { 20 | // model: db.Transaction, 21 | // as: 'transactions', 22 | // attributes: { exclude: ['assetId'] }, 23 | // }, 24 | ], 25 | }); 26 | 27 | const paginatedAssets = await Promise.all( 28 | assets.map(async (asset: any) => { 29 | const plainAsset = asset.get({ plain: true }); 30 | const { limit, offset } = getPagination(size, page); 31 | const dbTransactions = await db.Transaction.findAndCountAll({ 32 | where: { assetId: asset.id }, 33 | attributes: { exclude: ['assetId'] }, 34 | order: [['date', 'DESC']], 35 | limit, 36 | offset, 37 | }); 38 | const paginatedTransactions = getPaginatedData(dbTransactions, page, limit); 39 | 40 | return { 41 | ...plainAsset, 42 | transactions: paginatedTransactions, 43 | }; 44 | 45 | // return { 46 | // ...plainAsset, 47 | // transactions: { 48 | // totalItems: null, 49 | // totalPages: null, 50 | // currentPage: null, 51 | // items: plainAsset.transactions, 52 | // }, 53 | // }; 54 | }) 55 | ); 56 | 57 | if (!assets.length) return []; 58 | //if (currencyIds.length === 1) return paginatedAssets[0]; 59 | 60 | return paginatedAssets; 61 | }; 62 | -------------------------------------------------------------------------------- /utils/apiRoutes/getAssetsMarketData.ts: -------------------------------------------------------------------------------- 1 | import { fetcher, MarketApi } from 'api/marketApi'; 2 | import { Currency, Transaction } from 'store/slices/types'; 3 | const db = require('db/models/index'); 4 | 5 | export type DbAsset = { 6 | id: number; 7 | amount: number; 8 | currency: Omit; 9 | transactions: Transaction[]; 10 | }; 11 | 12 | export const getAssetsMarketData = async (dbAssets: any[]) => { 13 | const assetsMarketData = await Promise.all( 14 | dbAssets.map((asset: any) => { 15 | return fetcher(MarketApi.getCurrencyDataUrl(asset.currency.id)); 16 | }) 17 | ); 18 | 19 | const assets = assetsMarketData.map((mAsset: any, index) => { 20 | return { 21 | id: dbAssets[index].id, 22 | amount: dbAssets[index].amount, 23 | usdValue: +(mAsset.market_data.current_price.usd * dbAssets[index].amount).toFixed(2), 24 | usdValuePercentage: 0, 25 | currencyPrice: mAsset.market_data.current_price.usd, 26 | currency: { 27 | id: mAsset.id, 28 | name: mAsset.name, 29 | symbol: mAsset.symbol, 30 | image: mAsset.image.large, 31 | }, 32 | transactions: dbAssets[index].transactions || { 33 | totalItems: null, 34 | totalPages: null, 35 | currentPage: null, 36 | items: [], 37 | }, 38 | //transactions: [], 39 | }; 40 | }); 41 | let balance = 0; 42 | assets.forEach((asset) => (balance += asset.currencyPrice * asset.amount)); 43 | assets.forEach((asset, index) => { 44 | assets[index].usdValuePercentage = +( 45 | ((asset.currencyPrice * asset.amount) / balance) * 46 | 100 47 | ).toFixed(2); 48 | }); 49 | assets.sort((a, b) => (a.usdValue > b.usdValue ? -1 : 1)); 50 | 51 | return { 52 | assets, //: dbAssets.length === 1 ? (assets[0] as any) : (assets as any[]), 53 | balance: +balance.toFixed(2), 54 | }; 55 | }; 56 | -------------------------------------------------------------------------------- /utils/apiRoutes/getPaginatedData.ts: -------------------------------------------------------------------------------- 1 | export const getPaginatedData = (data: any, page: number, limit: number) => { 2 | const { count: totalItems, rows: items } = data; 3 | const currentPage = page ? page : 0; 4 | const totalPages = Math.ceil(totalItems / limit); 5 | 6 | return { totalItems, totalPages, currentPage, items }; 7 | }; 8 | -------------------------------------------------------------------------------- /utils/apiRoutes/getPagination.ts: -------------------------------------------------------------------------------- 1 | export const getPagination = (size: number, page: number) => { 2 | const limit = size ? size : 7; 3 | const offset = page ? page * limit : 0; 4 | 5 | return { limit, offset }; 6 | }; 7 | -------------------------------------------------------------------------------- /utils/apiRoutes/getTransactionHistory.ts: -------------------------------------------------------------------------------- 1 | import { getPaginatedData } from './getPaginatedData'; 2 | import { getPagination } from './getPagination'; 3 | const db = require('db/models/index'); 4 | 5 | export const getTransactionHistory = async (userId: string, size = 4, page = 0) => { 6 | const { limit, offset } = getPagination(size, page); 7 | 8 | const dbTransactions = await db.Transaction.findAndCountAll({ 9 | attributes: { exclude: ['assetId'] }, 10 | order: [['date', 'DESC']], 11 | limit, 12 | offset, 13 | include: [ 14 | { 15 | model: db.Asset, 16 | as: 'transactions', 17 | attributes: ['amount'], 18 | include: [{ model: db.Currency, as: 'currency' }], 19 | where: { userId }, 20 | }, 21 | ], 22 | }); 23 | const paginatedTransactions = getPaginatedData(dbTransactions, page, limit); 24 | 25 | const refactoredTransactions = paginatedTransactions.items.map((trn: any) => { 26 | let copy = JSON.parse(JSON.stringify(trn)); 27 | const { transactions, ...newTrn } = copy; //because sequelize...... 28 | newTrn.asset = transactions; 29 | return newTrn; 30 | }); 31 | 32 | return { 33 | ...paginatedTransactions, 34 | items: refactoredTransactions, 35 | }; 36 | }; 37 | -------------------------------------------------------------------------------- /utils/apiRoutes/routeHandler.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next'; 2 | import { NextApiReqWithUser } from 'pages/api/auth/[...slug]'; 3 | 4 | export const routeHandler = async ( 5 | req: NextApiReqWithUser | NextApiRequest, 6 | res: NextApiResponse, 7 | callback: (...args: any) => any 8 | ) => { 9 | try { 10 | return await callback(); 11 | } catch (err) { 12 | console.log(err); 13 | res.status(500).json({ 14 | status: 'error', 15 | data: err, 16 | }); 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /utils/apiRoutes/updateUsdtAsset.ts: -------------------------------------------------------------------------------- 1 | const db = require('db/models/index'); 2 | 3 | export const updateUsdtAsset = async (asset: any, amount: number) => { 4 | const transaction = await db.Transaction.create({ 5 | date: new Date().toISOString(), 6 | source: 'bonuses', 7 | type: 'receive', 8 | usdValue: amount, 9 | amount, 10 | total: amount, 11 | assetId: asset.id, 12 | }); 13 | 14 | const newAmount = +(asset.amount + transaction.amount).toFixed(6); 15 | asset.amount = newAmount; 16 | await asset.save(); 17 | }; 18 | -------------------------------------------------------------------------------- /utils/checkAuth.ts: -------------------------------------------------------------------------------- 1 | import { Store } from '@reduxjs/toolkit'; 2 | import { Api } from 'api'; 3 | import { RootState } from 'store'; 4 | import { User } from 'store/slices/types'; 5 | import { setUserData } from 'store/slices/userSlice'; 6 | 7 | export const checkAuth = async ( 8 | store: Store, 9 | token?: string 10 | ): Promise => { 11 | try { 12 | const user = await Api(token).getMe(); 13 | store.dispatch(setUserData(user)); 14 | } catch (error) { 15 | return { 16 | props: {}, 17 | redirect: { 18 | permanent: false, 19 | destination: '/login', 20 | }, 21 | }; 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /utils/createJwtToken.ts: -------------------------------------------------------------------------------- 1 | import jwt from 'jsonwebtoken'; 2 | 3 | export const createJwtToken = (data: any): string => { 4 | return jwt.sign({ data }, process.env.JWT_SECRET_KEY || 'qwerty', { 5 | expiresIn: '30 days', 6 | algorithm: 'HS256', 7 | }); 8 | }; 9 | -------------------------------------------------------------------------------- /utils/createPagination.ts: -------------------------------------------------------------------------------- 1 | type createPaginationType = { 2 | numberOfItems: number; 3 | itemsPerPage: number; 4 | currentPage: number; 5 | numberOfButtons: number; 6 | }; 7 | 8 | export const createPagination = ({ 9 | numberOfItems, 10 | itemsPerPage, 11 | currentPage, 12 | numberOfButtons, 13 | }: createPaginationType) => { 14 | const items = `${currentPage === 1 ? currentPage : itemsPerPage * (currentPage - 1) + 1} - ${ 15 | itemsPerPage * currentPage > numberOfItems ? numberOfItems : itemsPerPage * currentPage 16 | }`; 17 | let showing = { 18 | items: items, 19 | total: numberOfItems, 20 | }; 21 | const numberOfPages = Math.ceil(numberOfItems / itemsPerPage); 22 | if (currentPage > numberOfPages || currentPage < 1) { 23 | return { 24 | pagination: [], 25 | showing, 26 | }; 27 | } 28 | 29 | const buttons = Array(numberOfPages) 30 | .fill(1) 31 | .map((e, i) => e + i); 32 | const sideButtons = numberOfButtons % 2 === 0 ? numberOfButtons / 2 : (numberOfButtons - 1) / 2; 33 | 34 | const calculLeft = (rest = 0) => { 35 | return { 36 | array: buttons 37 | .slice(0, currentPage - 1) 38 | .reverse() 39 | .slice(0, sideButtons + rest) 40 | .reverse(), 41 | rest: function () { 42 | return sideButtons - this.array.length; 43 | }, 44 | }; 45 | }; 46 | 47 | const calculRight = (rest = 0) => { 48 | return { 49 | array: buttons.slice(currentPage).slice(0, sideButtons + rest), 50 | rest: function () { 51 | return sideButtons - this.array.length; 52 | }, 53 | }; 54 | }; 55 | 56 | const leftButtons = calculLeft(calculRight().rest()).array; 57 | const rightButtons = calculRight(calculLeft().rest()).array; 58 | return { 59 | pagination: [...leftButtons, currentPage, ...rightButtons], 60 | showing, 61 | }; 62 | }; 63 | -------------------------------------------------------------------------------- /utils/formatDollar.ts: -------------------------------------------------------------------------------- 1 | export const formatDollar = (number: number, maximumSignificantDigits: number) => { 2 | return new Intl.NumberFormat('en-US', { 3 | style: 'currency', 4 | currency: 'USD', 5 | maximumSignificantDigits, 6 | }).format(number); 7 | }; 8 | -------------------------------------------------------------------------------- /utils/formatPercent.ts: -------------------------------------------------------------------------------- 1 | export const formatPercent = (number: number) => `${new Number(number).toFixed(2)}%`; 2 | -------------------------------------------------------------------------------- /utils/generateHash.ts: -------------------------------------------------------------------------------- 1 | import crypto from 'crypto'; 2 | 3 | export const generateMD5 = (value: string) => { 4 | return crypto.createHash('md5').update(value).digest('hex'); 5 | }; 6 | -------------------------------------------------------------------------------- /utils/isDevMode.ts: -------------------------------------------------------------------------------- 1 | export const isDevMode = (): boolean => { 2 | if (!process.env.NODE_ENV || process.env.NODE_ENV === 'development') { 3 | return true; 4 | } else { 5 | return false; 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /utils/roundDec.ts: -------------------------------------------------------------------------------- 1 | export const roundDec = (number: number, precision: number) => { 2 | let mult = Math.pow(10, precision); 3 | return Math.floor(number * mult) / mult; 4 | }; 5 | --------------------------------------------------------------------------------