offers page

This commit is contained in:
ntr committed 2022-08-20 22:36:17 +10:00
1 parent 323c7877b1
commit 453c2da045
13 files changed
+123 -128

No files matched your search

-2
View File
@@ -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
View File
@@ -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;
+9
View File
@@ -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
View File
@@ -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 },
+9 -3
View File
@@ -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,
}; };
}); });
+2 -6
View File
@@ -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
View File
@@ -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>
); );
+2 -1
View File
@@ -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>
+19
View File
@@ -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>
)
}
+35
View File
@@ -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;
+8 -8
View File
@@ -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>
); );
} }
-70
View File
@@ -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;
+18 -1
View File
@@ -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);