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
queue up wantlist adds to gracefully handle rate limiting
push offers
cover off admin routes and commands
+30 -41
View File
@@ -11,12 +11,11 @@
"dependencies": {
"@koa/cors": "^3.3.0",
"@koa/router": "^12.0.0",
"@prisma/client": "^4.1.0",
"@prisma/client": "^4.5.0",
"bcrypt": "^5.0.1",
"bluebird": "^3.7.2",
"bunyan": "^1.8.15",
"cheerio": "^1.0.0-rc.12",
"json-rpc-2.0": "^1.4.1",
"koa": "^2.13.4",
"koa-body": "^5.0.0",
"koa-passport": "^5.0.0",
@@ -35,7 +34,7 @@
"axios": "^0.27.2",
"mocha": "^10.0.0",
"node-dev": "^7.4.3",
"prisma": "^4.1.0",
"prisma": "^4.5.0",
"ts-node-dev": "^2.0.0",
"typescript": "^4.7.4"
}
@@ -136,12 +135,12 @@
}
},
"node_modules/@prisma/client": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-4.1.0.tgz",
"integrity": "sha512-MvfPGAd42vHTiCYxwS6N+2U3F+ukoJ48D2QRnX1zSPJHBkh1CBtshl75daKzvVfgQwSouzSQeugKDej5di+E/g==",
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-4.5.0.tgz",
"integrity": "sha512-B2cV0OPI1smhdYUxsJoLYQLoMlLH06MUxgFUWQnHodGMX98VRVXKmQE/9OcrTNkqtke5RC+YU24Szxd04tZA2g==",
"hasInstallScript": true,
"dependencies": {
"@prisma/engines-version": "4.1.0-48.8d8414deb360336e4698a65aa45a1fbaf1ce13d8"
"@prisma/engines-version": "4.5.0-43.0362da9eebca54d94c8ef5edd3b2e90af99ba452"
},
"engines": {
"node": ">=14.17"
@@ -156,16 +155,16 @@
}
},
"node_modules/@prisma/engines": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-4.1.0.tgz",
"integrity": "sha512-quqHXD3P83NBLVtRlts4SgKHmqgA8GMiyDTJ7af03Wg0gl6F5t65mBYvIuwmD+52vHm42JtIsp/fAO9YIV0JBA==",
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-4.5.0.tgz",
"integrity": "sha512-4t9ir2SbQQr/wMCNU4YpHWp5hU14J2m3wHUZnGJPpmBF8YtkisxyVyQsKd1e6FyLTaGq8LOLhm6VLYHKqKNm+g==",
"devOptional": true,
"hasInstallScript": true
},
"node_modules/@prisma/engines-version": {
"version": "4.1.0-48.8d8414deb360336e4698a65aa45a1fbaf1ce13d8",
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-4.1.0-48.8d8414deb360336e4698a65aa45a1fbaf1ce13d8.tgz",
"integrity": "sha512-cRRJwpHFGFJZvtHbY3GZjMffNBEjjZk68ztn+S2hDgPCGB4H66IK26roK94GJxBodSehwRJ0wGyebC2GoIH1JQ=="
"version": "4.5.0-43.0362da9eebca54d94c8ef5edd3b2e90af99ba452",
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-4.5.0-43.0362da9eebca54d94c8ef5edd3b2e90af99ba452.tgz",
"integrity": "sha512-o7LyVx8PPJBLrEzLl6lpxxk2D5VnlM4Fwmrbq0NoT6pr5aa1OuHD9ZG+WJY6TlR/iD9bhmo2LNcxddCMr5Rv2A=="
},
"node_modules/@tsconfig/node10": {
"version": "1.0.9",
@@ -1873,11 +1872,6 @@
"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": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz",
@@ -2989,13 +2983,13 @@
}
},
"node_modules/prisma": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/prisma/-/prisma-4.1.0.tgz",
"integrity": "sha512-iwqpAT6In1uvMSwQAM3PqmaBdhh2OaQ/2t+n3RjpW4vAKP3R7E1T34FZUU4zGOWtMWm5dt0sPThQkT/h87r6gw==",
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/prisma/-/prisma-4.5.0.tgz",
"integrity": "sha512-9Aeg4qiKlv9Wsjz4NO8k2CzRzlvS3A4FYVJ5+28sBBZ0eEwbiVOE/Jj7v6rZC1tFW2s4GSICQOAyuOjc6WsNew==",
"devOptional": true,
"hasInstallScript": true,
"dependencies": {
"@prisma/engines": "4.1.0"
"@prisma/engines": "4.5.0"
},
"bin": {
"prisma": "build/index.js",
@@ -3790,23 +3784,23 @@
}
},
"@prisma/client": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-4.1.0.tgz",
"integrity": "sha512-MvfPGAd42vHTiCYxwS6N+2U3F+ukoJ48D2QRnX1zSPJHBkh1CBtshl75daKzvVfgQwSouzSQeugKDej5di+E/g==",
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-4.5.0.tgz",
"integrity": "sha512-B2cV0OPI1smhdYUxsJoLYQLoMlLH06MUxgFUWQnHodGMX98VRVXKmQE/9OcrTNkqtke5RC+YU24Szxd04tZA2g==",
"requires": {
"@prisma/engines-version": "4.1.0-48.8d8414deb360336e4698a65aa45a1fbaf1ce13d8"
"@prisma/engines-version": "4.5.0-43.0362da9eebca54d94c8ef5edd3b2e90af99ba452"
}
},
"@prisma/engines": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-4.1.0.tgz",
"integrity": "sha512-quqHXD3P83NBLVtRlts4SgKHmqgA8GMiyDTJ7af03Wg0gl6F5t65mBYvIuwmD+52vHm42JtIsp/fAO9YIV0JBA==",
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-4.5.0.tgz",
"integrity": "sha512-4t9ir2SbQQr/wMCNU4YpHWp5hU14J2m3wHUZnGJPpmBF8YtkisxyVyQsKd1e6FyLTaGq8LOLhm6VLYHKqKNm+g==",
"devOptional": true
},
"@prisma/engines-version": {
"version": "4.1.0-48.8d8414deb360336e4698a65aa45a1fbaf1ce13d8",
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-4.1.0-48.8d8414deb360336e4698a65aa45a1fbaf1ce13d8.tgz",
"integrity": "sha512-cRRJwpHFGFJZvtHbY3GZjMffNBEjjZk68ztn+S2hDgPCGB4H66IK26roK94GJxBodSehwRJ0wGyebC2GoIH1JQ=="
"version": "4.5.0-43.0362da9eebca54d94c8ef5edd3b2e90af99ba452",
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-4.5.0-43.0362da9eebca54d94c8ef5edd3b2e90af99ba452.tgz",
"integrity": "sha512-o7LyVx8PPJBLrEzLl6lpxxk2D5VnlM4Fwmrbq0NoT6pr5aa1OuHD9ZG+WJY6TlR/iD9bhmo2LNcxddCMr5Rv2A=="
},
"@tsconfig/node10": {
"version": "1.0.9",
@@ -5100,11 +5094,6 @@
"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": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz",
@@ -5939,12 +5928,12 @@
"dev": true
},
"prisma": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/prisma/-/prisma-4.1.0.tgz",
"integrity": "sha512-iwqpAT6In1uvMSwQAM3PqmaBdhh2OaQ/2t+n3RjpW4vAKP3R7E1T34FZUU4zGOWtMWm5dt0sPThQkT/h87r6gw==",
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/prisma/-/prisma-4.5.0.tgz",
"integrity": "sha512-9Aeg4qiKlv9Wsjz4NO8k2CzRzlvS3A4FYVJ5+28sBBZ0eEwbiVOE/Jj7v6rZC1tFW2s4GSICQOAyuOjc6WsNew==",
"devOptional": true,
"requires": {
"@prisma/engines": "4.1.0"
"@prisma/engines": "4.5.0"
}
},
"qs": {
+2 -2
View File
@@ -21,14 +21,14 @@
"axios": "^0.27.2",
"mocha": "^10.0.0",
"node-dev": "^7.4.3",
"prisma": "^4.1.0",
"prisma": "^4.5.0",
"ts-node-dev": "^2.0.0",
"typescript": "^4.7.4"
},
"dependencies": {
"@koa/cors": "^3.3.0",
"@koa/router": "^12.0.0",
"@prisma/client": "^4.1.0",
"@prisma/client": "^4.5.0",
"bcrypt": "^5.0.1",
"bluebird": "^3.7.2",
"bunyan": "^1.8.15",
+1
View File
@@ -65,6 +65,7 @@ model Offer {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
status String
condition String @default("")
item 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) {
const listings = await populateListings(want);
log.info({ listings: listings.length, ...wantDetails(want) }, 'listings added');
events.emit(Events.ListingsUpdated);
}
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');
await updateWantListings(wants);
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() {
+9 -1
View File
@@ -27,7 +27,7 @@ export default function createOffers(log: Log, pg: Pg) {
// log.info({ listings }, 'creating offers for listings')
const shipping = 10 + Math.random(); // yolt
const shipping = 7.5; // yolt
// const shipping = bulkShipping / listings.length;
// 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,
condition: l.data.condition,
item: 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;
}
async function userOffers(user: User) {
const offers = await pg.userOffers(user);
return groupBy(offers, (o) => o.wantId);
}
return {
generate,
accept,
bundles,
statusSet,
acpOffers,
userOffers,
};
}
+3 -3
View File
@@ -150,7 +150,7 @@ export default function createPg(log: Log, prisma: PrismaClient): Pg {
return prisma.$transaction(async (prisma) => {
log.debug({ currentListingIds });
const openOfferStatuses = [Status.Pending, Status.Accepted];
const protectedOfferStatuses = [Status.Confirmed, Status.Complete];
// can't delete a listing with an open offer
// so delete those offers first
@@ -161,7 +161,7 @@ export default function createPg(log: Log, prisma: PrismaClient): Pg {
notIn: currentListingIds,
},
status: {
in: openOfferStatuses,
notIn: protectedOfferStatuses,
},
}
});
@@ -174,7 +174,7 @@ export default function createPg(log: Log, prisma: PrismaClient): Pg {
where: {
wantId: want.id,
status: {
notIn: openOfferStatuses,
in: protectedOfferStatuses,
}
},
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> {
return [RpcMessageType.Offers, await pg.userOffers(user)];
return [RpcMessageType.Offers, await offers.userOffers(user)];
}
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');
const offer = await offers.statusSet(user, id, status);
const userOffers = await pg.userOffers(user);
const userOffers = await offers.userOffers(user);
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>
<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> 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>
</section>
@@ -33,12 +34,8 @@ const Info = () => {
<section>
<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>
<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>
<p> Please invite other collectors to the site. More users means offers can become cheaper and shipped sooner. </p>
</section>
+9 -20
View File
@@ -6,34 +6,23 @@ import { useSelector, useDispatch } from 'react-redux'
import http from './../http';
import { State, offersState } from './../store';
import Offer from './../components/offer';
import WantOffers from './../components/want.offers';
function Home() {
const dispatch = useDispatch()
const offers = useSelector((state: State) => state.offers);
useEffect(() => {
async function getOffers() {
const [error, offers] = await http.getOffers();
if (error) return console.error(error);
dispatch(offersState.actions.set(offers));
if (offers.length === 0) route('/search');
}
getOffers();
}, []);
if (!offers.length) {
return (
<div class={'offers'}>
loading...
</div>
);
}
// if (!offers.length) {
// return (
// <div class={'offers'}>
// loading...
// </div>
// );
// }
return (
<div class={'offers'}>
{offers.map(Offer)}
{Object.values(offers).map(WantOffers)}
</div>
);
}
+88 -69
View File
@@ -56,7 +56,7 @@ html {
font-family:var(--base-font-family);
line-height:var(--base-line-height);
-webkit-text-size-adjust:100%;
background:var(--base-background-color);
background:var(--background-0);
box-sizing:border-box
}
@@ -65,23 +65,6 @@ html {
--breakpoint-medium: 35em;
--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;
--base-font-size: 1em;
--smedium-font-size: 1.333em;
@@ -96,9 +79,6 @@ html {
--base-line-height: 1.4;
--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: 2px solid var(--base-border-color);
--hover-border: 2px solid var(--base-font-color);
@@ -107,11 +87,37 @@ html {
--base-focus: 2px solid var(--base-accent-color);
--base-outline-offset: 0.25em;
--base-spacing: 1rem;
--base-spacing-half: calc(var(--base-spacing)/2);
--base-spacing-double: calc(var(--base-spacing)*2);
--min-font-size: var(--base-font-size);
--max-font-size: var(--large-font-size);
--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 {
@@ -136,11 +142,11 @@ input:focus, textarea:focus {
}
button, input {
overflow:visible
overflow:visible;
}
button, select {
text-transform:none
text-transform: none;
}
button, [type=button], [type=reset], [type=submit] {
@@ -154,7 +160,7 @@ button, [type=button], [type=reset], [type=submit] {
text-transform: uppercase;
&:hover {
background: var(--tertiary-background-color);
background: var(--background-2);
border-color: transparent;
}
}
@@ -165,33 +171,33 @@ h1, h2, h3, h4, h5, h6 {
}
h1 {
margin-bottom:var(--base-spacing);
margin-bottom:var(--spacing);
font-size:var(--large-font-size)
}
h2 {
margin-bottom:var(--base-spacing);
margin-bottom:var(--spacing);
font-size:var(--medium-font-size);
}
h3 {
margin-bottom:calc(var(--base-spacing)/2);
margin-bottom:calc(var(--spacing)/2);
font-size:var(--smedium-font-size)
}
h4 {
margin-bottom:calc(var(--base-spacing)/3);
margin-bottom:calc(var(--spacing)/3);
font-size:var(--base-font-size)
}
p {
margin:0 0 var(--base-spacing)
margin:0 0 var(--spacing)
}
// ok actual styles
body {
padding-top: calc(var(--base-spacing)/2);
padding-top: calc(var(--spacing)/2);
}
main {
@@ -199,8 +205,8 @@ main {
}
button, [type=submit] {
padding: var(--base-spacing-half);
background: var(--secondary-background-color);
padding: var(--spacing-half);
background: var(--background-1);
}
@@ -222,7 +228,7 @@ button, [type=submit] {
grid-template-columns: 1fr 1fr;
section {
margin-bottom: var(--base-spacing);
margin-bottom: var(--spacing);
}
.contact {
@@ -237,10 +243,10 @@ button, [type=submit] {
}
article {
background-color: var(--secondary-background-color);
background-color: var(--background-1);
button {
border-color: var(--tertiary-background-color);
border-color: var(--background-2);
}
}
@@ -250,15 +256,15 @@ article {
article {
display: grid;
padding: var(--base-spacing);
margin-bottom: var(--base-spacing);
padding: var(--spacing);
margin-bottom: var(--spacing);
grid-template-columns: 1fr 4fr 1fr;
}
button {
margin-right: var(--base-spacing);
background: var(--tertiary-background-color);
border-color: var(--secondary-background-color);
margin-right: var(--spacing);
background: var(--background-2);
border-color: var(--background-1);
&.added {
background: var(--color-green);
@@ -270,7 +276,10 @@ article {
.list {
display: grid;
grid-template-columns:
1fr 1fr 1fr 1fr;
1fr 1fr 1fr;
grid-row-gap: var(--spacing);
grid-column-gap: var(--spacing);
article {
display: grid;
@@ -284,6 +293,7 @@ article {
"remove remove";
button {
margin-top: var(--spacing-half);
grid-area: remove
}
@@ -291,13 +301,12 @@ article {
grid-area: details;
}
padding: var(--base-spacing);
margin: var(--base-spacing-half);
padding: var(--spacing);
}
}
form {
margin-bottom: var(--base-spacing);
margin-bottom: var(--spacing);
label, input {
display: block;
@@ -308,15 +317,15 @@ form {
}
input {
padding: var(--base-spacing-half);
margin-bottom: var(--base-spacing);
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(--base-spacing-half);
margin-bottom: var(--spacing-half);
}
}
@@ -334,7 +343,7 @@ a {
nav {
a {
font-size: var(--small-font-size);
margin-right: var(--base-spacing);
margin-right: var(--spacing);
}
}
@@ -342,7 +351,7 @@ nav {
header {
display: grid;
grid-template-columns: 1fr 1fr min-content;
padding-bottom: var(--base-spacing);
padding-bottom: var(--spacing);
a {
white-space: nowrap;
@@ -353,10 +362,10 @@ header {
flex-flow: row;
.bundle {
margin-bottom: var(--base-spacing-double);
margin-bottom: var(--spacing-double);
section {
margin-bottom: var(--base-spacing);
margin-bottom: var(--spacing);
}
.items {
@@ -364,14 +373,14 @@ header {
grid-template-columns: repeat(5, 1fr);
div {
margin-right: var(--base-spacing);
margin-bottom: var(--base-spacing);
margin-right: var(--spacing);
margin-bottom: var(--spacing);
display: grid;
&.bold {
font-weight: bold;
background: var(--tertiary-background-color);
background: var(--background-2);
}
}
}
@@ -385,13 +394,14 @@ header {
.offer {
display: grid;
grid-template-columns: 1fr min-content;
grid-template-rows: 1fr min-content;
grid-template-areas:
"details img"
"btn neg";
"lines lines";
padding: var(--base-spacing);
margin: var(--base-spacing-half);
background: var(--secondary-background-color);
padding: var(--spacing);
margin: var(--spacing-half);
background: var(--background-1);
section {
grid-area: details;
@@ -400,22 +410,31 @@ header {
img {
grid-area: img;
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 {
margin-top: var(--base-spacing-half);
grid-area: btn;
border-color: var(--tertiary-background-color);
margin-top: var(--spacing-half);
border-color: var(--background-2);
&.accepted {
background: var(--color-accent-blue);
background: var(--color-accepted);
}
&.confirmed {
cursor: auto;
background: var(--color-confirmed);
}
&.neg {
margin-left: var(--base-spacing-half);
grid-area: neg;
margin-left: var(--spacing-half);
}
}
}
}