Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5b0c7c05ca | ||
|
|
a97560c1c7 | ||
|
|
00cb224fb3 | ||
|
|
6c3cac7381 | ||
|
|
b91f877045 |
No files matched your search
@@ -1,15 +1,12 @@
|
||||
# TODO
|
||||
push offers
|
||||
|
||||
listen / notify
|
||||
on offers
|
||||
notify admins
|
||||
add last synced time to want
|
||||
|
||||
go over and reorganise event logic
|
||||
|
||||
contact options on landing page
|
||||
|
||||
move want add to rpc
|
||||
|
||||
skip unofficial release
|
||||
|
||||
status stepper
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ async function start() {
|
||||
notify,
|
||||
} = createServices(log);
|
||||
|
||||
await notify.connect();
|
||||
await notify.listen();
|
||||
|
||||
api.listen();
|
||||
}
|
||||
|
||||
@@ -11,6 +11,11 @@ datasource db {
|
||||
}
|
||||
|
||||
model Listing {
|
||||
|
||||
// example listingId 2148851036
|
||||
// works fine as a JS number
|
||||
// but JS numbers are cast to int4 for pg by prisma
|
||||
// becomes -2146116260
|
||||
id BigInt @id
|
||||
seller BigInt
|
||||
data Json
|
||||
|
||||
@@ -63,16 +63,6 @@ export default function createApi(log: Log, pg: Pg, discogs: Discogs, offers: Of
|
||||
ctx.status = 200;
|
||||
});
|
||||
|
||||
router.post('/want', async function wantPost(ctx, next) {
|
||||
ctx.assert(ctx.isAuthenticated(), 401, 'log in required');
|
||||
const { releaseId } = ctx.request.body;
|
||||
const user = await pg.userFindId(ctx.state.user.id);
|
||||
const release = await discogs.release(releaseId);
|
||||
const want = await pg.wantCreate(release, user);
|
||||
events.emit(Events.WantsAdded, [want]);
|
||||
ctx.response.body = [want];
|
||||
});
|
||||
|
||||
router.post('/want/discogs', async function wantListPost(ctx, next) {
|
||||
ctx.assert(ctx.isAuthenticated(), 401, 'log in required');
|
||||
const { username } = ctx.request.body;
|
||||
|
||||
+1
-1
@@ -48,7 +48,7 @@ export default function createDiscogs(log: Log, pg: Pg) {
|
||||
// @ts-ignore
|
||||
const additions = results.filter(r => r.status === 'fulfilled').map(r => r.value);
|
||||
|
||||
log.info('want list additions', additions);
|
||||
log.info('want list additions', { additions: additions.length });
|
||||
|
||||
return additions;
|
||||
}
|
||||
|
||||
+16
-4
@@ -12,10 +12,11 @@ import routerAuth from './routers/router.auth';
|
||||
export enum Events {
|
||||
WantsAdded = 'WantsAdded',
|
||||
Sync = 'Sync',
|
||||
ListingsUpdated = 'ListingsUpdated',
|
||||
Connection = 'Connection',
|
||||
|
||||
OffersUpdated = 'OffersUpdated'
|
||||
ListingsUpdated = 'ListingsUpdated',
|
||||
OffersUpdated = 'OffersUpdated',
|
||||
WantsUpdated = 'WantsUpdated',
|
||||
}
|
||||
|
||||
export default function createEvents(log: Log, pg: Pg, discogs, offers) {
|
||||
@@ -43,7 +44,7 @@ export default function createEvents(log: Log, pg: Pg, discogs, offers) {
|
||||
async function updateWantListings(wants: Want[]) {
|
||||
for (const want of wants) {
|
||||
const listings = await populateListings(want);
|
||||
log.info({ listings: listings.length, ...wantDetails(want) }, 'listings added');
|
||||
log.info({ listings: listings.length, ...wantDetails(want) }, 'listings found');
|
||||
events.emit(Events.ListingsUpdated);
|
||||
}
|
||||
|
||||
@@ -67,6 +68,10 @@ export default function createEvents(log: Log, pg: Pg, discogs, offers) {
|
||||
events.on(Events.ListingsUpdated, async function listingsUpdated() {
|
||||
const updatedOffers = await offers.generate()
|
||||
log.debug({ offers: updatedOffers.length }, 'offers up to date');
|
||||
|
||||
for (const [id, rpc] of connections) {
|
||||
rpc.pushUserOffers();
|
||||
}
|
||||
});
|
||||
|
||||
events.on(Events.Connection, async function onConnection({ rpc, id }) {
|
||||
@@ -78,7 +83,14 @@ export default function createEvents(log: Log, pg: Pg, discogs, offers) {
|
||||
const rpc = connections.get(offer.userId);
|
||||
if (!rpc) return null;
|
||||
log.debug({ event: Events.OffersUpdated, rpc, offer });
|
||||
return rpc.sendUserOffers();
|
||||
return rpc.pushUserOffers();
|
||||
});
|
||||
|
||||
events.on(Events.WantsUpdated, async function onWantsUpdated(want: Want) {
|
||||
const rpc = connections.get(want.userId);
|
||||
if (!rpc) return null;
|
||||
log.debug({ event: Events.WantsUpdated, rpc, want });
|
||||
return rpc.pushUserWants();
|
||||
});
|
||||
|
||||
return events;
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import createSubscriber from 'pg-listen'
|
||||
import type Log from 'bunyan';
|
||||
|
||||
import { Events } from './events';
|
||||
|
||||
export default function createNotify(log: Log, events) {
|
||||
const CHANNEL = 'events';
|
||||
const subscriber = createSubscriber({ connectionString: process.env.DATABASE_URL });
|
||||
|
||||
subscriber.notifications.on(CHANNEL, ({ table, action, record }) => {
|
||||
if (table === 'offers') return events.emit(Events.OffersUpdated, record);
|
||||
})
|
||||
|
||||
async function connect() {
|
||||
await subscriber.connect();
|
||||
return subscriber.listenTo(CHANNEL);
|
||||
}
|
||||
|
||||
return { connect };
|
||||
}
|
||||
+11
-6
@@ -18,22 +18,27 @@ export default function createNotify(log: Log, pg: Pg, events) {
|
||||
|
||||
async function handleOffers(payload) {
|
||||
const { action, id } = payload;
|
||||
log.info({ channel: OFFERS_CHANNEL, action, id });
|
||||
log.debug({ channel: OFFERS_CHANNEL, action, id });
|
||||
if (action === 'DELETE') return null;
|
||||
|
||||
// const offer = await pg.offerFindId(id);
|
||||
// events.emit(Events.OffersUpdated, offer);
|
||||
}
|
||||
|
||||
async function handleUsers(payload) {
|
||||
const { action, id } = payload;
|
||||
log.info({ channel: USERS_CHANNEL, action, id });
|
||||
log.debug({ channel: USERS_CHANNEL, action, id });
|
||||
}
|
||||
|
||||
async function handleListings(payload) {
|
||||
const { action, id } = payload;
|
||||
log.info({ channel: LISTINGS_CHANNEL, action, id });
|
||||
log.debug({ channel: LISTINGS_CHANNEL, action, id });
|
||||
}
|
||||
|
||||
async function handleWants(payload) {
|
||||
const { action, id } = payload;
|
||||
log.info({ channel: WANTS_CHANNEL, action, id });
|
||||
// const want = await pg.offerFindId(id);
|
||||
log.debug({ channel: WANTS_CHANNEL, action, id });
|
||||
}
|
||||
|
||||
subscriber.notifications.on(OFFERS_CHANNEL, handleOffers);
|
||||
@@ -41,7 +46,7 @@ export default function createNotify(log: Log, pg: Pg, events) {
|
||||
subscriber.notifications.on(LISTINGS_CHANNEL, handleListings);
|
||||
subscriber.notifications.on(WANTS_CHANNEL, handleWants);
|
||||
|
||||
async function connect() {
|
||||
async function listen() {
|
||||
await subscriber.connect();
|
||||
await Promise.all([
|
||||
subscriber.listenTo(OFFERS_CHANNEL),
|
||||
@@ -51,5 +56,5 @@ export default function createNotify(log: Log, pg: Pg, events) {
|
||||
]);
|
||||
}
|
||||
|
||||
return { connect };
|
||||
return { listen };
|
||||
}
|
||||
@@ -24,6 +24,7 @@ export default function createPg(log: Log, prisma: PrismaClient) {
|
||||
return prisma.want.findMany({
|
||||
where: { userId: user.id },
|
||||
include: { _count: { select: { offers: true }} },
|
||||
orderBy: { offers: { _count: 'desc' } }
|
||||
});
|
||||
}
|
||||
|
||||
@@ -97,6 +98,12 @@ export default function createPg(log: Log, prisma: PrismaClient) {
|
||||
});
|
||||
}
|
||||
|
||||
async function wantFindId(id: UUID) {
|
||||
return prisma.want.findFirstOrThrow({
|
||||
where: { id },
|
||||
});
|
||||
}
|
||||
|
||||
async function wantList(user: User) {
|
||||
return prisma.want.findMany({
|
||||
where: { userId: user.id },
|
||||
|
||||
+99
-80
@@ -12,6 +12,81 @@ import { Status } from './offers';
|
||||
import { isAdmin } from './utils';
|
||||
import { Events } from './events';
|
||||
|
||||
export type Rpc = ReturnType<typeof createRpc>;
|
||||
|
||||
export enum RpcMessageType {
|
||||
Ping = 'Ping',
|
||||
Pong = 'Pong',
|
||||
|
||||
// points out user is anon
|
||||
WhoDis = 'WhoDis',
|
||||
|
||||
Bundles = 'Bundles',
|
||||
Offers = 'Offers',
|
||||
List = 'List',
|
||||
Wants = 'Wants',
|
||||
User = 'User',
|
||||
|
||||
Error = 'Error',
|
||||
|
||||
OfferSamples = 'OfferSamples',
|
||||
OfferStatusSet = 'OfferStatusSet',
|
||||
|
||||
AcpOfferStatusSet = 'AcpOfferStatusSet',
|
||||
AcpOffers = 'AcpOffers',
|
||||
AcpListingsSync = 'AcpListingsSync',
|
||||
|
||||
WantRm = 'WantRm',
|
||||
WantAdd = 'WantAdd',
|
||||
}
|
||||
|
||||
type ErrorMessage = [RpcMessageType.Error, string];
|
||||
type PongMessage = [RpcMessageType.Pong, []];
|
||||
type PingMessage = [RpcMessageType.Ping, []];
|
||||
type WhoDisMessage = [RpcMessageType.WhoDis, []];
|
||||
type UserMessage = [RpcMessageType.User, User];
|
||||
type WantsMessage = [RpcMessageType.Wants, Want[]];
|
||||
type OffersMessage = [RpcMessageType.Offers, Offer[]];
|
||||
type OfferStatusSetMessage = [RpcMessageType.OfferStatusSet, Offer[]];
|
||||
type OfferSamplesMessage = [RpcMessageType.OfferSamples, Offer[]];
|
||||
|
||||
type AcpOfferStatusSetMessage = [RpcMessageType.AcpOfferStatusSet, Offer[]];
|
||||
type AcpOffersMessage = [RpcMessageType.AcpOffers, Offer[]];
|
||||
type AcpListingsSyncMessage = [RpcMessageType.AcpListingsSync, []];
|
||||
|
||||
export type RpcMessage =
|
||||
PingMessage |
|
||||
PongMessage |
|
||||
WhoDisMessage |
|
||||
|
||||
UserMessage |
|
||||
|
||||
WantsMessage |
|
||||
|
||||
OffersMessage |
|
||||
OfferSamplesMessage |
|
||||
OfferStatusSetMessage |
|
||||
|
||||
AcpOfferStatusSetMessage |
|
||||
AcpOffersMessage |
|
||||
AcpListingsSyncMessage |
|
||||
|
||||
ErrorMessage;
|
||||
|
||||
|
||||
type OfferStatusSetParams = {
|
||||
id: UUID,
|
||||
status: Status,
|
||||
};
|
||||
|
||||
type WantRmParams = {
|
||||
id: UUID,
|
||||
};
|
||||
|
||||
type WantAddParams = {
|
||||
releaseId: number,
|
||||
};
|
||||
|
||||
function checkMsgType(msgType: RpcMessageType): msgType is RpcMessageType {
|
||||
if (RpcMessageType[msgType]) {
|
||||
return true;
|
||||
@@ -54,6 +129,18 @@ export default function createRpc(log: Log, pg: Pg, ws, discogs: Discogs, offers
|
||||
return [RpcMessageType.Wants, wants];
|
||||
}
|
||||
|
||||
async function wantAdd(params: WantAddParams): Promise<WantsMessage> {
|
||||
const { releaseId } = params;
|
||||
assert(releaseId, 'no id supplied');
|
||||
|
||||
const release = await discogs.release(releaseId);
|
||||
const want = await pg.wantCreate(release, user);
|
||||
events.emit(Events.WantsAdded, [want]);
|
||||
|
||||
const wants = await pg.userWants(user);
|
||||
return [RpcMessageType.Wants, wants];
|
||||
}
|
||||
|
||||
async function acpOfferStatusSet(params: OfferStatusSetParams): Promise<AcpOffersMessage> {
|
||||
const { id, status } = params;
|
||||
assert(isAdmin(user), 'not authorised');
|
||||
@@ -101,6 +188,8 @@ export default function createRpc(log: Log, pg: Pg, ws, discogs: Discogs, offers
|
||||
|
||||
case RpcMessageType.WantRm:
|
||||
return wantRm(params);
|
||||
case RpcMessageType.WantAdd:
|
||||
return wantAdd(params);
|
||||
|
||||
case RpcMessageType.AcpOfferStatusSet:
|
||||
return acpOfferStatusSet(params);
|
||||
@@ -134,11 +223,8 @@ export default function createRpc(log: Log, pg: Pg, ws, discogs: Discogs, offers
|
||||
const userMsg: UserMessage = [RpcMessageType.User, user];
|
||||
send(userMsg);
|
||||
|
||||
await sendUserOffers();
|
||||
|
||||
const wants = await pg.userWants(user);
|
||||
const wantsMsg: WantsMessage = [RpcMessageType.Wants, wants];
|
||||
send(wantsMsg);
|
||||
await pushUserOffers();
|
||||
await pushUserWants();
|
||||
|
||||
if (isAdmin(user)) return adminConnection();
|
||||
}
|
||||
@@ -151,10 +237,12 @@ export default function createRpc(log: Log, pg: Pg, ws, discogs: Discogs, offers
|
||||
))
|
||||
}
|
||||
|
||||
async function sendUserOffers() {
|
||||
const userOffers = await offers.userOffers(user);
|
||||
const offersMsg: OffersMessage = [RpcMessageType.Offers, userOffers];
|
||||
send(offersMsg);
|
||||
async function pushUserOffers() {
|
||||
send(await userOffers());
|
||||
}
|
||||
|
||||
async function pushUserWants() {
|
||||
send(await userWants());
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -162,76 +250,7 @@ export default function createRpc(log: Log, pg: Pg, ws, discogs: Discogs, offers
|
||||
send,
|
||||
onConnection,
|
||||
|
||||
sendUserOffers,
|
||||
pushUserOffers,
|
||||
pushUserWants,
|
||||
}
|
||||
}
|
||||
|
||||
export type Rpc = ReturnType<typeof createRpc>;
|
||||
|
||||
export enum RpcMessageType {
|
||||
Ping = 'Ping',
|
||||
Pong = 'Pong',
|
||||
|
||||
// points out user is anon
|
||||
WhoDis = 'WhoDis',
|
||||
|
||||
Bundles = 'Bundles',
|
||||
Offers = 'Offers',
|
||||
List = 'List',
|
||||
Wants = 'Wants',
|
||||
User = 'User',
|
||||
|
||||
Error = 'Error',
|
||||
|
||||
OfferSamples = 'OfferSamples',
|
||||
OfferStatusSet = 'OfferStatusSet',
|
||||
|
||||
AcpOfferStatusSet = 'AcpOfferStatusSet',
|
||||
AcpOffers = 'AcpOffers',
|
||||
AcpListingsSync = 'AcpListingsSync',
|
||||
|
||||
WantRm = 'WantRm',
|
||||
}
|
||||
|
||||
type ErrorMessage = [RpcMessageType.Error, string];
|
||||
type PongMessage = [RpcMessageType.Pong, []];
|
||||
type PingMessage = [RpcMessageType.Ping, []];
|
||||
type WhoDisMessage = [RpcMessageType.WhoDis, []];
|
||||
type UserMessage = [RpcMessageType.User, User];
|
||||
type WantsMessage = [RpcMessageType.Wants, Want[]];
|
||||
type OffersMessage = [RpcMessageType.Offers, Offer[]];
|
||||
type OfferStatusSetMessage = [RpcMessageType.OfferStatusSet, Offer[]];
|
||||
type OfferSamplesMessage = [RpcMessageType.OfferSamples, Offer[]];
|
||||
|
||||
type AcpOfferStatusSetMessage = [RpcMessageType.AcpOfferStatusSet, Offer[]];
|
||||
type AcpOffersMessage = [RpcMessageType.AcpOffers, Offer[]];
|
||||
type AcpListingsSyncMessage = [RpcMessageType.AcpListingsSync, []];
|
||||
|
||||
export type RpcMessage =
|
||||
PingMessage |
|
||||
PongMessage |
|
||||
WhoDisMessage |
|
||||
|
||||
UserMessage |
|
||||
|
||||
WantsMessage |
|
||||
|
||||
OffersMessage |
|
||||
OfferSamplesMessage |
|
||||
OfferStatusSetMessage |
|
||||
|
||||
AcpOfferStatusSetMessage |
|
||||
AcpOffersMessage |
|
||||
AcpListingsSyncMessage |
|
||||
|
||||
ErrorMessage;
|
||||
|
||||
|
||||
type OfferStatusSetParams = {
|
||||
id: UUID,
|
||||
status: Status,
|
||||
};
|
||||
|
||||
type WantRmParams = {
|
||||
id: UUID,
|
||||
};
|
||||
+6
-6
@@ -9,7 +9,7 @@ import store, { wsState } from './store'
|
||||
|
||||
import Nav from './components/nav';
|
||||
|
||||
import Offers from './routes/offers';
|
||||
import Main from './routes/main';
|
||||
import Acp from './routes/acp';
|
||||
import Login from './routes/login';
|
||||
import Search from './routes/search';
|
||||
@@ -18,7 +18,7 @@ import Info from './routes/info';
|
||||
|
||||
if (window.location.host === 'matescrates.au') LogRocket.init('yh0dy3/mcrates');
|
||||
|
||||
const Main = () => {
|
||||
const Mcrates = () => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
// i have no idea why this is needed,
|
||||
@@ -38,10 +38,10 @@ const Main = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<main id="app">
|
||||
<div id="mcrates">
|
||||
<Nav />
|
||||
<Router onChange={routeChange}>
|
||||
<Route path="/" component={Offers} />
|
||||
<Route path="/" component={Main} />
|
||||
<Route path="/login" component={Login} mode="login" />
|
||||
<Route path="/register" component={Login} mode="register" />
|
||||
<Route path="/search" component={Search} />
|
||||
@@ -51,14 +51,14 @@ const Main = () => {
|
||||
{/* ACP */}
|
||||
<Route path="/acp" component={Acp} />
|
||||
</Router>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const App = () => {
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<Main />
|
||||
<Mcrates />
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -49,8 +49,12 @@ function OfferPriceBtn(offer) {
|
||||
? offer.status
|
||||
: 'Accept';
|
||||
|
||||
const title = accepted
|
||||
? 'Click to cancel'
|
||||
: 'Click to accept this offer';
|
||||
|
||||
return (
|
||||
<button disabled={confirmed} onClick={() => setStatus()} class={classes}>${offer.total} {offer.condition}</button>
|
||||
<button disabled={confirmed} title={title} onClick={() => setStatus()} class={classes}>${offer.total} {offer.condition}</button>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -63,7 +67,7 @@ export default function OfferGroup(group, i) {
|
||||
const detailText = `${format} ${descriptions}`;
|
||||
|
||||
return (
|
||||
<div class="offer" key={i}>
|
||||
<div class="offer" key={i} id={group.details.id}>
|
||||
<section>
|
||||
<h3>{group.details.artists_sort} - {group.details.title} <a href={group.details.uri} target="_blank"><ExternalLink /></a></h3>
|
||||
<div class='alt-text details'>
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { h } from 'preact';
|
||||
import { useState, useCallback } from 'preact/hooks';
|
||||
import { useState } from 'preact/hooks';
|
||||
import { ExternalLink } from 'react-feather';
|
||||
|
||||
import { useSelector } from 'react-redux'
|
||||
import { State } from './../store'
|
||||
|
||||
import http from './../http';
|
||||
|
||||
function Want(want) {
|
||||
const r = want.data;
|
||||
|
||||
@@ -14,10 +12,13 @@ function Want(want) {
|
||||
const [confirm, setConfirm] = useState(false);
|
||||
|
||||
function onClick() {
|
||||
if (confirm) return ws.sendWantRm(want.id);
|
||||
if (confirm) {
|
||||
setConfirm(false);
|
||||
return ws.sendWantRm(want.id);
|
||||
}
|
||||
setTimeout(() => setConfirm(false), 2500);
|
||||
return setConfirm(true);
|
||||
};
|
||||
}
|
||||
|
||||
const text = confirm
|
||||
? 'confirm'
|
||||
|
||||
@@ -33,15 +33,10 @@ api.interceptors.response.use(onSuccess, onError);
|
||||
|
||||
const actions = {
|
||||
// TODO move to rpc
|
||||
|
||||
postWantSearch: async function postWantSearch(query) {
|
||||
return api.post('/want/search', { query });
|
||||
},
|
||||
|
||||
postWant: async function postWant(releaseId) {
|
||||
return api.post('/want', { releaseId });
|
||||
},
|
||||
|
||||
postDiscogs: async function postDiscogs(username) {
|
||||
return api.post('/want/discogs', { username });
|
||||
},
|
||||
@@ -71,14 +66,6 @@ const actions = {
|
||||
password,
|
||||
});
|
||||
},
|
||||
|
||||
postOffers: async function postOffers(listingIds, shipping, baseShipping) {
|
||||
return api.post('/offers', {
|
||||
listingIds,
|
||||
shipping,
|
||||
baseShipping,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default actions;
|
||||
@@ -10,7 +10,6 @@ import Want from './../components/want';
|
||||
|
||||
const List = () => {
|
||||
const wants = useSelector((state: State) => state.wants);
|
||||
const user = useSelector((state: State) => state.user);
|
||||
|
||||
if (!wants) {
|
||||
return <div> loading... </div>;
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { h } from 'preact';
|
||||
import { route } from 'preact-router';
|
||||
import { useSelector } from 'react-redux'
|
||||
|
||||
import { State } from './../store';
|
||||
import * as toast from './../toast';
|
||||
|
||||
import OfferGroup from './../components/offer.group';
|
||||
|
||||
const GroupNav = (want) => {
|
||||
const href = want._count.offers ? `#${want.release}` : null;
|
||||
return (
|
||||
<div class=''>
|
||||
<a href={href}>{want.data.artists_sort} - {want.data.title}</a>
|
||||
<span>{want._count.offers}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function Home() {
|
||||
const offers = useSelector((state: State) => state.offers);
|
||||
const wants = useSelector((state: State) => state.wants);
|
||||
|
||||
if (offers && offers.length === 0) {
|
||||
toast.info('no offers', 'You have no offers yet, search for some more items.');
|
||||
route('/search');
|
||||
}
|
||||
|
||||
if (!offers || !wants) return <div>loading...</div>;
|
||||
|
||||
return (
|
||||
<main >
|
||||
<nav>
|
||||
<div class='header'>
|
||||
<span>Record</span>
|
||||
<span>Offers</span>
|
||||
</div>
|
||||
{wants.map(GroupNav)}
|
||||
</nav>
|
||||
<div class={'offers'}>
|
||||
{offers.map(OfferGroup)}
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
export default Home;
|
||||
@@ -1,27 +0,0 @@
|
||||
import { h } from 'preact';
|
||||
import { route } from 'preact-router';
|
||||
import { useSelector } from 'react-redux'
|
||||
|
||||
import { State } from './../store';
|
||||
import * as toast from './../toast';
|
||||
|
||||
import OfferGroup from './../components/offer.group';
|
||||
|
||||
function Home() {
|
||||
const offers = useSelector((state: State) => state.offers);
|
||||
|
||||
if (offers && offers.length === 0) {
|
||||
toast.info('no offers', 'You have no offers yet, search for some more items.');
|
||||
route('/search');
|
||||
}
|
||||
|
||||
if (!offers) return <div>loading...</div>;
|
||||
|
||||
return (
|
||||
<div class={'offers'}>
|
||||
{offers.map(OfferGroup)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Home;
|
||||
@@ -7,25 +7,22 @@ import { State } from './../store';
|
||||
|
||||
import http from '../http';
|
||||
|
||||
function Result(r, wants) {
|
||||
const [added, setAdded] = useState(false)
|
||||
|
||||
function Result(r, wants, ws) {
|
||||
async function onClick() {
|
||||
const [error, want] = await http.postWant(r.id);
|
||||
if (error) return false;
|
||||
setAdded(true);
|
||||
ws.sendWantAdd(r.id);
|
||||
}
|
||||
|
||||
const wanted = wants.some(w => w.release === r.id);
|
||||
// releaseIds are bigints on server
|
||||
const wanted = wants.some(w => BigInt(w.release) === BigInt(r.id));
|
||||
|
||||
const classes = `${added || wanted ? 'added' : ''}`;
|
||||
const btnText = added
|
||||
const classes = `${wanted ? 'added' : ''}`;
|
||||
const btnText = wanted
|
||||
? <Check />
|
||||
: '+';
|
||||
|
||||
return (
|
||||
<article key={r.id}>
|
||||
<button onClick={onClick} disabled={added} class={classes} >{btnText}</button>
|
||||
<button onClick={onClick} disabled={wanted} class={classes} >{btnText}</button>
|
||||
<div>
|
||||
<h3>{r.title} <a href={`https://discogs.com${r.uri}`} target="_blank"><ExternalLink /></a></h3>
|
||||
<h5 class='alt-text' >{r.year}</h5>
|
||||
@@ -45,6 +42,7 @@ const User = () => {
|
||||
const [infoText, setInfoText] = useState('');
|
||||
|
||||
const wants = useSelector((state: State) => state.wants);
|
||||
const ws = useSelector((state: State) => state.ws);
|
||||
|
||||
async function fetchResults() {
|
||||
const [error, results] = await http.postWantSearch(query);
|
||||
@@ -68,7 +66,8 @@ const User = () => {
|
||||
setInfoText(`Added ${wants.length} items to your list.`);
|
||||
}
|
||||
|
||||
const searchRef = useRef<HTMLInputElement>()
|
||||
const searchRef = useRef<HTMLInputElement>();
|
||||
const resultsRef = useRef<HTMLDivElement>();
|
||||
|
||||
useEffect(() => {
|
||||
searchRef.current.focus()
|
||||
@@ -85,7 +84,7 @@ const User = () => {
|
||||
}
|
||||
|
||||
const topEl = results.length
|
||||
? <button onClick={() => window.scrollTo(0, 0)}> top </button>
|
||||
? <button onClick={() => resultsRef.current.scrollTo(0, 0)}> top </button>
|
||||
: null;
|
||||
|
||||
const infoEl = infoText
|
||||
@@ -111,9 +110,9 @@ const User = () => {
|
||||
<hr />
|
||||
<p>Data provided by Discogs.</p>
|
||||
</div>
|
||||
<div class='results'>
|
||||
<div class='results' ref={resultsRef}>
|
||||
{infoEl}
|
||||
{results.map(r => Result(r, wants))}
|
||||
{results.map(r => Result(r, wants, ws))}
|
||||
{topEl}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Barlow';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url(https://fonts.gstatic.com/s/barlow/v12/7cHpv4kjgoGqM7E_A8s52Hs.woff2) format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Barlow';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url(https://fonts.gstatic.com/s/barlow/v12/7cHpv4kjgoGqM7E_Ass52Hs.woff2) format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Barlow';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url(https://fonts.gstatic.com/s/barlow/v12/7cHpv4kjgoGqM7E_DMs5.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;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Noto Sans Mono';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-stretch: 100%;
|
||||
src: url(https://fonts.gstatic.com/s/notosansmono/v21/BngrUXNETWXI6LwhGYvaxZikqZqK6fBq6kPvUce2oAZcdthSBUsYck4-_FNJ09DdVXQQ.woff2) format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Noto Sans Mono';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-stretch: 100%;
|
||||
src: url(https://fonts.gstatic.com/s/notosansmono/v21/BngrUXNETWXI6LwhGYvaxZikqZqK6fBq6kPvUce2oAZcdthSBUsYck4-_FNJ09ndVXQQ.woff2) format('woff2');
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* greek-ext */
|
||||
@font-face {
|
||||
font-family: 'Noto Sans Mono';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-stretch: 100%;
|
||||
src: url(https://fonts.gstatic.com/s/notosansmono/v21/BngrUXNETWXI6LwhGYvaxZikqZqK6fBq6kPvUce2oAZcdthSBUsYck4-_FNJ09HdVXQQ.woff2) format('woff2');
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
/* greek */
|
||||
@font-face {
|
||||
font-family: 'Noto Sans Mono';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-stretch: 100%;
|
||||
src: url(https://fonts.gstatic.com/s/notosansmono/v21/BngrUXNETWXI6LwhGYvaxZikqZqK6fBq6kPvUce2oAZcdthSBUsYck4-_FNJ097dVXQQ.woff2) format('woff2');
|
||||
unicode-range: U+0370-03FF;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Noto Sans Mono';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-stretch: 100%;
|
||||
src: url(https://fonts.gstatic.com/s/notosansmono/v21/BngrUXNETWXI6LwhGYvaxZikqZqK6fBq6kPvUce2oAZcdthSBUsYck4-_FNJ09LdVXQQ.woff2) format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Noto Sans Mono';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-stretch: 100%;
|
||||
src: url(https://fonts.gstatic.com/s/notosansmono/v21/BngrUXNETWXI6LwhGYvaxZikqZqK6fBq6kPvUce2oAZcdthSBUsYck4-_FNJ09PdVXQQ.woff2) format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Noto Sans Mono';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-stretch: 100%;
|
||||
src: url(https://fonts.gstatic.com/s/notosansmono/v21/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;
|
||||
}
|
||||
+11
-379
@@ -23,32 +23,6 @@ img {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Raleway';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url(https://fonts.gstatic.com/s/raleway/v28/1Ptxg8zYS_SKggPN4iEgvnHyvveLxVvaorCIPrE.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: '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);
|
||||
@@ -71,7 +45,7 @@ html {
|
||||
--medium-font-size: 1.777em;
|
||||
--large-font-size: 2.369em;
|
||||
--xlarge-font-size: 3.157em;
|
||||
--base-font-family: 'Raleway', Arial, Sans-Serif;
|
||||
--base-font-family: 'Barlow', Arial, Sans-Serif;
|
||||
--secondary-font-family: 'Noto Sans Mono', monospace;
|
||||
--base-font-color: var(--color-off-white);
|
||||
--secondary-font-color: var(--color-mid-grey);
|
||||
@@ -131,6 +105,11 @@ button, input, optgroup, select, textarea {
|
||||
line-height:var(--base-line-height);
|
||||
}
|
||||
|
||||
button, [type=submit] {
|
||||
padding: var(--spacing-half);
|
||||
background: var(--background-1);
|
||||
}
|
||||
|
||||
input:hover, textarea:hover {
|
||||
color:var(--base-font-color);
|
||||
border:var(--hover-border)
|
||||
@@ -156,7 +135,7 @@ button, [type=button], [type=reset], [type=submit] {
|
||||
|
||||
font-family: var(--secondary-font-family);
|
||||
font-size: var(--small-font-size);
|
||||
font-weight: bold;
|
||||
// font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
|
||||
&:hover {
|
||||
@@ -194,180 +173,6 @@ p {
|
||||
margin:0 0 var(--spacing)
|
||||
}
|
||||
|
||||
// ok actual styles
|
||||
|
||||
body {
|
||||
padding-top: calc(var(--spacing)/2);
|
||||
}
|
||||
|
||||
main {
|
||||
padding: 0 var(--spacing-double) 0 var(--spacing-double);
|
||||
}
|
||||
|
||||
button, [type=submit] {
|
||||
padding: var(--spacing-half);
|
||||
background: var(--background-1);
|
||||
|
||||
}
|
||||
|
||||
.landing {
|
||||
display: grid;
|
||||
|
||||
grid-template-columns: 1fr 2fr;
|
||||
grid-gap: calc(var(--spacing-double) * 2);
|
||||
grid-template-areas:
|
||||
"login swirl"
|
||||
"login words";
|
||||
|
||||
.login {
|
||||
grid-area: login;
|
||||
button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.swirl {
|
||||
grid-area: swirl;
|
||||
height: 15rem;
|
||||
}
|
||||
|
||||
.words {
|
||||
grid-area: words;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.info {
|
||||
display: grid;
|
||||
|
||||
grid-template-areas:
|
||||
"words swirl"
|
||||
"words contact";
|
||||
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-rows: min-content 1fr;
|
||||
|
||||
section {
|
||||
margin-bottom: var(--spacing);
|
||||
}
|
||||
|
||||
.contact {
|
||||
grid-area: contact;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.words {
|
||||
grid-area: words;
|
||||
}
|
||||
|
||||
.swirl {
|
||||
grid-area: swirl;
|
||||
height: 15rem;
|
||||
}
|
||||
}
|
||||
|
||||
.alt-text, .alt {
|
||||
font-family: var(--secondary-font-family);
|
||||
// font-size: var(--small-font-size);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
article {
|
||||
background-color: var(--background-1);
|
||||
|
||||
button {
|
||||
border-color: var(--background-2);
|
||||
}
|
||||
}
|
||||
|
||||
.search {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 2fr;
|
||||
|
||||
.results {
|
||||
display: grid;
|
||||
padding-left: var(--spacing);
|
||||
grid-gap: var(--spacing);
|
||||
|
||||
article {
|
||||
display: grid;
|
||||
padding: var(--spacing);
|
||||
grid-template-columns: 1fr 4fr min-content;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
margin-right: var(--spacing);
|
||||
background: var(--background-2);
|
||||
border-color: var(--background-1);
|
||||
|
||||
&.added {
|
||||
background: var(--color-green);
|
||||
cursor: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.list {
|
||||
display: grid;
|
||||
grid-template-columns:
|
||||
1fr 1fr 1fr;
|
||||
|
||||
grid-gap: var(--spacing);
|
||||
|
||||
article {
|
||||
display: grid;
|
||||
|
||||
// grid-template-rows:
|
||||
// 1fr min-content min-content;
|
||||
|
||||
grid-template-areas:
|
||||
"title img"
|
||||
"details details"
|
||||
"remove remove";
|
||||
|
||||
button {
|
||||
margin-top: var(--spacing-half);
|
||||
grid-area: remove
|
||||
}
|
||||
|
||||
.details {
|
||||
grid-area: details;
|
||||
}
|
||||
|
||||
padding: var(--spacing);
|
||||
|
||||
img {
|
||||
justify-self: end;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
form {
|
||||
margin-bottom: var(--spacing);
|
||||
|
||||
label, input {
|
||||
display: block;
|
||||
}
|
||||
|
||||
input, button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
input {
|
||||
padding: var(--spacing-half);
|
||||
margin-bottom: var(--spacing);
|
||||
}
|
||||
|
||||
label {
|
||||
font-family: var(--secondary-font-family);
|
||||
font-size: var(--small-font-size);
|
||||
text-transform: uppercase;
|
||||
margin-bottom: var(--spacing-half);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--secondary-font-color);
|
||||
font-family: var(--secondary-font-family);
|
||||
@@ -378,184 +183,11 @@ a {
|
||||
}
|
||||
}
|
||||
|
||||
nav {
|
||||
a {
|
||||
font-size: var(--small-font-size);
|
||||
margin-right: var(--spacing);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
header {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr min-content;
|
||||
padding-bottom: var(--spacing);
|
||||
|
||||
a {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.bundles {
|
||||
flex-flow: row;
|
||||
|
||||
.bundle {
|
||||
margin-bottom: var(--spacing-double);
|
||||
|
||||
section {
|
||||
margin-bottom: var(--spacing);
|
||||
}
|
||||
|
||||
.items {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
|
||||
div {
|
||||
margin-right: var(--spacing);
|
||||
margin-bottom: var(--spacing);
|
||||
|
||||
display: grid;
|
||||
|
||||
&.bold {
|
||||
font-weight: bold;
|
||||
background: var(--background-2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.offers {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-gap: var(--spacing);
|
||||
|
||||
.offer {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr min-content;
|
||||
grid-template-rows: min-content min-content;
|
||||
grid-template-areas:
|
||||
"details img"
|
||||
"lines lines";
|
||||
|
||||
padding: var(--spacing);
|
||||
background: var(--background-1);
|
||||
|
||||
.details {
|
||||
font-size: var(--small-font-size);
|
||||
}
|
||||
|
||||
.status {
|
||||
margin-top: var(--spacing-half);
|
||||
}
|
||||
|
||||
section {
|
||||
grid-area: details;
|
||||
}
|
||||
|
||||
img {
|
||||
grid-area: img;
|
||||
justify-self: end;
|
||||
margin-left: var(--spacing-half);
|
||||
}
|
||||
|
||||
.lines {
|
||||
grid-area: lines;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
column-gap: var(--spacing-half);
|
||||
|
||||
button {
|
||||
margin-top: var(--spacing-half);
|
||||
border-color: var(--background-2);
|
||||
|
||||
&.accepted {
|
||||
background: var(--color-accepted);
|
||||
}
|
||||
|
||||
&.confirmed {
|
||||
cursor: auto;
|
||||
background: var(--color-confirmed);
|
||||
}
|
||||
|
||||
&.neg {
|
||||
margin-left: var(--spacing-half);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.number {
|
||||
.alt-text, .alt {
|
||||
font-family: var(--secondary-font-family);
|
||||
// font-size: var(--small-font-size);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.swirl {
|
||||
background-image: url("./../assets/swirl.fix.svg");
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
|
||||
margin-bottom: var(--spacing);
|
||||
}
|
||||
|
||||
@media (max-width:900px) {
|
||||
.landing {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-areas:
|
||||
"swirl"
|
||||
"login"
|
||||
"words";
|
||||
}
|
||||
|
||||
.list {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.search {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.offers {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.info {
|
||||
grid-template-columns: 1fr;
|
||||
// is necessary for some reason
|
||||
grid-template-areas:
|
||||
"swirl"
|
||||
"contact"
|
||||
"words";
|
||||
}
|
||||
|
||||
main {
|
||||
padding: 0 var(--spacing) 0 var(--spacing);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width:1200px) {
|
||||
img {
|
||||
width: 5rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ------------
|
||||
// toasts
|
||||
|
||||
.iziToast {
|
||||
border-radius: 0;
|
||||
border-color: var(--base-border-color);
|
||||
|
||||
.iziToast-body {
|
||||
font-family: var(--base-font-family);
|
||||
font-size: var(--base-font-size);
|
||||
}
|
||||
|
||||
.iziToast-title {
|
||||
font-family: var(--secondary-font-family);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
@import 'fonts.less';
|
||||
@import 'styles.less';
|
||||
@@ -0,0 +1,410 @@
|
||||
body {
|
||||
padding-top: calc(var(--spacing)/2);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#mcrates {
|
||||
padding: 0 var(--spacing-double) var(--spacing) var(--spacing-double);
|
||||
display: grid;
|
||||
|
||||
grid-template-areas:
|
||||
"header header"
|
||||
"main main"
|
||||
"footer footer";
|
||||
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: min-content 1fr min-content;
|
||||
}
|
||||
|
||||
.landing {
|
||||
display: grid;
|
||||
|
||||
grid-template-columns: 1fr 2fr;
|
||||
grid-gap: calc(var(--spacing-double) * 2);
|
||||
grid-template-areas:
|
||||
"login swirl"
|
||||
"login words";
|
||||
|
||||
.login {
|
||||
grid-area: login;
|
||||
button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.swirl {
|
||||
grid-area: swirl;
|
||||
height: 15rem;
|
||||
}
|
||||
|
||||
.words {
|
||||
grid-area: words;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.info {
|
||||
display: grid;
|
||||
|
||||
grid-template-areas:
|
||||
"words swirl"
|
||||
"words contact";
|
||||
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-rows: min-content 1fr;
|
||||
|
||||
section {
|
||||
margin-bottom: var(--spacing);
|
||||
}
|
||||
|
||||
.contact {
|
||||
grid-area: contact;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.words {
|
||||
grid-area: words;
|
||||
}
|
||||
|
||||
.swirl {
|
||||
grid-area: swirl;
|
||||
height: 15rem;
|
||||
}
|
||||
}
|
||||
|
||||
article {
|
||||
background-color: var(--background-1);
|
||||
|
||||
button {
|
||||
border-color: var(--background-2);
|
||||
}
|
||||
}
|
||||
|
||||
.search {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 2fr;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
.results {
|
||||
display: grid;
|
||||
padding-left: var(--spacing);
|
||||
grid-gap: var(--spacing);
|
||||
|
||||
article {
|
||||
display: grid;
|
||||
padding: var(--spacing);
|
||||
grid-template-columns: 1fr 4fr min-content;
|
||||
}
|
||||
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
button {
|
||||
margin-right: var(--spacing);
|
||||
background: var(--background-2);
|
||||
border-color: var(--background-1);
|
||||
|
||||
&.added {
|
||||
background: var(--color-green);
|
||||
cursor: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.list {
|
||||
display: grid;
|
||||
grid-template-columns:
|
||||
1fr 1fr 1fr;
|
||||
|
||||
grid-gap: var(--spacing);
|
||||
overflow: scroll;
|
||||
|
||||
article {
|
||||
display: grid;
|
||||
|
||||
grid-template-areas:
|
||||
"title img"
|
||||
"details details"
|
||||
"remove remove";
|
||||
|
||||
button {
|
||||
margin-top: var(--spacing-half);
|
||||
grid-area: remove
|
||||
}
|
||||
|
||||
.details {
|
||||
grid-area: details;
|
||||
}
|
||||
|
||||
padding: var(--spacing);
|
||||
|
||||
img {
|
||||
justify-self: end;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
form {
|
||||
margin-bottom: var(--spacing);
|
||||
|
||||
label, input {
|
||||
display: block;
|
||||
}
|
||||
|
||||
input, button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
input {
|
||||
padding: var(--spacing-half);
|
||||
margin-bottom: var(--spacing);
|
||||
}
|
||||
|
||||
label {
|
||||
font-family: var(--secondary-font-family);
|
||||
font-size: var(--small-font-size);
|
||||
text-transform: uppercase;
|
||||
margin-bottom: var(--spacing-half);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
header {
|
||||
grid-area: header;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr min-content;
|
||||
|
||||
border-bottom: var(--base-border);
|
||||
padding-bottom: var(--spacing-half);
|
||||
margin-bottom: var(--spacing-half);
|
||||
|
||||
a {
|
||||
font-size: var(--small-font-size);
|
||||
margin-right: var(--spacing);
|
||||
}
|
||||
|
||||
a {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
main {
|
||||
grid-area: main;
|
||||
|
||||
display: grid;
|
||||
grid-template-areas:
|
||||
"nav content";
|
||||
|
||||
grid-template-columns:
|
||||
1fr 1fr;
|
||||
|
||||
column-gap: var(--spacing);
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
nav {
|
||||
// duck scrollbars
|
||||
padding-right: var(--spacing-half);
|
||||
|
||||
grid-area: nav;
|
||||
|
||||
div {
|
||||
display: grid;
|
||||
grid-template-columns:
|
||||
1fr min-content min-content;
|
||||
|
||||
column-gap: var(--spacing-half);
|
||||
|
||||
a {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
.offers {
|
||||
grid-area: content;
|
||||
overflow: scroll;
|
||||
}
|
||||
}
|
||||
|
||||
.bundles {
|
||||
flex-flow: row;
|
||||
|
||||
.bundle {
|
||||
margin-bottom: var(--spacing-double);
|
||||
|
||||
section {
|
||||
margin-bottom: var(--spacing);
|
||||
}
|
||||
|
||||
.items {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
|
||||
div {
|
||||
margin-right: var(--spacing);
|
||||
margin-bottom: var(--spacing);
|
||||
|
||||
display: grid;
|
||||
|
||||
&.bold {
|
||||
font-weight: bold;
|
||||
background: var(--background-2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.offers {
|
||||
display: grid;
|
||||
// grid-template-columns: 1fr 1fr;
|
||||
grid-gap: var(--spacing);
|
||||
|
||||
.offer {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr min-content;
|
||||
grid-template-rows: min-content min-content;
|
||||
grid-template-areas:
|
||||
"details img"
|
||||
"lines lines";
|
||||
|
||||
padding: var(--spacing);
|
||||
background: var(--background-1);
|
||||
|
||||
.details {
|
||||
font-size: var(--small-font-size);
|
||||
}
|
||||
|
||||
.status {
|
||||
margin-top: var(--spacing-half);
|
||||
}
|
||||
|
||||
section {
|
||||
grid-area: details;
|
||||
}
|
||||
|
||||
img {
|
||||
grid-area: img;
|
||||
justify-self: end;
|
||||
margin-left: var(--spacing-half);
|
||||
}
|
||||
|
||||
.lines {
|
||||
grid-area: lines;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
column-gap: var(--spacing-half);
|
||||
|
||||
button {
|
||||
margin-top: var(--spacing-half);
|
||||
border-color: var(--background-2);
|
||||
|
||||
&.accepted {
|
||||
background: var(--color-accepted);
|
||||
}
|
||||
|
||||
&.confirmed {
|
||||
cursor: auto;
|
||||
background: var(--color-confirmed);
|
||||
}
|
||||
|
||||
&.neg {
|
||||
margin-left: var(--spacing-half);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.number {
|
||||
font-family: var(--secondary-font-family);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.swirl {
|
||||
background-image: url("./../assets/swirl.fix.svg");
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
|
||||
margin-bottom: var(--spacing);
|
||||
}
|
||||
|
||||
@media (max-width:900px) {
|
||||
.landing {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-areas:
|
||||
"swirl"
|
||||
"login"
|
||||
"words";
|
||||
}
|
||||
|
||||
.list {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.search {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.offers {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.info {
|
||||
grid-template-columns: 1fr;
|
||||
// is necessary for some reason
|
||||
grid-template-areas:
|
||||
"swirl"
|
||||
"contact"
|
||||
"words";
|
||||
}
|
||||
|
||||
#mcrates {
|
||||
padding: 0 var(--spacing) 0 var(--spacing);
|
||||
}
|
||||
|
||||
main {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: 1fr 3fr;
|
||||
grid-gap: var(--spacing-half);
|
||||
grid-template-areas:
|
||||
"nav"
|
||||
"content";
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width:1200px) {
|
||||
img {
|
||||
width: 5rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ------------
|
||||
// toasts
|
||||
|
||||
.iziToast {
|
||||
border-radius: 0;
|
||||
border-color: var(--base-border-color);
|
||||
|
||||
.iziToast-body {
|
||||
font-family: var(--base-font-family);
|
||||
font-size: var(--base-font-size);
|
||||
}
|
||||
|
||||
.iziToast-title {
|
||||
font-family: var(--secondary-font-family);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
|
||||
<% preact.headEnd %>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -3,7 +3,7 @@ import toast from 'izitoast';
|
||||
export function info(title, message) {
|
||||
toast.info({
|
||||
title,
|
||||
message,
|
||||
message: message || '',
|
||||
position: 'bottomCenter',
|
||||
color: '#1f2128',
|
||||
titleColor: '#F4F1D0',
|
||||
|
||||
+8
-1
@@ -1,5 +1,6 @@
|
||||
import { wantsState, wsState, userState, offersState, acpOffersState } from './store'
|
||||
import debounce from 'lodash/debounce';
|
||||
|
||||
import { wantsState, userState, offersState, acpOffersState } from './store'
|
||||
import * as toast from './toast';
|
||||
|
||||
const SOCKET_URL = `${window.location.protocol === 'https:' ? 'wss://' : 'ws://'}${window.location.host}/api/ws`;
|
||||
@@ -36,6 +37,10 @@ export default function createSocket(dispatch) {
|
||||
send(['WantRm', { id }]);
|
||||
}
|
||||
|
||||
function sendWantAdd(releaseId) {
|
||||
send(['WantAdd', { releaseId }]);
|
||||
}
|
||||
|
||||
function sendOffers() {
|
||||
send(['Offers', {}]);
|
||||
}
|
||||
@@ -116,6 +121,7 @@ export default function createSocket(dispatch) {
|
||||
// Connection opened
|
||||
function onOpen() {
|
||||
console.log('websocket connected', SOCKET_URL)
|
||||
toast.info('connected', 'connected to server');
|
||||
sendPing();
|
||||
}
|
||||
|
||||
@@ -155,6 +161,7 @@ export default function createSocket(dispatch) {
|
||||
|
||||
sendOfferStatusSet,
|
||||
sendWantRm,
|
||||
sendWantAdd,
|
||||
|
||||
sendOffers,
|
||||
sendWants,
|
||||
|
||||
Reference in new issue
Block a user