offers wip
This commit is contained in:
1 parent
453c2da045
commit
deb9eb3fc2
16 files changed
+275
-87
No files matched your search
@@ -5,6 +5,16 @@ import { Events } from './src/events';
|
|||||||
async function start() {
|
async function start() {
|
||||||
const log = createLog();
|
const log = createLog();
|
||||||
|
|
||||||
|
process.on('uncaughtException', (err, origin) => {
|
||||||
|
log.error({ err, origin }, 'uncaughtException');
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('unhandledRejection', (reason, promise) => {
|
||||||
|
log.error({ reason, promise }, 'unhandledRejection');
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
process.on('SIGTERM', () => {
|
process.on('SIGTERM', () => {
|
||||||
log.debug('SIGTERM received, exiting');
|
log.debug('SIGTERM received, exiting');
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ model Listing {
|
|||||||
wantId String @db.Uuid
|
wantId String @db.Uuid
|
||||||
want Want @relation(fields: [wantId], references: [id], onDelete: Cascade, onUpdate: Cascade)
|
want Want @relation(fields: [wantId], references: [id], onDelete: Cascade, onUpdate: Cascade)
|
||||||
|
|
||||||
|
offers Offer[]
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
@@ -32,6 +34,7 @@ model User {
|
|||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
wants Want[]
|
wants Want[]
|
||||||
|
offers Offer[]
|
||||||
|
|
||||||
@@map("users")
|
@@map("users")
|
||||||
}
|
}
|
||||||
@@ -48,9 +51,36 @@ model Want {
|
|||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
listings Listing[]
|
listings Listing[]
|
||||||
|
offers Offer[]
|
||||||
|
|
||||||
@@unique([userId, release])
|
@@unique([userId, release])
|
||||||
@@index([release])
|
@@index([release])
|
||||||
|
|
||||||
@@map("wants")
|
@@map("wants")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model Offer {
|
||||||
|
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||||
|
|
||||||
|
status String
|
||||||
|
|
||||||
|
item Decimal @db.Money
|
||||||
|
shipping Decimal @db.Money
|
||||||
|
total Decimal @db.Money
|
||||||
|
comparison Decimal @db.Money
|
||||||
|
|
||||||
|
sellerId Int
|
||||||
|
|
||||||
|
listingId Int
|
||||||
|
listing Listing @relation(fields: [listingId], references: [id])
|
||||||
|
|
||||||
|
userId String @db.Uuid
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade)
|
||||||
|
|
||||||
|
wantId String @db.Uuid
|
||||||
|
want Want @relation(fields: [wantId], references: [id], onDelete: Cascade, onUpdate: Cascade)
|
||||||
|
|
||||||
|
// only one offer per want for a user
|
||||||
|
@@unique([userId, wantId])
|
||||||
|
@@map("offers")
|
||||||
|
}
|
||||||
+35
-24
@@ -5,10 +5,13 @@ import createServices from './services';
|
|||||||
import createLog from './log';
|
import createLog from './log';
|
||||||
|
|
||||||
const log = createLog();
|
const log = createLog();
|
||||||
log.level('error');
|
log.level('debug');
|
||||||
const services = createServices(log);
|
const services = createServices(log);
|
||||||
|
|
||||||
const HOST = 'http://localhost:41337';
|
const api = axios.create({
|
||||||
|
baseURL: 'http://localhost:41337',
|
||||||
|
withCredentials: true
|
||||||
|
});
|
||||||
|
|
||||||
before(function setup() {
|
before(function setup() {
|
||||||
services.api.listen();
|
services.api.listen();
|
||||||
@@ -19,29 +22,37 @@ after(function teardown(done) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('api', function apiTests() {
|
describe('api', function apiTests() {
|
||||||
it('listens', async function listenTest() {
|
describe.skip('auth', function authTests() {
|
||||||
const res = await axios.get(`${HOST}/health`);
|
it('listens', async function listenTest() {
|
||||||
assert.equal(res.status, 200);
|
const res = await api.get('/health');
|
||||||
|
assert.equal(res.status, 200);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('register', async function register() {
|
||||||
|
const body = {
|
||||||
|
email: `test+${Date.now()}@user.com`,
|
||||||
|
password: 'woaaah theres no limit',
|
||||||
|
};
|
||||||
|
const { data } = await api.post('/auth/register', body);
|
||||||
|
assert.equal(data.email, body.email);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('login', async function login() {
|
||||||
|
const body = {
|
||||||
|
email: `test+${Date.now()}@user.com`,
|
||||||
|
password: 'woaaah theres no limit',
|
||||||
|
};
|
||||||
|
await api.post('/auth/register', body);
|
||||||
|
const res = await api.post('/auth/login', body);
|
||||||
|
assert(res.headers['set-cookie']);
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
it('register', async function register() {
|
describe('offers', function offerTests() {
|
||||||
const body = {
|
it('create offers', async function createOffersTest() {
|
||||||
email: `test+${Date.now()}@user.com`,
|
const { data: bundles } = await api.get('/bundles');
|
||||||
password: 'woaaah theres no limit',
|
const { seller } = bundles[0];
|
||||||
};
|
const { data } = await api.post('/offers', { seller, shipping: 50.0, baseShipping: 15.0 });
|
||||||
const { data } = await axios.post(`${HOST}/auth/register`, body);
|
});
|
||||||
assert.equal(data.email, body.email);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('login', async function login() {
|
|
||||||
const body = {
|
|
||||||
email: `test+${Date.now()}@user.com`,
|
|
||||||
password: 'woaaah theres no limit',
|
|
||||||
};
|
|
||||||
await axios.post(`${HOST}/auth/register`, body);
|
|
||||||
const res = await axios.post(`${HOST}/auth/login`, body);
|
|
||||||
assert(res.headers['set-cookie']);
|
|
||||||
})
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
+14
-14
@@ -16,7 +16,7 @@ import { Pg } from './pg';
|
|||||||
import routerAuth from './routers/router.auth';
|
import routerAuth from './routers/router.auth';
|
||||||
import { Events } from './events';
|
import { Events } from './events';
|
||||||
|
|
||||||
export default function createApi(log: Log, pg: Pg, discogs, events) {
|
export default function createApi(log: Log, pg: Pg, discogs, offers, events) {
|
||||||
const PORT = 41337;
|
const PORT = 41337;
|
||||||
const SECRET = 'ohja4Eif9eidah3aj5ta2haa4iemooth';
|
const SECRET = 'ohja4Eif9eidah3aj5ta2haa4iemooth';
|
||||||
|
|
||||||
@@ -46,13 +46,14 @@ export default function createApi(log: Log, pg: Pg, discogs, events) {
|
|||||||
const bundles = await pg.bundleCandidates();
|
const bundles = await pg.bundleCandidates();
|
||||||
|
|
||||||
return bundles.map((b: ListingData[]) => {
|
return bundles.map((b: ListingData[]) => {
|
||||||
const total = b.reduce((acc, l) => acc + l.data.original_price.value, 0);
|
const total = b.reduce((acc, l) => acc + l.data.price.value, 0);
|
||||||
|
|
||||||
const items = b.map(l => ({
|
const items = b.map(l => ({
|
||||||
id: l.data.id,
|
id: l.data.id,
|
||||||
description: l.data.release.description,
|
description: l.data.release.description,
|
||||||
price: l.data.original_price.value,
|
originalPrice: l.data.original_price.value,
|
||||||
currency: l.data.original_price.curr_abbr,
|
originalCurrency: l.data.original_price.curr_abbr,
|
||||||
|
price: l.data.price.value,
|
||||||
|
currency: l.data.price.currency,
|
||||||
condition: l.data.condition,
|
condition: l.data.condition,
|
||||||
uri: l.data.uri,
|
uri: l.data.uri,
|
||||||
user: l.want.user,
|
user: l.want.user,
|
||||||
@@ -65,7 +66,7 @@ export default function createApi(log: Log, pg: Pg, discogs, events) {
|
|||||||
seller: b[0]?.data.seller.id,
|
seller: b[0]?.data.seller.id,
|
||||||
shipsFrom: b[0]?.data.ships_from,
|
shipsFrom: b[0]?.data.ships_from,
|
||||||
total,
|
total,
|
||||||
currency: b[0]?.data.original_price.curr_abbr,
|
currency: b[0]?.data.price.currency,
|
||||||
shipping: '...',
|
shipping: '...',
|
||||||
items,
|
items,
|
||||||
mentionsAustralia: shipping.toLowerCase().includes('australia'),
|
mentionsAustralia: shipping.toLowerCase().includes('australia'),
|
||||||
@@ -108,17 +109,16 @@ export default function createApi(log: Log, pg: Pg, discogs, events) {
|
|||||||
ctx.response.body = bundles;
|
ctx.response.body = bundles;
|
||||||
})
|
})
|
||||||
|
|
||||||
|
router.post('/offers', async (ctx, next) => {
|
||||||
|
const { seller, shipping, baseShipping } = ctx.request.body;
|
||||||
|
ctx.response.body = await offers.generate(seller, shipping, baseShipping);
|
||||||
|
})
|
||||||
|
|
||||||
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;
|
||||||
const bundles = (await getBundles())
|
const offers = await pg.userOffers(user);
|
||||||
.map(b => {
|
ctx.response.body = offers;
|
||||||
b.items = b.items.filter(i => i.user.id === user.id);
|
|
||||||
return b;
|
|
||||||
})
|
|
||||||
.filter(b => b.items.length);
|
|
||||||
|
|
||||||
ctx.response.body = bundles;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post('/want', async function wantPost(ctx, next) {
|
router.post('/want', async function wantPost(ctx, next) {
|
||||||
|
|||||||
+1
-1
@@ -14,7 +14,7 @@ import { Pg } from './pg';
|
|||||||
// vanderbabe
|
// vanderbabe
|
||||||
// jmcdermid1
|
// jmcdermid1
|
||||||
|
|
||||||
export default function discogs(log: Log, pg: Pg) {
|
export default function createDiscogs(log: Log, pg: Pg) {
|
||||||
const TOKEN = 'Discogs token=wRTXuYZGOPsIQgKRGKCAkUkidGtvpSfQgPWkYIpD';
|
const TOKEN = 'Discogs token=wRTXuYZGOPsIQgKRGKCAkUkidGtvpSfQgPWkYIpD';
|
||||||
const apiBase = 'https://api.discogs.com'
|
const apiBase = 'https://api.discogs.com'
|
||||||
const discogsBase = 'https://www.discogs.com'
|
const discogsBase = 'https://www.discogs.com'
|
||||||
|
|||||||
+1
-1
@@ -11,7 +11,7 @@ export enum Events {
|
|||||||
StartupSync = 'StartupSync',
|
StartupSync = 'StartupSync',
|
||||||
}
|
}
|
||||||
|
|
||||||
export function eventService(log: Log, pg: Pg, discogs) {
|
export default function createEvents(log: Log, pg: Pg, discogs) {
|
||||||
const events = new EventEmitter({ captureRejections: true });
|
const events = new EventEmitter({ captureRejections: true });
|
||||||
|
|
||||||
events.on('error', function eventError(e) {
|
events.on('error', function eventError(e) {
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import type Log from 'bunyan';
|
||||||
|
import type { PrismaClient, Want, User, Listing, Offer, } from '@prisma/client';
|
||||||
|
import groupBy from 'lodash/groupBy';
|
||||||
|
import sortBy from 'lodash/sortBy';
|
||||||
|
|
||||||
|
import { logAxiosError } from './utils';
|
||||||
|
import { Pg } from './pg';
|
||||||
|
|
||||||
|
export enum Status {
|
||||||
|
Pending = 'pending',
|
||||||
|
Accepted = 'accepted',
|
||||||
|
Ordered = 'ordered',
|
||||||
|
Delivered = 'delivered',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function createOffers(log: Log, pg: Pg) {
|
||||||
|
async function generate(seller: number, bulkShipping: number, baseShipping: number): Offer[] {
|
||||||
|
log.info('generateOffers', { seller, bulkShipping, baseShipping });
|
||||||
|
const listings = await pg.listingFindSeller(seller);
|
||||||
|
|
||||||
|
log.info('creating offers for listings', { listings })
|
||||||
|
|
||||||
|
const shipping = bulkShipping / listings.length;
|
||||||
|
const offers = listings.map(l => ({
|
||||||
|
userId: l.want.user.id,
|
||||||
|
listingId: l.id,
|
||||||
|
wantId: l.want.id,
|
||||||
|
|
||||||
|
status: Status.Pending,
|
||||||
|
|
||||||
|
item: l.data.price.value,
|
||||||
|
shipping,
|
||||||
|
total: l.data.price.value + shipping,
|
||||||
|
|
||||||
|
// without mcrates...
|
||||||
|
comparison: l.data.price.value + baseShipping,
|
||||||
|
}))
|
||||||
|
|
||||||
|
// we must sort the offers by price,
|
||||||
|
// sometimes a seller has multiples of a want
|
||||||
|
// to deal with this we just ignore duplicates on insert
|
||||||
|
// and ensure the cheapest one goes in first
|
||||||
|
const sorted = sortBy(offers, ['item']);
|
||||||
|
const inserts = await pg.offersCreate(sorted);
|
||||||
|
console.log(inserts);
|
||||||
|
return inserts;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
generate,
|
||||||
|
};
|
||||||
|
}
|
||||||
+49
-4
@@ -1,6 +1,6 @@
|
|||||||
import bcrypt from 'bcrypt';
|
import bcrypt from 'bcrypt';
|
||||||
import type Log from 'bunyan';
|
import type Log from 'bunyan';
|
||||||
import type { PrismaClient, Want, User, Listing, 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';
|
||||||
|
|
||||||
export interface UserCreateParams {
|
export interface UserCreateParams {
|
||||||
@@ -13,15 +13,20 @@ export interface Pg {
|
|||||||
userFindEmail: (email: Email) => Promise<User>,
|
userFindEmail: (email: Email) => Promise<User>,
|
||||||
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[]>,
|
||||||
|
|
||||||
|
listingUpsert: (listing: Listing, want: Want) => Promise<Listing>,
|
||||||
|
listingFind: (id: DiscogsListing) => Promise<Listing>,
|
||||||
|
listingFindSeller: (id: DiscogsSeller) => Promise<Listing[]>,
|
||||||
|
listingRmUnlisted: (want: Want, ids: DiscogsListing[]) => Promise<number>,
|
||||||
|
|
||||||
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[]>,
|
||||||
listingUpsert: (listing: Listing, want: Want) => Promise<Listing>,
|
|
||||||
listingFind: (id: DiscogsListing) => Promise<Listing>,
|
|
||||||
listingRmUnlisted: (want: Want, ids: DiscogsListing[]) => Promise<number>,
|
|
||||||
|
|
||||||
bundleCandidates: () => Promise<Listing[][]>,
|
bundleCandidates: () => Promise<Listing[][]>,
|
||||||
|
|
||||||
|
offersCreate: (offers: Offer[]) => Promise<Offer[]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function createPg(log: Log, prisma: PrismaClient): Pg {
|
export default function createPg(log: Log, prisma: PrismaClient): Pg {
|
||||||
@@ -37,6 +42,15 @@ export default function createPg(log: Log, prisma: PrismaClient): Pg {
|
|||||||
return prisma.want.findMany({ where: { userId: user.id }});
|
return prisma.want.findMany({ where: { userId: user.id }});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function userOffers(user: User) {
|
||||||
|
return prisma.offer.findMany({
|
||||||
|
where: { userId: user.id },
|
||||||
|
include: {
|
||||||
|
want: true,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function userCreate(params: UserCreateParams) {
|
async function userCreate(params: UserCreateParams) {
|
||||||
const hashRounds = 10;
|
const hashRounds = 10;
|
||||||
const password = await bcrypt.hash(params.password, hashRounds);
|
const password = await bcrypt.hash(params.password, hashRounds);
|
||||||
@@ -131,6 +145,19 @@ export default function createPg(log: Log, prisma: PrismaClient): Pg {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function listingFindSeller(seller: DiscogsSeller): Promise<Listing[]> {
|
||||||
|
return prisma.listing.findMany({
|
||||||
|
where: {
|
||||||
|
seller,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
want: {
|
||||||
|
include: { user: true },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function bundleCandidates(): Promise<Listing[][]> {
|
async function bundleCandidates(): Promise<Listing[][]> {
|
||||||
const candidates = await prisma.$queryRaw`
|
const candidates = await prisma.$queryRaw`
|
||||||
select
|
select
|
||||||
@@ -157,11 +184,27 @@ export default function createPg(log: Log, prisma: PrismaClient): Pg {
|
|||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function offersCreate(offers: Offer[]) {
|
||||||
|
return await prisma.$transaction(
|
||||||
|
offers.map(offer => prisma.offer.upsert({
|
||||||
|
where: {
|
||||||
|
userId_wantId: {
|
||||||
|
userId: offer.userId,
|
||||||
|
wantId: offer.wantId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
create: offer,
|
||||||
|
update: offer
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
userFindId,
|
userFindId,
|
||||||
userFindEmail,
|
userFindEmail,
|
||||||
userCreate,
|
userCreate,
|
||||||
userWants,
|
userWants,
|
||||||
|
userOffers,
|
||||||
|
|
||||||
wantCreate,
|
wantCreate,
|
||||||
wantList,
|
wantList,
|
||||||
@@ -169,7 +212,9 @@ export default function createPg(log: Log, prisma: PrismaClient): Pg {
|
|||||||
listingUpsert,
|
listingUpsert,
|
||||||
listingFind,
|
listingFind,
|
||||||
listingRmUnlisted,
|
listingRmUnlisted,
|
||||||
|
listingFindSeller,
|
||||||
|
|
||||||
bundleCandidates,
|
bundleCandidates,
|
||||||
|
offersCreate,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
+10
-8
@@ -1,17 +1,19 @@
|
|||||||
import { PrismaClient } from '@prisma/client'
|
import { PrismaClient } from '@prisma/client'
|
||||||
|
|
||||||
import discogsService from './discogs';
|
import createDiscogs from './discogs';
|
||||||
import pgService from './pg';
|
import createOffers from './offers';
|
||||||
import createLog from './log';
|
import createLog from './log';
|
||||||
import createApi from './api';
|
import createApi from './api';
|
||||||
import { eventService } from './events';
|
import createEvents from './events';
|
||||||
|
import createPg from './pg';
|
||||||
|
|
||||||
export default function createServices(log) {
|
export default function creates(log) {
|
||||||
const prisma = new PrismaClient();
|
const prisma = new PrismaClient();
|
||||||
const pg = pgService(log, prisma);
|
const pg = createPg(log, prisma);
|
||||||
const discogs = discogsService(log, pg);
|
const discogs = createDiscogs(log, pg);
|
||||||
const events = eventService(log, pg, discogs);
|
const events = createEvents(log, pg, discogs);
|
||||||
const api = createApi(log, pg, discogs, events);
|
const offers = createOffers(log, pg);
|
||||||
|
const api = createApi(log, pg, discogs, offers, events);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
api,
|
api,
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
This application uses Discogs’ API but is not affiliated with, sponsored or endorsed by Discogs. ‘Discogs’ is a trademark of Zink Media, LLC.
|
This application uses Discogs’ API but is not affiliated with, sponsored or endorsed by Discogs. ‘Discogs’ is a trademark of Zink Media, LLC.
|
||||||
|
Data provided by Discogs.
|
||||||
+1
-1
@@ -20,7 +20,7 @@ function App() {
|
|||||||
const [error, user] = await actions.getLogin();
|
const [error, user] = await actions.getLogin();
|
||||||
if (error) return route('/login');
|
if (error) return route('/login');
|
||||||
setUser(user);
|
setUser(user);
|
||||||
route('/search');
|
route('/bundles');
|
||||||
}
|
}
|
||||||
|
|
||||||
getLogin();
|
getLogin();
|
||||||
|
|||||||
@@ -1,21 +1,43 @@
|
|||||||
import { h } from 'preact';
|
import { h } from 'preact';
|
||||||
|
|
||||||
export default function Bundle(bundle) {
|
export default function Bundle(bundle) {
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="bundle">
|
<div class="bundle">
|
||||||
<h2>{bundle.username} - {bundle.seller}</h2>
|
<section>
|
||||||
<h3>{bundle.items.length} items - {bundle.total.toFixed(2)} {bundle.currency}</h3>
|
<h2>{bundle.username} - {bundle.seller}</h2>
|
||||||
<h4>Ships: {bundle.mentionsAustralia && 'AU'} {bundle.mentionsWorldwide && 'WORLD'}</h4>
|
<h3>{bundle.items.length} items - {bundle.total.toFixed(2)} {bundle.currency}</h3>
|
||||||
{bundle.items.map(i => (
|
<h4>Ships: {bundle.mentionsAustralia && 'AU'} {bundle.mentionsWorldwide && 'WORLD'}</h4>
|
||||||
<div key={i.id}>
|
</section>
|
||||||
<a href={i.uri} target="_blank" >
|
|
||||||
<h4>{i.description}</h4>
|
<section>
|
||||||
</a>
|
<form action="">
|
||||||
<h5>{i.user.name}</h5>
|
<div class="item">
|
||||||
<h5>{i.price} {i.currency}</h5>
|
<label for="shipping">Full Shipping Cost</label>
|
||||||
<hr />
|
<input type="number" name="shipping" id="shipping" required />
|
||||||
</div>
|
</div>
|
||||||
))}
|
<div class="item">
|
||||||
|
<label for="baseShipping">Base Shipping Cost</label>
|
||||||
|
<input type="number" name="baseShipping" id="baseShipping" required />
|
||||||
|
</div>
|
||||||
|
<div class="item">
|
||||||
|
<input type="submit" value="Generate Offers" />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class='items'>
|
||||||
|
{bundle.items.map(i => (
|
||||||
|
<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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1,19 +1,15 @@
|
|||||||
import { h } from 'preact';
|
import { h } from 'preact';
|
||||||
|
import { ExternalLink } from 'react-feather';
|
||||||
|
|
||||||
export default function Bundle(bundle) {
|
export default function Offer(offer) {
|
||||||
|
const { want: { data: details } } = offer;
|
||||||
return (
|
return (
|
||||||
<div class="bundle">
|
<div class="offer">
|
||||||
<h3>{bundle.items.length} items - {bundle.total.toFixed(2)} {bundle.currency}</h3>
|
<h2>{details.artists_sort} - {details.title} <a href={details.uri} target="_blank"><ExternalLink /></a></h2>
|
||||||
{bundle.items.map(i => (
|
<h5 class='alt-text' >{details.year}</h5>
|
||||||
<div key={i.id}>
|
<h5 class='alt-text'>{details.formats[0].descriptions.join(' ')}</h5>
|
||||||
<a href={i.uri} target="_blank" >
|
<img src={details.thumb} />
|
||||||
<h4>{i.description}</h4>
|
<h5>${offer.total}</h5>
|
||||||
</a>
|
|
||||||
<h5>{i.user.name}</h5>
|
|
||||||
<h5>{i.price} {i.currency}</h5>
|
|
||||||
<hr />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -18,7 +18,7 @@ function Bundles() {
|
|||||||
|
|
||||||
if (!bundles.length) {
|
if (!bundles.length) {
|
||||||
return (
|
return (
|
||||||
<div class={'home'}>
|
<div class={'bundles'}>
|
||||||
loading...
|
loading...
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -26,7 +26,7 @@ function Bundles() {
|
|||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={'home'}>
|
<div class={'bundles'}>
|
||||||
{bundles.map(Bundle)}
|
{bundles.map(Bundle)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ function Home() {
|
|||||||
async function getOffers() {
|
async function getOffers() {
|
||||||
const [error, offers] = await actions.getOffers();
|
const [error, offers] = await actions.getOffers();
|
||||||
if (error) return console.error(error);
|
if (error) return console.error(error);
|
||||||
|
console.log(offers);
|
||||||
setOffers(offers);
|
setOffers(offers);
|
||||||
}
|
}
|
||||||
getOffers();
|
getOffers();
|
||||||
@@ -18,7 +19,7 @@ function Home() {
|
|||||||
|
|
||||||
if (!offers.length) {
|
if (!offers.length) {
|
||||||
return (
|
return (
|
||||||
<div class={'home'}>
|
<div class={'offers'}>
|
||||||
loading...
|
loading...
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -26,7 +27,7 @@ function Home() {
|
|||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={'home'}>
|
<div class={'offers'}>
|
||||||
{offers.map(Offer)}
|
{offers.map(Offer)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -283,12 +283,30 @@ header {
|
|||||||
padding-bottom: var(--base-spacing);
|
padding-bottom: var(--base-spacing);
|
||||||
}
|
}
|
||||||
|
|
||||||
.home {
|
.bundles {
|
||||||
display: flex;
|
flex-flow: row;
|
||||||
|
|
||||||
.bundle {
|
.bundle {
|
||||||
flex: 1;
|
display: grid;
|
||||||
padding-right: 1em;
|
grid-template-columns: 2fr 1fr;
|
||||||
|
margin-bottom: var(--base-spacing);
|
||||||
|
|
||||||
|
.items {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(5, 1fr);
|
||||||
|
align-items: stretch;
|
||||||
|
|
||||||
|
div {
|
||||||
|
margin-right: var(--base-spacing);
|
||||||
|
margin-bottom: var(--base-spacing);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.offers {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
Reference in new issue
Block a user