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
|
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||||
email String @unique
|
email String @unique
|
||||||
password String
|
password String
|
||||||
discogs String? @unique
|
|
||||||
data Json?
|
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
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() {
|
function listen() {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
app.on('error', err => {
|
app.on('error', err => {
|
||||||
@@ -125,15 +108,12 @@ export default function createApi(log: Log, pg: Pg, discogs, events) {
|
|||||||
ctx.response.body = bundles;
|
ctx.response.body = bundles;
|
||||||
})
|
})
|
||||||
|
|
||||||
router.get('/user/:id/offers', async function userOffersGet(ctx, next) {
|
router.get('/user/offers', async function userOffersGet(ctx, next) {
|
||||||
const { id } = ctx.request.params;
|
ctx.assert(ctx.isAuthenticated(), 401, 'log in required');
|
||||||
const user = await pg.userFindId(id);
|
const user = ctx.state.user;
|
||||||
|
|
||||||
ctx.assert(user, 404, `user ${id} not found`);
|
|
||||||
|
|
||||||
const bundles = (await getBundles())
|
const bundles = (await getBundles())
|
||||||
.map(b => {
|
.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;
|
return b;
|
||||||
})
|
})
|
||||||
.filter(b => b.items.length);
|
.filter(b => b.items.length);
|
||||||
@@ -141,13 +121,6 @@ export default function createApi(log: Log, pg: Pg, discogs, events) {
|
|||||||
ctx.response.body = bundles;
|
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) {
|
router.post('/want', async function wantPost(ctx, next) {
|
||||||
ctx.assert(ctx.isAuthenticated(), 401, 'log in required');
|
ctx.assert(ctx.isAuthenticated(), 401, 'log in required');
|
||||||
const { releaseId } = ctx.request.body;
|
const { releaseId } = ctx.request.body;
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ import type { PrismaClient, Want, User, Listing } from '@prisma/client';
|
|||||||
import { logAxiosError } from './utils';
|
import { logAxiosError } from './utils';
|
||||||
import { Pg } from './pg';
|
import { Pg } from './pg';
|
||||||
|
|
||||||
|
// margh
|
||||||
|
// vanderbabe
|
||||||
|
// jmcdermid1
|
||||||
|
|
||||||
export default function discogs(log: Log, pg: Pg) {
|
export default function discogs(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'
|
||||||
@@ -35,6 +39,8 @@ export default function discogs(log: Log, pg: Pg) {
|
|||||||
const { data: { wants, pagination }} = await api.get(`${apiBase}/users/${username}/wants`);
|
const { data: { wants, pagination }} = await api.get(`${apiBase}/users/${username}/wants`);
|
||||||
const releases = await Promise.all(wants.map(want => release(want.id)));
|
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
|
// convert want object to actual release and ignore duplicate errors
|
||||||
const results = await Promise.allSettled(releases.map(release => pg.wantCreate(release, user)));
|
const results = await Promise.allSettled(releases.map(release => pg.wantCreate(release, user)));
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@@ -74,6 +80,9 @@ export default function discogs(log: Log, pg: Pg) {
|
|||||||
|
|
||||||
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({ 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));
|
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';
|
import * as Bluebird from 'bluebird';
|
||||||
|
|
||||||
export interface UserCreateParams {
|
export interface UserCreateParams {
|
||||||
discogs: DiscogsName | null,
|
|
||||||
email: string,
|
email: string,
|
||||||
password: string,
|
password: string,
|
||||||
data: any,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Pg {
|
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 password = await bcrypt.hash(params.password, hashRounds);
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
discogs: params.discogs,
|
|
||||||
email: params.email,
|
email: params.email,
|
||||||
data: params.data,
|
|
||||||
password,
|
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) {
|
async function wantList(user: User) {
|
||||||
return prisma.want.findMany({
|
return prisma.want.findMany({
|
||||||
where: { userId: user.id },
|
where: { userId: user.id },
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ export default function authRouter(log, pg: Pg) {
|
|||||||
ctx.status = 200;
|
ctx.status = 200;
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
email: user.email,
|
email: user.email,
|
||||||
discogs: user.discogs,
|
|
||||||
};
|
};
|
||||||
})(ctx, next);
|
})(ctx, next);
|
||||||
});
|
});
|
||||||
@@ -57,10 +56,18 @@ export default function authRouter(log, pg: Pg) {
|
|||||||
ctx.body = {
|
ctx.body = {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
email: user.email,
|
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) => {
|
authRouter.post('/register', async (ctx, next) => {
|
||||||
const { discogs, email, password } = ctx.request.body;
|
const { discogs, email, password } = ctx.request.body;
|
||||||
// fetch discogs data if they have it
|
// fetch discogs data if they have it
|
||||||
@@ -71,7 +78,6 @@ export default function authRouter(log, pg: Pg) {
|
|||||||
ctx.body = {
|
ctx.body = {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
discogs: user.discogs,
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -36,12 +36,8 @@ const actions = {
|
|||||||
return api.get('/bundles');
|
return api.get('/bundles');
|
||||||
},
|
},
|
||||||
|
|
||||||
getUserOffers: async function getUserOffers(state, userId) {
|
getOffers: async function getOffers() {
|
||||||
return api.get(`/user/${userId}/offers`);
|
return api.get('/user/offers');
|
||||||
},
|
|
||||||
|
|
||||||
getOffers: async function getOffers(userId) {
|
|
||||||
return api.get(`/user/${userId}/offers`);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
getWants: async function getWants() {
|
getWants: async function getWants() {
|
||||||
|
|||||||
+2
-2
@@ -7,10 +7,10 @@ import actions from './actions';
|
|||||||
import Header from './components/header';
|
import Header from './components/header';
|
||||||
|
|
||||||
import Home from './routes/home';
|
import Home from './routes/home';
|
||||||
|
import Bundles from './routes/bundles';
|
||||||
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 Offers from './routes/offers';
|
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [user, setUser] = useState(null);
|
const [user, setUser] = useState(null);
|
||||||
@@ -36,10 +36,10 @@ 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} />
|
||||||
<Route path="/offers" component={Offers} user={user} />
|
|
||||||
</Router>
|
</Router>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { h } from 'preact';
|
import { h } from 'preact';
|
||||||
import { Link } from 'preact-router/match';
|
// import { Link } from 'preact-router/match';
|
||||||
|
|
||||||
const Header = ({ user }) => {
|
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>
|
||||||
|
|||||||
@@ -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 { useState, useEffect } from 'preact/hooks';
|
||||||
|
|
||||||
import actions from './../actions';
|
import actions from './../actions';
|
||||||
import Bundle from './../components/bundle';
|
import Offer from './../components/offer';
|
||||||
|
|
||||||
function Home() {
|
function Home() {
|
||||||
const [bundles, setBundles] = useState([]);
|
const [offers, setOffers] = useState([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function getBundles() {
|
async function getOffers() {
|
||||||
const [error, bundles] = await actions.getBundles();
|
const [error, offers] = await actions.getOffers();
|
||||||
if (error) return console.error(error);
|
if (error) return console.error(error);
|
||||||
setBundles(bundles);
|
setOffers(offers);
|
||||||
}
|
}
|
||||||
getBundles();
|
getOffers();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (!bundles.length) {
|
if (!offers.length) {
|
||||||
return (
|
return (
|
||||||
<div class={'home'}>
|
<div class={'home'}>
|
||||||
loading...
|
loading...
|
||||||
@@ -27,7 +27,7 @@ function Home() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={'home'}>
|
<div class={'home'}>
|
||||||
{bundles.map(Bundle)}
|
{offers.map(Offer)}
|
||||||
</div>
|
</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;
|
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
|
// stuff from UX laws
|
||||||
html {
|
html {
|
||||||
color:var(--base-font-color);
|
color:var(--base-font-color);
|
||||||
@@ -72,7 +89,7 @@ html {
|
|||||||
--large-font-size: 2.369em;
|
--large-font-size: 2.369em;
|
||||||
--xlarge-font-size: 3.157em;
|
--xlarge-font-size: 3.157em;
|
||||||
--base-font-family: 'Raleway', Arial, Sans-Serif;
|
--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);
|
--base-font-color: var(--color-off-white);
|
||||||
--secondary-font-color: var(--color-mid-grey);
|
--secondary-font-color: var(--color-mid-grey);
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user