diff --git a/TODO.md b/TODO.md index 5dcebbb..73731de 100644 --- a/TODO.md +++ b/TODO.md @@ -2,6 +2,8 @@ status stepper +queue up wantlist adds to gracefully handle rate limiting + push offers cover off admin routes and commands diff --git a/api/package-lock.json b/api/package-lock.json index 9e6fd95..eabc415 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -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": { diff --git a/api/package.json b/api/package.json index 3430b07..3786f54 100644 --- a/api/package.json +++ b/api/package.json @@ -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", diff --git a/api/prisma/schema.prisma b/api/prisma/schema.prisma index f8d03ec..f322485 100644 --- a/api/prisma/schema.prisma +++ b/api/prisma/schema.prisma @@ -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 diff --git a/api/src/events.ts b/api/src/events.ts index c06fa79..1a47f9f 100644 --- a/api/src/events.ts +++ b/api/src/events.ts @@ -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() { diff --git a/api/src/offers.ts b/api/src/offers.ts index 78601fc..c7b7034 100644 --- a/api/src/offers.ts +++ b/api/src/offers.ts @@ -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, }; } diff --git a/api/src/pg.ts b/api/src/pg.ts index 3dfc6fc..7708426 100644 --- a/api/src/pg.ts +++ b/api/src/pg.ts @@ -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: { diff --git a/api/src/rpc.ts b/api/src/rpc.ts index 574b208..5e62291 100644 --- a/api/src/rpc.ts +++ b/api/src/rpc.ts @@ -24,7 +24,7 @@ export default function createRpc(log: Log, pg: Pg, user: User, ws, discogs, off } async function userOffers(): Promise { - return [RpcMessageType.Offers, await pg.userOffers(user)]; + return [RpcMessageType.Offers, await offers.userOffers(user)]; } async function userWants(): Promise { @@ -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] } diff --git a/client/src/components/want.offers.tsx b/client/src/components/want.offers.tsx new file mode 100644 index 0000000..75c85cb --- /dev/null +++ b/client/src/components/want.offers.tsx @@ -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 + ? + : ; + + return ( +
+ ${offer.total} + + {negBtn} +
+ ) +} + +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 ( + + ) +} + + +export default function WantOffers(wantOffers) { + const ws = useSelector((state: State) => state.ws); + + const details = wantOffers[0].want.data; + + return ( +
+
+

{details.artists_sort} - {details.title}

+
{details.year}
+
{details.formats.map(f => f.descriptions.join(' ')).join(' ')}
+
+ +
+ {wantOffers.map(o => OfferPriceBtn(o))} +
+
+ ) +} diff --git a/client/src/routes/info.tsx b/client/src/routes/info.tsx index 14ab666..f9879aa 100644 --- a/client/src/routes/info.tsx +++ b/client/src/routes/info.tsx @@ -15,6 +15,7 @@ const Info = () => {

How It Works

First, add some records to your List on the Search Page.

Mates Crates will then make Offers to you for releases you add to your List.

+

Select all Offers you are interested in, the more you accept the more likely the item will be Confirmed.

Check back regularly for updated Offers, the global record marketplace is a living thing.

@@ -33,12 +34,8 @@ const Info = () => {

FAQ

-

Record Condition

-

All Mates Crates offers are for records that are either mint or very good condition.

Delivery Times

Records are usually available for pickup within a week. We order in bulk, which often means express, insured shipping to Melbourne.

-

Duplicates

-

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.

Friends

Please invite other collectors to the site. More users means offers can become cheaper and shipped sooner.

diff --git a/client/src/routes/offers.tsx b/client/src/routes/offers.tsx index 3c556ea..e5be866 100644 --- a/client/src/routes/offers.tsx +++ b/client/src/routes/offers.tsx @@ -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 ( -
- loading... -
- ); - } - + // if (!offers.length) { + // return ( + //
+ // loading... + //
+ // ); + // } return (
- {offers.map(Offer)} + {Object.values(offers).map(WantOffers)}
); } diff --git a/client/src/style/index.less b/client/src/style/index.less index 820e068..5ece7cd 100644 --- a/client/src/style/index.less +++ b/client/src/style/index.less @@ -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); } - button { - margin-top: var(--base-spacing-half); - grid-area: btn; - border-color: var(--tertiary-background-color); + .lines { + grid-area: lines; + display: flex; + flex-wrap: wrap; + column-gap: var(--spacing-half); - &.accepted { - background: var(--color-accent-blue); - cursor: auto; - } + button { + margin-top: var(--spacing-half); + border-color: var(--background-2); - &.neg { - margin-left: var(--base-spacing-half); - grid-area: neg; + &.accepted { + background: var(--color-accepted); + } + + &.confirmed { + cursor: auto; + background: var(--color-confirmed); + } + + &.neg { + margin-left: var(--spacing-half); + } } } }