big websocket change
This commit is contained in:
1 parent
0e3c272adc
commit
73248f7d9f
12 files changed
+126
-99
No files matched your search
@@ -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
|
||||
@@ -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
@@ -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
@@ -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() {}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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)}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
@@ -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,
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user