bundle offers grouped

This commit is contained in:
ntr committed 2022-10-23 15:22:15 +11:00
1 parent 7946259550
commit 623d0f3938
12 files changed
+223 -143

No files matched your search

+2
View File
@@ -2,6 +2,8 @@
status stepper status stepper
queue up wantlist adds to gracefully handle rate limiting
push offers push offers
cover off admin routes and commands cover off admin routes and commands
+30 -41
View File
@@ -11,12 +11,11 @@
"dependencies": { "dependencies": {
"@koa/cors": "^3.3.0", "@koa/cors": "^3.3.0",
"@koa/router": "^12.0.0", "@koa/router": "^12.0.0",
"@prisma/client": "^4.1.0", "@prisma/client": "^4.5.0",
"bcrypt": "^5.0.1", "bcrypt": "^5.0.1",
"bluebird": "^3.7.2", "bluebird": "^3.7.2",
"bunyan": "^1.8.15", "bunyan": "^1.8.15",
"cheerio": "^1.0.0-rc.12", "cheerio": "^1.0.0-rc.12",
"json-rpc-2.0": "^1.4.1",
"koa": "^2.13.4", "koa": "^2.13.4",
"koa-body": "^5.0.0", "koa-body": "^5.0.0",
"koa-passport": "^5.0.0", "koa-passport": "^5.0.0",
@@ -35,7 +34,7 @@
"axios": "^0.27.2", "axios": "^0.27.2",
"mocha": "^10.0.0", "mocha": "^10.0.0",
"node-dev": "^7.4.3", "node-dev": "^7.4.3",
"prisma": "^4.1.0", "prisma": "^4.5.0",
"ts-node-dev": "^2.0.0", "ts-node-dev": "^2.0.0",
"typescript": "^4.7.4" "typescript": "^4.7.4"
} }
@@ -136,12 +135,12 @@
} }
}, },
"node_modules/@prisma/client": { "node_modules/@prisma/client": {
"version": "4.1.0", "version": "4.5.0",
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-4.1.0.tgz", "resolved": "https://registry.npmjs.org/@prisma/client/-/client-4.5.0.tgz",
"integrity": "sha512-MvfPGAd42vHTiCYxwS6N+2U3F+ukoJ48D2QRnX1zSPJHBkh1CBtshl75daKzvVfgQwSouzSQeugKDej5di+E/g==", "integrity": "sha512-B2cV0OPI1smhdYUxsJoLYQLoMlLH06MUxgFUWQnHodGMX98VRVXKmQE/9OcrTNkqtke5RC+YU24Szxd04tZA2g==",
"hasInstallScript": true, "hasInstallScript": true,
"dependencies": { "dependencies": {
"@prisma/engines-version": "4.1.0-48.8d8414deb360336e4698a65aa45a1fbaf1ce13d8" "@prisma/engines-version": "4.5.0-43.0362da9eebca54d94c8ef5edd3b2e90af99ba452"
}, },
"engines": { "engines": {
"node": ">=14.17" "node": ">=14.17"
@@ -156,16 +155,16 @@
} }
}, },
"node_modules/@prisma/engines": { "node_modules/@prisma/engines": {
"version": "4.1.0", "version": "4.5.0",
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-4.1.0.tgz", "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-4.5.0.tgz",
"integrity": "sha512-quqHXD3P83NBLVtRlts4SgKHmqgA8GMiyDTJ7af03Wg0gl6F5t65mBYvIuwmD+52vHm42JtIsp/fAO9YIV0JBA==", "integrity": "sha512-4t9ir2SbQQr/wMCNU4YpHWp5hU14J2m3wHUZnGJPpmBF8YtkisxyVyQsKd1e6FyLTaGq8LOLhm6VLYHKqKNm+g==",
"devOptional": true, "devOptional": true,
"hasInstallScript": true "hasInstallScript": true
}, },
"node_modules/@prisma/engines-version": { "node_modules/@prisma/engines-version": {
"version": "4.1.0-48.8d8414deb360336e4698a65aa45a1fbaf1ce13d8", "version": "4.5.0-43.0362da9eebca54d94c8ef5edd3b2e90af99ba452",
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-4.1.0-48.8d8414deb360336e4698a65aa45a1fbaf1ce13d8.tgz", "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-4.5.0-43.0362da9eebca54d94c8ef5edd3b2e90af99ba452.tgz",
"integrity": "sha512-cRRJwpHFGFJZvtHbY3GZjMffNBEjjZk68ztn+S2hDgPCGB4H66IK26roK94GJxBodSehwRJ0wGyebC2GoIH1JQ==" "integrity": "sha512-o7LyVx8PPJBLrEzLl6lpxxk2D5VnlM4Fwmrbq0NoT6pr5aa1OuHD9ZG+WJY6TlR/iD9bhmo2LNcxddCMr5Rv2A=="
}, },
"node_modules/@tsconfig/node10": { "node_modules/@tsconfig/node10": {
"version": "1.0.9", "version": "1.0.9",
@@ -1873,11 +1872,6 @@
"js-yaml": "bin/js-yaml.js" "js-yaml": "bin/js-yaml.js"
} }
}, },
"node_modules/json-rpc-2.0": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/json-rpc-2.0/-/json-rpc-2.0-1.4.1.tgz",
"integrity": "sha512-OX1NJhpIfuK4GjDnJ/gKtZy1HOYo0l4eL0a4rb0rNeQheX1xlyQ9+JMmPzs/sFNthpS/TXKPWlGo09X7B5l81A=="
},
"node_modules/keygrip": { "node_modules/keygrip": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz", "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz",
@@ -2989,13 +2983,13 @@
} }
}, },
"node_modules/prisma": { "node_modules/prisma": {
"version": "4.1.0", "version": "4.5.0",
"resolved": "https://registry.npmjs.org/prisma/-/prisma-4.1.0.tgz", "resolved": "https://registry.npmjs.org/prisma/-/prisma-4.5.0.tgz",
"integrity": "sha512-iwqpAT6In1uvMSwQAM3PqmaBdhh2OaQ/2t+n3RjpW4vAKP3R7E1T34FZUU4zGOWtMWm5dt0sPThQkT/h87r6gw==", "integrity": "sha512-9Aeg4qiKlv9Wsjz4NO8k2CzRzlvS3A4FYVJ5+28sBBZ0eEwbiVOE/Jj7v6rZC1tFW2s4GSICQOAyuOjc6WsNew==",
"devOptional": true, "devOptional": true,
"hasInstallScript": true, "hasInstallScript": true,
"dependencies": { "dependencies": {
"@prisma/engines": "4.1.0" "@prisma/engines": "4.5.0"
}, },
"bin": { "bin": {
"prisma": "build/index.js", "prisma": "build/index.js",
@@ -3790,23 +3784,23 @@
} }
}, },
"@prisma/client": { "@prisma/client": {
"version": "4.1.0", "version": "4.5.0",
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-4.1.0.tgz", "resolved": "https://registry.npmjs.org/@prisma/client/-/client-4.5.0.tgz",
"integrity": "sha512-MvfPGAd42vHTiCYxwS6N+2U3F+ukoJ48D2QRnX1zSPJHBkh1CBtshl75daKzvVfgQwSouzSQeugKDej5di+E/g==", "integrity": "sha512-B2cV0OPI1smhdYUxsJoLYQLoMlLH06MUxgFUWQnHodGMX98VRVXKmQE/9OcrTNkqtke5RC+YU24Szxd04tZA2g==",
"requires": { "requires": {
"@prisma/engines-version": "4.1.0-48.8d8414deb360336e4698a65aa45a1fbaf1ce13d8" "@prisma/engines-version": "4.5.0-43.0362da9eebca54d94c8ef5edd3b2e90af99ba452"
} }
}, },
"@prisma/engines": { "@prisma/engines": {
"version": "4.1.0", "version": "4.5.0",
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-4.1.0.tgz", "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-4.5.0.tgz",
"integrity": "sha512-quqHXD3P83NBLVtRlts4SgKHmqgA8GMiyDTJ7af03Wg0gl6F5t65mBYvIuwmD+52vHm42JtIsp/fAO9YIV0JBA==", "integrity": "sha512-4t9ir2SbQQr/wMCNU4YpHWp5hU14J2m3wHUZnGJPpmBF8YtkisxyVyQsKd1e6FyLTaGq8LOLhm6VLYHKqKNm+g==",
"devOptional": true "devOptional": true
}, },
"@prisma/engines-version": { "@prisma/engines-version": {
"version": "4.1.0-48.8d8414deb360336e4698a65aa45a1fbaf1ce13d8", "version": "4.5.0-43.0362da9eebca54d94c8ef5edd3b2e90af99ba452",
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-4.1.0-48.8d8414deb360336e4698a65aa45a1fbaf1ce13d8.tgz", "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-4.5.0-43.0362da9eebca54d94c8ef5edd3b2e90af99ba452.tgz",
"integrity": "sha512-cRRJwpHFGFJZvtHbY3GZjMffNBEjjZk68ztn+S2hDgPCGB4H66IK26roK94GJxBodSehwRJ0wGyebC2GoIH1JQ==" "integrity": "sha512-o7LyVx8PPJBLrEzLl6lpxxk2D5VnlM4Fwmrbq0NoT6pr5aa1OuHD9ZG+WJY6TlR/iD9bhmo2LNcxddCMr5Rv2A=="
}, },
"@tsconfig/node10": { "@tsconfig/node10": {
"version": "1.0.9", "version": "1.0.9",
@@ -5100,11 +5094,6 @@
"argparse": "^2.0.1" "argparse": "^2.0.1"
} }
}, },
"json-rpc-2.0": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/json-rpc-2.0/-/json-rpc-2.0-1.4.1.tgz",
"integrity": "sha512-OX1NJhpIfuK4GjDnJ/gKtZy1HOYo0l4eL0a4rb0rNeQheX1xlyQ9+JMmPzs/sFNthpS/TXKPWlGo09X7B5l81A=="
},
"keygrip": { "keygrip": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz", "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz",
@@ -5939,12 +5928,12 @@
"dev": true "dev": true
}, },
"prisma": { "prisma": {
"version": "4.1.0", "version": "4.5.0",
"resolved": "https://registry.npmjs.org/prisma/-/prisma-4.1.0.tgz", "resolved": "https://registry.npmjs.org/prisma/-/prisma-4.5.0.tgz",
"integrity": "sha512-iwqpAT6In1uvMSwQAM3PqmaBdhh2OaQ/2t+n3RjpW4vAKP3R7E1T34FZUU4zGOWtMWm5dt0sPThQkT/h87r6gw==", "integrity": "sha512-9Aeg4qiKlv9Wsjz4NO8k2CzRzlvS3A4FYVJ5+28sBBZ0eEwbiVOE/Jj7v6rZC1tFW2s4GSICQOAyuOjc6WsNew==",
"devOptional": true, "devOptional": true,
"requires": { "requires": {
"@prisma/engines": "4.1.0" "@prisma/engines": "4.5.0"
} }
}, },
"qs": { "qs": {
+2 -2
View File
@@ -21,14 +21,14 @@
"axios": "^0.27.2", "axios": "^0.27.2",
"mocha": "^10.0.0", "mocha": "^10.0.0",
"node-dev": "^7.4.3", "node-dev": "^7.4.3",
"prisma": "^4.1.0", "prisma": "^4.5.0",
"ts-node-dev": "^2.0.0", "ts-node-dev": "^2.0.0",
"typescript": "^4.7.4" "typescript": "^4.7.4"
}, },
"dependencies": { "dependencies": {
"@koa/cors": "^3.3.0", "@koa/cors": "^3.3.0",
"@koa/router": "^12.0.0", "@koa/router": "^12.0.0",
"@prisma/client": "^4.1.0", "@prisma/client": "^4.5.0",
"bcrypt": "^5.0.1", "bcrypt": "^5.0.1",
"bluebird": "^3.7.2", "bluebird": "^3.7.2",
"bunyan": "^1.8.15", "bunyan": "^1.8.15",
+1
View File
@@ -65,6 +65,7 @@ model Offer {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
status String status String
condition String @default("")
item Decimal @db.Money item Decimal @db.Money
// markup Decimal @db.Money // markup Decimal @db.Money
+1 -1
View File
@@ -41,6 +41,7 @@ export default function createEvents(log: Log, pg: Pg, discogs, offers) {
for (const want of wants) { for (const want of wants) {
const listings = await populateListings(want); const listings = await populateListings(want);
log.info({ listings: listings.length, ...wantDetails(want) }, 'listings added'); log.info({ listings: listings.length, ...wantDetails(want) }, 'listings added');
events.emit(Events.ListingsUpdated);
} }
log.info({ wants: wants.length }, 'finished syncing'); log.info({ wants: wants.length }, 'finished syncing');
@@ -50,7 +51,6 @@ export default function createEvents(log: Log, pg: Pg, discogs, offers) {
log.info({ event: Events.WantsAdded, wants: wants.map(w => wantDetails(w)) }, 'starting listings population'); log.info({ event: Events.WantsAdded, wants: wants.map(w => wantDetails(w)) }, 'starting listings population');
await updateWantListings(wants); await updateWantListings(wants);
log.info({ event: Events.WantsAdded, wants: wants.map(w => wantDetails(w)) }, 'finished populating listings'); log.info({ event: Events.WantsAdded, wants: wants.map(w => wantDetails(w)) }, 'finished populating listings');
events.emit(Events.ListingsUpdated);
}); });
events.on(Events.StartupSync, async function startupSync() { events.on(Events.StartupSync, async function startupSync() {
+9 -1
View File
@@ -27,7 +27,7 @@ export default function createOffers(log: Log, pg: Pg) {
// log.info({ listings }, 'creating offers for listings') // log.info({ listings }, 'creating offers for listings')
const shipping = 10 + Math.random(); // yolt const shipping = 7.5; // yolt
// const shipping = bulkShipping / listings.length; // const shipping = bulkShipping / listings.length;
// how to deal with users who don't want all items in a bundle... // how to deal with users who don't want all items in a bundle...
@@ -41,6 +41,8 @@ export default function createOffers(log: Log, pg: Pg) {
status: Status.Pending, status: Status.Pending,
condition: l.data.condition,
item: l.data.price.value, item: l.data.price.value,
shipping, shipping,
total: l.data.price.value + shipping, total: l.data.price.value + shipping,
@@ -145,11 +147,17 @@ export default function createOffers(log: Log, pg: Pg) {
return sellerOffers; return sellerOffers;
} }
async function userOffers(user: User) {
const offers = await pg.userOffers(user);
return groupBy(offers, (o) => o.wantId);
}
return { return {
generate, generate,
accept, accept,
bundles, bundles,
statusSet, statusSet,
acpOffers, acpOffers,
userOffers,
}; };
} }
+3 -3
View File
@@ -150,7 +150,7 @@ export default function createPg(log: Log, prisma: PrismaClient): Pg {
return prisma.$transaction(async (prisma) => { return prisma.$transaction(async (prisma) => {
log.debug({ currentListingIds }); log.debug({ currentListingIds });
const openOfferStatuses = [Status.Pending, Status.Accepted]; const protectedOfferStatuses = [Status.Confirmed, Status.Complete];
// can't delete a listing with an open offer // can't delete a listing with an open offer
// so delete those offers first // so delete those offers first
@@ -161,7 +161,7 @@ export default function createPg(log: Log, prisma: PrismaClient): Pg {
notIn: currentListingIds, notIn: currentListingIds,
}, },
status: { status: {
in: openOfferStatuses, notIn: protectedOfferStatuses,
}, },
} }
}); });
@@ -174,7 +174,7 @@ export default function createPg(log: Log, prisma: PrismaClient): Pg {
where: { where: {
wantId: want.id, wantId: want.id,
status: { status: {
notIn: openOfferStatuses, in: protectedOfferStatuses,
} }
}, },
select: { select: {
+2 -2
View File
@@ -24,7 +24,7 @@ export default function createRpc(log: Log, pg: Pg, user: User, ws, discogs, off
} }
async function userOffers(): Promise<OffersMessage> { async function userOffers(): Promise<OffersMessage> {
return [RpcMessageType.Offers, await pg.userOffers(user)]; return [RpcMessageType.Offers, await offers.userOffers(user)];
} }
async function userWants(): Promise<WantsMessage> { async function userWants(): Promise<WantsMessage> {
@@ -37,7 +37,7 @@ export default function createRpc(log: Log, pg: Pg, user: User, ws, discogs, off
assert(Status[status], 'unknown status'); assert(Status[status], 'unknown status');
const offer = await offers.statusSet(user, id, status); const offer = await offers.statusSet(user, id, status);
const userOffers = await pg.userOffers(user); const userOffers = await offers.userOffers(user);
return [RpcMessageType.Offers, userOffers] return [RpcMessageType.Offers, userOffers]
} }
+75
View File
@@ -0,0 +1,75 @@
import { h } from 'preact';
import { ExternalLink, Check } from 'react-feather';
import { useState } from 'preact/hooks';
import { useSelector, useDispatch } from 'react-redux'
import { State } from './../store';
import http from '../http';
function OfferLine(offer) {
const ws = useSelector((state: State) => state.ws);
function setStatus(status) {
ws.sendOfferStatusSet(offer.id, status);
}
const accepted = offer.status === 'Accepted';
const classes = `${accepted ? 'accepted' : ''}`;
const btnText = accepted
? offer.status
: 'Accept';
const negBtn = accepted
? <button onClick={() => setStatus('Pending')} class='neg'>Cancel</button>
: <button onClick={() => setStatus('Declined')} class='neg'>Decline</button>;
return (
<div class="line">
<span class='number'>${offer.total}</span>
<button onClick={() => setStatus('Accepted')} disabled={accepted} class={classes} >{btnText}</button>
{negBtn}
</div>
)
}
function OfferPriceBtn(offer) {
const ws = useSelector((state: State) => state.ws);
const accepted = offer.status === 'Accepted';
const confirmed = offer.status === 'Confirmed';
function setStatus() {
if (accepted) return ws.sendOfferStatusSet(offer.id, 'Pending')
ws.sendOfferStatusSet(offer.id, 'Accepted');
}
const classes = `${accepted ? 'accepted' : ''} ${confirmed ? 'confirmed' : ''}`;
const btnText = accepted
? offer.status
: 'Accept';
return (
<button disabled={confirmed} onClick={() => setStatus()} class={classes}>${offer.total} {offer.condition}</button>
)
}
export default function WantOffers(wantOffers) {
const ws = useSelector((state: State) => state.ws);
const details = wantOffers[0].want.data;
return (
<div class="offer">
<section>
<h3>{details.artists_sort} - {details.title} <a href={details.uri} target="_blank"><ExternalLink /></a></h3>
<div class='alt-text' >{details.year}</div>
<div class='alt-text'>{details.formats.map(f => f.descriptions.join(' ')).join(' ')}</div>
</section>
<img src={details.thumb} />
<div class="lines">
{wantOffers.map(o => OfferPriceBtn(o))}
</div>
</div>
)
}
+1 -4
View File
@@ -15,6 +15,7 @@ const Info = () => {
<h2>How It Works</h2> <h2>How It Works</h2>
<p> First, add some records to your <span class='alt'>List</span> on the <a href='/search'>Search Page</a>. </p> <p> First, add some records to your <span class='alt'>List</span> on the <a href='/search'>Search Page</a>. </p>
<p> Mates Crates will then make <span class='alt'>Offers</span> to you for releases you add to your <a href='/list'>List</a>. </p> <p> Mates Crates will then make <span class='alt'>Offers</span> to you for releases you add to your <a href='/list'>List</a>. </p>
<p> Select all <span class='alt'>Offers</span> you are interested in, the more you accept the more likely the item will be <span class='alt-text'>Confirmed</span>.</p>
<p> Check back regularly for updated <span class='alt'>Offers</span>, the global record marketplace is a living thing. </p> <p> Check back regularly for updated <span class='alt'>Offers</span>, the global record marketplace is a living thing. </p>
</section> </section>
@@ -33,12 +34,8 @@ const Info = () => {
<section> <section>
<h2>FAQ</h2> <h2>FAQ</h2>
<h3> Record Condition </h3>
<p> All Mates Crates offers are for records that are either mint or very good condition. </p>
<h3> Delivery Times </h3> <h3> Delivery Times </h3>
<p> Records are usually available for pickup within a week. We order in bulk, which often means express, insured shipping to Melbourne. </p> <p> Records are usually available for pickup within a week. We order in bulk, which often means express, insured shipping to Melbourne. </p>
<h3> Duplicates </h3>
<p> Sometimes we can get a record from multiple sources, these appear as duplicate offers. Accepting multiple offers increases the chance the item will be confirmed. </p>
<h3> Friends </h3> <h3> Friends </h3>
<p> Please invite other collectors to the site. More users means offers can become cheaper and shipped sooner. </p> <p> Please invite other collectors to the site. More users means offers can become cheaper and shipped sooner. </p>
</section> </section>
+9 -20
View File
@@ -6,34 +6,23 @@ import { useSelector, useDispatch } from 'react-redux'
import http from './../http'; import http from './../http';
import { State, offersState } from './../store'; import { State, offersState } from './../store';
import Offer from './../components/offer'; import WantOffers from './../components/want.offers';
function Home() { function Home() {
const dispatch = useDispatch() const dispatch = useDispatch()
const offers = useSelector((state: State) => state.offers); const offers = useSelector((state: State) => state.offers);
useEffect(() => { // if (!offers.length) {
async function getOffers() { // return (
const [error, offers] = await http.getOffers(); // <div class={'offers'}>
if (error) return console.error(error); // loading...
dispatch(offersState.actions.set(offers)); // </div>
if (offers.length === 0) route('/search'); // );
} // }
getOffers();
}, []);
if (!offers.length) {
return (
<div class={'offers'}>
loading...
</div>
);
}
return ( return (
<div class={'offers'}> <div class={'offers'}>
{offers.map(Offer)} {Object.values(offers).map(WantOffers)}
</div> </div>
); );
} }
+88 -69
View File
@@ -56,7 +56,7 @@ html {
font-family:var(--base-font-family); font-family:var(--base-font-family);
line-height:var(--base-line-height); line-height:var(--base-line-height);
-webkit-text-size-adjust:100%; -webkit-text-size-adjust:100%;
background:var(--base-background-color); background:var(--background-0);
box-sizing:border-box box-sizing:border-box
} }
@@ -65,23 +65,6 @@ html {
--breakpoint-medium: 35em; --breakpoint-medium: 35em;
--breakpoint-large: 56em; --breakpoint-large: 56em;
--color-dark-blue: #101014;
--color-mid-blue: #16171d;
--color-light-blue: #1f2128;
--color-mid-grey: #9BA0B3;
--color-off-white: #F4F1D0;
--color-accent-blue: #5E9ED6;
--color-blue-gray: #506485;
--color-green: #537a34;
--base-accent-color: var(--color-accent-blue);
--fg-color: var(--color-off-white);
--hover-link-color: var(--color-off-white);
--base-background-color: var(--color-dark-blue);
--secondary-background-color: var(--color-mid-blue);
--tertiary-background-color: var(--color-light-blue);
--small-font-size: 0.75em; --small-font-size: 0.75em;
--base-font-size: 1em; --base-font-size: 1em;
--smedium-font-size: 1.333em; --smedium-font-size: 1.333em;
@@ -96,9 +79,6 @@ html {
--base-line-height: 1.4; --base-line-height: 1.4;
--header-line-height: 1.2; --header-line-height: 1.2;
--min-font-size: var(--base-font-size);
--max-font-size: var(--large-font-size);
--base-border-color: var(--secondary-font-color); --base-border-color: var(--secondary-font-color);
--base-border: 2px solid var(--base-border-color); --base-border: 2px solid var(--base-border-color);
--hover-border: 2px solid var(--base-font-color); --hover-border: 2px solid var(--base-font-color);
@@ -107,11 +87,37 @@ html {
--base-focus: 2px solid var(--base-accent-color); --base-focus: 2px solid var(--base-accent-color);
--base-outline-offset: 0.25em; --base-outline-offset: 0.25em;
--base-spacing: 1rem; --min-font-size: var(--base-font-size);
--base-spacing-half: calc(var(--base-spacing)/2); --max-font-size: var(--large-font-size);
--base-spacing-double: calc(var(--base-spacing)*2);
--utility-bar-header-height: calc(var(--base-spacing) * 1.5); --color-dark-blue: #101014;
--color-mid-blue: #16171d;
--color-light-blue: #1f2128;
--color-mid-grey: #9BA0B3;
--color-off-white: #F4F1D0;
--color-accent-blue: #5E9ED6;
--color-blue-gray: #506485;
--color-blue: #475977;
--color-green: #476d4a;
--color-accepted: var(--color-blue);
--color-confirmed: var(--color-green);
--base-accent-color: var(--color-accent-blue);
--fg-color: var(--color-off-white);
--hover-link-color: var(--color-off-white);
--background-0: var(--color-dark-blue);
--background-1: var(--color-mid-blue);
--background-2: var(--color-light-blue);
--spacing: 1rem;
--spacing-half: calc(var(--spacing)/2);
--spacing-double: calc(var(--spacing)*2);
--utility-bar-header-height: calc(var(--spacing) * 1.5);
} }
button, input, optgroup, select, textarea { button, input, optgroup, select, textarea {
@@ -136,11 +142,11 @@ input:focus, textarea:focus {
} }
button, input { button, input {
overflow:visible overflow:visible;
} }
button, select { button, select {
text-transform:none text-transform: none;
} }
button, [type=button], [type=reset], [type=submit] { button, [type=button], [type=reset], [type=submit] {
@@ -154,7 +160,7 @@ button, [type=button], [type=reset], [type=submit] {
text-transform: uppercase; text-transform: uppercase;
&:hover { &:hover {
background: var(--tertiary-background-color); background: var(--background-2);
border-color: transparent; border-color: transparent;
} }
} }
@@ -165,33 +171,33 @@ h1, h2, h3, h4, h5, h6 {
} }
h1 { h1 {
margin-bottom:var(--base-spacing); margin-bottom:var(--spacing);
font-size:var(--large-font-size) font-size:var(--large-font-size)
} }
h2 { h2 {
margin-bottom:var(--base-spacing); margin-bottom:var(--spacing);
font-size:var(--medium-font-size); font-size:var(--medium-font-size);
} }
h3 { h3 {
margin-bottom:calc(var(--base-spacing)/2); margin-bottom:calc(var(--spacing)/2);
font-size:var(--smedium-font-size) font-size:var(--smedium-font-size)
} }
h4 { h4 {
margin-bottom:calc(var(--base-spacing)/3); margin-bottom:calc(var(--spacing)/3);
font-size:var(--base-font-size) font-size:var(--base-font-size)
} }
p { p {
margin:0 0 var(--base-spacing) margin:0 0 var(--spacing)
} }
// ok actual styles // ok actual styles
body { body {
padding-top: calc(var(--base-spacing)/2); padding-top: calc(var(--spacing)/2);
} }
main { main {
@@ -199,8 +205,8 @@ main {
} }
button, [type=submit] { button, [type=submit] {
padding: var(--base-spacing-half); padding: var(--spacing-half);
background: var(--secondary-background-color); background: var(--background-1);
} }
@@ -222,7 +228,7 @@ button, [type=submit] {
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
section { section {
margin-bottom: var(--base-spacing); margin-bottom: var(--spacing);
} }
.contact { .contact {
@@ -237,10 +243,10 @@ button, [type=submit] {
} }
article { article {
background-color: var(--secondary-background-color); background-color: var(--background-1);
button { button {
border-color: var(--tertiary-background-color); border-color: var(--background-2);
} }
} }
@@ -250,15 +256,15 @@ article {
article { article {
display: grid; display: grid;
padding: var(--base-spacing); padding: var(--spacing);
margin-bottom: var(--base-spacing); margin-bottom: var(--spacing);
grid-template-columns: 1fr 4fr 1fr; grid-template-columns: 1fr 4fr 1fr;
} }
button { button {
margin-right: var(--base-spacing); margin-right: var(--spacing);
background: var(--tertiary-background-color); background: var(--background-2);
border-color: var(--secondary-background-color); border-color: var(--background-1);
&.added { &.added {
background: var(--color-green); background: var(--color-green);
@@ -270,7 +276,10 @@ article {
.list { .list {
display: grid; display: grid;
grid-template-columns: grid-template-columns:
1fr 1fr 1fr 1fr; 1fr 1fr 1fr;
grid-row-gap: var(--spacing);
grid-column-gap: var(--spacing);
article { article {
display: grid; display: grid;
@@ -284,6 +293,7 @@ article {
"remove remove"; "remove remove";
button { button {
margin-top: var(--spacing-half);
grid-area: remove grid-area: remove
} }
@@ -291,13 +301,12 @@ article {
grid-area: details; grid-area: details;
} }
padding: var(--base-spacing); padding: var(--spacing);
margin: var(--base-spacing-half);
} }
} }
form { form {
margin-bottom: var(--base-spacing); margin-bottom: var(--spacing);
label, input { label, input {
display: block; display: block;
@@ -308,15 +317,15 @@ form {
} }
input { input {
padding: var(--base-spacing-half); padding: var(--spacing-half);
margin-bottom: var(--base-spacing); margin-bottom: var(--spacing);
} }
label { label {
font-family: var(--secondary-font-family); font-family: var(--secondary-font-family);
font-size: var(--small-font-size); font-size: var(--small-font-size);
text-transform: uppercase; text-transform: uppercase;
margin-bottom: var(--base-spacing-half); margin-bottom: var(--spacing-half);
} }
} }
@@ -334,7 +343,7 @@ a {
nav { nav {
a { a {
font-size: var(--small-font-size); font-size: var(--small-font-size);
margin-right: var(--base-spacing); margin-right: var(--spacing);
} }
} }
@@ -342,7 +351,7 @@ nav {
header { header {
display: grid; display: grid;
grid-template-columns: 1fr 1fr min-content; grid-template-columns: 1fr 1fr min-content;
padding-bottom: var(--base-spacing); padding-bottom: var(--spacing);
a { a {
white-space: nowrap; white-space: nowrap;
@@ -353,10 +362,10 @@ header {
flex-flow: row; flex-flow: row;
.bundle { .bundle {
margin-bottom: var(--base-spacing-double); margin-bottom: var(--spacing-double);
section { section {
margin-bottom: var(--base-spacing); margin-bottom: var(--spacing);
} }
.items { .items {
@@ -364,14 +373,14 @@ header {
grid-template-columns: repeat(5, 1fr); grid-template-columns: repeat(5, 1fr);
div { div {
margin-right: var(--base-spacing); margin-right: var(--spacing);
margin-bottom: var(--base-spacing); margin-bottom: var(--spacing);
display: grid; display: grid;
&.bold { &.bold {
font-weight: bold; font-weight: bold;
background: var(--tertiary-background-color); background: var(--background-2);
} }
} }
} }
@@ -385,13 +394,14 @@ header {
.offer { .offer {
display: grid; display: grid;
grid-template-columns: 1fr min-content; grid-template-columns: 1fr min-content;
grid-template-rows: 1fr min-content;
grid-template-areas: grid-template-areas:
"details img" "details img"
"btn neg"; "lines lines";
padding: var(--base-spacing); padding: var(--spacing);
margin: var(--base-spacing-half); margin: var(--spacing-half);
background: var(--secondary-background-color); background: var(--background-1);
section { section {
grid-area: details; grid-area: details;
@@ -400,22 +410,31 @@ header {
img { img {
grid-area: img; grid-area: img;
justify-self: end; justify-self: end;
margin-left: var(--base-spacing-half); margin-left: var(--spacing-half);
} }
.lines {
grid-area: lines;
display: flex;
flex-wrap: wrap;
column-gap: var(--spacing-half);
button { button {
margin-top: var(--base-spacing-half); margin-top: var(--spacing-half);
grid-area: btn; border-color: var(--background-2);
border-color: var(--tertiary-background-color);
&.accepted { &.accepted {
background: var(--color-accent-blue); background: var(--color-accepted);
}
&.confirmed {
cursor: auto; cursor: auto;
background: var(--color-confirmed);
} }
&.neg { &.neg {
margin-left: var(--base-spacing-half); margin-left: var(--spacing-half);
grid-area: neg; }
} }
} }
} }