unistore wip

This commit is contained in:
ntr committed 2022-09-03 14:26:41 +10:00
1 parent a43d4f32e4
commit cf01670ee6
17 files changed
+253 -79

No files matched your search

+1
View File
@@ -1,3 +1,4 @@
module.exports = {
parserOpts: { strictMode: false },
plugins: ['@babel/plugin-transform-react-jsx-source']
};
+25
View File
@@ -19,6 +19,7 @@
"unistore": "^3.5.2"
},
"devDependencies": {
"@babel/plugin-transform-react-jsx-source": "^7.18.6",
"@typescript-eslint/eslint-plugin": "^5.30.6",
"@typescript-eslint/parser": "^5.30.6",
"eslint": "^8.20.0",
@@ -1598,6 +1599,21 @@
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-transform-react-jsx-source": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.18.6.tgz",
"integrity": "sha512-utZmlASneDfdaMh0m/WausbjUjEdGrQJz0vFK93d7wD3xf5wBtX219+q6IlCNZeguIcxS2f/CvLZrlLSvSHQXw==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.18.6"
},
"engines": {
"node": ">=6.9.0"
},
"peerDependencies": {
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-transform-regenerator": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.18.6.tgz",
@@ -22250,6 +22266,15 @@
"@babel/types": "^7.18.6"
}
},
"@babel/plugin-transform-react-jsx-source": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.18.6.tgz",
"integrity": "sha512-utZmlASneDfdaMh0m/WausbjUjEdGrQJz0vFK93d7wD3xf5wBtX219+q6IlCNZeguIcxS2f/CvLZrlLSvSHQXw==",
"dev": true,
"requires": {
"@babel/helper-plugin-utils": "^7.18.6"
}
},
"@babel/plugin-transform-regenerator": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.18.6.tgz",
+1
View File
@@ -29,6 +29,7 @@
"unistore": "^3.5.2"
},
"devDependencies": {
"@babel/plugin-transform-react-jsx-source": "^7.18.6",
"@typescript-eslint/eslint-plugin": "^5.30.6",
"@typescript-eslint/parser": "^5.30.6",
"eslint": "^8.20.0",
+29 -37
View File
@@ -1,8 +1,11 @@
import { h } from 'preact';
import { useState, useEffect } from 'preact/hooks';
import { Route, Router, route } from 'preact-router';
import { Provider, connect } from 'unistore/preact'
import actions from './actions';
import { createStore, actions } from './store';
import createWs from './ws';
import http from './http';
import Header from './components/header';
@@ -13,47 +16,36 @@ import Login from './routes/login';
import Search from './routes/search';
import List from './routes/list';
function App() {
const [user, setUser] = useState(null);
const store = createStore();
const ws = createWs(store);
useEffect(() => {
async function getLogin() {
const [error, user] = await actions.getLogin();
if (error) return route('/login');
setUser(user);
route('/bundles');
}
async function init() {
const [error, user] = await http.getLogin();
if (error) return route('/login');
route('/bundles');
store.setState({ user });
}
getLogin();
}, []);
function loginSuccess(user) {
setUser(user);
route('/search');
}
async function logout(e) {
e.preventDefault();
await actions.postLogout();
route('/login');
setUser(null);
}
init();
const App = () =>{
return (
<main id="app">
<Header user={user} logout={logout} />
<Router>
<Route path="/" component={Home} />
<Route path="/login" component={Login} success={loginSuccess} mode="login" />
<Route path="/register" component={Login} success={loginSuccess} mode="register" />
<Route path="/search" component={Search} />
<Route path="/list" component={List} user={user} />
<Provider store={store}>
<main id="app">
<Header />
<Router>
<Route path="/" component={Home} />
<Route path="/login" component={Login} mode="login" />
<Route path="/register" component={Login} mode="register" />
<Route path="/search" component={Search} />
<Route path="/list" component={List} />
{/* ACP */}
<Route path="/bundles" component={Bundles} />
<Route path="/offers/acp" component={Admin} />
</Router>
</main>
{/* ACP */}
<Route path="/bundles" component={Bundles} />
<Route path="/offers/acp" component={Admin} />
</Router>
</main>
</Provider>
);
}
+1 -1
View File
@@ -10,7 +10,7 @@ export default function Offer(offer) {
const [accepted, setAccepted] = useState(offer.status !== 'pending');
async function onClick() {
const [error] = await actions.postOfferAccept(offer.id);
const [error] = await http.postOfferAccept(offer.id);
if (error) return false;
setAccepted(true);
}
+1 -1
View File
@@ -48,7 +48,7 @@ export default function Bundle(bundle) {
async function submit(e) {
e.preventDefault();
const [error, offers] = await actions.postOffers(state.checkedIds, shipping, baseShipping);
const [error, offers] = await http.postOffers(state.checkedIds, shipping, baseShipping);
console.log(error, offers);
}
+11 -3
View File
@@ -1,9 +1,17 @@
import { h } from 'preact';
import { route } from 'preact-router';
import { connect } from 'unistore/preact'
import actions from './../actions';
import http from './../http';
import { actions } from './../store';
const Header = ({ user, logout }) => {
function logoutClick(e) {
e.preventDefault();
logout();
}
const nav = () => (
<nav>
<a href="/">Offers</a>
@@ -29,7 +37,7 @@ const Header = ({ user, logout }) => {
const userNav = () => (
<nav>
<a href="/account">Account</a>
<a href="/auth/logout" onClick={(e) => logout(e)}>Logout</a>
<a href="/auth/logout" onClick={logoutClick}>Logout</a>
</nav>
)
@@ -43,4 +51,4 @@ const Header = ({ user, logout }) => {
);
}
export default Header;
export default connect(['user'], actions)(Header);
+2 -2
View File
@@ -2,7 +2,7 @@ import { h } from 'preact';
import { ExternalLink, Check } from 'react-feather';
import { useState } from 'preact/hooks';
import actions from '../actions';
import http from '../http';
export default function Offer(offer) {
const { want: { data: details } } = offer;
@@ -10,7 +10,7 @@ export default function Offer(offer) {
const [accepted, setAccepted] = useState(offer.status !== 'pending');
async function onClick() {
const [error] = await actions.postOfferAccept(offer.id);
const [error] = await http.postOfferAccept(offer.id);
if (error) return false;
setAccepted(true);
}
File renamed without changes.
+2 -2
View File
@@ -1,7 +1,7 @@
import { h } from 'preact';
import { useState, useEffect } from 'preact/hooks';
import actions from './../actions';
import http from './../http';
import AcpOffer from './../components/acp.offer';
function Item(item) {
@@ -40,7 +40,7 @@ function Home() {
useEffect(() => {
async function getOffers() {
const [error, offers] = await actions.getOffersAcp();
const [error, offers] = await http.getOffersAcp();
if (error) return console.error(error);
setOffers(offers);
}
+2 -2
View File
@@ -1,7 +1,7 @@
import { h } from 'preact';
import { useState, useEffect } from 'preact/hooks';
import actions from './../actions';
import http from './../http';
import Bundle from './../components/bundle';
function Bundles() {
@@ -9,7 +9,7 @@ function Bundles() {
useEffect(() => {
async function getBundles() {
const [error, bundles] = await actions.getBundles();
const [error, bundles] = await http.getBundles();
if (error) return console.error(error);
setBundles(bundles);
}
+8 -8
View File
@@ -1,18 +1,18 @@
import { h } from 'preact';
import { useState, useEffect } from 'preact/hooks';
import { route } from 'preact-router';
import { route } from 'preact-router';
import { connect } from 'unistore/preact'
import http from './../http';
import { actions } from './../store';
import actions from './../actions';
import Offer from './../components/offer';
function Home() {
const [offers, setOffers] = useState([]);
function Home({ offers, setOffers }) {
useEffect(() => {
async function getOffers() {
const [error, offers] = await actions.getOffers();
const [error, offers] = await http.getOffers();
if (error) return console.error(error);
console.log(offers);
setOffers(offers);
if (offers.length === 0) route('/search');
}
@@ -35,4 +35,4 @@ function Home() {
);
}
export default Home;
export default connect(['offers'], actions)(Home);
+2 -2
View File
@@ -2,7 +2,7 @@ import { h } from 'preact';
import { useState, useEffect } from 'preact/hooks';
// import style from './style.less';
import actions from './../actions';
import http from './../http';
import Want from './../components/want';
interface Props {
@@ -16,7 +16,7 @@ const List = ({ user }: Props) => {
useEffect(() => {
async function getWants() {
const [error, wants] = await actions.getWants();
const [error, wants] = await http.getWants();
if (error) return console.error(error);
setOffers(wants);
}
+23 -11
View File
@@ -1,23 +1,27 @@
import { h } from 'preact';
import { useState } from 'preact/hooks';
import actions from './../actions';
import { route } from 'preact-router';
import { useState, useEffect, useRef } from 'preact/hooks';
import { connect } from 'unistore/preact'
const Login = ({ success, mode }) => {
import http from './../http';
import { actions } from './../store';
const Login = ({ setUser, mode }) => {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [error, setError] = useState('');
async function postLogin() {
const [error, user] = await actions.postLogin(email, password);
const [error, user] = await http.postLogin(email, password);
if (error) return setError(error);
success(user);
setUser(user, '/offers');
}
async function postRegister() {
const [error, user] = await actions.postRegister(email, password);
const [error, user] = await http.postRegister(email, password);
if (error) return setError(error);
success(user);
setUser(user, '/search');
}
function handleSubmit(e) {
@@ -26,6 +30,12 @@ const Login = ({ success, mode }) => {
if (mode === 'register') return postRegister();
}
const emailRef = useRef<HTMLInputElement>()
useEffect(() => {
emailRef.current.focus()
}, [])
const errorEl = error
? <div class='alt-text'>{error}</div>
: null;
@@ -36,7 +46,7 @@ const Login = ({ success, mode }) => {
<form action="">
<div class="item">
<label for="email">email</label>
<input type="email" name="email" id="email" value={email} required onInput={(e) => setEmail((e.target as HTMLInputElement).value)} />
<input type="email" name="email" id="email" ref={emailRef} value={email} required onInput={(e) => setEmail((e.target as HTMLInputElement).value)} />
</div>
<div class="item">
<label for="password">password</label>
@@ -60,7 +70,7 @@ const Login = ({ success, mode }) => {
<form action="">
<div class="item">
<label for="email">email</label>
<input type="email" name="email" id="email" value={email} required onInput={(e) => setEmail((e.target as HTMLInputElement).value)} />
<input type="email" name="email" id="email" ref={emailRef} value={email} required onInput={(e) => setEmail((e.target as HTMLInputElement).value)} />
</div>
<div class="item">
<label for="password">password</label>
@@ -74,10 +84,12 @@ const Login = ({ success, mode }) => {
);
};
function LoginPage({ success, mode }) {
const LoginForm = connect([], actions)(Login);
function LoginPage({ mode }) {
return (
<div class="landing">
<Login success={success} mode={mode} />
<LoginForm mode={mode} />
<div></div>
<div class="info">
<p>Mates Crates is a record importing service.</p>
+4 -4
View File
@@ -2,13 +2,13 @@ import { h } from 'preact';
import { useState, useRef, useEffect } from 'preact/hooks';
import { ExternalLink, Check } from 'react-feather';
import actions from '../actions';
import http from '../http';
function Result(r) {
const [added, setAdded] = useState(false)
async function onClick() {
const [error, want] = await actions.postWant(r.id);
const [error, want] = await http.postWant(r.id);
if (error) return false;
setAdded(true);
}
@@ -40,7 +40,7 @@ const User = () => {
const [infoText, setInfoText] = useState('');
async function fetchResults() {
const [error, results] = await actions.postWantSearch(artist, title);
const [error, results] = await http.postWantSearch(artist, title);
if (results.length === 0) {
setResults([]);
@@ -52,7 +52,7 @@ const User = () => {
}
async function submitWantlist() {
const [error, wants] = await actions.postDiscogs(username);
const [error, wants] = await http.postDiscogs(username);
if (wants.length === 0) {
setResults([]);
return setInfoText('No new additions');
+26 -6
View File
@@ -1,9 +1,29 @@
import createStore from 'unistore'
import { route } from 'preact-router';
import unistore from 'unistore'
export default function mcratesStore() {
return createStore({
bundles: [],
offers: []
});
import http from './http';
export function createStore() {
return unistore({
bundles: [],
offers: [],
user: null,
});
}
export const actions = store => ({
setUser: async function setUser(state, user, dest) {
route(dest);
return { user };
},
setOffers: async function setOffers(state, offers) {
return { offers };
},
logout: async function logout(store, e) {
await http.postLogout();
route('/login');
return { user: null };
},
});
+115
View File
@@ -0,0 +1,115 @@
const SOCKET_URL = (window.location.host.includes('localhost') || window.location.host === '' || window.location.host.includes('192.168.0'))
? `ws://${window.location.hostname}:40080`
: `${window.location.protocol === 'https:' ? 'wss://' : 'ws://'}${window.location.host}/api/ws`;
export default function createSocket(store) {
let ws;
// -------------
// Outgoing
// -------------
function send(msg) {
if (msg[0] !== 'Ping') console.log('outgoing msg', msg);
if (ws.readyState !== 1) return false;
try {
ws.send(JSON.stringify(msg));
} catch (e) {
console.warn(e);
}
}
// let ping;
function sendPing() {
// ping = Date.now();
send(['Ping', {}]);
// setTimeout(sendPing, 50);
}
function sendSourcesAvailable() {
send(['SourcesAvailable', {}]);
}
let pongTimeout;
function onPong() {
pongTimeout = setTimeout(sendPing, 1000);
}
// -------------
// Incoming
// -------------
function onSourcesAvailable(clouds) {
// store.dispatch(http.LIBRARY_RECEIVE_POINTCLOUDS, clouds);
}
// -------------
// Setup
// -------------
// when the server sends a reply it will have one of these message types
// this object wraps the reply types to a function
const handlers = {
SourcesAvailable: onSourcesAvailable,
Pong: onPong,
Error: errHandler,
};
function errHandler(error) {
// store.dispatch(http.LIBRARY_EVENT, { level: 'error', log: error });
console.error(error);
}
function onMessage(event) {
const res = event.data;
const [msgType, params] = JSON.parse(res);
if (msgType !== 'Pong') {
console.log(msgType, params);
}
// check for error and split into response type and data
if (!handlers[msgType]) return console.error(`${msgType} handler missing`);
return handlers[msgType](params);
}
// Connection opened
function onOpen() {
sendPing();
}
function onError(event) {
console.error('WebSocket error', event);
}
function onClose(event) {
console.error('WebSocket closed', event);
return setTimeout(connect, 2000);
}
function connect() {
if (ws) {
clearTimeout(pongTimeout);
ws.removeEventListener('open', onOpen);
ws.removeEventListener('message', onMessage);
ws.removeEventListener('error', onError);
ws.removeEventListener('close', onClose);
ws = null;
}
console.log('connecting to websocket', SOCKET_URL);
ws = new WebSocket(SOCKET_URL);
// Listen for messages
ws.addEventListener('open', onOpen);
ws.addEventListener('message', onMessage);
ws.addEventListener('error', onError);
ws.addEventListener('close', onClose);
return ws;
}
return {
connect,
sendSourcesAvailable,
};
}