Last active
April 20, 2021 04:37
-
-
Save beardlessman/6abc05b932b44a2641a1892de9b0cb53 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8" /> | |
<meta http-equiv="X-UA-Compatible" content="IE=edge" /> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
<title>Document</title> | |
<link rel="stylesheet" href="https://unpkg.com/mocha@8/mocha.css" /> | |
<link rel="stylesheet" href="style.css" /> | |
<script | |
src="https://unpkg.com/react@17/umd/react.development.js" | |
crossorigin | |
></script> | |
<script | |
src="https://unpkg.com/react-dom@17/umd/react-dom.development.js" | |
crossorigin | |
></script> | |
<script | |
src="https://unpkg.com/@babel/standalone/babel.min.js" | |
crossorigin | |
></script> | |
<script | |
src="https://unpkg.com/[email protected]/umd/react-router-dom.js" | |
crossorigin | |
></script> | |
<script | |
src="https://unpkg.com/[email protected]/dist/redux.js" | |
crossorigin | |
></script> | |
<script | |
src="https://unpkg.com/react-redux@7/dist/react-redux.js" | |
crossorigin | |
></script> | |
<script | |
src="https://unpkg.com/redux-thunk@2/dist/redux-thunk.js" | |
crossorigin | |
></script> | |
<script | |
type="module" | |
src="https://unpkg.com/[email protected]/dist/ionicons/ionicons.esm.js" | |
></script> | |
<script src="https://unpkg.com/mocha@8/mocha.js" crossorigin></script> | |
<script src="https://unpkg.com/chai@2/chai.js" crossorigin></script> | |
</head> | |
<body> | |
<div id="mocha"></div> | |
<div id="root"></div> | |
<script type="text/babel"> | |
const lots = [ | |
{ | |
id: 1, | |
name: "Apple", | |
price: 16, | |
description: "Apple description", | |
favorite: true, | |
}, | |
{ | |
id: 2, | |
name: "Orange", | |
price: 41, | |
description: "Orange description", | |
favorite: false, | |
}, | |
]; | |
const api = { | |
get(url) { | |
switch (url) { | |
case "/lots": | |
return new Promise((resolve, reject) => { | |
setTimeout(() => { | |
if (Math.random() > 0.25) { | |
resolve(lots); | |
} else { | |
reject(new Error("ШТОТО ПОШЛО НЕ ТАК(((")); | |
} | |
}, 2000); | |
}); | |
default: | |
return new Promise((resolve, reject) => { | |
reject(new Error("Unknown address")); | |
}); | |
} | |
}, | |
post(url) { | |
if (/^\/lots\/(\d+)\/favorite$/.exec(url)) { | |
return new Promise((resolve) => { | |
setTimeout(() => { | |
resolve({}); | |
}, 500); | |
}); | |
} | |
if (/^\/lots\/(\d+)\/unfavorite$/.exec(url)) { | |
return new Promise((resolve) => { | |
setTimeout(() => { | |
resolve({}); | |
}, 500); | |
}); | |
} | |
new Error("Unknown address"); | |
}, | |
}; | |
const stream = { | |
subscribe(channel, listener) { | |
const match = /price-(\d+)/.exec(channel); | |
if (match) { | |
const interval = setInterval(() => { | |
listener({ | |
id: parseInt(match[1]), | |
price: Math.round(Math.random() * 100), | |
}); | |
}, 1000); | |
return () => clearInterval(interval); | |
} | |
}, | |
}; | |
// AUCTION | |
const auctionInitialState = { | |
lots: [], | |
loading: false, | |
loaded: false, | |
error: null, | |
}; | |
const LOTS_CLEAR = "LOTS_CLEAR"; | |
const LOTS_LOADING_PENDING = "LOTS_LOADING_PENDING"; | |
const LOTS_LOADING_SUCCESS = "LOTS_LOADING_SUCCESS"; | |
const LOTS_LOADING_ERROR = "LOTS_LOADING_ERROR"; | |
const CHANGE_PRICE = "CHANGE_PRICE"; | |
const FAVORITE_LOT = "FAVORITE_LOT"; | |
const UNFAVORITE_LOT = "UNFAVORITE_LOT"; | |
function lotsLoadingSuccess(lots) { | |
return { type: LOTS_LOADING_SUCCESS, lots }; | |
} | |
function lotsClear() { | |
return { type: LOTS_CLEAR }; | |
} | |
function lotsLoadingPending() { | |
return { type: LOTS_LOADING_PENDING }; | |
} | |
function lotsLoadingError(error) { | |
return { type: LOTS_LOADING_ERROR, error }; | |
} | |
function changePrice(id, price) { | |
return { type: CHANGE_PRICE, id, price }; | |
} | |
function favoriteLot(id) { | |
return { type: FAVORITE_LOT, id }; | |
} | |
function favoriteAsync(id) { | |
return (dispatch, getState, { api }) => { | |
api.post(`/lots/${id}/favorite`).then(() => { | |
dispatch(favoriteLot(id)); | |
}); | |
}; | |
} | |
function unfavoriteLot(id) { | |
return { type: UNFAVORITE_LOT, id }; | |
} | |
function unfavoriteAsync(id) { | |
return (dispatch, getState, { api }) => { | |
api.post(`/lots/${id}/unfavorite`).then(() => { | |
dispatch(unfavoriteLot(id)); | |
}); | |
}; | |
} | |
function loadLotsAsync() { | |
return (dispatch, getState, { api }) => { | |
dispatch(lotsLoadingPending()); | |
api | |
.get("/lots") | |
.then((lots) => { | |
console.log(lots); | |
dispatch(lotsLoadingSuccess(lots)); | |
}) | |
.catch((err) => dispatch(lotsLoadingError(err.toString()))); | |
}; | |
} | |
function subscribePriceChange(id) { | |
return (dispatch, getState, { stream }) => { | |
return stream.subscribe(`price-${id}`, (data) => { | |
dispatch(changePrice(data.id, data.price)); | |
}); | |
}; | |
} | |
function auctionReducer(state = auctionInitialState, action) { | |
switch (action.type) { | |
case LOTS_CLEAR: | |
return { | |
...state, | |
loading: false, | |
loaded: false, | |
lots: [], | |
error: null, | |
}; | |
case LOTS_LOADING_SUCCESS: | |
return { | |
...state, | |
loading: false, | |
loaded: true, | |
lots: action.lots, | |
error: null, | |
}; | |
case LOTS_LOADING_PENDING: | |
return { | |
...state, | |
lots: [], | |
loading: true, | |
loaded: false, | |
error: null, | |
}; | |
case LOTS_LOADING_ERROR: | |
return { | |
...state, | |
lots: [], | |
loading: false, | |
loaded: false, | |
error: action.error, | |
}; | |
case CHANGE_PRICE: | |
return { | |
...state, | |
lots: state.lots.map((lot) => { | |
if (lot.id === action.id) { | |
return { | |
...lot, | |
price: action.price, | |
}; | |
} | |
return lot; | |
}), | |
}; | |
case FAVORITE_LOT: | |
return { | |
...state, | |
lots: state.lots.map((lot) => { | |
if (lot.id === action.id) { | |
return { | |
...lot, | |
favorite: true, | |
}; | |
} | |
return lot; | |
}), | |
}; | |
case UNFAVORITE_LOT: | |
return { | |
...state, | |
lots: state.lots.map((lot) => { | |
if (lot.id === action.id) { | |
return { | |
...lot, | |
favorite: false, | |
}; | |
} | |
return lot; | |
}), | |
}; | |
default: | |
return state; | |
} | |
} | |
const thunk = ReduxThunk.default; | |
const store = Redux.createStore( | |
Redux.combineReducers({ | |
auction: auctionReducer, | |
}), | |
Redux.applyMiddleware(thunk.withExtraArgument({ api, stream })) | |
); | |
const StoreContext = React.createContext(); | |
const { BrowserRouter, Switch, Route, NavLink, useParams } = ReactRouterDOM; | |
function App() { | |
return ( | |
<BrowserRouter> | |
<div className="app"> | |
<Header /> | |
<Content /> | |
</div> | |
</BrowserRouter> | |
); | |
} | |
function Logo() { | |
return <img className="logo" src="logo.jpeg" />; | |
} | |
function Header() { | |
return ( | |
<header className="header"> | |
<Logo /> | |
<Nav /> | |
</header> | |
); | |
} | |
function Nav() { | |
return ( | |
<nav> | |
<ul> | |
<li> | |
<NavLink to="/" exact>Home</NavLink> | |
</li> | |
<li> | |
<NavLink to="/lots">Lots</NavLink> | |
</li> | |
<li> | |
<NavLink to="/help">Help</NavLink> | |
</li> | |
</ul> | |
</nav> | |
); | |
} | |
function Content() { | |
return ( | |
<Switch> | |
<Route path="/" exact> | |
<HomePage /> | |
</Route> | |
<Route path="/lots" exact> | |
<LotsPage /> | |
</Route> | |
<Route path="/lots/:id" exact> | |
<LotPage /> | |
</Route> | |
<Route path="/help" exact> | |
<HelpPage /> | |
</Route> | |
<Route path="*"> | |
<NotFound /> | |
</Route> | |
</Switch> | |
); | |
} | |
function NotFound() { | |
return <h1>Page not found</h1>; | |
} | |
function HomePage() { | |
return <h1>Welcome</h1>; | |
} | |
function HelpPage() { | |
return <h1>Help</h1>; | |
} | |
function LotsPage() { | |
return ( | |
<div> | |
<ClockContainer /> | |
<LotsContainer /> | |
</div> | |
); | |
} | |
function LotPage() { | |
const { id } = useParams(); | |
return <div>Lot #{id}</div>; | |
} | |
class ClockContainer extends React.Component { | |
constructor(props) { | |
super(props); | |
this.state = { | |
time: new Date(), | |
}; | |
this.tick = this.tick.bind(this); | |
} | |
componentDidMount() { | |
this.interval = setInterval(this.tick, 1000); | |
} | |
componentWillUnmount() { | |
clearInterval(this.interval); | |
} | |
tick() { | |
this.setState({ time: new Date() }); | |
} | |
render() { | |
return <Clock time={this.state.time} />; | |
} | |
} | |
function Clock({ time }) { | |
const isDay = time.getHours() >= 7 && time.getHours() <= 21; | |
return ( | |
<div className="clock"> | |
<span className="value">{time.toLocaleTimeString()}</span> | |
<span className={isDay ? "icon day" : "icon night"}></span> | |
</div> | |
); | |
} | |
function Loading() { | |
return <div className="loading">Loading...</div>; | |
} | |
const lotsMapStateToProps = (state) => ({ | |
lots: state.auction.lots, | |
loaded: state.auction.loaded, | |
loading: state.auction.loading, | |
error: state.auction.error, | |
}); | |
const lotsDispatchToProps = { | |
load: loadLotsAsync, | |
clear: lotsClear, | |
}; | |
const LotsContainer = ReactRedux.connect( | |
lotsMapStateToProps, | |
lotsDispatchToProps | |
)(Lots); | |
function Lots({ lots, loaded, loading, error, load, clear }) { | |
React.useEffect(() => { | |
if (!loaded && !loading && error === null) { | |
load(); | |
} | |
}, [loaded, loading, error]); | |
React.useEffect(() => { | |
if (loaded || error !== null) { | |
return clear; | |
} | |
}, [loaded, error]); | |
if (loading) { | |
return <Loading />; | |
} | |
if (error) { | |
return ( | |
<div> | |
{error} <ion-icon name="reload-circle" onClick={load}></ion-icon> | |
</div> | |
); | |
} | |
if (!loaded) { | |
return null; | |
} | |
return ( | |
<div className="lots"> | |
{lots.map((lot) => ( | |
<LotContainer key={lot.id} lot={lot} /> | |
))} | |
</div> | |
); | |
} | |
const lotDispatchToProps = { | |
favorite: favoriteAsync, | |
unfavorite: unfavoriteAsync, | |
subscribe: subscribePriceChange, | |
}; | |
const LotContainer = ReactRedux.connect(null, lotDispatchToProps)(Lot); | |
function Lot({ lot, favorite, unfavorite, subscribe }) { | |
React.useEffect(() => { | |
return subscribe(lot.id); | |
}, [lot.id]); | |
return ( | |
<article | |
className={lot.favorite ? "lot favorite" : "lot"} | |
key={lot.id} | |
> | |
<div className="price">{lot.price}</div> | |
<h1> | |
<NavLink to={`/lots/${lot.id}`}>{lot.name}</NavLink> | |
</h1> | |
<p>{lot.description}</p> | |
<Favorite | |
isFavorite={lot.favorite} | |
favorite={() => favorite(lot.id)} | |
unfavorite={() => unfavorite(lot.id)} | |
/> | |
</article> | |
); | |
} | |
function Favorite({ isFavorite, favorite, unfavorite }) { | |
return isFavorite ? ( | |
<button type="button" className="unfavorite" onClick={unfavorite}> | |
<ion-icon name="heart-sharp" /> Unfavorite | |
</button> | |
) : ( | |
<button type="button" className="favorite" onClick={favorite}> | |
<ion-icon name="heart-outline" /> Favorite | |
</button> | |
); | |
} | |
ReactDOM.render( | |
<ReactRedux.Provider store={store}> | |
<App /> | |
</ReactRedux.Provider>, | |
document.getElementById("root") | |
); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment