acp stuff
This commit is contained in:
1 parent
deb9eb3fc2
commit
047f606e83
14 files changed
+365
-41
No files matched your search
+2
-2
@@ -51,8 +51,8 @@ describe('api', function apiTests() {
|
|||||||
describe('offers', function offerTests() {
|
describe('offers', function offerTests() {
|
||||||
it('create offers', async function createOffersTest() {
|
it('create offers', async function createOffersTest() {
|
||||||
const { data: bundles } = await api.get('/bundles');
|
const { data: bundles } = await api.get('/bundles');
|
||||||
const { seller } = bundles[0];
|
const listingIds = bundles[0].items.map(i => i.id);
|
||||||
const { data } = await api.post('/offers', { seller, shipping: 50.0, baseShipping: 15.0 });
|
const { data } = await api.post('/offers', { listingIds, shipping: 50.0, baseShipping: 15.0 });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
+37
-5
@@ -6,7 +6,7 @@ import session from 'koa-session';
|
|||||||
import koaRouter from '@koa/router'
|
import koaRouter from '@koa/router'
|
||||||
import cors from '@koa/cors';
|
import cors from '@koa/cors';
|
||||||
|
|
||||||
import { groupBy } from 'lodash';
|
import { groupBy, sortBy } from 'lodash';
|
||||||
import Bluebird from 'bluebird';
|
import Bluebird from 'bluebird';
|
||||||
|
|
||||||
import type Log from 'bunyan';
|
import type Log from 'bunyan';
|
||||||
@@ -104,14 +104,46 @@ export default function createApi(log: Log, pg: Pg, discogs, offers, events) {
|
|||||||
ctx.status = 200;
|
ctx.status = 200;
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get('/bundles', async (ctx, next) => {
|
router.get('/acp/bundles', async function bundlesGet(ctx, next) {
|
||||||
const bundles = await getBundles();
|
const bundles = await getBundles();
|
||||||
ctx.response.body = bundles;
|
ctx.response.body = bundles;
|
||||||
})
|
})
|
||||||
|
|
||||||
router.post('/offers', async (ctx, next) => {
|
router.get('/acp/offers', async function acpOffers(ctx, next) {
|
||||||
const { seller, shipping, baseShipping } = ctx.request.body;
|
const offers = await pg.offersAdmin();
|
||||||
ctx.response.body = await offers.generate(seller, shipping, baseShipping);
|
|
||||||
|
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) {
|
||||||
|
|||||||
+20
-5
@@ -2,6 +2,7 @@ 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 groupBy from 'lodash/groupBy';
|
import groupBy from 'lodash/groupBy';
|
||||||
import sortBy from 'lodash/sortBy';
|
import sortBy from 'lodash/sortBy';
|
||||||
|
import { Context } from 'koa';
|
||||||
|
|
||||||
import { logAxiosError } from './utils';
|
import { logAxiosError } from './utils';
|
||||||
import { Pg } from './pg';
|
import { Pg } from './pg';
|
||||||
@@ -14,18 +15,22 @@ export enum Status {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function createOffers(log: Log, pg: Pg) {
|
export default function createOffers(log: Log, pg: Pg) {
|
||||||
async function generate(seller: number, bulkShipping: number, baseShipping: number): Offer[] {
|
async function generate(listingIds: DiscogsListing[], bulkShipping: number, baseShipping: number) {
|
||||||
log.info('generateOffers', { seller, bulkShipping, baseShipping });
|
log.info('generateOffers', { listingIds, bulkShipping, baseShipping });
|
||||||
const listings = await pg.listingFindSeller(seller);
|
const listings = await pg.listingByIds(listingIds);
|
||||||
|
|
||||||
log.info('creating offers for listings', { listings })
|
log.info({ listings }, 'creating offers for listings')
|
||||||
|
|
||||||
const shipping = bulkShipping / listings.length;
|
const shipping = bulkShipping / listings.length;
|
||||||
|
// how to deal with users who don't want all items in a bundle...
|
||||||
|
|
||||||
const offers = listings.map(l => ({
|
const offers = listings.map(l => ({
|
||||||
userId: l.want.user.id,
|
userId: l.want.user.id,
|
||||||
listingId: l.id,
|
listingId: l.id,
|
||||||
wantId: l.want.id,
|
wantId: l.want.id,
|
||||||
|
|
||||||
|
// sellerId: l.seller,
|
||||||
|
|
||||||
status: Status.Pending,
|
status: Status.Pending,
|
||||||
|
|
||||||
item: l.data.price.value,
|
item: l.data.price.value,
|
||||||
@@ -42,11 +47,21 @@ export default function createOffers(log: Log, pg: Pg) {
|
|||||||
// and ensure the cheapest one goes in first
|
// and ensure the cheapest one goes in first
|
||||||
const sorted = sortBy(offers, ['item']);
|
const sorted = sortBy(offers, ['item']);
|
||||||
const inserts = await pg.offersCreate(sorted);
|
const inserts = await pg.offersCreate(sorted);
|
||||||
console.log(inserts);
|
|
||||||
return inserts;
|
return inserts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function accept(ctx: Context) {
|
||||||
|
const { id } = ctx.request.body;
|
||||||
|
ctx.assert(id, 401, 'no id supplied');
|
||||||
|
|
||||||
|
// check the user actually has this offer
|
||||||
|
const offer = await pg.userOfferFind(ctx.state.user, id);
|
||||||
|
return pg.offerSetStatus(offer.id, Status.Accepted);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
generate,
|
generate,
|
||||||
|
accept,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
+45
-4
@@ -3,6 +3,8 @@ import type Log from 'bunyan';
|
|||||||
import type { PrismaClient, Want, User, Listing, Offer, Prisma } from '@prisma/client';
|
import type { PrismaClient, Want, User, Listing, Offer, Prisma } from '@prisma/client';
|
||||||
import * as Bluebird from 'bluebird';
|
import * as Bluebird from 'bluebird';
|
||||||
|
|
||||||
|
import { Status } from './offers';
|
||||||
|
|
||||||
export interface UserCreateParams {
|
export interface UserCreateParams {
|
||||||
email: string,
|
email: string,
|
||||||
password: string,
|
password: string,
|
||||||
@@ -14,10 +16,11 @@ export interface Pg {
|
|||||||
userCreate: (params: UserCreateParams) => Promise<User>,
|
userCreate: (params: UserCreateParams) => Promise<User>,
|
||||||
userWants: (user: User) => Promise<Want[]>,
|
userWants: (user: User) => Promise<Want[]>,
|
||||||
userOffers: (user: User) => Promise<Offer[]>,
|
userOffers: (user: User) => Promise<Offer[]>,
|
||||||
|
userOfferFind: (user: User, id: UUID) => Promise<Offer>,
|
||||||
|
|
||||||
listingUpsert: (listing: Listing, want: Want) => Promise<Listing>,
|
listingUpsert: (listing: Listing, want: Want) => Promise<Listing>,
|
||||||
listingFind: (id: DiscogsListing) => Promise<Listing>,
|
listingFind: (id: DiscogsListing) => Promise<Listing>,
|
||||||
listingFindSeller: (id: DiscogsSeller) => Promise<Listing[]>,
|
listingByIds: (ids: DiscogsListing[]) => Promise<Listing[]>,
|
||||||
listingRmUnlisted: (want: Want, ids: DiscogsListing[]) => Promise<number>,
|
listingRmUnlisted: (want: Want, ids: DiscogsListing[]) => Promise<number>,
|
||||||
|
|
||||||
wantCreate: (discogsWant: any, user: User) => Promise<Want>,
|
wantCreate: (discogsWant: any, user: User) => Promise<Want>,
|
||||||
@@ -27,6 +30,8 @@ export interface Pg {
|
|||||||
bundleCandidates: () => Promise<Listing[][]>,
|
bundleCandidates: () => Promise<Listing[][]>,
|
||||||
|
|
||||||
offersCreate: (offers: Offer[]) => Promise<Offer[]>,
|
offersCreate: (offers: Offer[]) => Promise<Offer[]>,
|
||||||
|
offerSetStatus: (id: UUID, status: Status) => Promise<Offer>,
|
||||||
|
offersAdmin: () => Promise<Offer[]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function createPg(log: Log, prisma: PrismaClient): Pg {
|
export default function createPg(log: Log, prisma: PrismaClient): Pg {
|
||||||
@@ -45,6 +50,21 @@ 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 },
|
||||||
|
include: {
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
orderBy: {
|
||||||
|
status: 'desc',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function userOfferFind(user: User, id: UUID) {
|
||||||
|
return prisma.offer.findFirstOrThrow({
|
||||||
|
where: {
|
||||||
|
userId: user.id,
|
||||||
|
id,
|
||||||
|
},
|
||||||
include: {
|
include: {
|
||||||
want: true,
|
want: true,
|
||||||
}
|
}
|
||||||
@@ -145,10 +165,10 @@ export default function createPg(log: Log, prisma: PrismaClient): Pg {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function listingFindSeller(seller: DiscogsSeller): Promise<Listing[]> {
|
async function listingByIds(ids: DiscogsListing[]) {
|
||||||
return prisma.listing.findMany({
|
return prisma.listing.findMany({
|
||||||
where: {
|
where: {
|
||||||
seller,
|
id: { in: ids },
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
want: {
|
want: {
|
||||||
@@ -199,12 +219,30 @@ export default function createPg(log: Log, prisma: PrismaClient): Pg {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function offerSetStatus(id: UUID, status: Status) {
|
||||||
|
return prisma.offer.update({
|
||||||
|
where: { id: id },
|
||||||
|
data: { status },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function offersAdmin() {
|
||||||
|
return prisma.offer.findMany({
|
||||||
|
include: {
|
||||||
|
listing: true,
|
||||||
|
user: true,
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
userFindId,
|
userFindId,
|
||||||
userFindEmail,
|
userFindEmail,
|
||||||
userCreate,
|
userCreate,
|
||||||
userWants,
|
userWants,
|
||||||
userOffers,
|
userOffers,
|
||||||
|
userOfferFind,
|
||||||
|
|
||||||
wantCreate,
|
wantCreate,
|
||||||
wantList,
|
wantList,
|
||||||
@@ -212,9 +250,12 @@ export default function createPg(log: Log, prisma: PrismaClient): Pg {
|
|||||||
listingUpsert,
|
listingUpsert,
|
||||||
listingFind,
|
listingFind,
|
||||||
listingRmUnlisted,
|
listingRmUnlisted,
|
||||||
listingFindSeller,
|
listingByIds,
|
||||||
|
|
||||||
bundleCandidates,
|
bundleCandidates,
|
||||||
|
|
||||||
offersCreate,
|
offersCreate,
|
||||||
|
offerSetStatus,
|
||||||
|
offersAdmin,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
Generated
+3
-4
@@ -9,6 +9,7 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"lodash": "^4.17.21",
|
||||||
"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",
|
||||||
@@ -11861,8 +11862,7 @@
|
|||||||
"node_modules/lodash": {
|
"node_modules/lodash": {
|
||||||
"version": "4.17.21",
|
"version": "4.17.21",
|
||||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"node_modules/lodash.debounce": {
|
"node_modules/lodash.debounce": {
|
||||||
"version": "4.0.8",
|
"version": "4.0.8",
|
||||||
@@ -30319,8 +30319,7 @@
|
|||||||
"lodash": {
|
"lodash": {
|
||||||
"version": "4.17.21",
|
"version": "4.17.21",
|
||||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"lodash.debounce": {
|
"lodash.debounce": {
|
||||||
"version": "4.0.8",
|
"version": "4.0.8",
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"lodash": "^4.17.21",
|
||||||
"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",
|
||||||
|
|||||||
+19
-1
@@ -33,13 +33,17 @@ api.interceptors.response.use(onSuccess, onError);
|
|||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
getBundles: async function getBundles() {
|
getBundles: async function getBundles() {
|
||||||
return api.get('/bundles');
|
return api.get('/acp/bundles');
|
||||||
},
|
},
|
||||||
|
|
||||||
getOffers: async function getOffers() {
|
getOffers: async function getOffers() {
|
||||||
return api.get('/user/offers');
|
return api.get('/user/offers');
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getOffersAcp: async function getOffersAcp() {
|
||||||
|
return api.get('/acp/offers');
|
||||||
|
},
|
||||||
|
|
||||||
getWants: async function getWants() {
|
getWants: async function getWants() {
|
||||||
return api.get('/want/all');
|
return api.get('/want/all');
|
||||||
},
|
},
|
||||||
@@ -74,6 +78,20 @@ const actions = {
|
|||||||
password,
|
password,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
postOffers: async function postOffers(listingIds, shipping, baseShipping) {
|
||||||
|
return api.post('/offers', {
|
||||||
|
listingIds,
|
||||||
|
shipping,
|
||||||
|
baseShipping,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
postOfferAccept: async function postOffers(id) {
|
||||||
|
return api.post('/offer/accept', {
|
||||||
|
id,
|
||||||
|
});
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default actions;
|
export default actions;
|
||||||
+5
-1
@@ -8,6 +8,7 @@ import Header from './components/header';
|
|||||||
|
|
||||||
import Home from './routes/home';
|
import Home from './routes/home';
|
||||||
import Bundles from './routes/bundles';
|
import Bundles from './routes/bundles';
|
||||||
|
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';
|
||||||
@@ -36,10 +37,13 @@ function App() {
|
|||||||
<Header user={user} />
|
<Header user={user} />
|
||||||
<Router>
|
<Router>
|
||||||
<Route path="/" component={Home} />
|
<Route path="/" component={Home} />
|
||||||
<Route path="/bundles" component={Bundles} />
|
|
||||||
<Route path="/login" component={Login} success={loginSuccess} />
|
<Route path="/login" component={Login} success={loginSuccess} />
|
||||||
<Route path="/search" component={Search} />
|
<Route path="/search" component={Search} />
|
||||||
<Route path="/list" component={List} user={user} />
|
<Route path="/list" component={List} user={user} />
|
||||||
|
|
||||||
|
{/* ACP */}
|
||||||
|
<Route path="/bundles" component={Bundles} />
|
||||||
|
<Route path="/offers/acp" component={Admin} />
|
||||||
</Router>
|
</Router>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { h } from 'preact';
|
||||||
|
import { ExternalLink, Check } from 'react-feather';
|
||||||
|
import { useState } from 'preact/hooks';
|
||||||
|
|
||||||
|
import actions from '../actions';
|
||||||
|
|
||||||
|
export default function Offer(offer) {
|
||||||
|
const { want: { data: details } } = offer;
|
||||||
|
|
||||||
|
const [accepted, setAccepted] = useState(offer.status !== 'pending');
|
||||||
|
|
||||||
|
async function onClick() {
|
||||||
|
const [error] = await actions.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,7 +1,59 @@
|
|||||||
import { h } from 'preact';
|
import { h } from 'preact';
|
||||||
|
import { Square, CheckSquare } from 'react-feather';
|
||||||
|
import { useState, useReducer } from 'preact/hooks';
|
||||||
|
|
||||||
|
import actions from '../actions';
|
||||||
|
|
||||||
|
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) {
|
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));
|
||||||
|
|
||||||
|
// 100.71
|
||||||
|
// 33.08
|
||||||
|
|
||||||
|
async function submit(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const [error, offers] = await actions.postOffers(state.checkedIds, shipping, baseShipping);
|
||||||
|
console.log(error, offers);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="bundle">
|
<div class="bundle">
|
||||||
@@ -15,28 +67,20 @@ export default function Bundle(bundle) {
|
|||||||
<form action="">
|
<form action="">
|
||||||
<div class="item">
|
<div class="item">
|
||||||
<label for="shipping">Full Shipping Cost</label>
|
<label for="shipping">Full Shipping Cost</label>
|
||||||
<input type="number" name="shipping" id="shipping" required />
|
<input type="number" name="shipping" id="shipping" value={shipping} onChange={(e) => setShipping(Number.parseFloat(e.target.value))} step="0.01" min="0" required />
|
||||||
</div>
|
</div>
|
||||||
<div class="item">
|
<div class="item">
|
||||||
<label for="baseShipping">Base Shipping Cost</label>
|
<label for="baseShipping">Base Shipping Cost</label>
|
||||||
<input type="number" name="baseShipping" id="baseShipping" required />
|
<input type="number" name="baseShipping" id="baseShipping" value={baseShipping} onChange={(e) => setBaseShipping(Number.parseFloat(e.target.value))} step="0.01" min="0" required />
|
||||||
</div>
|
</div>
|
||||||
<div class="item">
|
<div class="item">
|
||||||
<input type="submit" value="Generate Offers" />
|
<input type="submit" value="Generate Offers" onClick={(e) => submit(e)} />
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div class='items'>
|
<div class='items'>
|
||||||
{bundle.items.map(i => (
|
{items}
|
||||||
<div key={i.id}>
|
|
||||||
<a href={i.uri} target="_blank" >
|
|
||||||
<h4>{i.description}</h4>
|
|
||||||
</a>
|
|
||||||
<h5>{i.user.name}</h5>
|
|
||||||
<h5>{i.price.toFixed(2)} {i.currency}</h5>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,16 +5,24 @@ const Header = ({ user }) => {
|
|||||||
const nav = () => (
|
const nav = () => (
|
||||||
<nav>
|
<nav>
|
||||||
<a href="/">Offers</a>
|
<a href="/">Offers</a>
|
||||||
<a href="/bundles">Bundles</a>
|
|
||||||
<a href='/list'>List</a>
|
<a href='/list'>List</a>
|
||||||
<a href="/search">Search</a>
|
<a href="/search">Search</a>
|
||||||
</nav>
|
</nav>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const adminNav = () => (
|
||||||
|
<nav>
|
||||||
|
<a href="/bundles">Bundles</a>
|
||||||
|
<a href='/offers/acp'>Offers ACP</a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header>
|
<header>
|
||||||
{user ? nav() : null}
|
{user ? nav() : null}
|
||||||
<div></div>
|
<div></div>
|
||||||
|
{user ? adminNav() : null}
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,35 @@
|
|||||||
import { h } from 'preact';
|
import { h } from 'preact';
|
||||||
import { ExternalLink } from 'react-feather';
|
import { ExternalLink, Check } from 'react-feather';
|
||||||
|
import { useState } from 'preact/hooks';
|
||||||
|
|
||||||
|
import actions from '../actions';
|
||||||
|
|
||||||
export default function Offer(offer) {
|
export default function Offer(offer) {
|
||||||
const { want: { data: details } } = offer;
|
const { want: { data: details } } = offer;
|
||||||
|
|
||||||
|
const [accepted, setAccepted] = useState(offer.status !== 'pending');
|
||||||
|
|
||||||
|
async function onClick() {
|
||||||
|
const [error] = await actions.postOfferAccept(offer.id);
|
||||||
|
if (error) return false;
|
||||||
|
setAccepted(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const classes = `${accepted ? 'accepted' : ''}`;
|
||||||
|
const btnText = accepted
|
||||||
|
? offer.status
|
||||||
|
: 'Accept';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="offer">
|
<div class="offer">
|
||||||
|
<section>
|
||||||
<h2>{details.artists_sort} - {details.title} <a href={details.uri} target="_blank"><ExternalLink /></a></h2>
|
<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.year}</h5>
|
||||||
<h5 class='alt-text'>{details.formats[0].descriptions.join(' ')}</h5>
|
<h5 class='alt-text'>{details.formats[0].descriptions.join(' ')}</h5>
|
||||||
|
<h5 class='number'>${offer.total}</h5>
|
||||||
|
</section>
|
||||||
<img src={details.thumb} />
|
<img src={details.thumb} />
|
||||||
<h5>${offer.total}</h5>
|
<button onClick={onClick} disabled={accepted} class={classes} >{btnText}</button>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import { h } from 'preact';
|
||||||
|
import { useState, useEffect } from 'preact/hooks';
|
||||||
|
|
||||||
|
import actions from './../actions';
|
||||||
|
import AcpOffer from './../components/acp.offer';
|
||||||
|
|
||||||
|
function Item(item) {
|
||||||
|
|
||||||
|
console.log(item);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={item.id}>
|
||||||
|
<a href={item.listing.data.uri} target="_blank" >
|
||||||
|
<h4>{item.listing.data.release.description}</h4>
|
||||||
|
</a>
|
||||||
|
<h5 class='alt-text'>{item.total}</h5>
|
||||||
|
<h5>{item.status}</h5>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SellerGroup(group) {
|
||||||
|
return (
|
||||||
|
<div class="bundle">
|
||||||
|
<section>
|
||||||
|
<h2>{group.username} - {group.seller}</h2>
|
||||||
|
<h3>{group.offers.length} offers</h3>
|
||||||
|
<h4>Ships: {group.mentionsAustralia && 'AU'} {group.mentionsWorldwide && 'WORLD'}</h4>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class='items'>
|
||||||
|
{group.offers.map(Item)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Home() {
|
||||||
|
const [offers, setOffers] = useState([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function getOffers() {
|
||||||
|
const [error, offers] = await actions.getOffersAcp();
|
||||||
|
if (error) return console.error(error);
|
||||||
|
setOffers(offers);
|
||||||
|
}
|
||||||
|
getOffers();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!offers.length) {
|
||||||
|
return (
|
||||||
|
<div class={'offers'}>
|
||||||
|
loading...
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class='bundles'>
|
||||||
|
{offers.map(SellerGroup)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Home;
|
||||||
@@ -19,7 +19,7 @@ ol, ul {
|
|||||||
}
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
max-width: 100%;
|
// max-width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,7 +194,7 @@ button, [type=submit] {
|
|||||||
|
|
||||||
.alt-text {
|
.alt-text {
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -279,8 +279,12 @@ a {
|
|||||||
|
|
||||||
header {
|
header {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr min-content 1fr;
|
grid-template-columns: 1fr 1fr min-content;
|
||||||
padding-bottom: var(--base-spacing);
|
padding-bottom: var(--base-spacing);
|
||||||
|
|
||||||
|
a {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.bundles {
|
.bundles {
|
||||||
@@ -289,7 +293,7 @@ header {
|
|||||||
.bundle {
|
.bundle {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 2fr 1fr;
|
grid-template-columns: 2fr 1fr;
|
||||||
margin-bottom: var(--base-spacing);
|
margin-bottom: var(--base-spacing-double);
|
||||||
|
|
||||||
.items {
|
.items {
|
||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
@@ -300,6 +304,9 @@ header {
|
|||||||
div {
|
div {
|
||||||
margin-right: var(--base-spacing);
|
margin-right: var(--base-spacing);
|
||||||
margin-bottom: var(--base-spacing);
|
margin-bottom: var(--base-spacing);
|
||||||
|
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: 1fr 1fr 1fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -308,5 +315,39 @@ header {
|
|||||||
.offers {
|
.offers {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
|
|
||||||
|
.offer {
|
||||||
|
display: grid;
|
||||||
|
grid-template-areas:
|
||||||
|
"details img"
|
||||||
|
"btn btn";
|
||||||
|
|
||||||
|
padding: var(--base-spacing);
|
||||||
|
margin: var(--base-spacing-half);
|
||||||
|
background: var(--secondary-background-color);
|
||||||
|
|
||||||
|
section {
|
||||||
|
grid-area: details;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
grid-area: img;
|
||||||
|
justify-self: end;
|
||||||
|
margin-left: var(--base-spacing-half);
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
grid-area: btn;
|
||||||
|
|
||||||
|
&.accepted {
|
||||||
|
background: var(--color-accent-blue);
|
||||||
|
cursor: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.number {
|
||||||
|
font-family: var(--secondary-font-family);
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
Reference in new issue
Block a user