├── 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 | 
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 |
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;
--------------------------------------------------------------------------------