Compare commits
27
Commits
ws
...
bundle-offers
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
623d0f3938 | ||
|
|
7946259550 | ||
|
|
015a2fd66c | ||
|
|
77428aa7de | ||
|
|
2761456009 | ||
|
|
6b88740595 | ||
|
|
9fad9a69ec | ||
|
|
9f0bb35d09 | ||
|
|
e792951d24 | ||
|
|
ef33baacba | ||
|
|
9b8676809d | ||
|
|
e45b2ca9cd | ||
|
|
1e368913ab | ||
|
|
f6f03730e3 | ||
|
|
3bd8691ebf | ||
|
|
470270954f | ||
|
|
6b8ebe3f35 | ||
|
|
acf7e54be9 | ||
|
|
73248f7d9f | ||
|
|
0e3c272adc | ||
|
|
d2ec08159d | ||
|
|
12c3da9f5e | ||
|
|
70a67b0c53 | ||
|
|
644398319d | ||
|
|
2c66c47163 | ||
|
|
111f690482 | ||
|
|
b0572cb63d |
No files matched your search
@@ -1,9 +1,14 @@
|
|||||||
# TODO
|
# TODO
|
||||||
|
|
||||||
|
status stepper
|
||||||
|
|
||||||
|
queue up wantlist adds to gracefully handle rate limiting
|
||||||
|
|
||||||
push offers
|
push offers
|
||||||
redux that bitch
|
cover off admin routes and commands
|
||||||
|
|
||||||
|
listen / notify
|
||||||
|
on offers
|
||||||
|
notify admins
|
||||||
|
|
||||||
|
|
||||||
seller
|
|
||||||
eg min 6 items
|
|
||||||
shipping is 101aud
|
|
||||||
Generated
+30
-41
@@ -11,12 +11,11 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@koa/cors": "^3.3.0",
|
"@koa/cors": "^3.3.0",
|
||||||
"@koa/router": "^12.0.0",
|
"@koa/router": "^12.0.0",
|
||||||
"@prisma/client": "^4.1.0",
|
"@prisma/client": "^4.5.0",
|
||||||
"bcrypt": "^5.0.1",
|
"bcrypt": "^5.0.1",
|
||||||
"bluebird": "^3.7.2",
|
"bluebird": "^3.7.2",
|
||||||
"bunyan": "^1.8.15",
|
"bunyan": "^1.8.15",
|
||||||
"cheerio": "^1.0.0-rc.12",
|
"cheerio": "^1.0.0-rc.12",
|
||||||
"json-rpc-2.0": "^1.4.1",
|
|
||||||
"koa": "^2.13.4",
|
"koa": "^2.13.4",
|
||||||
"koa-body": "^5.0.0",
|
"koa-body": "^5.0.0",
|
||||||
"koa-passport": "^5.0.0",
|
"koa-passport": "^5.0.0",
|
||||||
@@ -35,7 +34,7 @@
|
|||||||
"axios": "^0.27.2",
|
"axios": "^0.27.2",
|
||||||
"mocha": "^10.0.0",
|
"mocha": "^10.0.0",
|
||||||
"node-dev": "^7.4.3",
|
"node-dev": "^7.4.3",
|
||||||
"prisma": "^4.1.0",
|
"prisma": "^4.5.0",
|
||||||
"ts-node-dev": "^2.0.0",
|
"ts-node-dev": "^2.0.0",
|
||||||
"typescript": "^4.7.4"
|
"typescript": "^4.7.4"
|
||||||
}
|
}
|
||||||
@@ -136,12 +135,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/client": {
|
"node_modules/@prisma/client": {
|
||||||
"version": "4.1.0",
|
"version": "4.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-4.5.0.tgz",
|
||||||
"integrity": "sha512-MvfPGAd42vHTiCYxwS6N+2U3F+ukoJ48D2QRnX1zSPJHBkh1CBtshl75daKzvVfgQwSouzSQeugKDej5di+E/g==",
|
"integrity": "sha512-B2cV0OPI1smhdYUxsJoLYQLoMlLH06MUxgFUWQnHodGMX98VRVXKmQE/9OcrTNkqtke5RC+YU24Szxd04tZA2g==",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/engines-version": "4.1.0-48.8d8414deb360336e4698a65aa45a1fbaf1ce13d8"
|
"@prisma/engines-version": "4.5.0-43.0362da9eebca54d94c8ef5edd3b2e90af99ba452"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.17"
|
"node": ">=14.17"
|
||||||
@@ -156,16 +155,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/engines": {
|
"node_modules/@prisma/engines": {
|
||||||
"version": "4.1.0",
|
"version": "4.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-4.5.0.tgz",
|
||||||
"integrity": "sha512-quqHXD3P83NBLVtRlts4SgKHmqgA8GMiyDTJ7af03Wg0gl6F5t65mBYvIuwmD+52vHm42JtIsp/fAO9YIV0JBA==",
|
"integrity": "sha512-4t9ir2SbQQr/wMCNU4YpHWp5hU14J2m3wHUZnGJPpmBF8YtkisxyVyQsKd1e6FyLTaGq8LOLhm6VLYHKqKNm+g==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"hasInstallScript": true
|
"hasInstallScript": true
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/engines-version": {
|
"node_modules/@prisma/engines-version": {
|
||||||
"version": "4.1.0-48.8d8414deb360336e4698a65aa45a1fbaf1ce13d8",
|
"version": "4.5.0-43.0362da9eebca54d94c8ef5edd3b2e90af99ba452",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-4.1.0-48.8d8414deb360336e4698a65aa45a1fbaf1ce13d8.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-4.5.0-43.0362da9eebca54d94c8ef5edd3b2e90af99ba452.tgz",
|
||||||
"integrity": "sha512-cRRJwpHFGFJZvtHbY3GZjMffNBEjjZk68ztn+S2hDgPCGB4H66IK26roK94GJxBodSehwRJ0wGyebC2GoIH1JQ=="
|
"integrity": "sha512-o7LyVx8PPJBLrEzLl6lpxxk2D5VnlM4Fwmrbq0NoT6pr5aa1OuHD9ZG+WJY6TlR/iD9bhmo2LNcxddCMr5Rv2A=="
|
||||||
},
|
},
|
||||||
"node_modules/@tsconfig/node10": {
|
"node_modules/@tsconfig/node10": {
|
||||||
"version": "1.0.9",
|
"version": "1.0.9",
|
||||||
@@ -1873,11 +1872,6 @@
|
|||||||
"js-yaml": "bin/js-yaml.js"
|
"js-yaml": "bin/js-yaml.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/json-rpc-2.0": {
|
|
||||||
"version": "1.4.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/json-rpc-2.0/-/json-rpc-2.0-1.4.1.tgz",
|
|
||||||
"integrity": "sha512-OX1NJhpIfuK4GjDnJ/gKtZy1HOYo0l4eL0a4rb0rNeQheX1xlyQ9+JMmPzs/sFNthpS/TXKPWlGo09X7B5l81A=="
|
|
||||||
},
|
|
||||||
"node_modules/keygrip": {
|
"node_modules/keygrip": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz",
|
||||||
@@ -2989,13 +2983,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/prisma": {
|
"node_modules/prisma": {
|
||||||
"version": "4.1.0",
|
"version": "4.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/prisma/-/prisma-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/prisma/-/prisma-4.5.0.tgz",
|
||||||
"integrity": "sha512-iwqpAT6In1uvMSwQAM3PqmaBdhh2OaQ/2t+n3RjpW4vAKP3R7E1T34FZUU4zGOWtMWm5dt0sPThQkT/h87r6gw==",
|
"integrity": "sha512-9Aeg4qiKlv9Wsjz4NO8k2CzRzlvS3A4FYVJ5+28sBBZ0eEwbiVOE/Jj7v6rZC1tFW2s4GSICQOAyuOjc6WsNew==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/engines": "4.1.0"
|
"@prisma/engines": "4.5.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"prisma": "build/index.js",
|
"prisma": "build/index.js",
|
||||||
@@ -3790,23 +3784,23 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@prisma/client": {
|
"@prisma/client": {
|
||||||
"version": "4.1.0",
|
"version": "4.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-4.5.0.tgz",
|
||||||
"integrity": "sha512-MvfPGAd42vHTiCYxwS6N+2U3F+ukoJ48D2QRnX1zSPJHBkh1CBtshl75daKzvVfgQwSouzSQeugKDej5di+E/g==",
|
"integrity": "sha512-B2cV0OPI1smhdYUxsJoLYQLoMlLH06MUxgFUWQnHodGMX98VRVXKmQE/9OcrTNkqtke5RC+YU24Szxd04tZA2g==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@prisma/engines-version": "4.1.0-48.8d8414deb360336e4698a65aa45a1fbaf1ce13d8"
|
"@prisma/engines-version": "4.5.0-43.0362da9eebca54d94c8ef5edd3b2e90af99ba452"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@prisma/engines": {
|
"@prisma/engines": {
|
||||||
"version": "4.1.0",
|
"version": "4.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-4.5.0.tgz",
|
||||||
"integrity": "sha512-quqHXD3P83NBLVtRlts4SgKHmqgA8GMiyDTJ7af03Wg0gl6F5t65mBYvIuwmD+52vHm42JtIsp/fAO9YIV0JBA==",
|
"integrity": "sha512-4t9ir2SbQQr/wMCNU4YpHWp5hU14J2m3wHUZnGJPpmBF8YtkisxyVyQsKd1e6FyLTaGq8LOLhm6VLYHKqKNm+g==",
|
||||||
"devOptional": true
|
"devOptional": true
|
||||||
},
|
},
|
||||||
"@prisma/engines-version": {
|
"@prisma/engines-version": {
|
||||||
"version": "4.1.0-48.8d8414deb360336e4698a65aa45a1fbaf1ce13d8",
|
"version": "4.5.0-43.0362da9eebca54d94c8ef5edd3b2e90af99ba452",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-4.1.0-48.8d8414deb360336e4698a65aa45a1fbaf1ce13d8.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-4.5.0-43.0362da9eebca54d94c8ef5edd3b2e90af99ba452.tgz",
|
||||||
"integrity": "sha512-cRRJwpHFGFJZvtHbY3GZjMffNBEjjZk68ztn+S2hDgPCGB4H66IK26roK94GJxBodSehwRJ0wGyebC2GoIH1JQ=="
|
"integrity": "sha512-o7LyVx8PPJBLrEzLl6lpxxk2D5VnlM4Fwmrbq0NoT6pr5aa1OuHD9ZG+WJY6TlR/iD9bhmo2LNcxddCMr5Rv2A=="
|
||||||
},
|
},
|
||||||
"@tsconfig/node10": {
|
"@tsconfig/node10": {
|
||||||
"version": "1.0.9",
|
"version": "1.0.9",
|
||||||
@@ -5100,11 +5094,6 @@
|
|||||||
"argparse": "^2.0.1"
|
"argparse": "^2.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"json-rpc-2.0": {
|
|
||||||
"version": "1.4.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/json-rpc-2.0/-/json-rpc-2.0-1.4.1.tgz",
|
|
||||||
"integrity": "sha512-OX1NJhpIfuK4GjDnJ/gKtZy1HOYo0l4eL0a4rb0rNeQheX1xlyQ9+JMmPzs/sFNthpS/TXKPWlGo09X7B5l81A=="
|
|
||||||
},
|
|
||||||
"keygrip": {
|
"keygrip": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz",
|
||||||
@@ -5939,12 +5928,12 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"prisma": {
|
"prisma": {
|
||||||
"version": "4.1.0",
|
"version": "4.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/prisma/-/prisma-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/prisma/-/prisma-4.5.0.tgz",
|
||||||
"integrity": "sha512-iwqpAT6In1uvMSwQAM3PqmaBdhh2OaQ/2t+n3RjpW4vAKP3R7E1T34FZUU4zGOWtMWm5dt0sPThQkT/h87r6gw==",
|
"integrity": "sha512-9Aeg4qiKlv9Wsjz4NO8k2CzRzlvS3A4FYVJ5+28sBBZ0eEwbiVOE/Jj7v6rZC1tFW2s4GSICQOAyuOjc6WsNew==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@prisma/engines": "4.1.0"
|
"@prisma/engines": "4.5.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"qs": {
|
"qs": {
|
||||||
|
|||||||
+2
-2
@@ -21,14 +21,14 @@
|
|||||||
"axios": "^0.27.2",
|
"axios": "^0.27.2",
|
||||||
"mocha": "^10.0.0",
|
"mocha": "^10.0.0",
|
||||||
"node-dev": "^7.4.3",
|
"node-dev": "^7.4.3",
|
||||||
"prisma": "^4.1.0",
|
"prisma": "^4.5.0",
|
||||||
"ts-node-dev": "^2.0.0",
|
"ts-node-dev": "^2.0.0",
|
||||||
"typescript": "^4.7.4"
|
"typescript": "^4.7.4"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@koa/cors": "^3.3.0",
|
"@koa/cors": "^3.3.0",
|
||||||
"@koa/router": "^12.0.0",
|
"@koa/router": "^12.0.0",
|
||||||
"@prisma/client": "^4.1.0",
|
"@prisma/client": "^4.5.0",
|
||||||
"bcrypt": "^5.0.1",
|
"bcrypt": "^5.0.1",
|
||||||
"bluebird": "^3.7.2",
|
"bluebird": "^3.7.2",
|
||||||
"bunyan": "^1.8.15",
|
"bunyan": "^1.8.15",
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ model Offer {
|
|||||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||||
|
|
||||||
status String
|
status String
|
||||||
|
condition String @default("")
|
||||||
|
|
||||||
item Decimal @db.Money
|
item Decimal @db.Money
|
||||||
// markup Decimal @db.Money
|
// markup Decimal @db.Money
|
||||||
|
|||||||
+17
-51
@@ -16,6 +16,7 @@ import type { Listing, User, Want, Prisma } from '@prisma/client';
|
|||||||
import { Pg } from './pg';
|
import { Pg } from './pg';
|
||||||
import routerAuth from './routers/router.auth';
|
import routerAuth from './routers/router.auth';
|
||||||
import { Events } from './events';
|
import { Events } from './events';
|
||||||
|
import createRpc, { RpcMessageType } from './rpc';
|
||||||
|
|
||||||
export default function createApi(log: Log, pg: Pg, discogs, offers, events) {
|
export default function createApi(log: Log, pg: Pg, discogs, offers, events) {
|
||||||
const PORT = 41337;
|
const PORT = 41337;
|
||||||
@@ -58,48 +59,6 @@ export default function createApi(log: Log, pg: Pg, discogs, offers, events) {
|
|||||||
ctx.status = 200;
|
ctx.status = 200;
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get('/acp/bundles', async function bundlesGet(ctx, next) {
|
|
||||||
const bundles = await offers.bundles();
|
|
||||||
ctx.response.body = bundles;
|
|
||||||
})
|
|
||||||
|
|
||||||
router.get('/acp/offers', async function acpOffers(ctx, next) {
|
|
||||||
const offers = await pg.offersAdmin();
|
|
||||||
|
|
||||||
const grouped = groupBy(offers, o => o.listing.data.seller.id);
|
|
||||||
|
|
||||||
const bundles = 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);
|
|
||||||
|
|
||||||
ctx.response.body = bundles;
|
|
||||||
})
|
|
||||||
|
|
||||||
router.post('/offers', async function postOffers(ctx, next) {
|
|
||||||
const { listingIds, shipping, baseShipping } = ctx.request.body;
|
|
||||||
|
|
||||||
ctx.assert(listingIds.length, 400, 'listingIds required');
|
|
||||||
ctx.assert([listingIds, shipping, baseShipping].every(i => i), 400, 'listingIds, shipping, baseShipping required');
|
|
||||||
|
|
||||||
ctx.response.body = await offers.generate(listingIds, shipping, baseShipping);
|
|
||||||
})
|
|
||||||
|
|
||||||
router.post('/offer/accept', async function postOffers(ctx, next) {
|
|
||||||
ctx.assert(ctx.isAuthenticated(), 401, 'log in required');
|
|
||||||
ctx.response.body = await offers.accept(ctx);
|
|
||||||
})
|
|
||||||
|
|
||||||
router.get('/user/offers', async function userOffersGet(ctx, next) {
|
router.get('/user/offers', async function userOffersGet(ctx, next) {
|
||||||
ctx.assert(ctx.isAuthenticated(), 401, 'log in required');
|
ctx.assert(ctx.isAuthenticated(), 401, 'log in required');
|
||||||
const user = ctx.state.user;
|
const user = ctx.state.user;
|
||||||
@@ -113,7 +72,7 @@ export default function createApi(log: Log, pg: Pg, discogs, offers, events) {
|
|||||||
const user = await pg.userFindId(ctx.state.user.id);
|
const user = await pg.userFindId(ctx.state.user.id);
|
||||||
const release = await discogs.release(releaseId);
|
const release = await discogs.release(releaseId);
|
||||||
const want = await pg.wantCreate(release, user);
|
const want = await pg.wantCreate(release, user);
|
||||||
events.emit(Events.WantAdded, want);
|
events.emit(Events.WantsAdded, [want]);
|
||||||
ctx.response.body = [want];
|
ctx.response.body = [want];
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -122,7 +81,7 @@ export default function createApi(log: Log, pg: Pg, discogs, offers, events) {
|
|||||||
const { username } = ctx.request.body;
|
const { username } = ctx.request.body;
|
||||||
const user = await pg.userFindId(ctx.state.user.id);
|
const user = await pg.userFindId(ctx.state.user.id);
|
||||||
const wants = await discogs.wantListSync(username, user);
|
const wants = await discogs.wantListSync(username, user);
|
||||||
wants.forEach(w => events.emit(Events.WantAdded, w));
|
events.emit(Events.WantsAdded, [wants]);
|
||||||
ctx.response.body = wants;
|
ctx.response.body = wants;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -153,24 +112,31 @@ export default function createApi(log: Log, pg: Pg, discogs, offers, events) {
|
|||||||
app.ws.use(passport.session());
|
app.ws.use(passport.session());
|
||||||
app.ws.use(async (ctx, next) => {
|
app.ws.use(async (ctx, next) => {
|
||||||
log.info('new ws connection', { user: ctx.state.user });
|
log.info('new ws connection', { user: ctx.state.user });
|
||||||
ctx.websocket.on('message', async function incoming(message) {
|
|
||||||
log.info(message.toString());
|
// log.child...
|
||||||
|
|
||||||
|
const ws = ctx.websocket;
|
||||||
|
const rpc = createRpc(log, pg, ctx.state.user, ws, discogs, offers);
|
||||||
|
|
||||||
|
if (ctx.state.user) events.emit(Events.Connection, { ws, id: ctx.state.user.id });
|
||||||
|
ws.on('message', async function incoming(message) {
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(message.toString());
|
const parsed = JSON.parse(message.toString());
|
||||||
log.info(parsed);
|
const response = await rpc.receive(parsed);
|
||||||
// const response = await rpc.receive(parsed);
|
rpc.send(response);
|
||||||
// rpc.send(ws, response);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log.warn(e);
|
log.warn(e);
|
||||||
log.warn(message.toString());
|
log.warn(message.toString());
|
||||||
// rpc.send(ws, [RpcMessageType.Error, e.message || e.msg])
|
rpc.send([RpcMessageType.Error, e.message || e.msg]);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
ctx.websocket.on('close', function onClose() {
|
ws.on('close', function onClose() {
|
||||||
log.info('ws disconnected', { user: ctx.state.user });
|
log.info('ws disconnected', { user: ctx.state.user });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await rpc.onConnection();
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
+1
-3
@@ -84,11 +84,9 @@ export default function createDiscogs(log: Log, pg: Pg) {
|
|||||||
.get();
|
.get();
|
||||||
|
|
||||||
const unlisted = await pg.listingRmUnlisted(want, ids);
|
const unlisted = await pg.listingRmUnlisted(want, ids);
|
||||||
if (unlisted) log.info({ wantId: want.id, unlisted: unlisted }, 'missing listings removed');
|
if (unlisted) log.info({ unlisted: unlisted, ...wantDetails(want) }, 'missing listings removed');
|
||||||
|
|
||||||
// preferentially take listings from sellers if we hit req limits
|
// preferentially take listings from sellers if we hit req limits
|
||||||
|
|
||||||
log.info({ listings: ids.length, ...wantDetails(want)}, 'found listings');
|
|
||||||
return Bluebird.mapSeries(ids, async id => listing(id, want));
|
return Bluebird.mapSeries(ids, async id => listing(id, want));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+21
-25
@@ -9,13 +9,15 @@ import { wantDetails } from './utils';
|
|||||||
import routerAuth from './routers/router.auth';
|
import routerAuth from './routers/router.auth';
|
||||||
|
|
||||||
export enum Events {
|
export enum Events {
|
||||||
WantAdded = 'WantAdded',
|
WantsAdded = 'WantsAdded',
|
||||||
StartupSync = 'StartupSync',
|
StartupSync = 'StartupSync',
|
||||||
ListingsUpdated = 'ListingsUpdated',
|
ListingsUpdated = 'ListingsUpdated',
|
||||||
|
Connection = 'Connection',
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function createEvents(log: Log, pg: Pg, discogs, offers) {
|
export default function createEvents(log: Log, pg: Pg, discogs, offers) {
|
||||||
const events = new EventEmitter({ captureRejections: true });
|
const events = new EventEmitter({ captureRejections: true });
|
||||||
|
const connections = new Map();
|
||||||
|
|
||||||
async function populateListings(want: Want) {
|
async function populateListings(want: Want) {
|
||||||
try {
|
try {
|
||||||
@@ -35,43 +37,33 @@ export default function createEvents(log: Log, pg: Pg, discogs, offers) {
|
|||||||
log.error('event error', e);
|
log.error('event error', e);
|
||||||
});
|
});
|
||||||
|
|
||||||
events.on(Events.WantAdded, async function wantAdded(want: Want) {
|
async function updateWantListings(wants: Want[]) {
|
||||||
log.info({ event: Events.WantAdded, ...wantDetails(want) });
|
|
||||||
const listings = await populateListings(want);
|
|
||||||
log.info({ listings: listings.length, ...wantDetails(want) }, 'listings added');
|
|
||||||
events.emit(Events.ListingsUpdated);
|
|
||||||
});
|
|
||||||
|
|
||||||
events.on(Events.StartupSync, async function wantAdded() {
|
|
||||||
const wants = await pg.wantsToSync();
|
|
||||||
log.info({ event: Events.StartupSync, wants: wants.length });
|
|
||||||
|
|
||||||
for (const want of wants) {
|
for (const want of wants) {
|
||||||
const listings = await populateListings(want);
|
const listings = await populateListings(want);
|
||||||
log.info({ listings: listings.length, ...wantDetails(want) }, 'listings added');
|
log.info({ listings: listings.length, ...wantDetails(want) }, 'listings added');
|
||||||
|
events.emit(Events.ListingsUpdated);
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info({ wants: wants.length }, 'finished syncing');
|
log.info({ wants: wants.length }, 'finished syncing');
|
||||||
events.emit(Events.ListingsUpdated);
|
}
|
||||||
|
|
||||||
|
events.on(Events.WantsAdded, async function wantsAdded(wants: Want[]) {
|
||||||
|
log.info({ event: Events.WantsAdded, wants: wants.map(w => wantDetails(w)) }, 'starting listings population');
|
||||||
|
await updateWantListings(wants);
|
||||||
|
log.info({ event: Events.WantsAdded, wants: wants.map(w => wantDetails(w)) }, 'finished populating listings');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
events.on(Events.StartupSync, async function startupSync() {
|
||||||
|
const wants = await pg.wantsToSync();
|
||||||
|
log.info({ event: Events.StartupSync, wants: wants.length });
|
||||||
|
await updateWantListings(wants);
|
||||||
|
events.emit(Events.ListingsUpdated);
|
||||||
|
});
|
||||||
|
|
||||||
events.on(Events.ListingsUpdated, async function listingsUpdated() {
|
events.on(Events.ListingsUpdated, async function listingsUpdated() {
|
||||||
const bundles = await offers.bundles();
|
const bundles = await offers.bundles();
|
||||||
log.info({ event: Events.ListingsUpdated, bundles: bundles.length });
|
log.info({ event: Events.ListingsUpdated, bundles: bundles.length });
|
||||||
|
|
||||||
// const bundle = {
|
|
||||||
// username: b[0]?.data.seller.username,
|
|
||||||
// seller: b[0]?.data.seller.id,
|
|
||||||
// shipsFrom: b[0]?.data.ships_from,
|
|
||||||
// total,
|
|
||||||
// currency: b[0]?.data.price.currency,
|
|
||||||
// shipping: '...',
|
|
||||||
// items,
|
|
||||||
// mentionsAustralia: shipping.toLowerCase().includes('australia'),
|
|
||||||
// mentionsWorldwide: shipping.toLowerCase().includes('world'),
|
|
||||||
// }
|
|
||||||
|
|
||||||
for (const bundle of bundles) {
|
for (const bundle of bundles) {
|
||||||
log.info({ seller: bundle.seller, items: bundle.items.length }, 'generating offers...');
|
log.info({ seller: bundle.seller, items: bundle.items.length }, 'generating offers...');
|
||||||
const listingIds = bundle.items.map(i => i.id);
|
const listingIds = bundle.items.map(i => i.id);
|
||||||
@@ -82,5 +74,9 @@ export default function createEvents(log: Log, pg: Pg, discogs, offers) {
|
|||||||
log.info('offers up to date');
|
log.info('offers up to date');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
events.on(Events.Connection, async function onConnection({ ws, id }) {
|
||||||
|
connections.set(id, ws);
|
||||||
|
})
|
||||||
|
|
||||||
return events;
|
return events;
|
||||||
}
|
}
|
||||||
+59
-8
@@ -1,18 +1,23 @@
|
|||||||
import type Log from 'bunyan';
|
import type Log from 'bunyan';
|
||||||
import type { PrismaClient, Want, User, Listing, Offer, } from '@prisma/client';
|
import type { PrismaClient, Want, User, Listing, Offer, } from '@prisma/client';
|
||||||
|
import assert from 'assert/strict';
|
||||||
import groupBy from 'lodash/groupBy';
|
import groupBy from 'lodash/groupBy';
|
||||||
import sortBy from 'lodash/sortBy';
|
import sortBy from 'lodash/sortBy';
|
||||||
import { Context } from 'koa';
|
import { Context } from 'koa';
|
||||||
|
|
||||||
import { logAxiosError } from './utils';
|
import { logAxiosError, wantDetails } from './utils';
|
||||||
import { Pg } from './pg';
|
import { Pg } from './pg';
|
||||||
|
|
||||||
export enum Status {
|
export enum Status {
|
||||||
Pending = 'pending',
|
Complete = 'Complete',
|
||||||
Accepted = 'accepted',
|
Confirmed = 'Confirmed',
|
||||||
Declined = 'declined',
|
Accepted = 'Accepted',
|
||||||
Ordered = 'ordered',
|
Pending = 'Pending',
|
||||||
Complete = 'complete',
|
Declined = 'Declined',
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortOffers(o) {
|
||||||
|
return Object.values(Status).indexOf(o.status);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function createOffers(log: Log, pg: Pg) {
|
export default function createOffers(log: Log, pg: Pg) {
|
||||||
@@ -22,7 +27,7 @@ export default function createOffers(log: Log, pg: Pg) {
|
|||||||
|
|
||||||
// log.info({ listings }, 'creating offers for listings')
|
// log.info({ listings }, 'creating offers for listings')
|
||||||
|
|
||||||
const shipping = 10 + Math.random(); // yolt
|
const shipping = 7.5; // yolt
|
||||||
|
|
||||||
// const shipping = bulkShipping / listings.length;
|
// const shipping = bulkShipping / listings.length;
|
||||||
// how to deal with users who don't want all items in a bundle...
|
// how to deal with users who don't want all items in a bundle...
|
||||||
@@ -36,6 +41,8 @@ export default function createOffers(log: Log, pg: Pg) {
|
|||||||
|
|
||||||
status: Status.Pending,
|
status: Status.Pending,
|
||||||
|
|
||||||
|
condition: l.data.condition,
|
||||||
|
|
||||||
item: l.data.price.value,
|
item: l.data.price.value,
|
||||||
shipping,
|
shipping,
|
||||||
total: l.data.price.value + shipping,
|
total: l.data.price.value + shipping,
|
||||||
@@ -60,7 +67,21 @@ export default function createOffers(log: Log, pg: Pg) {
|
|||||||
|
|
||||||
// check the user actually has this offer
|
// check the user actually has this offer
|
||||||
const offer = await pg.userOfferFind(ctx.state.user, id);
|
const offer = await pg.userOfferFind(ctx.state.user, id);
|
||||||
return pg.offerSetStatus(offer.id, Status.Accepted);
|
return pg.offerStatusSet(offer.id, Status.Accepted);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function statusSet(user: User, id: UUID, status: Status) {
|
||||||
|
log.info({ status, id }, 'setting offer status');
|
||||||
|
|
||||||
|
const offer = await pg.offerFindId(id);
|
||||||
|
const update = await pg.offerStatusSet(offer.id, status);
|
||||||
|
|
||||||
|
if (status === Status.Complete) {
|
||||||
|
log.info({ id, status }, 'completed offer, removing want...');
|
||||||
|
await pg.wantRm(offer.wantId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return update;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function bundles() {
|
async function bundles() {
|
||||||
@@ -103,10 +124,40 @@ 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: sortBy(offers, sortOffers),
|
||||||
|
mentionsAustralia: shipping.toLowerCase().includes('australia'),
|
||||||
|
mentionsWorldwide: shipping.toLowerCase().includes('world'),
|
||||||
|
}
|
||||||
|
|
||||||
|
return bundle;
|
||||||
|
}), bundle => -bundle.offers.length);
|
||||||
|
|
||||||
|
return sellerOffers;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function userOffers(user: User) {
|
||||||
|
const offers = await pg.userOffers(user);
|
||||||
|
return groupBy(offers, (o) => o.wantId);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
generate,
|
generate,
|
||||||
accept,
|
accept,
|
||||||
bundles,
|
bundles,
|
||||||
|
statusSet,
|
||||||
|
acpOffers,
|
||||||
|
userOffers,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
+33
-7
@@ -27,12 +27,14 @@ export interface Pg {
|
|||||||
wantCreate: (discogsWant: any, user: User) => Promise<Want>,
|
wantCreate: (discogsWant: any, user: User) => Promise<Want>,
|
||||||
wantList: (user: User) => Promise<Want[]>,
|
wantList: (user: User) => Promise<Want[]>,
|
||||||
wantsToSync: () => Promise<Want[]>,
|
wantsToSync: () => Promise<Want[]>,
|
||||||
|
wantRm: (id: UUID) => Promise<void>,
|
||||||
|
|
||||||
bundleCandidates: () => Promise<Listing[][]>,
|
bundleCandidates: () => Promise<Listing[][]>,
|
||||||
|
|
||||||
offersCreate: (offers: Offer[]) => Promise<Offer[]>,
|
offersCreate: (offers: Offer[]) => Promise<Offer[]>,
|
||||||
offerSetStatus: (id: UUID, status: Status) => Promise<Offer>,
|
offerStatusSet: (id: UUID, status: Status) => Promise<Offer>,
|
||||||
offersAdmin: () => Promise<Offer[]>,
|
offersAdmin: () => Promise<Offer[]>,
|
||||||
|
offerFindId: (id: UUID) => Promise<Offer>,
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function createPg(log: Log, prisma: PrismaClient): Pg {
|
export default function createPg(log: Log, prisma: PrismaClient): Pg {
|
||||||
@@ -50,7 +52,10 @@ export default function createPg(log: Log, prisma: PrismaClient): Pg {
|
|||||||
|
|
||||||
async function userOffers(user: User) {
|
async function userOffers(user: User) {
|
||||||
return prisma.offer.findMany({
|
return prisma.offer.findMany({
|
||||||
where: { userId: user.id },
|
where: {
|
||||||
|
userId: user.id,
|
||||||
|
status: { not: Status.Declined }
|
||||||
|
},
|
||||||
include: {
|
include: {
|
||||||
want: true,
|
want: true,
|
||||||
},
|
},
|
||||||
@@ -60,6 +65,15 @@ export default function createPg(log: Log, prisma: PrismaClient): Pg {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function offerFindId(id: UUID) {
|
||||||
|
return prisma.offer.findFirstOrThrow({
|
||||||
|
where: { id },
|
||||||
|
include: {
|
||||||
|
want: true,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function userOfferFind(user: User, id: UUID) {
|
async function userOfferFind(user: User, id: UUID) {
|
||||||
return prisma.offer.findFirstOrThrow({
|
return prisma.offer.findFirstOrThrow({
|
||||||
where: {
|
where: {
|
||||||
@@ -86,6 +100,9 @@ export default function createPg(log: Log, prisma: PrismaClient): Pg {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// userUpdate({ email: 'jen@vdk.com', password: 'jen' });
|
||||||
|
// userUpdate({ email: 'james@mcd', password: 'james' });
|
||||||
|
|
||||||
async function userUpdate(params: UserCreateParams) {
|
async function userUpdate(params: UserCreateParams) {
|
||||||
const hashRounds = 10;
|
const hashRounds = 10;
|
||||||
const password = await bcrypt.hash(params.password, hashRounds);
|
const password = await bcrypt.hash(params.password, hashRounds);
|
||||||
@@ -111,6 +128,12 @@ export default function createPg(log: Log, prisma: PrismaClient): Pg {
|
|||||||
return prisma.want.findMany({});
|
return prisma.want.findMany({});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function wantRm(id: UUID) {
|
||||||
|
await prisma.want.delete({
|
||||||
|
where: { id },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function wantCreate(discogsRelease, user) {
|
async function wantCreate(discogsRelease, user) {
|
||||||
const data: { userId: UUID, release: DiscogsListing, data: any } = {
|
const data: { userId: UUID, release: DiscogsListing, data: any } = {
|
||||||
data: discogsRelease,
|
data: discogsRelease,
|
||||||
@@ -127,7 +150,7 @@ export default function createPg(log: Log, prisma: PrismaClient): Pg {
|
|||||||
return prisma.$transaction(async (prisma) => {
|
return prisma.$transaction(async (prisma) => {
|
||||||
log.debug({ currentListingIds });
|
log.debug({ currentListingIds });
|
||||||
|
|
||||||
const openOfferStatuses = [Status.Pending, Status.Accepted];
|
const protectedOfferStatuses = [Status.Confirmed, Status.Complete];
|
||||||
|
|
||||||
// can't delete a listing with an open offer
|
// can't delete a listing with an open offer
|
||||||
// so delete those offers first
|
// so delete those offers first
|
||||||
@@ -138,7 +161,7 @@ export default function createPg(log: Log, prisma: PrismaClient): Pg {
|
|||||||
notIn: currentListingIds,
|
notIn: currentListingIds,
|
||||||
},
|
},
|
||||||
status: {
|
status: {
|
||||||
in: openOfferStatuses,
|
notIn: protectedOfferStatuses,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -151,7 +174,7 @@ export default function createPg(log: Log, prisma: PrismaClient): Pg {
|
|||||||
where: {
|
where: {
|
||||||
wantId: want.id,
|
wantId: want.id,
|
||||||
status: {
|
status: {
|
||||||
notIn: openOfferStatuses,
|
in: protectedOfferStatuses,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
@@ -258,7 +281,7 @@ export default function createPg(log: Log, prisma: PrismaClient): Pg {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function offerSetStatus(id: UUID, status: Status) {
|
async function offerStatusSet(id: UUID, status: Status) {
|
||||||
return prisma.offer.update({
|
return prisma.offer.update({
|
||||||
where: { id: id },
|
where: { id: id },
|
||||||
data: { status },
|
data: { status },
|
||||||
@@ -286,6 +309,8 @@ export default function createPg(log: Log, prisma: PrismaClient): Pg {
|
|||||||
wantCreate,
|
wantCreate,
|
||||||
wantList,
|
wantList,
|
||||||
wantsToSync,
|
wantsToSync,
|
||||||
|
wantRm,
|
||||||
|
|
||||||
listingUpsert,
|
listingUpsert,
|
||||||
listingFind,
|
listingFind,
|
||||||
listingRmUnlisted,
|
listingRmUnlisted,
|
||||||
@@ -294,7 +319,8 @@ export default function createPg(log: Log, prisma: PrismaClient): Pg {
|
|||||||
bundleCandidates,
|
bundleCandidates,
|
||||||
|
|
||||||
offersCreate,
|
offersCreate,
|
||||||
offerSetStatus,
|
offerStatusSet,
|
||||||
offersAdmin,
|
offersAdmin,
|
||||||
|
offerFindId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
+196
@@ -0,0 +1,196 @@
|
|||||||
|
import { EventEmitter } from 'node:events';
|
||||||
|
import { setTimeout } from 'timers/promises';
|
||||||
|
import assert from 'assert/strict';
|
||||||
|
|
||||||
|
import type Log from 'bunyan';
|
||||||
|
|
||||||
|
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]) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// rpc is created per connection from the api
|
||||||
|
export default function createRpc(log: Log, pg: Pg, user: User, ws, discogs, offers) {
|
||||||
|
async function ping(): Promise<PongMessage> {
|
||||||
|
return [RpcMessageType.Pong, []];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function userOffers(): Promise<OffersMessage> {
|
||||||
|
return [RpcMessageType.Offers, await offers.userOffers(user)];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function userWants(): Promise<WantsMessage> {
|
||||||
|
return [RpcMessageType.Wants, await pg.userWants(user)];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function offerStatusSet(params: OfferStatusSetParams): Promise<OffersMessage> {
|
||||||
|
const { id, status } = params;
|
||||||
|
assert(id, 'no id supplied');
|
||||||
|
assert(Status[status], 'unknown status');
|
||||||
|
|
||||||
|
const offer = await offers.statusSet(user, id, status);
|
||||||
|
const userOffers = await offers.userOffers(user);
|
||||||
|
|
||||||
|
return [RpcMessageType.Offers, userOffers]
|
||||||
|
}
|
||||||
|
|
||||||
|
async function wantRm(params: WantRmParams): Promise<WantsMessage> {
|
||||||
|
const { id } = params;
|
||||||
|
assert(id, 'no id supplied');
|
||||||
|
|
||||||
|
await pg.wantRm(id);
|
||||||
|
|
||||||
|
const wants = await pg.userWants(user);
|
||||||
|
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;
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
log.debug('anon rpc call');
|
||||||
|
return [RpcMessageType.WhoDis, []];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msgType !== RpcMessageType.Ping) log.info({ msgType, params }, 'rpc message');
|
||||||
|
|
||||||
|
// // this sucks but can't figure out how to convince typescript to agree yet
|
||||||
|
if (checkMsgType(msgType)) {
|
||||||
|
switch (msgType) {
|
||||||
|
case RpcMessageType.Ping:
|
||||||
|
return ping();
|
||||||
|
|
||||||
|
case RpcMessageType.Offers:
|
||||||
|
return userOffers();
|
||||||
|
case RpcMessageType.Wants:
|
||||||
|
return userWants();
|
||||||
|
case RpcMessageType.OfferStatusSet:
|
||||||
|
return offerStatusSet(params);
|
||||||
|
|
||||||
|
case RpcMessageType.WantRm:
|
||||||
|
return wantRm(params);
|
||||||
|
|
||||||
|
case RpcMessageType.AcpOfferStatusSet:
|
||||||
|
return acpOfferStatusSet(params);
|
||||||
|
case RpcMessageType.AcpOffers:
|
||||||
|
return acpOffers();
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error(`Message type ${msgType} is known but unhandled`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error(`Unknown message type ${msgType}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Invalid parameters for message type ${msgType}: ${JSON.stringify(message)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onConnection() {
|
||||||
|
const userMsg: UserMessage = [RpcMessageType.User, user];
|
||||||
|
send(userMsg);
|
||||||
|
|
||||||
|
const wants = await pg.userWants(user);
|
||||||
|
const wantsMsg: WantsMessage = [RpcMessageType.Wants, wants];
|
||||||
|
send(wantsMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
function send(msg: RpcMessage) {
|
||||||
|
ws.send(JSON.stringify(msg, (key, value) =>
|
||||||
|
typeof value === 'bigint'
|
||||||
|
? value.toString()
|
||||||
|
: value // return everything else unchanged
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
receive,
|
||||||
|
send,
|
||||||
|
onConnection,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum RpcMessageType {
|
||||||
|
Ping = 'Ping',
|
||||||
|
Pong = 'Pong',
|
||||||
|
|
||||||
|
// points out user is anon
|
||||||
|
WhoDis = 'WhoDis',
|
||||||
|
|
||||||
|
Bundles = 'Bundles',
|
||||||
|
Offers = 'Offers',
|
||||||
|
List = 'List',
|
||||||
|
Wants = 'Wants',
|
||||||
|
User = 'User',
|
||||||
|
|
||||||
|
Error = 'Error',
|
||||||
|
|
||||||
|
OfferStatusSet = 'OfferStatusSet',
|
||||||
|
|
||||||
|
AcpOfferStatusSet = 'AcpOfferStatusSet',
|
||||||
|
AcpOffers = 'AcpOffers',
|
||||||
|
|
||||||
|
WantRm = 'WantRm',
|
||||||
|
}
|
||||||
|
|
||||||
|
type ErrorMessage = [RpcMessageType.Error, string];
|
||||||
|
type PongMessage = [RpcMessageType.Pong, []];
|
||||||
|
type PingMessage = [RpcMessageType.Ping, []];
|
||||||
|
type WhoDisMessage = [RpcMessageType.WhoDis, []];
|
||||||
|
type UserMessage = [RpcMessageType.User, User];
|
||||||
|
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 |
|
||||||
|
WhoDisMessage |
|
||||||
|
|
||||||
|
UserMessage |
|
||||||
|
|
||||||
|
WantsMessage |
|
||||||
|
|
||||||
|
OffersMessage |
|
||||||
|
OfferStatusSetMessage |
|
||||||
|
|
||||||
|
AcpOfferStatusSetMessage |
|
||||||
|
AcpOffersMessage |
|
||||||
|
|
||||||
|
ErrorMessage;
|
||||||
|
|
||||||
|
|
||||||
|
type OfferStatusSetParams = {
|
||||||
|
id: UUID,
|
||||||
|
status: Status,
|
||||||
|
};
|
||||||
|
|
||||||
|
type WantRmParams = {
|
||||||
|
id: UUID,
|
||||||
|
};
|
||||||
+7
-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({
|
||||||
@@ -24,3 +24,9 @@ export function wantDetails(want: Want) {
|
|||||||
id: want.id
|
id: want.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isAdmin(user: User) {
|
||||||
|
return user.email === 'ntr@mnml.gg';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function offerDetails() {}
|
||||||
Generated
+85
-20
@@ -9,14 +9,15 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@reduxjs/toolkit": "^1.8.5",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
|
"logrocket": "^3.0.1",
|
||||||
"preact": "^10.10.0",
|
"preact": "^10.10.0",
|
||||||
"preact-render-to-string": "^5.2.1",
|
"preact-render-to-string": "^5.2.1",
|
||||||
"preact-router": "^3.2.1",
|
"preact-router": "^3.2.1",
|
||||||
"react-feather": "^2.0.10",
|
"react-feather": "^2.0.10",
|
||||||
"react-redux": "^8.0.2",
|
"react-redux": "^8.0.2",
|
||||||
"redux": "^4.2.0",
|
"redux": "^4.2.0"
|
||||||
"unistore": "^3.5.2"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/plugin-transform-react-jsx-source": "^7.18.6",
|
"@babel/plugin-transform-react-jsx-source": "^7.18.6",
|
||||||
@@ -2663,6 +2664,29 @@
|
|||||||
"webpack": "^4.0.0 || ^5.0.0"
|
"webpack": "^4.0.0 || ^5.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@reduxjs/toolkit": {
|
||||||
|
"version": "1.8.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.8.5.tgz",
|
||||||
|
"integrity": "sha512-f4D5EXO7A7Xq35T0zRbWq5kJQyXzzscnHKmjnu2+37B3rwHU6mX9PYlbfXdnxcY6P/7zfmjhgan0Z+yuOfeBmA==",
|
||||||
|
"dependencies": {
|
||||||
|
"immer": "^9.0.7",
|
||||||
|
"redux": "^4.1.2",
|
||||||
|
"redux-thunk": "^2.4.1",
|
||||||
|
"reselect": "^4.1.5"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.9.0 || ^17.0.0 || ^18",
|
||||||
|
"react-redux": "^7.2.1 || ^8.0.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-redux": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@rollup/plugin-babel": {
|
"node_modules/@rollup/plugin-babel": {
|
||||||
"version": "5.3.1",
|
"version": "5.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz",
|
||||||
@@ -10063,6 +10087,15 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/immer": {
|
||||||
|
"version": "9.0.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/immer/-/immer-9.0.15.tgz",
|
||||||
|
"integrity": "sha512-2eB/sswms9AEUSkOm4SbV5Y7Vmt/bKRwByd52jfLkW4OLYeaTP3EEiJ9agqU0O/tq6Dk62Zfj+TJSqfm1rLVGQ==",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/immer"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/import-fresh": {
|
"node_modules/import-fresh": {
|
||||||
"version": "3.3.0",
|
"version": "3.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
|
||||||
@@ -11926,6 +11959,11 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/logrocket": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/logrocket/-/logrocket-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-jOWG+jEzobKVxGytzZ+4KGm2kiMQMRTHab2uDWQyVZcHfEF38BlCH1yjQVY4LCmuQUwZitP9biMzJZnyUQ0dtQ=="
|
||||||
|
},
|
||||||
"node_modules/loose-envify": {
|
"node_modules/loose-envify": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||||
@@ -16224,6 +16262,14 @@
|
|||||||
"@babel/runtime": "^7.9.2"
|
"@babel/runtime": "^7.9.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/redux-thunk": {
|
||||||
|
"version": "2.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.1.tgz",
|
||||||
|
"integrity": "sha512-OOYGNY5Jy2TWvTL1KgAlVy6dcx3siPJ1wTq741EPyUKfn6W6nChdICjZwCd0p8AZBs5kWpZlbkXW2nE/zjUa+Q==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"redux": "^4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/regenerate": {
|
"node_modules/regenerate": {
|
||||||
"version": "1.4.2",
|
"version": "1.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz",
|
||||||
@@ -16523,6 +16569,11 @@
|
|||||||
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
|
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/reselect": {
|
||||||
|
"version": "4.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.6.tgz",
|
||||||
|
"integrity": "sha512-ZovIuXqto7elwnxyXbBtCPo9YFEr3uJqj2rRbcOOog1bmu2Ag85M4hixSwFWyaBMKXNgvPaJ9OSu9SkBPIeJHQ=="
|
||||||
|
},
|
||||||
"node_modules/resolve": {
|
"node_modules/resolve": {
|
||||||
"version": "2.0.0-next.4",
|
"version": "2.0.0-next.4",
|
||||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.4.tgz",
|
"resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.4.tgz",
|
||||||
@@ -19013,19 +19064,6 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/unistore": {
|
|
||||||
"version": "3.5.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/unistore/-/unistore-3.5.2.tgz",
|
|
||||||
"integrity": "sha512-2Aa4eX0Ua1umyiI3Eai6Li+wXYOHgaDBGOPB3Hvw7PAVuD30TAyh5kS4yNKb2fLDbQgizvPhKQRcYnOdfsm4VQ==",
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"preact": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"react": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/universalify": {
|
"node_modules/universalify": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
|
||||||
@@ -23102,6 +23140,17 @@
|
|||||||
"@prefresh/utils": "^1.1.2"
|
"@prefresh/utils": "^1.1.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@reduxjs/toolkit": {
|
||||||
|
"version": "1.8.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.8.5.tgz",
|
||||||
|
"integrity": "sha512-f4D5EXO7A7Xq35T0zRbWq5kJQyXzzscnHKmjnu2+37B3rwHU6mX9PYlbfXdnxcY6P/7zfmjhgan0Z+yuOfeBmA==",
|
||||||
|
"requires": {
|
||||||
|
"immer": "^9.0.7",
|
||||||
|
"redux": "^4.1.2",
|
||||||
|
"redux-thunk": "^2.4.1",
|
||||||
|
"reselect": "^4.1.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@rollup/plugin-babel": {
|
"@rollup/plugin-babel": {
|
||||||
"version": "5.3.1",
|
"version": "5.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz",
|
||||||
@@ -28975,6 +29024,11 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
"immer": {
|
||||||
|
"version": "9.0.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/immer/-/immer-9.0.15.tgz",
|
||||||
|
"integrity": "sha512-2eB/sswms9AEUSkOm4SbV5Y7Vmt/bKRwByd52jfLkW4OLYeaTP3EEiJ9agqU0O/tq6Dk62Zfj+TJSqfm1rLVGQ=="
|
||||||
|
},
|
||||||
"import-fresh": {
|
"import-fresh": {
|
||||||
"version": "3.3.0",
|
"version": "3.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
|
||||||
@@ -30386,6 +30440,11 @@
|
|||||||
"is-unicode-supported": "^0.1.0"
|
"is-unicode-supported": "^0.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"logrocket": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/logrocket/-/logrocket-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-jOWG+jEzobKVxGytzZ+4KGm2kiMQMRTHab2uDWQyVZcHfEF38BlCH1yjQVY4LCmuQUwZitP9biMzJZnyUQ0dtQ=="
|
||||||
|
},
|
||||||
"loose-envify": {
|
"loose-envify": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||||
@@ -33670,6 +33729,12 @@
|
|||||||
"@babel/runtime": "^7.9.2"
|
"@babel/runtime": "^7.9.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"redux-thunk": {
|
||||||
|
"version": "2.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.1.tgz",
|
||||||
|
"integrity": "sha512-OOYGNY5Jy2TWvTL1KgAlVy6dcx3siPJ1wTq741EPyUKfn6W6nChdICjZwCd0p8AZBs5kWpZlbkXW2nE/zjUa+Q==",
|
||||||
|
"requires": {}
|
||||||
|
},
|
||||||
"regenerate": {
|
"regenerate": {
|
||||||
"version": "1.4.2",
|
"version": "1.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz",
|
||||||
@@ -33902,6 +33967,11 @@
|
|||||||
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
|
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"reselect": {
|
||||||
|
"version": "4.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.6.tgz",
|
||||||
|
"integrity": "sha512-ZovIuXqto7elwnxyXbBtCPo9YFEr3uJqj2rRbcOOog1bmu2Ag85M4hixSwFWyaBMKXNgvPaJ9OSu9SkBPIeJHQ=="
|
||||||
|
},
|
||||||
"resolve": {
|
"resolve": {
|
||||||
"version": "2.0.0-next.4",
|
"version": "2.0.0-next.4",
|
||||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.4.tgz",
|
"resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.4.tgz",
|
||||||
@@ -35905,11 +35975,6 @@
|
|||||||
"crypto-random-string": "^2.0.0"
|
"crypto-random-string": "^2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"unistore": {
|
|
||||||
"version": "3.5.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/unistore/-/unistore-3.5.2.tgz",
|
|
||||||
"integrity": "sha512-2Aa4eX0Ua1umyiI3Eai6Li+wXYOHgaDBGOPB3Hvw7PAVuD30TAyh5kS4yNKb2fLDbQgizvPhKQRcYnOdfsm4VQ=="
|
|
||||||
},
|
|
||||||
"universalify": {
|
"universalify": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
|
||||||
|
|||||||
+4
-3
@@ -4,19 +4,20 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "preact build",
|
"build": "preact build --no-prerender",
|
||||||
"start": "preact --no-clear watch",
|
"start": "preact --no-clear watch",
|
||||||
"lint": "eslint src"
|
"lint": "eslint src"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@reduxjs/toolkit": "^1.8.5",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
|
"logrocket": "^3.0.1",
|
||||||
"preact": "^10.10.0",
|
"preact": "^10.10.0",
|
||||||
"preact-render-to-string": "^5.2.1",
|
"preact-render-to-string": "^5.2.1",
|
||||||
"preact-router": "^3.2.1",
|
"preact-router": "^3.2.1",
|
||||||
"react-feather": "^2.0.10",
|
"react-feather": "^2.0.10",
|
||||||
"react-redux": "^8.0.2",
|
"react-redux": "^8.0.2",
|
||||||
"redux": "^4.2.0",
|
"redux": "^4.2.0"
|
||||||
"unistore": "^3.5.2"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/plugin-transform-react-jsx-source": "^7.18.6",
|
"@babel/plugin-transform-react-jsx-source": "^7.18.6",
|
||||||
|
|||||||
+36
-27
@@ -1,53 +1,62 @@
|
|||||||
import { h } from 'preact';
|
import { h } from 'preact';
|
||||||
import { useState, useEffect } from 'preact/hooks';
|
import { useEffect } from 'preact/hooks';
|
||||||
import { Route, Router, route } from 'preact-router';
|
import { useDispatch, Provider } from 'react-redux'
|
||||||
import { Provider, connect } from 'unistore/preact'
|
import { Route, Router, } from 'preact-router';
|
||||||
|
import LogRocket from 'logrocket';
|
||||||
|
|
||||||
import createStore from './store';
|
|
||||||
import createWs from './ws';
|
import createWs from './ws';
|
||||||
import http from './http';
|
import store, { wsState } from './store'
|
||||||
|
|
||||||
import Header from './components/header';
|
import Nav from './components/nav';
|
||||||
|
|
||||||
import Home from './routes/home';
|
import Offers from './routes/offers';
|
||||||
import Bundles from './routes/bundles';
|
import Acp from './routes/acp';
|
||||||
import Admin from './routes/acp';
|
|
||||||
import Login from './routes/login';
|
import Login from './routes/login';
|
||||||
import Search from './routes/search';
|
import Search from './routes/search';
|
||||||
import List from './routes/list';
|
import List from './routes/list';
|
||||||
|
import Info from './routes/info';
|
||||||
|
|
||||||
const store = createStore();
|
if (window.location.host === 'matescrates.au') LogRocket.init('yh0dy3/mcrates');
|
||||||
const ws = createWs(store);
|
|
||||||
|
|
||||||
ws.connect();
|
const Main = () => {
|
||||||
|
const dispatch = useDispatch()
|
||||||
|
|
||||||
async function init() {
|
// i have no idea why this is needed,
|
||||||
const [error, user] = await http.getLogin();
|
// but it makes sure the user changes actually rerender
|
||||||
if (error) return route('/login');
|
useEffect(() => {
|
||||||
route('/bundles');
|
const ws = createWs(dispatch);
|
||||||
store.setState({ user });
|
ws.connect();
|
||||||
store.setState({ ws });
|
dispatch(wsState.actions.set(ws));
|
||||||
}
|
}, [dispatch])
|
||||||
|
|
||||||
init();
|
function routeChange(e) {
|
||||||
|
const { ws } = store.getState();
|
||||||
|
if (!ws) return null;
|
||||||
|
if (e.url === '/acp') return ws.sendAcpOffers();
|
||||||
|
}
|
||||||
|
|
||||||
const App = () => {
|
|
||||||
return (
|
return (
|
||||||
<Provider store={store}>
|
|
||||||
<main id="app">
|
<main id="app">
|
||||||
<Header />
|
<Nav />
|
||||||
<Router>
|
<Router onChange={routeChange}>
|
||||||
<Route path="/" component={Home} />
|
<Route path="/" component={Offers} />
|
||||||
<Route path="/login" component={Login} mode="login" />
|
<Route path="/login" component={Login} mode="login" />
|
||||||
<Route path="/register" component={Login} mode="register" />
|
<Route path="/register" component={Login} mode="register" />
|
||||||
<Route path="/search" component={Search} />
|
<Route path="/search" component={Search} />
|
||||||
<Route path="/list" component={List} />
|
<Route path="/list" component={List} />
|
||||||
|
<Route path="/info" component={Info} />
|
||||||
|
|
||||||
{/* ACP */}
|
{/* ACP */}
|
||||||
<Route path="/bundles" component={Bundles} />
|
<Route path="/acp" component={Acp} />
|
||||||
<Route path="/offers/acp" component={Admin} />
|
|
||||||
</Router>
|
</Router>
|
||||||
</main>
|
</main>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const App = () => {
|
||||||
|
return (
|
||||||
|
<Provider store={store}>
|
||||||
|
<Main />
|
||||||
</Provider>
|
</Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
import { h } from 'preact';
|
|
||||||
import { Square, CheckSquare } from 'react-feather';
|
|
||||||
import { useState, useReducer } from 'preact/hooks';
|
|
||||||
|
|
||||||
import http from '../http';
|
|
||||||
|
|
||||||
function Item(item, state, dispatch) {
|
|
||||||
const icon = state.checkedIds.includes(item.id)
|
|
||||||
? <CheckSquare onClick={() => dispatch({ id: item.id})} />
|
|
||||||
: <Square onClick={() => dispatch({ id: item.id})} />;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={item.id}>
|
|
||||||
<a href={item.uri} target="_blank" >
|
|
||||||
<h4>{item.description}</h4>
|
|
||||||
</a>
|
|
||||||
<h5 class='alt-text'>{item.price.toFixed(2)} {item.currency}</h5>
|
|
||||||
{icon}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const reducer = (state, action) => {
|
|
||||||
if (state.checkedIds.includes(action.id)) {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
checkedIds: state.checkedIds.filter(id => id !== action.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
checkedIds: [
|
|
||||||
...state.checkedIds,
|
|
||||||
action.id
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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>
|
|
||||||
<h2>{bundle.username} - {bundle.seller}</h2>
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
import { h } from 'preact';
|
|
||||||
import { route } from 'preact-router';
|
|
||||||
import { connect } from 'unistore/preact'
|
|
||||||
|
|
||||||
import http from './../http';
|
|
||||||
import actions from './../actions';
|
|
||||||
|
|
||||||
const Header = ({ user, logout }) => {
|
|
||||||
|
|
||||||
function logoutClick(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
logout();
|
|
||||||
}
|
|
||||||
|
|
||||||
const nav = () => (
|
|
||||||
<nav>
|
|
||||||
<a href="/">Offers</a>
|
|
||||||
<a href='/list'>List</a>
|
|
||||||
<a href="/search">Search</a>
|
|
||||||
</nav>
|
|
||||||
);
|
|
||||||
|
|
||||||
const anonNav = () => (
|
|
||||||
<nav>
|
|
||||||
<a href="/login">Login</a>
|
|
||||||
<a href="/register">Register</a>
|
|
||||||
</nav>
|
|
||||||
);
|
|
||||||
|
|
||||||
const adminNav = () => (
|
|
||||||
<nav>
|
|
||||||
<a href="/bundles">Bundles</a>
|
|
||||||
<a href='/offers/acp'>Offers ACP</a>
|
|
||||||
</nav>
|
|
||||||
)
|
|
||||||
|
|
||||||
const userNav = () => (
|
|
||||||
<nav>
|
|
||||||
<a href="/account">Account</a>
|
|
||||||
<a href="/auth/logout" onClick={logoutClick}>Logout</a>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<header>
|
|
||||||
{user ? nav() : anonNav()}
|
|
||||||
{user ? adminNav() : null}
|
|
||||||
{user ? userNav() : null}
|
|
||||||
</header>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// @ts-ignore unistore types
|
|
||||||
export default connect(['user'], actions)(Header);
|
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import { h } from 'preact';
|
||||||
|
import { route } from 'preact-router';
|
||||||
|
import { useEffect } from 'preact/hooks';
|
||||||
|
import { useSelector } from 'react-redux'
|
||||||
|
import LogRocket from 'logrocket';
|
||||||
|
|
||||||
|
import http from './../http';
|
||||||
|
import { State } from './../store';
|
||||||
|
|
||||||
|
function showAdmin(user) {
|
||||||
|
return user && user.email === 'ntr@mnml.gg';
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_ROUTE = '/';
|
||||||
|
|
||||||
|
const Nav = () => {
|
||||||
|
const user = useSelector((state: State) => state.user);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!user) {
|
||||||
|
route('/login');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
route(DEFAULT_ROUTE)
|
||||||
|
|
||||||
|
if (window.location.host === 'matescrates.au') {
|
||||||
|
LogRocket.identify(user.id, {
|
||||||
|
name: user.email,
|
||||||
|
email: user.email,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}, [user])
|
||||||
|
|
||||||
|
async function logoutClick(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
await http.postLogout();
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
const nav = () => (
|
||||||
|
<nav>
|
||||||
|
<a href="/">Offers</a>
|
||||||
|
<a href='/list'>List</a>
|
||||||
|
<a href="/search">Search</a>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
|
||||||
|
const anonNav = () => (
|
||||||
|
<nav>
|
||||||
|
<a href="/login">Login</a>
|
||||||
|
<a href="/register">Register</a>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
|
||||||
|
const adminNav = () => (
|
||||||
|
<nav>
|
||||||
|
<a href='/acp'>Offers ACP</a>
|
||||||
|
</nav>
|
||||||
|
)
|
||||||
|
|
||||||
|
const userNav = () => (
|
||||||
|
<nav>
|
||||||
|
<a href="/info">Info</a>
|
||||||
|
<a href="/auth/logout" onClick={logoutClick}>Logout</a>
|
||||||
|
</nav>
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header>
|
||||||
|
{user ? nav() : anonNav()}
|
||||||
|
{showAdmin(user) ? adminNav() : <div></div>}
|
||||||
|
{user ? userNav() : null}
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Nav;
|
||||||
@@ -2,35 +2,42 @@ 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
|
||||||
|
? <button onClick={() => setStatus('Pending')} class='neg'>Cancel</button>
|
||||||
|
: <button onClick={() => setStatus('Declined')} class='neg'>Decline</button>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="offer">
|
<div class="offer">
|
||||||
<section>
|
<section>
|
||||||
<h2>{details.artists_sort} - {details.title} <a href={details.uri} target="_blank"><ExternalLink /></a></h2>
|
<h3>{details.artists_sort} - {details.title} <a href={details.uri} target="_blank"><ExternalLink /></a></h3>
|
||||||
<h5 class='alt-text' >{details.year}</h5>
|
<div class='alt-text' >{details.year}</div>
|
||||||
<h5 class='alt-text'>{details.formats[0].descriptions.join(' ')}</h5>
|
<div class='alt-text'>{details.formats[0].descriptions.join(' ')}</div>
|
||||||
<h5 class='number'>mcrates price: ${offer.total}</h5>
|
<div class='number'>mcrates price: ${offer.total}</div>
|
||||||
<h5 class='number'>shipped by itself: ${offer.comparison}</h5>
|
<div class='number'>shipped by itself: ${offer.comparison}</div>
|
||||||
</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>
|
||||||
|
{negBtn}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
function OfferLine(offer) {
|
||||||
|
const ws = useSelector((state: State) => state.ws);
|
||||||
|
|
||||||
|
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
|
||||||
|
? <button onClick={() => setStatus('Pending')} class='neg'>Cancel</button>
|
||||||
|
: <button onClick={() => setStatus('Declined')} class='neg'>Decline</button>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="line">
|
||||||
|
<span class='number'>${offer.total}</span>
|
||||||
|
<button onClick={() => setStatus('Accepted')} disabled={accepted} class={classes} >{btnText}</button>
|
||||||
|
{negBtn}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function OfferPriceBtn(offer) {
|
||||||
|
const ws = useSelector((state: State) => state.ws);
|
||||||
|
const accepted = offer.status === 'Accepted';
|
||||||
|
const confirmed = offer.status === 'Confirmed';
|
||||||
|
|
||||||
|
function setStatus() {
|
||||||
|
if (accepted) return ws.sendOfferStatusSet(offer.id, 'Pending')
|
||||||
|
ws.sendOfferStatusSet(offer.id, 'Accepted');
|
||||||
|
}
|
||||||
|
|
||||||
|
const classes = `${accepted ? 'accepted' : ''} ${confirmed ? 'confirmed' : ''}`;
|
||||||
|
const btnText = accepted
|
||||||
|
? offer.status
|
||||||
|
: 'Accept';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button disabled={confirmed} onClick={() => setStatus()} class={classes}>${offer.total} {offer.condition}</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default function WantOffers(wantOffers) {
|
||||||
|
const ws = useSelector((state: State) => state.ws);
|
||||||
|
|
||||||
|
const details = wantOffers[0].want.data;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="offer">
|
||||||
|
<section>
|
||||||
|
<h3>{details.artists_sort} - {details.title} <a href={details.uri} target="_blank"><ExternalLink /></a></h3>
|
||||||
|
<div class='alt-text' >{details.year}</div>
|
||||||
|
<div class='alt-text'>{details.formats.map(f => f.descriptions.join(' ')).join(' ')}</div>
|
||||||
|
</section>
|
||||||
|
<img src={details.thumb} />
|
||||||
|
<div class="lines">
|
||||||
|
{wantOffers.map(o => OfferPriceBtn(o))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,17 +1,39 @@
|
|||||||
import { h } from 'preact';
|
import { h } from 'preact';
|
||||||
|
import { useState, useCallback } from 'preact/hooks';
|
||||||
|
import { ExternalLink } from 'react-feather';
|
||||||
|
|
||||||
|
import { useSelector } from 'react-redux'
|
||||||
|
import { State } from './../store'
|
||||||
|
|
||||||
|
import http from './../http';
|
||||||
|
|
||||||
function Want(want) {
|
function Want(want) {
|
||||||
console.log(want);
|
|
||||||
const r = want.data;
|
const r = want.data;
|
||||||
|
|
||||||
|
const ws = useSelector((state: State) => state.ws);
|
||||||
|
const [confirm, setConfirm] = useState(false);
|
||||||
|
|
||||||
|
function onClick() {
|
||||||
|
if (confirm) return ws.sendWantRm(want.id);
|
||||||
|
setTimeout(() => setConfirm(false), 2500);
|
||||||
|
return setConfirm(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const text = confirm
|
||||||
|
? 'confirm'
|
||||||
|
: 'remove';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<article key={r.id}>
|
<article key={r.id}>
|
||||||
<div>
|
<div>
|
||||||
<h3>{r.artists_sort} - {r.title}</h3>
|
<h3>{r.artists_sort} - {r.title} <a href={r.uri} target="_blank"><ExternalLink /></a></h3>
|
||||||
<h5 class='alt-text' >{r.year}</h5>
|
|
||||||
<h5 class='alt-text'>{r.formats[0].descriptions.join(' ')}</h5>
|
|
||||||
</div>
|
</div>
|
||||||
<img src={r.thumb} />
|
<img src={r.thumb} />
|
||||||
|
<div class='details'>
|
||||||
|
<div class='alt'>{r.year}</div>
|
||||||
|
<div class='alt'>{r.formats[0].descriptions.join(' ')}</div>
|
||||||
|
</div>
|
||||||
|
<button onClick={onClick} >{text}</button>
|
||||||
</article>
|
</article>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
+32
-21
@@ -1,30 +1,46 @@
|
|||||||
import { h } from 'preact';
|
import { h } from 'preact';
|
||||||
import { useState, useEffect } from 'preact/hooks';
|
import { useState, useEffect } from 'preact/hooks';
|
||||||
import { connect } from 'unistore/preact'
|
|
||||||
|
|
||||||
import http from './../http';
|
import http from './../http';
|
||||||
import AcpOffer from './../components/acp.offer';
|
|
||||||
|
|
||||||
import { Status } from './../../../api/src/offers';
|
import { useSelector, useDispatch } from 'react-redux'
|
||||||
|
import { State } from './../store';
|
||||||
|
|
||||||
function ItemEl({ item, ws }) {
|
enum Status {
|
||||||
|
Accepted = 'Accepted',
|
||||||
|
Pending = 'Pending',
|
||||||
|
Declined = 'Declined',
|
||||||
|
Confirmed = 'Confirmed',
|
||||||
|
Complete = 'Complete',
|
||||||
|
}
|
||||||
|
|
||||||
|
const POSITIVE_STATUS = [Status.Accepted, Status.Confirmed, Status.Complete];
|
||||||
|
|
||||||
|
function Item({ item }) {
|
||||||
|
const ws = useSelector((state: State) => state.ws);
|
||||||
console.log(item);
|
console.log(item);
|
||||||
console.log(ws);
|
|
||||||
const statuses = Object.entries(Status).map(([label, value]) => (
|
const statuses = Object.entries(Status).map(([label, value]) => (
|
||||||
<option value={value}>{label}</option>
|
<option selected={item.status === value} value={value}>{value}</option>
|
||||||
));
|
));
|
||||||
|
|
||||||
function onChange(e) {
|
function onChange(e) {
|
||||||
console.log(item.id, e.target.value);
|
ws.sendAcpOfferStatusSet(item.id, e.target.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const bold = POSITIVE_STATUS.includes(item.status)
|
||||||
|
? 'bold'
|
||||||
|
: '';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={item.id}>
|
<div key={item.id} class={bold}>
|
||||||
<a href={item.listing.data.uri} target="_blank" >
|
<a href={item.listing.data.uri} target="_blank" >
|
||||||
<h4>{item.listing.data.release.description}</h4>
|
<h4>{item.listing.data.release.description}</h4>
|
||||||
</a>
|
</a>
|
||||||
|
<h5>{item.user.email}</h5>
|
||||||
<h5 class='alt-text'>{item.total}</h5>
|
<h5 class='alt-text'>{item.total}</h5>
|
||||||
<h5>{item.status}</h5>
|
<h5>{item.status}</h5>
|
||||||
|
<label>Set Status:</label>
|
||||||
<select onChange={onChange}>
|
<select onChange={onChange}>
|
||||||
{statuses}
|
{statuses}
|
||||||
</select>
|
</select>
|
||||||
@@ -32,14 +48,14 @@ function ItemEl({ item, ws }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const Item = connect(['ws'], {}, ItemEl);
|
|
||||||
|
|
||||||
function SellerGroup(group) {
|
function SellerGroup(group) {
|
||||||
|
const ready = group.offers.filter(item => POSITIVE_STATUS.includes(item.status)).length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="bundle">
|
<div class="bundle">
|
||||||
<section>
|
<section>
|
||||||
<h2>{group.username} - {group.seller}</h2>
|
<h2>{group.username} - {group.seller}</h2>
|
||||||
<h3>{group.offers.length} offers</h3>
|
<h3>{ready} / {group.offers.length} offers</h3>
|
||||||
<h4>Ships: {group.mentionsAustralia && 'AU'} {group.mentionsWorldwide && 'WORLD'}</h4>
|
<h4>Ships: {group.mentionsAustralia && 'AU'} {group.mentionsWorldwide && 'WORLD'}</h4>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -51,16 +67,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 (
|
||||||
@@ -70,7 +82,6 @@ function Home() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class='bundles'>
|
<div class='bundles'>
|
||||||
{offers.map(SellerGroup)}
|
{offers.map(SellerGroup)}
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
import { h } from 'preact';
|
|
||||||
import { useState, useEffect } from 'preact/hooks';
|
|
||||||
|
|
||||||
import http from './../http';
|
|
||||||
import Bundle from './../components/bundle';
|
|
||||||
|
|
||||||
function Bundles() {
|
|
||||||
const [bundles, setBundles] = useState([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
async function getBundles() {
|
|
||||||
const [error, bundles] = await http.getBundles();
|
|
||||||
if (error) return console.error(error);
|
|
||||||
setBundles(bundles);
|
|
||||||
}
|
|
||||||
getBundles();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (!bundles.length) {
|
|
||||||
return (
|
|
||||||
<div class={'bundles'}>
|
|
||||||
loading...
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div class={'bundles'}>
|
|
||||||
{bundles.map(Bundle)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Bundles;
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import { h } from 'preact';
|
|
||||||
import { useState, useEffect } from 'preact/hooks';
|
|
||||||
import { route } from 'preact-router';
|
|
||||||
import { connect } from 'unistore/preact'
|
|
||||||
|
|
||||||
import http from './../http';
|
|
||||||
import actions from './../actions';
|
|
||||||
|
|
||||||
import Offer from './../components/offer';
|
|
||||||
|
|
||||||
function Home({ offers, setOffers }) {
|
|
||||||
useEffect(() => {
|
|
||||||
async function getOffers() {
|
|
||||||
const [error, offers] = await http.getOffers();
|
|
||||||
if (error) return console.error(error);
|
|
||||||
setOffers(offers);
|
|
||||||
if (offers.length === 0) route('/search');
|
|
||||||
}
|
|
||||||
getOffers();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (!offers.length) {
|
|
||||||
return (
|
|
||||||
<div class={'offers'}>
|
|
||||||
loading...
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div class={'offers'}>
|
|
||||||
{offers.map(Offer)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(['offers'], actions)(Home);
|
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { h } from 'preact';
|
||||||
|
|
||||||
|
const Info = () => {
|
||||||
|
return (
|
||||||
|
<div class='info'>
|
||||||
|
<div class='words'>
|
||||||
|
<section>
|
||||||
|
<h1>Info</h1>
|
||||||
|
<p> Thanks for signing up! </p>
|
||||||
|
<p> Mates Crates is a record importing service in alpha. </p>
|
||||||
|
<p> Its purpose is to reduce the cost of shipping vinyl records to Melbourne, Australia. </p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>How It Works</h2>
|
||||||
|
<p> First, add some records to your <span class='alt'>List</span> on the <a href='/search'>Search Page</a>. </p>
|
||||||
|
<p> Mates Crates will then make <span class='alt'>Offers</span> to you for releases you add to your <a href='/list'>List</a>. </p>
|
||||||
|
<p> Select all <span class='alt'>Offers</span> you are interested in, the more you accept the more likely the item will be <span class='alt-text'>Confirmed</span>.</p>
|
||||||
|
<p> Check back regularly for updated <span class='alt'>Offers</span>, the global record marketplace is a living thing. </p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Offers</h2>
|
||||||
|
<p> Not all items on your <span class='alt'>List</span> will have offers. However, as new users join the site the number of offers we can make increases, so be sure to tell your friends!</p>
|
||||||
|
<p> Think of offers like an expression of interest, they are not confirmed until they are set to <span class='alt-text'>Confirmed</span> status. </p>
|
||||||
|
<p> After that they will be shipped to Melbourne and ready for pick up. </p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Pick Up</h2>
|
||||||
|
<p> Use the contact options to get in touch. </p>
|
||||||
|
<p> Payment is cash on delivery. </p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>FAQ</h2>
|
||||||
|
<h3> Delivery Times </h3>
|
||||||
|
<p> Records are usually available for pickup within a week. We order in bulk, which often means express, insured shipping to Melbourne. </p>
|
||||||
|
<h3> Friends </h3>
|
||||||
|
<p> Please invite other collectors to the site. More users means offers can become cheaper and shipped sooner. </p>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
<section class='contact'>
|
||||||
|
<h2>Contact</h2>
|
||||||
|
<p><a href='mailto:ntr@mnml.gg?subject=Mates%20Crates%20Enquiry'>Email</a></p>
|
||||||
|
<p><a href='https://discord.gg/QfjY8WNQyx'>Discord</a></p>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Info;
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { h } from 'preact';
|
import { h } from 'preact';
|
||||||
import { useState, useEffect } from 'preact/hooks';
|
|
||||||
|
|
||||||
// import style from './style.less';
|
import { useSelector } from 'react-redux'
|
||||||
import http from './../http';
|
import { State } from './../store'
|
||||||
|
|
||||||
import Want from './../components/want';
|
import Want from './../components/want';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -12,17 +12,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const List = ({ user }: Props) => {
|
const List = ({ user }: Props) => {
|
||||||
const [wants, setOffers] = useState([]);
|
const wants = useSelector((state: State) => state.wants);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
async function getWants() {
|
|
||||||
const [error, wants] = await http.getWants();
|
|
||||||
if (error) return console.error(error);
|
|
||||||
setOffers(wants);
|
|
||||||
}
|
|
||||||
getWants();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
|
|
||||||
if (!wants.length) {
|
if (!wants.length) {
|
||||||
return <div> loading... </div>;
|
return <div> loading... </div>;
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
import { h } from 'preact';
|
import { h } from 'preact';
|
||||||
import { route } from 'preact-router';
|
import { route } from 'preact-router';
|
||||||
import { useState, useEffect, useRef } from 'preact/hooks';
|
import { useState, useEffect, useRef } from 'preact/hooks';
|
||||||
import { connect } from 'unistore/preact'
|
import { useSelector, useDispatch } from 'react-redux'
|
||||||
|
|
||||||
import http from './../http';
|
import http from './../http';
|
||||||
import actions from './../actions';
|
import { State, userState } from './../store';
|
||||||
|
|
||||||
|
const Login = ({ mode }) => {
|
||||||
|
const dispatch = useDispatch()
|
||||||
|
const user = useSelector((state: State) => state.user);
|
||||||
|
const ws = useSelector((state: State) => state.ws);
|
||||||
|
|
||||||
const Login = ({ setUser, mode }) => {
|
|
||||||
const [email, setEmail] = useState('')
|
const [email, setEmail] = useState('')
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const [confirmPassword, setConfirmPassword] = useState('');
|
const [confirmPassword, setConfirmPassword] = useState('');
|
||||||
@@ -16,14 +20,16 @@ const Login = ({ setUser, mode }) => {
|
|||||||
const [error, user] = await http.postLogin(email, password);
|
const [error, user] = await http.postLogin(email, password);
|
||||||
if (error) return setError(error);
|
if (error) return setError(error);
|
||||||
route('/');
|
route('/');
|
||||||
setUser(user);
|
dispatch(userState.actions.set(user));
|
||||||
|
// reconnect ws to get logged in methods
|
||||||
|
ws.connect();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function postRegister() {
|
async function postRegister() {
|
||||||
const [error, user] = await http.postRegister(email, password);
|
const [error, user] = await http.postRegister(email, password);
|
||||||
if (error) return setError(error);
|
if (error) return setError(error);
|
||||||
route('/search');
|
route('/search');
|
||||||
setUser(user);
|
dispatch(userState.actions.set(user));
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSubmit(e) {
|
function handleSubmit(e) {
|
||||||
@@ -86,14 +92,12 @@ const Login = ({ setUser, mode }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const LoginForm = connect([], actions)(Login);
|
|
||||||
|
|
||||||
function LoginPage({ mode }) {
|
function LoginPage({ mode }) {
|
||||||
return (
|
return (
|
||||||
<div class="landing">
|
<div class="landing">
|
||||||
<LoginForm mode={mode} />
|
<Login mode={mode} />
|
||||||
<div></div>
|
<div></div>
|
||||||
<div class="info">
|
<div>
|
||||||
<p>Mates Crates is a record importing service.</p>
|
<p>Mates Crates is a record importing service.</p>
|
||||||
<p>We bundle orders together from thousands of online stores to reduce the cost of shipping vinyl.</p>
|
<p>We bundle orders together from thousands of online stores to reduce the cost of shipping vinyl.</p>
|
||||||
<p>Based in Preston.</p>
|
<p>Based in Preston.</p>
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { h } from 'preact';
|
||||||
|
import { useState, useEffect } from 'preact/hooks';
|
||||||
|
import { route } from 'preact-router';
|
||||||
|
import { useSelector, useDispatch } from 'react-redux'
|
||||||
|
|
||||||
|
import http from './../http';
|
||||||
|
import { State, offersState } from './../store';
|
||||||
|
|
||||||
|
import WantOffers from './../components/want.offers';
|
||||||
|
|
||||||
|
function Home() {
|
||||||
|
const dispatch = useDispatch()
|
||||||
|
const offers = useSelector((state: State) => state.offers);
|
||||||
|
|
||||||
|
// if (!offers.length) {
|
||||||
|
// return (
|
||||||
|
// <div class={'offers'}>
|
||||||
|
// loading...
|
||||||
|
// </div>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class={'offers'}>
|
||||||
|
{Object.values(offers).map(WantOffers)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Home;
|
||||||
+60
-9
@@ -1,10 +1,61 @@
|
|||||||
import unistore from 'unistore'
|
import { createStore, combineReducers } from 'redux'
|
||||||
|
import { configureStore, createSlice } from '@reduxjs/toolkit'
|
||||||
|
|
||||||
export default function createStore() {
|
export const userState = createSlice({
|
||||||
return unistore({
|
name: 'user',
|
||||||
bundles: [],
|
initialState: null,
|
||||||
offers: [],
|
reducers: {
|
||||||
user: null,
|
set: (state, action) => action.payload
|
||||||
ws: null,
|
},
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
export const offersState = createSlice({
|
||||||
|
name: 'offers',
|
||||||
|
initialState: [],
|
||||||
|
reducers: {
|
||||||
|
set: (state, action) => action.payload
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const acpOffersState = createSlice({
|
||||||
|
name: 'acpOffers',
|
||||||
|
initialState: [],
|
||||||
|
reducers: {
|
||||||
|
set: (state, action) => action.payload
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const wantsState = createSlice({
|
||||||
|
name: 'wants',
|
||||||
|
initialState: [],
|
||||||
|
reducers: {
|
||||||
|
set: (state, action) => action.payload
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const wsState = createSlice({
|
||||||
|
name: 'ws',
|
||||||
|
initialState: null,
|
||||||
|
reducers: {
|
||||||
|
set: (state, action) => action.payload
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const reducer = combineReducers({
|
||||||
|
offers: offersState.reducer,
|
||||||
|
wants: wantsState.reducer,
|
||||||
|
ws: wsState.reducer,
|
||||||
|
user: userState.reducer,
|
||||||
|
|
||||||
|
acpOffers: acpOffersState.reducer,
|
||||||
|
});
|
||||||
|
|
||||||
|
const store = createStore(
|
||||||
|
reducer,
|
||||||
|
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
|
||||||
|
);
|
||||||
|
|
||||||
|
export default store;
|
||||||
|
|
||||||
|
export type State = ReturnType<typeof store.getState>
|
||||||
|
export type Dispatch = typeof store.dispatch
|
||||||
+170
-77
@@ -8,7 +8,7 @@ html {
|
|||||||
box-sizing: inherit;
|
box-sizing: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
body, h1, h2, h3, h4, h5, h6, p, ol, ul {
|
body, h4, h5, h6, p, ol, ul {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
@@ -56,7 +56,7 @@ html {
|
|||||||
font-family:var(--base-font-family);
|
font-family:var(--base-font-family);
|
||||||
line-height:var(--base-line-height);
|
line-height:var(--base-line-height);
|
||||||
-webkit-text-size-adjust:100%;
|
-webkit-text-size-adjust:100%;
|
||||||
background:var(--base-background-color);
|
background:var(--background-0);
|
||||||
box-sizing:border-box
|
box-sizing:border-box
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,23 +65,6 @@ html {
|
|||||||
--breakpoint-medium: 35em;
|
--breakpoint-medium: 35em;
|
||||||
--breakpoint-large: 56em;
|
--breakpoint-large: 56em;
|
||||||
|
|
||||||
--color-dark-blue: #101014;
|
|
||||||
--color-mid-blue: #16171d;
|
|
||||||
--color-light-blue: #1f2128;
|
|
||||||
--color-mid-grey: #9BA0B3;
|
|
||||||
--color-off-white: #F4F1D0;
|
|
||||||
--color-accent-blue: #5E9ED6;
|
|
||||||
|
|
||||||
--color-blue-gray: #506485;
|
|
||||||
--color-green: #537a34;
|
|
||||||
|
|
||||||
--base-accent-color: var(--color-accent-blue);
|
|
||||||
--fg-color: var(--color-off-white);
|
|
||||||
--hover-link-color: var(--color-off-white);
|
|
||||||
--base-background-color: var(--color-dark-blue);
|
|
||||||
--secondary-background-color: var(--color-mid-blue);
|
|
||||||
--tertiary-background-color: var(--color-light-blue);
|
|
||||||
|
|
||||||
--small-font-size: 0.75em;
|
--small-font-size: 0.75em;
|
||||||
--base-font-size: 1em;
|
--base-font-size: 1em;
|
||||||
--smedium-font-size: 1.333em;
|
--smedium-font-size: 1.333em;
|
||||||
@@ -96,22 +79,45 @@ html {
|
|||||||
--base-line-height: 1.4;
|
--base-line-height: 1.4;
|
||||||
--header-line-height: 1.2;
|
--header-line-height: 1.2;
|
||||||
|
|
||||||
--min-font-size: var(--base-font-size);
|
|
||||||
--max-font-size: var(--large-font-size);
|
|
||||||
|
|
||||||
--base-border-color: var(--secondary-font-color);
|
--base-border-color: var(--secondary-font-color);
|
||||||
--base-border: 1px solid var(--base-border-color);
|
--base-border: 2px solid var(--base-border-color);
|
||||||
--hover-border: 1px solid var(--base-font-color);
|
--hover-border: 2px solid var(--base-font-color);
|
||||||
--focus-border: 1px solid var(--base-font-color);
|
--focus-border: 2px solid var(--base-font-color);
|
||||||
--base-border-radius: .2rem;
|
--base-border-radius: .2rem;
|
||||||
--base-focus: 2px solid var(--base-accent-color);
|
--base-focus: 2px solid var(--base-accent-color);
|
||||||
--base-outline-offset: 0.25em;
|
--base-outline-offset: 0.25em;
|
||||||
|
|
||||||
--base-spacing: 1rem;
|
--min-font-size: var(--base-font-size);
|
||||||
--base-spacing-half: calc(var(--base-spacing)/2);
|
--max-font-size: var(--large-font-size);
|
||||||
--base-spacing-double: calc(var(--base-spacing)*2);
|
|
||||||
|
|
||||||
--utility-bar-header-height: calc(var(--base-spacing) * 1.5);
|
--color-dark-blue: #101014;
|
||||||
|
--color-mid-blue: #16171d;
|
||||||
|
--color-light-blue: #1f2128;
|
||||||
|
--color-mid-grey: #9BA0B3;
|
||||||
|
--color-off-white: #F4F1D0;
|
||||||
|
--color-accent-blue: #5E9ED6;
|
||||||
|
|
||||||
|
--color-blue-gray: #506485;
|
||||||
|
|
||||||
|
--color-blue: #475977;
|
||||||
|
--color-green: #476d4a;
|
||||||
|
|
||||||
|
--color-accepted: var(--color-blue);
|
||||||
|
--color-confirmed: var(--color-green);
|
||||||
|
|
||||||
|
--base-accent-color: var(--color-accent-blue);
|
||||||
|
--fg-color: var(--color-off-white);
|
||||||
|
--hover-link-color: var(--color-off-white);
|
||||||
|
|
||||||
|
--background-0: var(--color-dark-blue);
|
||||||
|
--background-1: var(--color-mid-blue);
|
||||||
|
--background-2: var(--color-light-blue);
|
||||||
|
|
||||||
|
--spacing: 1rem;
|
||||||
|
--spacing-half: calc(var(--spacing)/2);
|
||||||
|
--spacing-double: calc(var(--spacing)*2);
|
||||||
|
|
||||||
|
--utility-bar-header-height: calc(var(--spacing) * 1.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
button, input, optgroup, select, textarea {
|
button, input, optgroup, select, textarea {
|
||||||
@@ -136,11 +142,11 @@ input:focus, textarea:focus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
button, input {
|
button, input {
|
||||||
overflow:visible
|
overflow:visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
button, select {
|
button, select {
|
||||||
text-transform:none
|
text-transform: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
button, [type=button], [type=reset], [type=submit] {
|
button, [type=button], [type=reset], [type=submit] {
|
||||||
@@ -150,18 +156,48 @@ button, [type=button], [type=reset], [type=submit] {
|
|||||||
|
|
||||||
font-family: var(--secondary-font-family);
|
font-family: var(--secondary-font-family);
|
||||||
font-size: var(--small-font-size);
|
font-size: var(--small-font-size);
|
||||||
|
font-weight: bold;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: var(--tertiary-background-color);
|
background: var(--background-2);
|
||||||
border-color: transparent;
|
border-color: transparent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
margin-top:0;
|
||||||
|
line-height:var(--header-line-height);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin-bottom:var(--spacing);
|
||||||
|
font-size:var(--large-font-size)
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin-bottom:var(--spacing);
|
||||||
|
font-size:var(--medium-font-size);
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin-bottom:calc(var(--spacing)/2);
|
||||||
|
font-size:var(--smedium-font-size)
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
margin-bottom:calc(var(--spacing)/3);
|
||||||
|
font-size:var(--base-font-size)
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin:0 0 var(--spacing)
|
||||||
|
}
|
||||||
|
|
||||||
// ok actual styles
|
// ok actual styles
|
||||||
|
|
||||||
body {
|
body {
|
||||||
padding-top: calc(var(--base-spacing)/2);
|
padding-top: calc(var(--spacing)/2);
|
||||||
}
|
}
|
||||||
|
|
||||||
main {
|
main {
|
||||||
@@ -169,8 +205,8 @@ main {
|
|||||||
}
|
}
|
||||||
|
|
||||||
button, [type=submit] {
|
button, [type=submit] {
|
||||||
padding: var(--base-spacing-half);
|
padding: var(--spacing-half);
|
||||||
background: var(--secondary-background-color);
|
background: var(--background-1);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,24 +220,35 @@ button, [type=submit] {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.info {
|
|
||||||
p {
|
|
||||||
margin-bottom: var(--base-spacing);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.alt-text {
|
.info {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
|
||||||
|
section {
|
||||||
|
margin-bottom: var(--spacing);
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.alt-text, .alt {
|
||||||
font-family: var(--secondary-font-family);
|
font-family: var(--secondary-font-family);
|
||||||
// font-size: var(--small-font-size);
|
// font-size: var(--small-font-size);
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
article {
|
article {
|
||||||
background-color: var(--secondary-background-color);
|
background-color: var(--background-1);
|
||||||
}
|
|
||||||
|
|
||||||
|
button {
|
||||||
|
border-color: var(--background-2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.search {
|
.search {
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -209,15 +256,15 @@ article {
|
|||||||
|
|
||||||
article {
|
article {
|
||||||
display: grid;
|
display: grid;
|
||||||
padding: var(--base-spacing);
|
padding: var(--spacing);
|
||||||
margin-bottom: var(--base-spacing);
|
margin-bottom: var(--spacing);
|
||||||
grid-template-columns: 1fr 4fr 1fr;
|
grid-template-columns: 1fr 4fr 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
margin-right: var(--base-spacing);
|
margin-right: var(--spacing);
|
||||||
background: var(--tertiary-background-color);
|
background: var(--background-2);
|
||||||
border-color: var(--secondary-background-color);
|
border-color: var(--background-1);
|
||||||
|
|
||||||
&.added {
|
&.added {
|
||||||
background: var(--color-green);
|
background: var(--color-green);
|
||||||
@@ -227,18 +274,39 @@ article {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.list {
|
.list {
|
||||||
display: flex;
|
display: grid;
|
||||||
flex-flow: row wrap;
|
grid-template-columns:
|
||||||
|
1fr 1fr 1fr;
|
||||||
|
|
||||||
|
grid-row-gap: var(--spacing);
|
||||||
|
grid-column-gap: var(--spacing);
|
||||||
|
|
||||||
article {
|
article {
|
||||||
flex: 1 1 20%;
|
display: grid;
|
||||||
padding: var(--base-spacing);
|
|
||||||
margin: var(--base-spacing-half);
|
// grid-template-rows:
|
||||||
|
// 1fr min-content min-content;
|
||||||
|
|
||||||
|
grid-template-areas:
|
||||||
|
"title img"
|
||||||
|
"details details"
|
||||||
|
"remove remove";
|
||||||
|
|
||||||
|
button {
|
||||||
|
margin-top: var(--spacing-half);
|
||||||
|
grid-area: remove
|
||||||
|
}
|
||||||
|
|
||||||
|
.details {
|
||||||
|
grid-area: details;
|
||||||
|
}
|
||||||
|
|
||||||
|
padding: var(--spacing);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
form {
|
form {
|
||||||
margin-bottom: var(--base-spacing);
|
margin-bottom: var(--spacing);
|
||||||
|
|
||||||
label, input {
|
label, input {
|
||||||
display: block;
|
display: block;
|
||||||
@@ -249,26 +317,22 @@ form {
|
|||||||
}
|
}
|
||||||
|
|
||||||
input {
|
input {
|
||||||
padding: var(--base-spacing-half);
|
padding: var(--spacing-half);
|
||||||
margin-bottom: var(--base-spacing);
|
margin-bottom: var(--spacing);
|
||||||
}
|
}
|
||||||
|
|
||||||
label {
|
label {
|
||||||
font-family: var(--secondary-font-family);
|
font-family: var(--secondary-font-family);
|
||||||
font-size: var(--small-font-size);
|
font-size: var(--small-font-size);
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
margin-bottom: var(--base-spacing-half);
|
margin-bottom: var(--spacing-half);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
// padding-left: var(--base-spacing);
|
|
||||||
margin-right: var(--base-spacing);
|
|
||||||
|
|
||||||
color: var(--secondary-font-color);
|
color: var(--secondary-font-color);
|
||||||
font-family: var(--secondary-font-family);
|
font-family: var(--secondary-font-family);
|
||||||
font-size: var(--small-font-size);
|
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
@@ -276,11 +340,18 @@ a {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nav {
|
||||||
|
a {
|
||||||
|
font-size: var(--small-font-size);
|
||||||
|
margin-right: var(--spacing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
header {
|
header {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr min-content;
|
grid-template-columns: 1fr 1fr min-content;
|
||||||
padding-bottom: var(--base-spacing);
|
padding-bottom: var(--spacing);
|
||||||
|
|
||||||
a {
|
a {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -291,22 +362,26 @@ header {
|
|||||||
flex-flow: row;
|
flex-flow: row;
|
||||||
|
|
||||||
.bundle {
|
.bundle {
|
||||||
display: grid;
|
margin-bottom: var(--spacing-double);
|
||||||
grid-template-columns: 2fr 1fr;
|
|
||||||
margin-bottom: var(--base-spacing-double);
|
section {
|
||||||
|
margin-bottom: var(--spacing);
|
||||||
|
}
|
||||||
|
|
||||||
.items {
|
.items {
|
||||||
grid-column: 1 / -1;
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(5, 1fr);
|
grid-template-columns: repeat(5, 1fr);
|
||||||
align-items: stretch;
|
|
||||||
|
|
||||||
div {
|
div {
|
||||||
margin-right: var(--base-spacing);
|
margin-right: var(--spacing);
|
||||||
margin-bottom: var(--base-spacing);
|
margin-bottom: var(--spacing);
|
||||||
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: 1fr 1fr 1fr;
|
|
||||||
|
&.bold {
|
||||||
|
font-weight: bold;
|
||||||
|
background: var(--background-2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -318,13 +393,15 @@ header {
|
|||||||
|
|
||||||
.offer {
|
.offer {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
grid-template-columns: 1fr min-content;
|
||||||
|
grid-template-rows: 1fr min-content;
|
||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
"details img"
|
"details img"
|
||||||
"btn btn";
|
"lines lines";
|
||||||
|
|
||||||
padding: var(--base-spacing);
|
padding: var(--spacing);
|
||||||
margin: var(--base-spacing-half);
|
margin: var(--spacing-half);
|
||||||
background: var(--secondary-background-color);
|
background: var(--background-1);
|
||||||
|
|
||||||
section {
|
section {
|
||||||
grid-area: details;
|
grid-area: details;
|
||||||
@@ -333,15 +410,31 @@ header {
|
|||||||
img {
|
img {
|
||||||
grid-area: img;
|
grid-area: img;
|
||||||
justify-self: end;
|
justify-self: end;
|
||||||
margin-left: var(--base-spacing-half);
|
margin-left: var(--spacing-half);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.lines {
|
||||||
|
grid-area: lines;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
column-gap: var(--spacing-half);
|
||||||
|
|
||||||
button {
|
button {
|
||||||
grid-area: btn;
|
margin-top: var(--spacing-half);
|
||||||
|
border-color: var(--background-2);
|
||||||
|
|
||||||
&.accepted {
|
&.accepted {
|
||||||
background: var(--color-accent-blue);
|
background: var(--color-accepted);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.confirmed {
|
||||||
cursor: auto;
|
cursor: auto;
|
||||||
|
background: var(--color-confirmed);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.neg {
|
||||||
|
margin-left: var(--spacing-half);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+56
-8
@@ -1,6 +1,8 @@
|
|||||||
|
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`;
|
||||||
|
|
||||||
export default function createSocket(store) {
|
export default function createSocket(dispatch) {
|
||||||
let ws;
|
let ws;
|
||||||
|
|
||||||
// -------------
|
// -------------
|
||||||
@@ -24,20 +26,52 @@ export default function createSocket(store) {
|
|||||||
// setTimeout(sendPing, 50);
|
// setTimeout(sendPing, 50);
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendSourcesAvailable() {
|
function sendOfferStatusSet(id, status) {
|
||||||
send(['SourcesAvailable', {}]);
|
send(['OfferStatusSet', { id, status }]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendWantRm(id) {
|
||||||
|
send(['WantRm', { id }]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendOffers() {
|
||||||
|
send(['Offers', {}]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendWants() {
|
||||||
|
send(['Wants', {}]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendAcpOffers() {
|
||||||
|
send(['AcpOffers', {}]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendAcpOfferStatusSet(id, status) {
|
||||||
|
send(['AcpOfferStatusSet', { id, status }]);
|
||||||
}
|
}
|
||||||
|
|
||||||
let pongTimeout;
|
let pongTimeout;
|
||||||
function onPong() {
|
function onPong() {
|
||||||
pongTimeout = setTimeout(sendPing, 1000);
|
pongTimeout = setTimeout(sendPing, 5000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------
|
// -------------
|
||||||
// Incoming
|
// Incoming
|
||||||
// -------------
|
// -------------
|
||||||
function onSourcesAvailable(clouds) {
|
function onUser(user) {
|
||||||
// store.dispatch(http.LIBRARY_RECEIVE_POINTCLOUDS, clouds);
|
dispatch(userState.actions.set(user));
|
||||||
|
}
|
||||||
|
|
||||||
|
function onWants(wants) {
|
||||||
|
dispatch(wantsState.actions.set(wants));
|
||||||
|
}
|
||||||
|
|
||||||
|
function onOffers(offers) {
|
||||||
|
dispatch(offersState.actions.set(offers));
|
||||||
|
}
|
||||||
|
|
||||||
|
function onAcpOffers(offers) {
|
||||||
|
dispatch(acpOffersState.actions.set(offers));
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------
|
// -------------
|
||||||
@@ -46,7 +80,11 @@ export default function createSocket(store) {
|
|||||||
// when the server sends a reply it will have one of these message types
|
// when the server sends a reply it will have one of these message types
|
||||||
// this object wraps the reply types to a function
|
// this object wraps the reply types to a function
|
||||||
const handlers = {
|
const handlers = {
|
||||||
SourcesAvailable: onSourcesAvailable,
|
Wants: onWants,
|
||||||
|
Offers: onOffers,
|
||||||
|
User: onUser,
|
||||||
|
|
||||||
|
AcpOffers: onAcpOffers,
|
||||||
|
|
||||||
Pong: onPong,
|
Pong: onPong,
|
||||||
Error: errHandler,
|
Error: errHandler,
|
||||||
@@ -71,7 +109,11 @@ export default function createSocket(store) {
|
|||||||
|
|
||||||
// Connection opened
|
// Connection opened
|
||||||
function onOpen() {
|
function onOpen() {
|
||||||
|
console.log('websocket connected', SOCKET_URL)
|
||||||
sendPing();
|
sendPing();
|
||||||
|
sendAcpOffers();
|
||||||
|
sendOffers();
|
||||||
|
sendWants();
|
||||||
}
|
}
|
||||||
|
|
||||||
function onError(event) {
|
function onError(event) {
|
||||||
@@ -108,6 +150,12 @@ export default function createSocket(store) {
|
|||||||
return {
|
return {
|
||||||
connect,
|
connect,
|
||||||
|
|
||||||
sendSourcesAvailable,
|
sendOfferStatusSet,
|
||||||
|
sendWantRm,
|
||||||
|
|
||||||
|
sendOffers,
|
||||||
|
|
||||||
|
sendAcpOffers,
|
||||||
|
sendAcpOfferStatusSet,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -24,17 +24,11 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- "./api:/opt/mcrates/api"
|
- "./api:/opt/mcrates/api"
|
||||||
|
|
||||||
client:
|
|
||||||
image: node:16
|
|
||||||
working_dir: /opt/mcrates/client
|
|
||||||
entrypoint: npm start
|
|
||||||
volumes:
|
|
||||||
- "./client:/opt/mcrates/client"
|
|
||||||
|
|
||||||
nginx:
|
nginx:
|
||||||
image: nginx:latest
|
image: nginx:latest
|
||||||
volumes:
|
volumes:
|
||||||
- './nginx/matescrates.au.nginx.conf:/etc/nginx/conf.d/default.conf'
|
- './nginx/matescrates.au.nginx.conf:/etc/nginx/conf.d/default.conf'
|
||||||
|
- './client/build:/srv/matescrates'
|
||||||
- '/etc/letsencrypt:/etc/letsencrypt'
|
- '/etc/letsencrypt:/etc/letsencrypt'
|
||||||
ports:
|
ports:
|
||||||
- '80:80'
|
- '80:80'
|
||||||
|
|||||||
@@ -2,20 +2,11 @@ upstream mcrates_api {
|
|||||||
server api:41337;
|
server api:41337;
|
||||||
}
|
}
|
||||||
|
|
||||||
upstream mcrates_client {
|
|
||||||
server client:8080;
|
|
||||||
}
|
|
||||||
|
|
||||||
# upstream mcrates_ws {
|
|
||||||
# server 127.0.0.1:40055;
|
|
||||||
# }
|
|
||||||
|
|
||||||
map $http_upgrade $connection_upgrade {
|
map $http_upgrade $connection_upgrade {
|
||||||
default upgrade;
|
default upgrade;
|
||||||
'' close;
|
'' close;
|
||||||
}
|
}
|
||||||
|
|
||||||
# DEV
|
|
||||||
server {
|
server {
|
||||||
server_name matescrates.au;
|
server_name matescrates.au;
|
||||||
|
|
||||||
@@ -26,17 +17,19 @@ server {
|
|||||||
gzip_types text/plain text/css text/xml application/json application/javascript application/xml+rss application/atom+xml image/svg+xml;
|
gzip_types text/plain text/css text/xml application/json application/javascript application/xml+rss application/atom+xml image/svg+xml;
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
proxy_pass http://mcrates_client;
|
root /srv/matescrates;
|
||||||
proxy_read_timeout 600s;
|
index index.html;
|
||||||
|
try_files $uri $uri/ index.html;
|
||||||
}
|
}
|
||||||
|
|
||||||
# location /api/ws {
|
location /api/ws {
|
||||||
# proxy_pass http://mcrates_ws;
|
proxy_pass http://mcrates_api;
|
||||||
# proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
# proxy_set_header Upgrade $http_upgrade;
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
# proxy_set_header Connection $connection_upgrade;
|
proxy_set_header Connection $connection_upgrade;
|
||||||
# proxy_read_timeout 600s;
|
proxy_set_header Host $host;
|
||||||
# }
|
proxy_read_timeout 600s;
|
||||||
|
}
|
||||||
|
|
||||||
location /api/ {
|
location /api/ {
|
||||||
# trailing slash means api will receive url WITHOUT /api
|
# trailing slash means api will receive url WITHOUT /api
|
||||||
@@ -44,6 +37,8 @@ server {
|
|||||||
proxy_read_timeout 600s;
|
proxy_read_timeout 600s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
error_page 404 /;
|
||||||
|
|
||||||
listen [::]:443 ssl ipv6only=on; # managed by Certbot
|
listen [::]:443 ssl ipv6only=on; # managed by Certbot
|
||||||
listen 443 ssl; # managed by Certbot
|
listen 443 ssl; # managed by Certbot
|
||||||
ssl_certificate /etc/letsencrypt/live/matescrates.au/fullchain.pem; # managed by Certbot
|
ssl_certificate /etc/letsencrypt/live/matescrates.au/fullchain.pem; # managed by Certbot
|
||||||
|
|||||||
Reference in new issue
Block a user