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';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
<ThemeProvider theme={theme}>
<App />
</ThemeProvider>
</React.StrictMode>
);
app.tsx
import React from 'react';
import styled, { createGlobalStyle } from "styled-components";
import Router from './Router';
const GlobalStyle = createGlobalStyle`
@import url('https://fonts.googleapis.com/css2?family=Hind&display=swap');
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, menu, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header, hgroup,
main, menu, nav, output, ruby, section, summary,
time, mark, audio, video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure,
footer, header, hgroup, main, menu, nav, section {
display: block;
}
/* HTML5 hidden-attribute fix for newer browsers */
*[hidden] {
display: none;
}
body {
line-height: 1;
}
menu, ol, ul {
list-style: none;
}
blockquote, q {
quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
content: '';
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
*{
box-sizing: border-box;
}
body {
font-family: 'Hind', sans-serif;
background-color: ${(props)=> props.theme.bgColor};
color: ${(props)=> props.theme.textColor}
}
a {
text-decoration: none;
color: inherit;
}
`;
function App() {
return (
<>
<GlobalStyle />
<Router/>
</>
);
}
export default App;
Router.tsx
import { BrowserRouter, Routes, Route, Link } from "react-router-dom";
import Coins from "./routes/Coins";
import Coin from "./routes/Coin";
import Chart from "./routes/Chart";
import Price from "./routes/Price";
function Router(){
return <BrowserRouter>
<Routes>
<Route path="/" element={<Coins />}/>
<Route path="/:coinId" element={<Coin />} />
<Route path="/:coinId/chart" element={<Chart />}/>
<Route path="/:coinId/price" element={<Price />}/>
</Routes>
</BrowserRouter>
}
export default Router;
theme.ts
import { DefaultTheme } from "styled-components";
export const theme:DefaultTheme = {
bgColor: "#1e272e",
textColor: "#f5f6fa",
accentColor: "#f53b57",
}
styled.d.ts
// import original module declarations
import 'styled-components';
// and extend them!
declare module 'styled-components' {
export interface DefaultTheme {
textColor: string;
bgColor: string;
accentColor: string;
// borderRadius: string;
// colors: {
// main: string;
// secondary: string;
// };
}
}
Chart.tsx
function Chart(){
return <h1>Chart</h1>
}
export default Chart;
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";
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 [loading, setLoading] = useState(true);
const { coinId } = useParams<{coinId: string}>();
/* const location = useLocation(); */
/* console.log(location); */
const {state} = useLocation() as RouterState;
const [info, setInfo] = useState<IInfoData>();
const [priceInfo, setPriceInfo] = useState<IPriceData>();
const priceMatch = useMatch("/:coinId/price");
const chartMatch = useMatch("/:coinId/chart");
useEffect(()=> {
(async ()=> {
const response = await fetch(`https://api.coinpaprika.com/v1/coins/${coinId}`)
const infoData = await response.json();
const priceData = await (
await fetch(`https://api.coinpaprika.com/v1/tickers/${coinId}`)
).json();
console.log(infoData);
console.log(priceData);
setInfo(infoData);
setPriceInfo(priceData);
setLoading(false);
})();
}, [])
return (
<Container>
<Header>
<Title>{state ? state : loading}</Title>
</Header>
{loading ? (<Loader>Loading...</Loader>
) : (
<>
<Overview>
<OverviewItem>
<span>Rank:</span>
<span>{info?.rank}</span>
</OverviewItem>
<OverviewItem>
<span>Symbol:</span>
<span>${info?.symbol}</span>
</OverviewItem>
<OverviewItem>
<span>Open Source:</span>
<span>{info?.open_source ? "Yes" : "No"}</span>
</OverviewItem>
</Overview>
<Description>{info?.description}</Description>
<Overview>
<OverviewItem>
<span>Total Supply:</span>
<span>{priceInfo?.total_supply}</span>
</OverviewItem>
<OverviewItem>
<span>Max Suply:</span>
<span>{priceInfo?.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";
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 [coins, setCoins] = useState<CoinInterface[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
(async() =>{
const response = await fetch("https://api.coinpaprika.com/v1/coins");
const json = await response.json();
setCoins(json.slice(0, 100))
setLoading(false);
})();
}, []);
console.log(coins)
return <Container>
<Header>
<Title>코인</Title>
</Header>
{loading ? (<Loader>Loading...</Loader>)
: (<CoinsList>
{coins.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;
Price.tsx
function Price(){
return <h1>Price</h1>
}
export default Price;
728x90
'부트캠프교육중 > react' 카테고리의 다른 글
[React] React query 적용하기 2 (0) | 2023.08.26 |
---|---|
[React] React query 적용하기!!! (0) | 2023.08.26 |
[React] useRouteMatch, useMatch (0) | 2023.08.25 |
[React] theme, globalstyled (0) | 2023.08.24 |
[React] react-router-dom 6.4 (0) | 2023.08.24 |