├── docs └── app.png ├── .gitignore ├── frontend ├── postcss.config.js ├── src │ ├── utils.js │ ├── main.jsx │ ├── index.css │ ├── components │ │ └── CryptocurrencyCard.jsx │ └── App.jsx ├── tailwind.config.js ├── vite.config.js ├── index.html ├── .eslintrc.cjs └── package.json ├── backend ├── src │ ├── __pycache__ │ │ ├── init.cpython-311.pyc │ │ ├── main.cpython-311.pyc │ │ ├── config.cpython-311.pyc │ │ ├── router.cpython-311.pyc │ │ └── http_client.cpython-311.pyc │ ├── init.py │ ├── config.py │ ├── router.py │ ├── main.py │ └── http_client.py └── requirements.txt └── README.md /docs/app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artemonsh/fullstack-fastapi-react/HEAD/docs/app.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # frontend 2 | node_modules 3 | 4 | # backend 5 | .idea 6 | .env 7 | venv 8 | 9 | .DS_Store -------------------------------------------------------------------------------- /frontend/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /frontend/src/utils.js: -------------------------------------------------------------------------------- 1 | export default function numberWithCommas(x) { 2 | return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); 3 | } -------------------------------------------------------------------------------- /backend/src/__pycache__/init.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artemonsh/fullstack-fastapi-react/HEAD/backend/src/__pycache__/init.cpython-311.pyc -------------------------------------------------------------------------------- /backend/src/__pycache__/main.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artemonsh/fullstack-fastapi-react/HEAD/backend/src/__pycache__/main.cpython-311.pyc -------------------------------------------------------------------------------- /backend/src/__pycache__/config.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artemonsh/fullstack-fastapi-react/HEAD/backend/src/__pycache__/config.cpython-311.pyc -------------------------------------------------------------------------------- /backend/src/__pycache__/router.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artemonsh/fullstack-fastapi-react/HEAD/backend/src/__pycache__/router.cpython-311.pyc -------------------------------------------------------------------------------- /backend/src/__pycache__/http_client.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artemonsh/fullstack-fastapi-react/HEAD/backend/src/__pycache__/http_client.cpython-311.pyc -------------------------------------------------------------------------------- /backend/src/init.py: -------------------------------------------------------------------------------- 1 | from src.config import settings 2 | from src.http_client import CMCHTTPClient 3 | 4 | 5 | cmc_client = CMCHTTPClient( 6 | base_url="https://pro-api.coinmarketcap.com", 7 | api_key=settings.CMC_API_KEY 8 | ) 9 | -------------------------------------------------------------------------------- /frontend/src/main.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import App from './App.jsx' 4 | import './index.css' 5 | 6 | ReactDOM.createRoot(document.getElementById('root')).render( 7 | 8 | ) 9 | -------------------------------------------------------------------------------- /frontend/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 7 | line-height: 1.5; 8 | font-weight: 400; 9 | 10 | } 11 | -------------------------------------------------------------------------------- /backend/src/config.py: -------------------------------------------------------------------------------- 1 | from pydantic_settings import BaseSettings, SettingsConfigDict 2 | 3 | 4 | class Settings(BaseSettings): 5 | CMC_API_KEY: str 6 | 7 | model_config = SettingsConfigDict(env_file=".env") 8 | 9 | 10 | settings = Settings() 11 | -------------------------------------------------------------------------------- /frontend/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: [ 4 | "./index.html", 5 | "./src/**/*.{js,ts,jsx,tsx}", 6 | ], 7 | theme: { 8 | extend: {}, 9 | }, 10 | plugins: [], 11 | corePlugins: { 12 | preflight: false // <== disable this! 13 | }, 14 | } -------------------------------------------------------------------------------- /frontend/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | import tailwindcss from "tailwindcss"; 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [react()], 8 | css: { 9 | postcss: { 10 | plugins: [tailwindcss()], 11 | }, 12 | }, 13 | }) 14 | -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /backend/src/router.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | 3 | from src.init import cmc_client 4 | 5 | router = APIRouter( 6 | prefix="/cryptocurrencies" 7 | ) 8 | 9 | 10 | @router.get("") 11 | async def get_cryptocurrencies(): 12 | return await cmc_client.get_listings() 13 | 14 | 15 | @router.get("/{currency_id}") 16 | async def get_cryptocurrency(currency_id: int): 17 | return await cmc_client.get_currency(currency_id) 18 | -------------------------------------------------------------------------------- /backend/requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp==3.9.3 2 | aiosignal==1.3.1 3 | annotated-types==0.6.0 4 | anyio==4.3.0 5 | async-lru==2.0.4 6 | attrs==23.2.0 7 | click==8.1.7 8 | fastapi==0.110.0 9 | frozenlist==1.4.1 10 | h11==0.14.0 11 | idna==3.6 12 | multidict==6.0.5 13 | pydantic==2.6.3 14 | pydantic-settings==2.2.1 15 | pydantic_core==2.16.3 16 | python-dotenv==1.0.1 17 | sniffio==1.3.1 18 | starlette==0.36.3 19 | typing_extensions==4.10.0 20 | uvicorn==0.27.1 21 | yarl==1.9.4 22 | -------------------------------------------------------------------------------- /backend/src/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from fastapi.middleware.cors import CORSMiddleware 3 | 4 | from src.router import router as router_crypto 5 | 6 | app = FastAPI() 7 | 8 | app.include_router(router_crypto) 9 | 10 | 11 | origins = [ 12 | "http://localhost:5173", 13 | "http://127.0.0.1:5173", 14 | ] 15 | 16 | app.add_middleware( 17 | CORSMiddleware, 18 | allow_origins=origins, 19 | allow_credentials=True, 20 | allow_methods=["*"], 21 | allow_headers=["*"], 22 | ) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Крипто трекер / Crypto Tracker 2 | ![app.png](docs/app.png) 3 | #### Ссылка на видео урок по этому проекту: https://youtu.be/C9C2EqtVJbQ 4 | 5 | ## Стек 6 | - FastAPI + pydantic, pydantic-settings, aiohttp 7 | - React + axios, ant design, tailwind 8 | 9 | 10 | ### Инструкции 11 | #### Backend 12 | - `python3 -m venv venv` 13 | - `. venv/bin/activate` или `.\venv\Scripts\activate.bat` 14 | - `pip install -r requirements.txt` 15 | - `uvicorn src.main:app --reload` (обязательно находясь внутри папки backend) 16 | 17 | #### Frontend 18 | - `npm create vite@latest` 19 | - `npm install` 20 | - `npm run dev` -------------------------------------------------------------------------------- /frontend/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:react/recommended', 7 | 'plugin:react/jsx-runtime', 8 | 'plugin:react-hooks/recommended', 9 | ], 10 | ignorePatterns: ['dist', '.eslintrc.cjs'], 11 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, 12 | settings: { react: { version: '18.2' } }, 13 | plugins: ['react-refresh'], 14 | rules: { 15 | 'react/jsx-no-target-blank': 'off', 16 | 'react-refresh/only-export-components': [ 17 | 'warn', 18 | { allowConstantExport: true }, 19 | ], 20 | }, 21 | } 22 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "antd": "^5.15.0", 14 | "axios": "^1.6.7", 15 | "react": "^18.2.0", 16 | "react-dom": "^18.2.0" 17 | }, 18 | "devDependencies": { 19 | "@types/react": "^18.2.56", 20 | "@types/react-dom": "^18.2.19", 21 | "@vitejs/plugin-react": "^4.2.1", 22 | "autoprefixer": "^10.4.18", 23 | "eslint": "^8.56.0", 24 | "eslint-plugin-react": "^7.33.2", 25 | "eslint-plugin-react-hooks": "^4.6.0", 26 | "eslint-plugin-react-refresh": "^0.4.5", 27 | "postcss": "^8.4.35", 28 | "tailwindcss": "^3.4.1", 29 | "vite": "^5.1.4" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /backend/src/http_client.py: -------------------------------------------------------------------------------- 1 | from aiohttp import ClientSession 2 | 3 | from async_lru import alru_cache 4 | 5 | 6 | class HTTPClient: 7 | def __init__(self, base_url: str, api_key: str): 8 | self._session = ClientSession( 9 | base_url=base_url, 10 | headers={ 11 | 'X-CMC_PRO_API_KEY': api_key, 12 | } 13 | ) 14 | 15 | 16 | class CMCHTTPClient(HTTPClient): 17 | @alru_cache 18 | async def get_listings(self): 19 | async with self._session.get("/v1/cryptocurrency/listings/latest") as resp: 20 | result = await resp.json() 21 | return result["data"] 22 | 23 | @alru_cache 24 | async def get_currency(self, currency_id: int): 25 | async with self._session.get( 26 | "/v2/cryptocurrency/quotes/latest", 27 | params={"id": currency_id} 28 | ) as resp: 29 | result = await resp.json() 30 | return result["data"][str(currency_id)] 31 | -------------------------------------------------------------------------------- /frontend/src/components/CryptocurrencyCard.jsx: -------------------------------------------------------------------------------- 1 | import {Card} from "antd"; 2 | import numberWithCommas from "../utils.js"; 3 | 4 | 5 | function CryptocurrencyCard(props) { 6 | const { currency } = props 7 | 8 | const priceChangeColor = currency.quote.USD.percent_change_24h > 0 ? 'text-green-400' : 'text-red-400' 9 | const formattedPrice = numberWithCommas(Math.round(currency.quote.USD.price)) 10 | const formattedMarketCap = numberWithCommas(Math.round(currency.quote.USD.market_cap/1_000_000_000)) 11 | const priceChange = Math.round(100 * currency.quote.USD.percent_change_24h) / 100 12 | 13 | const price = Math.round(currency.quote.USD.price) 14 | 15 | return ( 16 |
17 | 20 | logo 21 |

{currency.name}

22 |
23 | } 24 | bordered={false} 25 | style={{ 26 | width: 700, 27 | height: 340, 28 | 'box-shadow': '0 3px 10px rgb(0,0,0,0.2)', 29 | }} 30 | className="text-2xl" 31 | > 32 |

Текущая цена: {formattedPrice}$

33 | Изменение цены за 24 часа: 34 | 35 | {priceChange}% 36 | 37 |

Текущая капитализация: ${formattedMarketCap}B

38 | 39 | 40 | ) 41 | } 42 | 43 | export default CryptocurrencyCard -------------------------------------------------------------------------------- /frontend/src/App.jsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useState} from 'react'; 2 | import { AppstoreOutlined, MailOutlined, SettingOutlined } from '@ant-design/icons'; 3 | import {Menu, Spin} from 'antd'; 4 | import axios from "axios"; 5 | import CryptocurrencyCard from "./components/CryptocurrencyCard.jsx"; 6 | function getItem(label, key, icon, children, type) { 7 | return { 8 | key, 9 | icon, 10 | children, 11 | label, 12 | type, 13 | }; 14 | } 15 | 16 | const App = () => { 17 | const [currencies, setCurrencies] = useState([]) 18 | const [currencyId, setCurrencyId] = useState(1) 19 | const [currencyData, setCurrencyData] = useState(null) 20 | 21 | const fetchCurrencies = () => { 22 | axios.get('http://127.0.0.1:8000/cryptocurrencies').then(r => { 23 | const currenciesResponse = r.data 24 | const menuItems = [ 25 | getItem('Список криптовалют', 'g1', null, 26 | currenciesResponse.map(c => { 27 | return {label: c.name, key: c.id} 28 | }), 29 | 'group' 30 | ) 31 | ] 32 | setCurrencies(menuItems) 33 | }) 34 | } 35 | 36 | const fetchCurrency = () => { 37 | axios.get(`http://127.0.0.1:8000/cryptocurrencies/${currencyId}`).then(r => { 38 | setCurrencyData(r.data) 39 | }) 40 | } 41 | 42 | useEffect(() => { 43 | fetchCurrencies() 44 | }, []); 45 | 46 | 47 | useEffect(() => { 48 | setCurrencyData(null) 49 | fetchCurrency() 50 | }, [currencyId]); 51 | 52 | const onClick = (e) => { 53 | setCurrencyId(e.key) 54 | }; 55 | 56 | return ( 57 |
58 | 69 |
70 | {currencyData ? : } 71 |
72 |
73 | ); 74 | }; 75 | export default App; --------------------------------------------------------------------------------