navbar and notifications

This commit is contained in:
ntr committed 2023-01-17 23:44:16 +11:00
1 parent a97560c1c7
commit 5b0c7c05ca
11 files changed
+65 -46

No files matched your search

+2 -2
View File
@@ -3,9 +3,9 @@ push offers
add last synced time to want add last synced time to want
contact options on landing page go over and reorganise event logic
move want add to rpc contact options on landing page
skip unofficial release skip unofficial release
+5
View File
@@ -11,6 +11,11 @@ datasource db {
} }
model Listing { model Listing {
// example listingId 2148851036
// works fine as a JS number
// but JS numbers are cast to int4 for pg by prisma
// becomes -2146116260
id BigInt @id id BigInt @id
seller BigInt seller BigInt
data Json data Json
-10
View File
@@ -63,16 +63,6 @@ export default function createApi(log: Log, pg: Pg, discogs: Discogs, offers: Of
ctx.status = 200; ctx.status = 200;
}); });
router.post('/want', async function wantPost(ctx, next) {
ctx.assert(ctx.isAuthenticated(), 401, 'log in required');
const { releaseId } = ctx.request.body;
const user = await pg.userFindId(ctx.state.user.id);
const release = await discogs.release(releaseId);
const want = await pg.wantCreate(release, user);
events.emit(Events.WantsAdded, [want]);
ctx.response.body = [want];
});
router.post('/want/discogs', async function wantListPost(ctx, next) { router.post('/want/discogs', async function wantListPost(ctx, next) {
ctx.assert(ctx.isAuthenticated(), 401, 'log in required'); ctx.assert(ctx.isAuthenticated(), 401, 'log in required');
const { username } = ctx.request.body; const { username } = ctx.request.body;
+11 -7
View File
@@ -12,11 +12,11 @@ import routerAuth from './routers/router.auth';
export enum Events { export enum Events {
WantsAdded = 'WantsAdded', WantsAdded = 'WantsAdded',
Sync = 'Sync', Sync = 'Sync',
ListingsUpdated = 'ListingsUpdated',
Connection = 'Connection', Connection = 'Connection',
OfferUpdated = 'OfferUpdated', ListingsUpdated = 'ListingsUpdated',
WantUpdated = 'WantUpdated', OffersUpdated = 'OffersUpdated',
WantsUpdated = 'WantsUpdated',
} }
export default function createEvents(log: Log, pg: Pg, discogs, offers) { export default function createEvents(log: Log, pg: Pg, discogs, offers) {
@@ -68,6 +68,10 @@ export default function createEvents(log: Log, pg: Pg, discogs, offers) {
events.on(Events.ListingsUpdated, async function listingsUpdated() { events.on(Events.ListingsUpdated, async function listingsUpdated() {
const updatedOffers = await offers.generate() const updatedOffers = await offers.generate()
log.debug({ offers: updatedOffers.length }, 'offers up to date'); log.debug({ offers: updatedOffers.length }, 'offers up to date');
for (const [id, rpc] of connections) {
rpc.pushUserOffers();
}
}); });
events.on(Events.Connection, async function onConnection({ rpc, id }) { events.on(Events.Connection, async function onConnection({ rpc, id }) {
@@ -75,17 +79,17 @@ export default function createEvents(log: Log, pg: Pg, discogs, offers) {
connections.set(id, rpc); connections.set(id, rpc);
}) })
events.on(Events.OfferUpdated, async function onOfferUpdated(offer: Offer) { events.on(Events.OffersUpdated, async function onOffersUpdated(offer: Offer) {
const rpc = connections.get(offer.userId); const rpc = connections.get(offer.userId);
if (!rpc) return null; if (!rpc) return null;
log.debug({ event: Events.OfferUpdated, rpc, offer }); log.debug({ event: Events.OffersUpdated, rpc, offer });
return rpc.pushUserOffers(); return rpc.pushUserOffers();
}); });
events.on(Events.WantUpdated, async function onWantUpdated(want: Want) { events.on(Events.WantsUpdated, async function onWantsUpdated(want: Want) {
const rpc = connections.get(want.userId); const rpc = connections.get(want.userId);
if (!rpc) return null; if (!rpc) return null;
log.debug({ event: Events.WantUpdated, rpc, want }); log.debug({ event: Events.WantsUpdated, rpc, want });
return rpc.pushUserWants(); return rpc.pushUserWants();
}); });
+3 -2
View File
@@ -21,8 +21,8 @@ export default function createNotify(log: Log, pg: Pg, events) {
log.debug({ channel: OFFERS_CHANNEL, action, id }); log.debug({ channel: OFFERS_CHANNEL, action, id });
if (action === 'DELETE') return null; if (action === 'DELETE') return null;
const offer = await pg.offerFindId(id); // const offer = await pg.offerFindId(id);
// events.emit(Events.OfferUpdated, offer); // events.emit(Events.OffersUpdated, offer);
} }
async function handleUsers(payload) { async function handleUsers(payload) {
@@ -37,6 +37,7 @@ export default function createNotify(log: Log, pg: Pg, events) {
async function handleWants(payload) { async function handleWants(payload) {
const { action, id } = payload; const { action, id } = payload;
// const want = await pg.offerFindId(id);
log.debug({ channel: WANTS_CHANNEL, action, id }); log.debug({ channel: WANTS_CHANNEL, action, id });
} }
+6
View File
@@ -98,6 +98,12 @@ export default function createPg(log: Log, prisma: PrismaClient) {
}); });
} }
async function wantFindId(id: UUID) {
return prisma.want.findFirstOrThrow({
where: { id },
});
}
async function wantList(user: User) { async function wantList(user: User) {
return prisma.want.findMany({ return prisma.want.findMany({
where: { userId: user.id }, where: { userId: user.id },
+19
View File
@@ -37,6 +37,7 @@ export enum RpcMessageType {
AcpListingsSync = 'AcpListingsSync', AcpListingsSync = 'AcpListingsSync',
WantRm = 'WantRm', WantRm = 'WantRm',
WantAdd = 'WantAdd',
} }
type ErrorMessage = [RpcMessageType.Error, string]; type ErrorMessage = [RpcMessageType.Error, string];
@@ -82,6 +83,10 @@ type WantRmParams = {
id: UUID, id: UUID,
}; };
type WantAddParams = {
releaseId: number,
};
function checkMsgType(msgType: RpcMessageType): msgType is RpcMessageType { function checkMsgType(msgType: RpcMessageType): msgType is RpcMessageType {
if (RpcMessageType[msgType]) { if (RpcMessageType[msgType]) {
return true; return true;
@@ -124,6 +129,18 @@ export default function createRpc(log: Log, pg: Pg, ws, discogs: Discogs, offers
return [RpcMessageType.Wants, wants]; return [RpcMessageType.Wants, wants];
} }
async function wantAdd(params: WantAddParams): Promise<WantsMessage> {
const { releaseId } = params;
assert(releaseId, 'no id supplied');
const release = await discogs.release(releaseId);
const want = await pg.wantCreate(release, user);
events.emit(Events.WantsAdded, [want]);
const wants = await pg.userWants(user);
return [RpcMessageType.Wants, wants];
}
async function acpOfferStatusSet(params: OfferStatusSetParams): Promise<AcpOffersMessage> { async function acpOfferStatusSet(params: OfferStatusSetParams): Promise<AcpOffersMessage> {
const { id, status } = params; const { id, status } = params;
assert(isAdmin(user), 'not authorised'); assert(isAdmin(user), 'not authorised');
@@ -171,6 +188,8 @@ export default function createRpc(log: Log, pg: Pg, ws, discogs: Discogs, offers
case RpcMessageType.WantRm: case RpcMessageType.WantRm:
return wantRm(params); return wantRm(params);
case RpcMessageType.WantAdd:
return wantAdd(params);
case RpcMessageType.AcpOfferStatusSet: case RpcMessageType.AcpOfferStatusSet:
return acpOfferStatusSet(params); return acpOfferStatusSet(params);
+5 -1
View File
@@ -49,8 +49,12 @@ function OfferPriceBtn(offer) {
? offer.status ? offer.status
: 'Accept'; : 'Accept';
const title = accepted
? 'Click to cancel'
: 'Click to accept this offer';
return ( return (
<button disabled={confirmed} onClick={() => setStatus()} class={classes}>${offer.total} {offer.condition}</button> <button disabled={confirmed} title={title} onClick={() => setStatus()} class={classes}>${offer.total} {offer.condition}</button>
) )
} }
-13
View File
@@ -33,15 +33,10 @@ api.interceptors.response.use(onSuccess, onError);
const actions = { const actions = {
// TODO move to rpc // TODO move to rpc
postWantSearch: async function postWantSearch(query) { postWantSearch: async function postWantSearch(query) {
return api.post('/want/search', { query }); return api.post('/want/search', { query });
}, },
postWant: async function postWant(releaseId) {
return api.post('/want', { releaseId });
},
postDiscogs: async function postDiscogs(username) { postDiscogs: async function postDiscogs(username) {
return api.post('/want/discogs', { username }); return api.post('/want/discogs', { username });
}, },
@@ -71,14 +66,6 @@ const actions = {
password, password,
}); });
}, },
postOffers: async function postOffers(listingIds, shipping, baseShipping) {
return api.post('/offers', {
listingIds,
shipping,
baseShipping,
});
},
}; };
export default actions; export default actions;
+9 -11
View File
@@ -7,25 +7,22 @@ import { State } from './../store';
import http from '../http'; import http from '../http';
function Result(r, wants) { function Result(r, wants, ws) {
const [added, setAdded] = useState(false)
async function onClick() { async function onClick() {
const [error, want] = await http.postWant(r.id); ws.sendWantAdd(r.id);
if (error) return false;
setAdded(true);
} }
const wanted = wants.some(w => w.release === r.id); // releaseIds are bigints on server
const wanted = wants.some(w => BigInt(w.release) === BigInt(r.id));
const classes = `${added || wanted ? 'added' : ''}`; const classes = `${wanted ? 'added' : ''}`;
const btnText = added const btnText = wanted
? <Check /> ? <Check />
: '+'; : '+';
return ( return (
<article key={r.id}> <article key={r.id}>
<button onClick={onClick} disabled={added} class={classes} >{btnText}</button> <button onClick={onClick} disabled={wanted} class={classes} >{btnText}</button>
<div> <div>
<h3>{r.title} <a href={`https://discogs.com${r.uri}`} target="_blank"><ExternalLink /></a></h3> <h3>{r.title} <a href={`https://discogs.com${r.uri}`} target="_blank"><ExternalLink /></a></h3>
<h5 class='alt-text' >{r.year}</h5> <h5 class='alt-text' >{r.year}</h5>
@@ -45,6 +42,7 @@ const User = () => {
const [infoText, setInfoText] = useState(''); const [infoText, setInfoText] = useState('');
const wants = useSelector((state: State) => state.wants); const wants = useSelector((state: State) => state.wants);
const ws = useSelector((state: State) => state.ws);
async function fetchResults() { async function fetchResults() {
const [error, results] = await http.postWantSearch(query); const [error, results] = await http.postWantSearch(query);
@@ -114,7 +112,7 @@ const User = () => {
</div> </div>
<div class='results' ref={resultsRef}> <div class='results' ref={resultsRef}>
{infoEl} {infoEl}
{results.map(r => Result(r, wants))} {results.map(r => Result(r, wants, ws))}
{topEl} {topEl}
</div> </div>
</div> </div>
+5
View File
@@ -37,6 +37,10 @@ export default function createSocket(dispatch) {
send(['WantRm', { id }]); send(['WantRm', { id }]);
} }
function sendWantAdd(releaseId) {
send(['WantAdd', { releaseId }]);
}
function sendOffers() { function sendOffers() {
send(['Offers', {}]); send(['Offers', {}]);
} }
@@ -157,6 +161,7 @@ export default function createSocket(dispatch) {
sendOfferStatusSet, sendOfferStatusSet,
sendWantRm, sendWantRm,
sendWantAdd,
sendOffers, sendOffers,
sendWants, sendWants,