big websocket change

This commit is contained in:
ntr committed 2022-09-16 19:21:11 +10:00
1 parent 0e3c272adc
commit 73248f7d9f
12 files changed
+126 -99

No files matched your search

+4 -2
View File
@@ -2,6 +2,8 @@
push offers
cover off admin routes and commands
remove want on offer complete
do we keep historical offers / wants?
decline / unaccept
minimum quality
listen / notify
+23
View File
@@ -118,11 +118,34 @@ export default function createOffers(log: Log, pg: Pg) {
});
}
async function acpOffers() {
const offers = await pg.offersAdmin();
const grouped = groupBy(offers, o => o.listing.data.seller.id);
const sellerOffers = sortBy(Object.values(grouped).map(offers => {
const shipping = offers[0]?.listing.data.seller.shipping;
const bundle = {
username: offers[0]?.listing.data.seller.username,
seller: offers[0]?.listing.data.seller.id,
shipsFrom: offers[0]?.listing.data.ships_from,
shipping,
offers,
mentionsAustralia: shipping.toLowerCase().includes('australia'),
mentionsWorldwide: shipping.toLowerCase().includes('world'),
}
return bundle;
}), bundle => -bundle.offers.length);
return sellerOffers;
}
return {
generate,
accept,
bundles,
statusSet,
acpOffers,
};
}
+34 -1
View File
@@ -8,6 +8,7 @@ import type { Want, Offer, User } from '@prisma/client';
import { Pg } from './pg';
import { Status } from './offers';
import { isAdmin } from './utils';
function checkMsgType(msgType: RpcMessageType): msgType is RpcMessageType {
if (RpcMessageType[msgType]) {
@@ -28,7 +29,9 @@ export default function createRpc(log: Log, pg: Pg, user: User, ws, discogs, off
assert(Status[status], 'unknown status');
const offer = await offers.statusSet(user, id, status);
return [RpcMessageType.Offers, []]
const userOffers = await pg.userOffers(user);
return [RpcMessageType.Offers, userOffers]
}
async function wantRm(params: WantRmParams): Promise<WantsMessage> {
@@ -41,6 +44,22 @@ export default function createRpc(log: Log, pg: Pg, user: User, ws, discogs, off
return [RpcMessageType.Wants, wants];
}
async function acpOfferStatusSet(params: OfferStatusSetParams): Promise<AcpOffersMessage> {
const { id, status } = params;
assert(isAdmin(user), 'not authorised');
assert(id, 'no id supplied');
assert(Status[status], 'unknown status');
const offer = await offers.statusSet(user, id, status);
return [RpcMessageType.AcpOffers, await offers.acpOffers()]
}
async function acpOffers(): Promise<AcpOffersMessage> {
assert(isAdmin(user), 'not authorised');
return [RpcMessageType.AcpOffers, await offers.acpOffers()];
}
async function receive(message: any): Promise<RpcMessage> {
const [msgType, params] = message;
@@ -53,6 +72,10 @@ export default function createRpc(log: Log, pg: Pg, user: User, ws, discogs, off
return ping();
case RpcMessageType.OfferStatusSet:
return offerStatusSet(params);
case RpcMessageType.AcpOfferStatusSet:
return acpOfferStatusSet(params);
case RpcMessageType.AcpOffers:
return acpOffers();
case RpcMessageType.WantRm:
return wantRm(params);
default:
@@ -103,6 +126,9 @@ export enum RpcMessageType {
OfferStatusSet = 'OfferStatusSet',
AcpOfferStatusSet = 'AcpOfferStatusSet',
AcpOffers = 'AcpOffers',
WantRm = 'WantRm',
}
@@ -114,6 +140,9 @@ type WantsMessage = [RpcMessageType.Wants, Want[]];
type OffersMessage = [RpcMessageType.Offers, Offer[]];
type OfferStatusSetMessage = [RpcMessageType.OfferStatusSet, Offer[]];
type AcpOfferStatusSetMessage = [RpcMessageType.AcpOfferStatusSet, Offer[]];
type AcpOffersMessage = [RpcMessageType.AcpOffers, Offer[]];
export type RpcMessage =
PingMessage |
PongMessage |
@@ -124,6 +153,10 @@ export type RpcMessage =
OffersMessage |
OfferStatusSetMessage |
AcpOfferStatusSetMessage |
AcpOffersMessage |
ErrorMessage;
+5 -1
View File
@@ -1,4 +1,4 @@
import type { Want } from '@prisma/client';
import type { Want, User } from '@prisma/client';
export function logAxiosError(e) {
console.warn({
@@ -25,4 +25,8 @@ export function wantDetails(want: Want) {
}
}
export function isAdmin(user: User) {
return user.email === 'ntr@mnml.gg';
}
export function offerDetails() {}
-35
View File
@@ -1,35 +0,0 @@
import { h } from 'preact';
import { ExternalLink, Check } from 'react-feather';
import { useState } from 'preact/hooks';
import http from '../http';
export default function Offer(offer) {
const { want: { data: details } } = offer;
const [accepted, setAccepted] = useState(offer.status !== 'pending');
async function onClick() {
const [error] = await http.postOfferAccept(offer.id);
if (error) return false;
setAccepted(true);
}
const classes = `${accepted ? 'accepted' : ''}`;
const btnText = accepted
? offer.status
: 'Accept';
return (
<div class="offer">
<section>
<h2>{details.artists_sort} - {details.title} <a href={details.uri} target="_blank"><ExternalLink /></a></h2>
<h5 class='alt-text' >{details.year}</h5>
<h5 class='alt-text'>{details.formats[0].descriptions.join(' ')}</h5>
<h5 class='number'>${offer.total}</h5>
</section>
<img src={details.thumb} />
<button onClick={onClick} disabled={accepted} class={classes} >{btnText}</button>
</div>
)
}
-26
View File
@@ -41,17 +41,8 @@ export default function Bundle(bundle) {
const initialState = { checkedIds: bundle.items.map(i => i.id) }
const [state, dispatch] = useReducer(reducer, initialState)
const [shipping, setShipping] = useState(0);
const [baseShipping, setBaseShipping] = useState(0);
const items = bundle.items.map(item => Item(item, state, dispatch));
async function submit(e) {
e.preventDefault();
const [error, offers] = await http.postOffers(state.checkedIds, shipping, baseShipping);
console.log(error, offers);
}
return (
<div class="bundle">
<section>
@@ -59,23 +50,6 @@ export default function Bundle(bundle) {
<h3>{bundle.items.length} items - {bundle.total.toFixed(2)} {bundle.currency}</h3>
<h4>Ships: {bundle.mentionsAustralia && 'AU'} {bundle.mentionsWorldwide && 'WORLD'}</h4>
</section>
<section>
<form action="">
<div class="item">
<label for="shipping">Full Shipping Cost</label>
<input type="number" name="shipping" id="shipping" value={shipping} onChange={(e) => setShipping(Number.parseFloat((e.target as HTMLInputElement).value))} step="0.01" min="0" required />
</div>
<div class="item">
<label for="baseShipping">Base Shipping Cost</label>
<input type="number" name="baseShipping" id="baseShipping" value={baseShipping} onChange={(e) => setBaseShipping(Number.parseFloat((e.target as HTMLInputElement).value))} step="0.01" min="0" required />
</div>
<div class="item">
<input type="submit" value="Generate Offers" onClick={(e) => submit(e)} />
</div>
</form>
</section>
<div class='items'>
{items}
</div>
+14 -7
View File
@@ -2,24 +2,30 @@ import { h } from 'preact';
import { ExternalLink, Check } from 'react-feather';
import { useState } from 'preact/hooks';
import { useSelector, useDispatch } from 'react-redux'
import { State } from './../store';
import http from '../http';
export default function Offer(offer) {
const ws = useSelector((state: State) => state.ws);
const { want: { data: details } } = offer;
const [accepted, setAccepted] = useState(offer.status !== 'Pending');
async function onClick() {
const [error] = await http.postOfferAccept(offer.id);
if (error) return false;
setAccepted(true);
function setStatus(status) {
ws.sendOfferStatusSet(offer.id, status);
}
const accepted = offer.status === 'Accepted';
const classes = `${accepted ? 'accepted' : ''}`;
const btnText = accepted
? offer.status
: 'Accept';
const negBtn = accepted
? 'Cancel'
: 'decline';
return (
<div class="offer">
<section>
@@ -30,7 +36,8 @@ export default function Offer(offer) {
<h5 class='number'>shipped by itself: ${offer.comparison}</h5>
</section>
<img src={details.thumb} />
<button onClick={onClick} disabled={accepted} class={classes} >{btnText}</button>
<button onClick={() => setStatus('Accepted')} disabled={accepted} class={classes} >{btnText}</button>
<button class='neg'>{negBtn}</button>
</div>
)
}
-13
View File
@@ -90,19 +90,6 @@ const actions = {
baseShipping,
});
},
postOfferAccept: async function postOffers(id) {
return api.post('/offer/accept', {
id,
});
},
postOfferStatus: async function postOfferStatus(id, status) {
return api.post('/acp/offer/status', {
id,
status,
});
},
};
export default actions;
+6 -12
View File
@@ -2,7 +2,6 @@ import { h } from 'preact';
import { useState, useEffect } from 'preact/hooks';
import http from './../http';
import AcpOffer from './../components/acp.offer';
import { useSelector, useDispatch } from 'react-redux'
import { State } from './../store';
@@ -24,7 +23,7 @@ function Item({ item }) {
));
function onChange(e) {
ws.sendOfferStatusSet(item.id, e.target.value);
ws.sendAcpOfferStatusSet(item.id, e.target.value);
}
return (
@@ -58,16 +57,12 @@ function SellerGroup(group) {
}
function Home() {
const [offers, setOffers] = useState([]);
const offers = useSelector((state: State) => state.acpOffers);
const ws = useSelector((state: State) => state.ws);
useEffect(() => {
async function getOffers() {
const [error, offers] = await http.getOffersAcp();
if (error) return console.error(error);
setOffers(offers);
}
getOffers();
}, []);
// useEffect(() => {
// ws.sendAcpOffers();
// }, [offers, ws]);
if (!offers.length) {
return (
@@ -77,7 +72,6 @@ function Home() {
);
}
return (
<div class='bundles'>
{offers.map(SellerGroup)}
+10
View File
@@ -17,6 +17,14 @@ export const offersState = createSlice({
},
});
export const acpOffersState = createSlice({
name: 'acpOffers',
initialState: [],
reducers: {
set: (state, action) => action.payload
},
});
export const wantsState = createSlice({
name: 'wants',
initialState: [],
@@ -38,6 +46,8 @@ const reducer = combineReducers({
wants: wantsState.reducer,
ws: wsState.reducer,
user: userState.reducer,
acpOffers: acpOffersState.reducer,
});
const store = createStore(
+6 -1
View File
@@ -329,7 +329,7 @@ header {
display: grid;
grid-template-areas:
"details img"
"btn btn";
"btn neg";
padding: var(--base-spacing);
margin: var(--base-spacing-half);
@@ -352,6 +352,11 @@ header {
background: var(--color-accent-blue);
cursor: auto;
}
&.neg {
margin-left: var(--base-spacing-half);
grid-area: neg;
}
}
}
}
+24 -1
View File
@@ -1,4 +1,4 @@
import { wantsState, wsState, userState } from './store'
import { wantsState, wsState, userState, offersState, acpOffersState } from './store'
const SOCKET_URL = `${window.location.protocol === 'https:' ? 'wss://' : 'ws://'}${window.location.host}/api/ws`;
@@ -34,6 +34,14 @@ export default function createSocket(dispatch) {
send(['WantRm', { id }]);
}
function sendAcpOffers() {
send(['AcpOffers', {}]);
}
function sendAcpOfferStatusSet(id, status) {
send(['AcpOfferStatusSet', { id, status }]);
}
let pongTimeout;
function onPong() {
pongTimeout = setTimeout(sendPing, 5000);
@@ -50,6 +58,14 @@ export default function createSocket(dispatch) {
dispatch(wantsState.actions.set(wants));
}
function onOffers(offers) {
dispatch(offersState.actions.set(offers));
}
function onAcpOffers(offers) {
dispatch(acpOffersState.actions.set(offers));
}
// -------------
// Setup
// -------------
@@ -57,8 +73,11 @@ export default function createSocket(dispatch) {
// this object wraps the reply types to a function
const handlers = {
Wants: onWants,
Offers: onOffers,
User: onUser,
AcpOffers: onAcpOffers,
Pong: onPong,
Error: errHandler,
};
@@ -84,6 +103,7 @@ export default function createSocket(dispatch) {
function onOpen() {
console.log('websocket connected', SOCKET_URL)
sendPing();
sendAcpOffers();
}
function onError(event) {
@@ -122,5 +142,8 @@ export default function createSocket(dispatch) {
sendOfferStatusSet,
sendWantRm,
sendAcpOffers,
sendAcpOfferStatusSet,
};
}