offers page
This commit is contained in:
1 parent
323c7877b1
commit
453c2da045
13 files changed
+123
-128
No files matched your search
@@ -27,8 +27,6 @@ model User {
|
||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
email String @unique
|
||||
password String
|
||||
discogs String? @unique
|
||||
data Json?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
+4
-31
@@ -76,23 +76,6 @@ export default function createApi(log: Log, pg: Pg, discogs, events) {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
async function populateUserListings(user: User) {
|
||||
const wants = await discogs.wantlist(user);
|
||||
|
||||
// const latest = await discogs.wantlistLatest(user);
|
||||
// if (wants.length !== latest.length)
|
||||
|
||||
log.debug({ username: user.discogs, wantCount: wants.length });
|
||||
|
||||
return Bluebird.mapSeries<Want, Listing[]>(wants, async want => {
|
||||
const listings = await discogs.listings(want);
|
||||
const populated = listings.filter(l => l);
|
||||
log.debug({ id: want.id, release: want.release, populated: populated.length, total: listings.length });
|
||||
return populated;
|
||||
});
|
||||
}
|
||||
|
||||
function listen() {
|
||||
return new Promise((resolve, reject) => {
|
||||
app.on('error', err => {
|
||||
@@ -125,15 +108,12 @@ export default function createApi(log: Log, pg: Pg, discogs, events) {
|
||||
ctx.response.body = bundles;
|
||||
})
|
||||
|
||||
router.get('/user/:id/offers', async function userOffersGet(ctx, next) {
|
||||
const { id } = ctx.request.params;
|
||||
const user = await pg.userFindId(id);
|
||||
|
||||
ctx.assert(user, 404, `user ${id} not found`);
|
||||
|
||||
router.get('/user/offers', async function userOffersGet(ctx, next) {
|
||||
ctx.assert(ctx.isAuthenticated(), 401, 'log in required');
|
||||
const user = ctx.state.user;
|
||||
const bundles = (await getBundles())
|
||||
.map(b => {
|
||||
b.items = b.items.filter(i => i.user.id === user.id)
|
||||
b.items = b.items.filter(i => i.user.id === user.id);
|
||||
return b;
|
||||
})
|
||||
.filter(b => b.items.length);
|
||||
@@ -141,13 +121,6 @@ export default function createApi(log: Log, pg: Pg, discogs, events) {
|
||||
ctx.response.body = bundles;
|
||||
});
|
||||
|
||||
router.post('/user/populate', async function usersPopulate(ctx, next) {
|
||||
const { userId } = ctx.request.body;
|
||||
const user = await pg.userFindId(userId);
|
||||
const listings = await populateUserListings(user);
|
||||
ctx.response.body = listings;
|
||||
});
|
||||
|
||||
router.post('/want', async function wantPost(ctx, next) {
|
||||
ctx.assert(ctx.isAuthenticated(), 401, 'log in required');
|
||||
const { releaseId } = ctx.request.body;
|
||||
|
||||
@@ -10,6 +10,10 @@ import type { PrismaClient, Want, User, Listing } from '@prisma/client';
|
||||
import { logAxiosError } from './utils';
|
||||
import { Pg } from './pg';
|
||||
|
||||
// margh
|
||||
// vanderbabe
|
||||
// jmcdermid1
|
||||
|
||||
export default function discogs(log: Log, pg: Pg) {
|
||||
const TOKEN = 'Discogs token=wRTXuYZGOPsIQgKRGKCAkUkidGtvpSfQgPWkYIpD';
|
||||
const apiBase = 'https://api.discogs.com'
|
||||
@@ -35,6 +39,8 @@ export default function discogs(log: Log, pg: Pg) {
|
||||
const { data: { wants, pagination }} = await api.get(`${apiBase}/users/${username}/wants`);
|
||||
const releases = await Promise.all(wants.map(want => release(want.id)));
|
||||
|
||||
// TODO filter releases to be vinyl format only
|
||||
|
||||
// convert want object to actual release and ignore duplicate errors
|
||||
const results = await Promise.allSettled(releases.map(release => pg.wantCreate(release, user)));
|
||||
// @ts-ignore
|
||||
@@ -74,6 +80,9 @@ export default function discogs(log: Log, pg: Pg) {
|
||||
|
||||
const unlisted = await pg.listingRmUnlisted(want, ids);
|
||||
if (unlisted) log.info({ wantId: want.id, unlisted: unlisted }, 'missing listings removed');
|
||||
|
||||
// preferentially take listings from sellers if we hit req limits
|
||||
|
||||
return Bluebird.mapSeries(ids, async id => listing(id, want));
|
||||
}
|
||||
|
||||
|
||||
+15
-4
@@ -4,10 +4,8 @@ import type { PrismaClient, Want, User, Listing, Prisma } from '@prisma/client';
|
||||
import * as Bluebird from 'bluebird';
|
||||
|
||||
export interface UserCreateParams {
|
||||
discogs: DiscogsName | null,
|
||||
email: string,
|
||||
password: string,
|
||||
data: any,
|
||||
}
|
||||
|
||||
export interface Pg {
|
||||
@@ -44,9 +42,7 @@ export default function createPg(log: Log, prisma: PrismaClient): Pg {
|
||||
const password = await bcrypt.hash(params.password, hashRounds);
|
||||
|
||||
const data = {
|
||||
discogs: params.discogs,
|
||||
email: params.email,
|
||||
data: params.data,
|
||||
password,
|
||||
};
|
||||
|
||||
@@ -55,6 +51,21 @@ export default function createPg(log: Log, prisma: PrismaClient): Pg {
|
||||
});
|
||||
}
|
||||
|
||||
async function userUpdate(params: UserCreateParams) {
|
||||
const hashRounds = 10;
|
||||
const password = await bcrypt.hash(params.password, hashRounds);
|
||||
|
||||
const data = {
|
||||
email: params.email,
|
||||
password,
|
||||
};
|
||||
|
||||
return prisma.user.update({
|
||||
where: { email: params.email },
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
async function wantList(user: User) {
|
||||
return prisma.want.findMany({
|
||||
where: { userId: user.id },
|
||||
|
||||
@@ -44,7 +44,6 @@ export default function authRouter(log, pg: Pg) {
|
||||
ctx.status = 200;
|
||||
ctx.body = {
|
||||
email: user.email,
|
||||
discogs: user.discogs,
|
||||
};
|
||||
})(ctx, next);
|
||||
});
|
||||
@@ -57,10 +56,18 @@ export default function authRouter(log, pg: Pg) {
|
||||
ctx.body = {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
discogs: user.discogs,
|
||||
};
|
||||
});
|
||||
|
||||
authRouter.post('/logout', async (ctx, next) => {
|
||||
await passport.authenticate('local', (err, user, info, status) => {
|
||||
ctx.assert(user, 401, 'incorrect login');
|
||||
ctx.logout();
|
||||
ctx.status = 200;
|
||||
ctx.body = { success: true };
|
||||
})(ctx, next);
|
||||
});
|
||||
|
||||
authRouter.post('/register', async (ctx, next) => {
|
||||
const { discogs, email, password } = ctx.request.body;
|
||||
// fetch discogs data if they have it
|
||||
@@ -71,7 +78,6 @@ export default function authRouter(log, pg: Pg) {
|
||||
ctx.body = {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
discogs: user.discogs,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -36,12 +36,8 @@ const actions = {
|
||||
return api.get('/bundles');
|
||||
},
|
||||
|
||||
getUserOffers: async function getUserOffers(state, userId) {
|
||||
return api.get(`/user/${userId}/offers`);
|
||||
},
|
||||
|
||||
getOffers: async function getOffers(userId) {
|
||||
return api.get(`/user/${userId}/offers`);
|
||||
getOffers: async function getOffers() {
|
||||
return api.get('/user/offers');
|
||||
},
|
||||
|
||||
getWants: async function getWants() {
|
||||
|
||||
+2
-2
@@ -7,10 +7,10 @@ import actions from './actions';
|
||||
import Header from './components/header';
|
||||
|
||||
import Home from './routes/home';
|
||||
import Bundles from './routes/bundles';
|
||||
import Login from './routes/login';
|
||||
import Search from './routes/search';
|
||||
import List from './routes/list';
|
||||
import Offers from './routes/offers';
|
||||
|
||||
function App() {
|
||||
const [user, setUser] = useState(null);
|
||||
@@ -36,10 +36,10 @@ function App() {
|
||||
<Header user={user} />
|
||||
<Router>
|
||||
<Route path="/" component={Home} />
|
||||
<Route path="/bundles" component={Bundles} />
|
||||
<Route path="/login" component={Login} success={loginSuccess} />
|
||||
<Route path="/search" component={Search} />
|
||||
<Route path="/list" component={List} user={user} />
|
||||
<Route path="/offers" component={Offers} user={user} />
|
||||
</Router>
|
||||
</main>
|
||||
);
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { h } from 'preact';
|
||||
import { Link } from 'preact-router/match';
|
||||
// import { Link } from 'preact-router/match';
|
||||
|
||||
const Header = ({ user }) => {
|
||||
const nav = () => (
|
||||
<nav>
|
||||
<a href="/">Offers</a>
|
||||
<a href="/bundles">Bundles</a>
|
||||
<a href='/list'>List</a>
|
||||
<a href="/search">Search</a>
|
||||
</nav>
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { h } from 'preact';
|
||||
|
||||
export default function Bundle(bundle) {
|
||||
return (
|
||||
<div class="bundle">
|
||||
<h3>{bundle.items.length} items - {bundle.total.toFixed(2)} {bundle.currency}</h3>
|
||||
{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} {i.currency}</h5>
|
||||
<hr />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { h } from 'preact';
|
||||
import { useState, useEffect } from 'preact/hooks';
|
||||
|
||||
import actions from './../actions';
|
||||
import Bundle from './../components/bundle';
|
||||
|
||||
function Bundles() {
|
||||
const [bundles, setBundles] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
async function getBundles() {
|
||||
const [error, bundles] = await actions.getBundles();
|
||||
if (error) return console.error(error);
|
||||
setBundles(bundles);
|
||||
}
|
||||
getBundles();
|
||||
}, []);
|
||||
|
||||
if (!bundles.length) {
|
||||
return (
|
||||
<div class={'home'}>
|
||||
loading...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div class={'home'}>
|
||||
{bundles.map(Bundle)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Bundles;
|
||||
@@ -2,21 +2,21 @@ import { h } from 'preact';
|
||||
import { useState, useEffect } from 'preact/hooks';
|
||||
|
||||
import actions from './../actions';
|
||||
import Bundle from './../components/bundle';
|
||||
import Offer from './../components/offer';
|
||||
|
||||
function Home() {
|
||||
const [bundles, setBundles] = useState([]);
|
||||
const [offers, setOffers] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
async function getBundles() {
|
||||
const [error, bundles] = await actions.getBundles();
|
||||
async function getOffers() {
|
||||
const [error, offers] = await actions.getOffers();
|
||||
if (error) return console.error(error);
|
||||
setBundles(bundles);
|
||||
setOffers(offers);
|
||||
}
|
||||
getBundles();
|
||||
getOffers();
|
||||
}, []);
|
||||
|
||||
if (!bundles.length) {
|
||||
if (!offers.length) {
|
||||
return (
|
||||
<div class={'home'}>
|
||||
loading...
|
||||
@@ -27,7 +27,7 @@ function Home() {
|
||||
|
||||
return (
|
||||
<div class={'home'}>
|
||||
{bundles.map(Bundle)}
|
||||
{offers.map(Offer)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
import { h } from 'preact';
|
||||
import { useState } from 'preact/hooks';
|
||||
import axios from 'axios';
|
||||
|
||||
import actions from '../actions';
|
||||
|
||||
function result(r) {
|
||||
function onClick() {
|
||||
actions.postWant(r.id);
|
||||
}
|
||||
|
||||
return (
|
||||
<article key={r.id}>
|
||||
<button onClick={onClick}> + </button>
|
||||
<div>
|
||||
<h3>{r.title}</h3>
|
||||
<h5 class='alt-text' >{r.year}</h5>
|
||||
<h5 class='alt-text'>{r.country}</h5>
|
||||
<h5 class='alt-text'>{r.format.join(' ')}</h5>
|
||||
</div>
|
||||
<img src={r.thumb} />
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
const User = () => {
|
||||
const [results, setResults] = useState([]);
|
||||
const [noResults, setNoResults] = useState(false);
|
||||
|
||||
async function fetchResults() {
|
||||
const { data } = await actions.getBundles();
|
||||
|
||||
console.log(data);
|
||||
|
||||
if (data.length === 0) {
|
||||
setResults([]);
|
||||
return setNoResults(true);
|
||||
}
|
||||
|
||||
setNoResults(false);
|
||||
setResults(data);
|
||||
}
|
||||
|
||||
function handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
fetchResults();
|
||||
}
|
||||
|
||||
const topEl = results.length
|
||||
? <button onClick={() => window.scrollTo(0, 0)}> top </button>
|
||||
: null;
|
||||
|
||||
const noResultsEl = noResults
|
||||
? <article>No Results</article>
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div class='search'>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div>
|
||||
{noResultsEl}
|
||||
{results.map(result)}
|
||||
{topEl}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default User;
|
||||
@@ -32,6 +32,23 @@ img {
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Source Code Pro';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url(https://fonts.gstatic.com/s/sourcecodepro/v21/HI_diYsKILxRpg3hIP6sJ7fM7PqPMcMnZFqUwX28DMyQtMlrTA.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Noto Sans Mono';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-stretch: normal;
|
||||
src: url(https://fonts.gstatic.com/s/notosansmono/v20/BngrUXNETWXI6LwhGYvaxZikqZqK6fBq6kPvUce2oAZcdthSBUsYck4-_FNJ093dVQ.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
// stuff from UX laws
|
||||
html {
|
||||
color:var(--base-font-color);
|
||||
@@ -72,7 +89,7 @@ html {
|
||||
--large-font-size: 2.369em;
|
||||
--xlarge-font-size: 3.157em;
|
||||
--base-font-family: 'Raleway', Arial, Sans-Serif;
|
||||
--secondary-font-family: monospace;
|
||||
--secondary-font-family: 'Noto Sans Mono', monospace;
|
||||
--base-font-color: var(--color-off-white);
|
||||
--secondary-font-color: var(--color-mid-grey);
|
||||
|
||||
|
||||
Reference in new issue
Block a user