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
|
push offers
|
||||||
cover off admin routes and commands
|
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 {
|
return {
|
||||||
generate,
|
generate,
|
||||||
accept,
|
accept,
|
||||||
bundles,
|
bundles,
|
||||||
statusSet,
|
statusSet,
|
||||||
|
acpOffers,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
+34
-1
@@ -8,6 +8,7 @@ import type { Want, Offer, User } from '@prisma/client';
|
|||||||
|
|
||||||
import { Pg } from './pg';
|
import { Pg } from './pg';
|
||||||
import { Status } from './offers';
|
import { Status } from './offers';
|
||||||
|
import { isAdmin } from './utils';
|
||||||
|
|
||||||
function checkMsgType(msgType: RpcMessageType): msgType is RpcMessageType {
|
function checkMsgType(msgType: RpcMessageType): msgType is RpcMessageType {
|
||||||
if (RpcMessageType[msgType]) {
|
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');
|
assert(Status[status], 'unknown status');
|
||||||
|
|
||||||
const offer = await offers.statusSet(user, id, 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> {
|
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];
|
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> {
|
async function receive(message: any): Promise<RpcMessage> {
|
||||||
const [msgType, params] = message;
|
const [msgType, params] = message;
|
||||||
|
|
||||||
@@ -53,6 +72,10 @@ export default function createRpc(log: Log, pg: Pg, user: User, ws, discogs, off
|
|||||||
return ping();
|
return ping();
|
||||||
case RpcMessageType.OfferStatusSet:
|
case RpcMessageType.OfferStatusSet:
|
||||||
return offerStatusSet(params);
|
return offerStatusSet(params);
|
||||||
|
case RpcMessageType.AcpOfferStatusSet:
|
||||||
|
return acpOfferStatusSet(params);
|
||||||
|
case RpcMessageType.AcpOffers:
|
||||||
|
return acpOffers();
|
||||||
case RpcMessageType.WantRm:
|
case RpcMessageType.WantRm:
|
||||||
return wantRm(params);
|
return wantRm(params);
|
||||||
default:
|
default:
|
||||||
@@ -103,6 +126,9 @@ export enum RpcMessageType {
|
|||||||
|
|
||||||
OfferStatusSet = 'OfferStatusSet',
|
OfferStatusSet = 'OfferStatusSet',
|
||||||
|
|
||||||
|
AcpOfferStatusSet = 'AcpOfferStatusSet',
|
||||||
|
AcpOffers = 'AcpOffers',
|
||||||
|
|
||||||
WantRm = 'WantRm',
|
WantRm = 'WantRm',
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,6 +140,9 @@ type WantsMessage = [RpcMessageType.Wants, Want[]];
|
|||||||
type OffersMessage = [RpcMessageType.Offers, Offer[]];
|
type OffersMessage = [RpcMessageType.Offers, Offer[]];
|
||||||
type OfferStatusSetMessage = [RpcMessageType.OfferStatusSet, Offer[]];
|
type OfferStatusSetMessage = [RpcMessageType.OfferStatusSet, Offer[]];
|
||||||
|
|
||||||
|
type AcpOfferStatusSetMessage = [RpcMessageType.AcpOfferStatusSet, Offer[]];
|
||||||
|
type AcpOffersMessage = [RpcMessageType.AcpOffers, Offer[]];
|
||||||
|
|
||||||
export type RpcMessage =
|
export type RpcMessage =
|
||||||
PingMessage |
|
PingMessage |
|
||||||
PongMessage |
|
PongMessage |
|
||||||
@@ -124,6 +153,10 @@ export type RpcMessage =
|
|||||||
|
|
||||||
OffersMessage |
|
OffersMessage |
|
||||||
OfferStatusSetMessage |
|
OfferStatusSetMessage |
|
||||||
|
|
||||||
|
AcpOfferStatusSetMessage |
|
||||||
|
AcpOffersMessage |
|
||||||
|
|
||||||
ErrorMessage;
|
ErrorMessage;
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+5
-1
@@ -1,4 +1,4 @@
|
|||||||
import type { Want } from '@prisma/client';
|
import type { Want, User } from '@prisma/client';
|
||||||
|
|
||||||
export function logAxiosError(e) {
|
export function logAxiosError(e) {
|
||||||
console.warn({
|
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() {}
|
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 initialState = { checkedIds: bundle.items.map(i => i.id) }
|
||||||
const [state, dispatch] = useReducer(reducer, initialState)
|
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));
|
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 (
|
return (
|
||||||
<div class="bundle">
|
<div class="bundle">
|
||||||
<section>
|
<section>
|
||||||
@@ -59,23 +50,6 @@ export default function Bundle(bundle) {
|
|||||||
<h3>{bundle.items.length} items - {bundle.total.toFixed(2)} {bundle.currency}</h3>
|
<h3>{bundle.items.length} items - {bundle.total.toFixed(2)} {bundle.currency}</h3>
|
||||||
<h4>Ships: {bundle.mentionsAustralia && 'AU'} {bundle.mentionsWorldwide && 'WORLD'}</h4>
|
<h4>Ships: {bundle.mentionsAustralia && 'AU'} {bundle.mentionsWorldwide && 'WORLD'}</h4>
|
||||||
</section>
|
</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'>
|
<div class='items'>
|
||||||
{items}
|
{items}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,24 +2,30 @@ import { h } from 'preact';
|
|||||||
import { ExternalLink, Check } from 'react-feather';
|
import { ExternalLink, Check } from 'react-feather';
|
||||||
import { useState } from 'preact/hooks';
|
import { useState } from 'preact/hooks';
|
||||||
|
|
||||||
|
import { useSelector, useDispatch } from 'react-redux'
|
||||||
|
import { State } from './../store';
|
||||||
|
|
||||||
import http from '../http';
|
import http from '../http';
|
||||||
|
|
||||||
export default function Offer(offer) {
|
export default function Offer(offer) {
|
||||||
|
const ws = useSelector((state: State) => state.ws);
|
||||||
|
|
||||||
const { want: { data: details } } = offer;
|
const { want: { data: details } } = offer;
|
||||||
|
|
||||||
const [accepted, setAccepted] = useState(offer.status !== 'Pending');
|
function setStatus(status) {
|
||||||
|
ws.sendOfferStatusSet(offer.id, status);
|
||||||
async function onClick() {
|
|
||||||
const [error] = await http.postOfferAccept(offer.id);
|
|
||||||
if (error) return false;
|
|
||||||
setAccepted(true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const accepted = offer.status === 'Accepted';
|
||||||
const classes = `${accepted ? 'accepted' : ''}`;
|
const classes = `${accepted ? 'accepted' : ''}`;
|
||||||
const btnText = accepted
|
const btnText = accepted
|
||||||
? offer.status
|
? offer.status
|
||||||
: 'Accept';
|
: 'Accept';
|
||||||
|
|
||||||
|
const negBtn = accepted
|
||||||
|
? 'Cancel'
|
||||||
|
: 'decline';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="offer">
|
<div class="offer">
|
||||||
<section>
|
<section>
|
||||||
@@ -30,7 +36,8 @@ export default function Offer(offer) {
|
|||||||
<h5 class='number'>shipped by itself: ${offer.comparison}</h5>
|
<h5 class='number'>shipped by itself: ${offer.comparison}</h5>
|
||||||
</section>
|
</section>
|
||||||
<img src={details.thumb} />
|
<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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -90,19 +90,6 @@ const actions = {
|
|||||||
baseShipping,
|
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;
|
export default actions;
|
||||||
@@ -2,7 +2,6 @@ import { h } from 'preact';
|
|||||||
import { useState, useEffect } from 'preact/hooks';
|
import { useState, useEffect } from 'preact/hooks';
|
||||||
|
|
||||||
import http from './../http';
|
import http from './../http';
|
||||||
import AcpOffer from './../components/acp.offer';
|
|
||||||
|
|
||||||
import { useSelector, useDispatch } from 'react-redux'
|
import { useSelector, useDispatch } from 'react-redux'
|
||||||
import { State } from './../store';
|
import { State } from './../store';
|
||||||
@@ -24,7 +23,7 @@ function Item({ item }) {
|
|||||||
));
|
));
|
||||||
|
|
||||||
function onChange(e) {
|
function onChange(e) {
|
||||||
ws.sendOfferStatusSet(item.id, e.target.value);
|
ws.sendAcpOfferStatusSet(item.id, e.target.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -58,16 +57,12 @@ function SellerGroup(group) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function Home() {
|
function Home() {
|
||||||
const [offers, setOffers] = useState([]);
|
const offers = useSelector((state: State) => state.acpOffers);
|
||||||
|
const ws = useSelector((state: State) => state.ws);
|
||||||
|
|
||||||
useEffect(() => {
|
// useEffect(() => {
|
||||||
async function getOffers() {
|
// ws.sendAcpOffers();
|
||||||
const [error, offers] = await http.getOffersAcp();
|
// }, [offers, ws]);
|
||||||
if (error) return console.error(error);
|
|
||||||
setOffers(offers);
|
|
||||||
}
|
|
||||||
getOffers();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (!offers.length) {
|
if (!offers.length) {
|
||||||
return (
|
return (
|
||||||
@@ -77,7 +72,6 @@ function Home() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class='bundles'>
|
<div class='bundles'>
|
||||||
{offers.map(SellerGroup)}
|
{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({
|
export const wantsState = createSlice({
|
||||||
name: 'wants',
|
name: 'wants',
|
||||||
initialState: [],
|
initialState: [],
|
||||||
@@ -38,6 +46,8 @@ const reducer = combineReducers({
|
|||||||
wants: wantsState.reducer,
|
wants: wantsState.reducer,
|
||||||
ws: wsState.reducer,
|
ws: wsState.reducer,
|
||||||
user: userState.reducer,
|
user: userState.reducer,
|
||||||
|
|
||||||
|
acpOffers: acpOffersState.reducer,
|
||||||
});
|
});
|
||||||
|
|
||||||
const store = createStore(
|
const store = createStore(
|
||||||
|
|||||||
@@ -329,7 +329,7 @@ header {
|
|||||||
display: grid;
|
display: grid;
|
||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
"details img"
|
"details img"
|
||||||
"btn btn";
|
"btn neg";
|
||||||
|
|
||||||
padding: var(--base-spacing);
|
padding: var(--base-spacing);
|
||||||
margin: var(--base-spacing-half);
|
margin: var(--base-spacing-half);
|
||||||
@@ -352,6 +352,11 @@ header {
|
|||||||
background: var(--color-accent-blue);
|
background: var(--color-accent-blue);
|
||||||
cursor: auto;
|
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`;
|
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 }]);
|
send(['WantRm', { id }]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sendAcpOffers() {
|
||||||
|
send(['AcpOffers', {}]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendAcpOfferStatusSet(id, status) {
|
||||||
|
send(['AcpOfferStatusSet', { id, status }]);
|
||||||
|
}
|
||||||
|
|
||||||
let pongTimeout;
|
let pongTimeout;
|
||||||
function onPong() {
|
function onPong() {
|
||||||
pongTimeout = setTimeout(sendPing, 5000);
|
pongTimeout = setTimeout(sendPing, 5000);
|
||||||
@@ -50,6 +58,14 @@ export default function createSocket(dispatch) {
|
|||||||
dispatch(wantsState.actions.set(wants));
|
dispatch(wantsState.actions.set(wants));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onOffers(offers) {
|
||||||
|
dispatch(offersState.actions.set(offers));
|
||||||
|
}
|
||||||
|
|
||||||
|
function onAcpOffers(offers) {
|
||||||
|
dispatch(acpOffersState.actions.set(offers));
|
||||||
|
}
|
||||||
|
|
||||||
// -------------
|
// -------------
|
||||||
// Setup
|
// Setup
|
||||||
// -------------
|
// -------------
|
||||||
@@ -57,8 +73,11 @@ export default function createSocket(dispatch) {
|
|||||||
// this object wraps the reply types to a function
|
// this object wraps the reply types to a function
|
||||||
const handlers = {
|
const handlers = {
|
||||||
Wants: onWants,
|
Wants: onWants,
|
||||||
|
Offers: onOffers,
|
||||||
User: onUser,
|
User: onUser,
|
||||||
|
|
||||||
|
AcpOffers: onAcpOffers,
|
||||||
|
|
||||||
Pong: onPong,
|
Pong: onPong,
|
||||||
Error: errHandler,
|
Error: errHandler,
|
||||||
};
|
};
|
||||||
@@ -84,6 +103,7 @@ export default function createSocket(dispatch) {
|
|||||||
function onOpen() {
|
function onOpen() {
|
||||||
console.log('websocket connected', SOCKET_URL)
|
console.log('websocket connected', SOCKET_URL)
|
||||||
sendPing();
|
sendPing();
|
||||||
|
sendAcpOffers();
|
||||||
}
|
}
|
||||||
|
|
||||||
function onError(event) {
|
function onError(event) {
|
||||||
@@ -122,5 +142,8 @@ export default function createSocket(dispatch) {
|
|||||||
|
|
||||||
sendOfferStatusSet,
|
sendOfferStatusSet,
|
||||||
sendWantRm,
|
sendWantRm,
|
||||||
|
|
||||||
|
sendAcpOffers,
|
||||||
|
sendAcpOfferStatusSet,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user