728x90
index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { ThemeProvider } from 'styled-components';
import { theme } from './theme';
import { QueryClient, QueryClientProvider } from 'react-query';
const queryClient = new QueryClient();
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<ThemeProvider theme={theme}>
<App />
</ThemeProvider>
</QueryClientProvider>
</React.StrictMode>
);
api.ts
const BASE_URL = `https://api.coinpaprika.com/v1`
export function fetchCoins(){
return fetch(`${BASE_URL}/coins`)
.then((res)=> res.json());
}
export function fetchCoinInfo(coinId: string | undefined){
return fetch(`${BASE_URL}/coins/${coinId}`)
.then((res)=> res.json());
}
export function fetchCoinTickers(coinId: string | undefined){
return fetch(`${BASE_URL}/tickers/${coinId}`)
.then((res)=> res.json());
}
Coin.tsx
import { useEffect, useState } from "react";
import { useParams, useLocation } from "react-router";
import { Outlet, Link, useMatch } from "react-router-dom";
import { styled } from "styled-components";
import Chart from "./Chart";
import { useQuery } from "react-query";
import { fetchCoinInfo, fetchCoinTickers } from "../api";
interface RouterState {
state: string;
}
interface IInfoData {
id:"string";
name:"string";
symbol:"string";
rank:"number";
is_new:"boolean";
is_active:"boolean";
type:"string";
logo:"string";
// tags:"object";
// team:"object";
description:"string";
message:"string";
open_source:"boolean";
started_at:"string";
development_status:"string";
hardware_wallet:"boolean";
proof_type:"string";
org_structure:"string";
hash_algorithm:"string";
// links:"object";
// links_extended:"object";
// whitepaper:"object";
first_data_at:"string";
last_data_at:"string";
}
interface IPriceData {
id:"string"
name:"string"
symbol:"string"
rank: "number"
circulating_supply:"number"
total_supply:"number"
max_supply:"number"
beta_value:"number"
first_data_at:"string"
last_updated:"string"
quotes:{
USD: {
ath_date: string;
ath_price: number;
market_cap: number;
market_cap_change_24h: number;
percent_change_1h: number;
percent_change_1y: number;
percent_change_6h: number;
percent_change_7d: number;
percent_change_12h: number;
percent_change_15m: number;
percent_change_24h: number;
percent_change_30d: number;
percent_change_30m: number;
percent_from_price_ath:number;
price: number;
volume_24h: number;
volume_24h_change_24h: number;
}
}
}
const Container = styled.div`
/* border: 5px solid blue; */
padding: 0px 20px;
max-width: 480px;
margin: 0 auto;
`;
const Header = styled.header`
height: 10vh;
display: flex;
justify-content: center;
align-items: center;
`;
const Title = styled.h1`
font-size: 48px;
color: ${props => props.theme.accentColor}
`;
const Loader = styled.span`
text-align: center;
display: block;
`;
const Overview = styled.div`
display: flex;
justify-content: space-between;
background-color: rgba(0,0,0,0.5);
padding: 10px, 20px;
border-radius: 10px;
`;
const OverviewItem = styled.div`
display: flex;
flex-direction: column;
align-items: center;
span:first-child {
font-size: 10px;
font-weight: 400;
text-transform: uppercase;
margin-bottom: 5px;
}
`;
const Description = styled.p`
margin: 20px 0px;
`;
const Tabs = styled.div`
display: grid;
grid-template-columns: repeat()(2, 1fr);
margin: 25px 0px;
gap: 10px;
`;
const Tab = styled.span<{ isActive: boolean }>`
text-align: center;
text-transform: uppercase;
font-size: 12px;
font-weight: 400;
background-color: rgba(0,0,0,0.5);
padding: 7px 0px;
border-radius: 10px;
color: ${props => props.isActive ? props.theme.accentColor : props.theme.textColor}
a {
display: block;
}
`;
// react-router-dom v6부터 제네릭을 지원하지 않아서 interface이름으로 넣으면 오류가 난다....
/* interface RouteParams {
coinId: string;
} */
function Coin(){
const { coinId } = useParams<{coinId: string}>();
const {state} = useLocation() as RouterState;
const priceMatch = useMatch("/:coinId/price");
const chartMatch = useMatch("/:coinId/chart");
const { isLoading: infoLoading, data:infoData } = useQuery<IInfoData>(["info", coinId], () => fetchCoinInfo(coinId));
const { isLoading: tickersLoading, data:tickersData } = useQuery<IPriceData>(["tickers", coinId], () => fetchCoinTickers(coinId));
const loading = infoLoading || tickersLoading;
return (
<Container>
<Header>
<Title>{state ? state : loading ? "Loading..." : infoData?.name}</Title>
</Header>
{loading ? (<Loader>Loading...</Loader>
) : (
<>
<Overview>
<OverviewItem>
<span>Rank:</span>
<span>{infoData?.rank}</span>
</OverviewItem>
<OverviewItem>
<span>Symbol:</span>
<span>${infoData?.symbol}</span>
</OverviewItem>
<OverviewItem>
<span>Open Source:</span>
<span>{infoData?.open_source ? "Yes" : "No"}</span>
</OverviewItem>
</Overview>
<Description>{infoData?.description}</Description>
<Overview>
<OverviewItem>
<span>Total Supply:</span>
<span>{tickersData?.total_supply}</span>
</OverviewItem>
<OverviewItem>
<span>Max Suply:</span>
<span>{tickersData?.max_supply}</span>
</OverviewItem>
</Overview>
<Tabs>
<Tab isActive={chartMatch !== null}>
<Link to={`/${coinId}/chart`}>Chart</Link>
</Tab>
<Tab isActive={priceMatch !== null}>
<Link to={`/${coinId}/price`}>Price</Link>
</Tab>
</Tabs>
</>
)}
</Container>
);
}
export default Coin;
Coins.tsx
import styled from "styled-components";
import {Link} from "react-router-dom"
import { useEffect, useState } from "react";
import { useQuery } from "react-query";
import { fetchCoins } from "../api";
const Container = styled.div`
padding: 0px 20px;
max-width: 480px;
margin: 0 auto;
`;
const Header = styled.header`
height: 10vh;
display: flex;
justify-content: center;
align-items: center;
`;
const CoinsList = styled.ul``;
const Coin = styled.li`
background-color: white;
color: ${props => props.theme.bgColor};
padding: 20px;
margin-bottom: 10px;
border-radius: 10px;
a {
display: flex;
align-items: center;
padding: 20px;
transition: color 0.4s ease-in;
}
&:hover {
a {
color: ${props => props.theme.accentColor}
}
}
`;
const Title = styled.h1`
font-size: 48px;
color: ${props => props.theme.accentColor}
`;
const Loader = styled.span`
text-align: center;
display: block;
`;
const Img = styled.img`
width: 35px;
height: 35px;
margin-right: 10px;
`;
interface CoinInterface {
id:string;
name: string;
symbol: string;
rank: number;
is_new: boolean;
is_active: boolean;
type: string;
}
function Coins(){
const { isLoading, data } = useQuery<CoinInterface[]>("allCoins", fetchCoins);
return <Container>
<Header>
<Title>코인</Title>
</Header>
{isLoading ? (<Loader>Loading...</Loader>)
: (<CoinsList>
{data?.slice(0, 100).map(coin =>
<Coin key={coin.id}>
<Link to={`/${coin.id}`} state={coin.name}>
<Img
src={`https://coinicons-api.vercel.app/api/icon/${coin.symbol.toLowerCase()}`}
/>
{coin.name} →
</Link>
</Coin>)
}
</CoinsList>)}
</Container>
}
export default Coins;
728x90
'부트캠프교육중 > react' 카테고리의 다른 글
[React] recoil적용하기 (1) | 2023.08.27 |
---|---|
[React] Dark모드, Light모드 적용하기 (0) | 2023.08.27 |
[React] React query 적용하기 2 (0) | 2023.08.26 |
[React] React query 적용하기!!! (0) | 2023.08.26 |
[React] React query 적용 전 코드 (0) | 2023.08.26 |