diff --git a/TODO.md b/TODO.md index 102f2cc..a7c6b34 100644 --- a/TODO.md +++ b/TODO.md @@ -1,11 +1,10 @@ # TODO -push offers - add last synced time to want -contact options on landing page +go over and reorganise event logic + should be connection.service ? -move want add to rpc +contact options on landing page skip unofficial release diff --git a/api/index.ts b/api/index.ts index 5094a43..21fa04b 100644 --- a/api/index.ts +++ b/api/index.ts @@ -29,7 +29,7 @@ async function start() { notify, } = createServices(log); - await notify.connect(); + await notify.listen(); api.listen(); } diff --git a/api/prisma/schema.prisma b/api/prisma/schema.prisma index a336aa9..2098e28 100644 --- a/api/prisma/schema.prisma +++ b/api/prisma/schema.prisma @@ -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 diff --git a/api/src/api.ts b/api/src/api.ts index 77fbc40..f4bc9d9 100644 --- a/api/src/api.ts +++ b/api/src/api.ts @@ -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; diff --git a/api/src/events.ts b/api/src/events.ts index d3d901d..5d06a75 100644 --- a/api/src/events.ts +++ b/api/src/events.ts @@ -12,17 +12,21 @@ import routerAuth from './routers/router.auth'; export enum Events { WantsAdded = 'WantsAdded', Sync = 'Sync', - ListingsUpdated = 'ListingsUpdated', Connection = 'Connection', - OfferUpdated = 'OfferUpdated', - WantUpdated = 'WantUpdated', + ListingsUpdated = 'ListingsUpdated', + OffersUpdated = 'OffersUpdated', + WantsUpdated = 'WantsUpdated', } export default function createEvents(log: Log, pg: Pg, discogs, offers) { const events = new EventEmitter({ captureRejections: true }); const connections = new Map(); + events.on('error', function eventError(e) { + log.error('event error', e); + }); + async function populateListings(want: Want) { try { const listings = await discogs.listings(want); @@ -37,57 +41,62 @@ export default function createEvents(log: Log, pg: Pg, discogs, offers) { } } - events.on('error', function eventError(e) { - log.error('event error', e); - }); - async function updateWantListings(wants: Want[]) { for (const want of wants) { const listings = await populateListings(want); log.info({ listings: listings.length, ...wantDetails(want) }, 'listings found'); - events.emit(Events.ListingsUpdated); + await offers.generate(); + log.info('offers updated'); + await notifyUser(want.userId); } log.info({ wants: wants.length }, 'finished syncing'); } + async function notifyUser(id: UUID) { + const rpc = connections.get(id); + if (!rpc) return null; + rpc.pushUserWants(); + rpc.pushUserOffers(); + }; + events.on(Events.WantsAdded, async function wantsAdded(wants: Want[]) { log.info({ event: Events.WantsAdded, wants: wants.map(w => wantDetails(w)) }, 'starting listings population'); await updateWantListings(wants); log.info({ event: Events.WantsAdded, wants: wants.map(w => wantDetails(w)) }, 'finished populating listings'); - events.emit(Events.ListingsUpdated); }); events.on(Events.Sync, async function sync() { const wants = await pg.wantsToSync(); log.info({ event: Events.Sync, wants: wants.length }); await updateWantListings(wants); - events.emit(Events.ListingsUpdated); }); - events.on(Events.ListingsUpdated, async function listingsUpdated() { - const updatedOffers = await offers.generate() - log.debug({ offers: updatedOffers.length }, 'offers up to date'); - }); + // events.on(Events.ListingsUpdated, async function listingsUpdated() { + // const updatedOffers = await offers.generate() + // log.debug({ offers: updatedOffers.length }, 'offers up to date'); + // }); events.on(Events.Connection, async function onConnection({ rpc, id }) { log.debug('new rpc connection', { id, rpc }); connections.set(id, rpc); }) - events.on(Events.OfferUpdated, async function onOfferUpdated(offer: Offer) { - const rpc = connections.get(offer.userId); - if (!rpc) return null; - log.debug({ event: Events.OfferUpdated, rpc, offer }); - return rpc.pushUserOffers(); - }); + // emitted by listen/notify + // disabled for now + // events.on(Events.OffersUpdated, async function onOffersUpdated(offer: Offer) { + // const rpc = connections.get(offer.userId); + // if (!rpc) return null; + // log.debug({ event: Events.OffersUpdated, rpc, offer }); + // return rpc.pushUserOffers(); + // }); - events.on(Events.WantUpdated, async function onWantUpdated(want: Want) { - const rpc = connections.get(want.userId); - if (!rpc) return null; - log.debug({ event: Events.WantUpdated, rpc, want }); - return rpc.pushUserWants(); - }); + // 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; } \ No newline at end of file diff --git a/api/src/notify.ts b/api/src/notify.ts index f6c9bdf..13145d8 100644 --- a/api/src/notify.ts +++ b/api/src/notify.ts @@ -21,8 +21,8 @@ export default function createNotify(log: Log, pg: Pg, events) { log.debug({ channel: OFFERS_CHANNEL, action, id }); if (action === 'DELETE') return null; - const offer = await pg.offerFindId(id); - // events.emit(Events.OfferUpdated, offer); + // const offer = await pg.offerFindId(id); + // events.emit(Events.OffersUpdated, offer); } async function handleUsers(payload) { @@ -37,6 +37,7 @@ export default function createNotify(log: Log, pg: Pg, events) { async function handleWants(payload) { const { action, id } = payload; + // const want = await pg.offerFindId(id); log.debug({ channel: WANTS_CHANNEL, action, id }); } @@ -45,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), @@ -55,5 +56,5 @@ export default function createNotify(log: Log, pg: Pg, events) { ]); } - return { connect }; + return { listen }; } \ No newline at end of file diff --git a/api/src/pg.ts b/api/src/pg.ts index d2d06c2..e408431 100644 --- a/api/src/pg.ts +++ b/api/src/pg.ts @@ -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 }, diff --git a/api/src/rpc.ts b/api/src/rpc.ts index a007f03..e5b111c 100644 --- a/api/src/rpc.ts +++ b/api/src/rpc.ts @@ -37,6 +37,7 @@ export enum RpcMessageType { AcpListingsSync = 'AcpListingsSync', WantRm = 'WantRm', + WantAdd = 'WantAdd', } type ErrorMessage = [RpcMessageType.Error, string]; @@ -82,6 +83,10 @@ type WantRmParams = { id: UUID, }; +type WantAddParams = { + releaseId: number, +}; + function checkMsgType(msgType: RpcMessageType): msgType is RpcMessageType { if (RpcMessageType[msgType]) { return true; @@ -124,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 { + 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 { const { id, status } = params; assert(isAdmin(user), 'not authorised'); @@ -171,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); diff --git a/client/src/app.tsx b/client/src/app.tsx index 58eb9ee..ebd4a87 100644 --- a/client/src/app.tsx +++ b/client/src/app.tsx @@ -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 ( -
+
+ ); }; const App = () => { return ( -
+ ); } diff --git a/client/src/components/offer.group.tsx b/client/src/components/offer.group.tsx index 68c4bec..ee3f12b 100644 --- a/client/src/components/offer.group.tsx +++ b/client/src/components/offer.group.tsx @@ -49,8 +49,12 @@ function OfferPriceBtn(offer) { ? offer.status : 'Accept'; + const title = accepted + ? 'Click to cancel' + : 'Click to accept this offer'; + return ( - + ) } @@ -63,7 +67,7 @@ export default function OfferGroup(group, i) { const detailText = `${format} ${descriptions}`; return ( -
+

{group.details.artists_sort} - {group.details.title}

diff --git a/client/src/http.ts b/client/src/http.ts index 095b3fb..c62d685 100644 --- a/client/src/http.ts +++ b/client/src/http.ts @@ -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; diff --git a/client/src/routes/main.tsx b/client/src/routes/main.tsx new file mode 100644 index 0000000..4749dad --- /dev/null +++ b/client/src/routes/main.tsx @@ -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 ( + + ); +}; + +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
loading...
; + + return ( +
+ +
+ {offers.map(OfferGroup)} +
+
+ ); +} + +export default Home; diff --git a/client/src/routes/offers.tsx b/client/src/routes/offers.tsx deleted file mode 100644 index 8852cfc..0000000 --- a/client/src/routes/offers.tsx +++ /dev/null @@ -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
loading...
; - - return ( -
- {offers.map(OfferGroup)} -
- ); -} - -export default Home; diff --git a/client/src/routes/search.tsx b/client/src/routes/search.tsx index 9d0ce0a..430e324 100644 --- a/client/src/routes/search.tsx +++ b/client/src/routes/search.tsx @@ -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 ? : '+'; return (
- +

{r.title}

{r.year}
@@ -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() + const searchRef = useRef(); + const resultsRef = useRef(); useEffect(() => { searchRef.current.focus() @@ -85,7 +84,7 @@ const User = () => { } const topEl = results.length - ? + ? : null; const infoEl = infoText @@ -111,9 +110,9 @@ const User = () => {

Data provided by Discogs.

-
+
{infoEl} - {results.map(r => Result(r, wants))} + {results.map(r => Result(r, wants, ws))} {topEl}
diff --git a/client/src/style/fonts.less b/client/src/style/fonts.less new file mode 100644 index 0000000..9741720 --- /dev/null +++ b/client/src/style/fonts.less @@ -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; +} diff --git a/client/src/style/index.less b/client/src/style/index.less index 5baa384..1e17034 100644 --- a/client/src/style/index.less +++ b/client/src/style/index.less @@ -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,181 +183,11 @@ a { } } -header { - display: grid; - grid-template-columns: 1fr 1fr min-content; - padding-bottom: var(--spacing); - - a { - font-size: var(--small-font-size); - margin-right: 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'; diff --git a/client/src/style/styles.less b/client/src/style/styles.less new file mode 100644 index 0000000..6626b5c --- /dev/null +++ b/client/src/style/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; + } +} diff --git a/client/src/template.html b/client/src/template.html index ce1e78f..662c6a0 100644 --- a/client/src/template.html +++ b/client/src/template.html @@ -6,6 +6,7 @@ + <% preact.headEnd %> diff --git a/client/src/ws.ts b/client/src/ws.ts index 0a238ee..b22a6b5 100644 --- a/client/src/ws.ts +++ b/client/src/ws.ts @@ -37,6 +37,10 @@ export default function createSocket(dispatch) { send(['WantRm', { id }]); } + function sendWantAdd(releaseId) { + send(['WantAdd', { releaseId }]); + } + function sendOffers() { send(['Offers', {}]); } @@ -157,6 +161,7 @@ export default function createSocket(dispatch) { sendOfferStatusSet, sendWantRm, + sendWantAdd, sendOffers, sendWants,