Skip to content

Instantly share code, notes, and snippets.

@beardlessman
Last active April 20, 2021 04:37
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save beardlessman/6abc05b932b44a2641a1892de9b0cb53 to your computer and use it in GitHub Desktop.
Save beardlessman/6abc05b932b44a2641a1892de9b0cb53 to your computer and use it in GitHub Desktop.
<!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/react-router-dom@5.2.0/umd/react-router-dom.js"
crossorigin
></script>
<script
src="https://unpkg.com/redux@4.0.5/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/ionicons@5.4.0/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