Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f2d805b7e5 | ||
|
|
54b1900c88 | ||
|
|
5e86a52d43 | ||
|
|
445821aa10 | ||
|
|
29610b49b2 | ||
|
|
ae83ad54c4 | ||
|
|
b340fc7b61 | ||
|
|
986b61962a | ||
|
|
3a0525fffe | ||
|
|
278e1c4a95 | ||
|
|
c379ef925d | ||
|
|
e4b7356b6e | ||
|
|
19e0376edf | ||
|
|
2dbbf75369 | ||
|
|
fa703b678f | ||
|
|
ac0a31511d | ||
|
|
2ffb5f9bc3 | ||
|
|
306dc30499 | ||
|
|
36fc3665dd | ||
|
|
c6bf50f963 | ||
|
|
1ca9a7225c | ||
|
|
1ee8dbc749 | ||
|
|
c4cc12850d | ||
|
|
f3813c0f61 | ||
|
|
f76ceb011e | ||
|
|
84a19fbde3 | ||
|
|
5b0c7c05ca | ||
|
|
a97560c1c7 | ||
|
|
00cb224fb3 | ||
|
|
6c3cac7381 | ||
|
|
b91f877045 | ||
|
|
b86a4216cf | ||
|
|
57fefdc9e2 | ||
|
|
358958c127 | ||
|
|
c205ae81e2 | ||
|
|
0cec6414ae | ||
|
|
43173a833d | ||
|
|
0e785e0c8c | ||
|
|
6b73831b51 | ||
|
|
f0623cb74b | ||
|
|
2f8c883d75 | ||
|
|
7262d6b37b | ||
|
|
dfc783c936 | ||
|
|
631942bb41 | ||
|
|
e74ab36313 | ||
|
|
5115dc5f8e | ||
|
|
9e854f5d81 | ||
|
|
5a19bf62fd | ||
|
|
e185d04039 | ||
|
|
f726c960cd | ||
|
|
18d5f26326 | ||
|
|
a0cd49b411 | ||
|
|
f546d76912 | ||
|
|
28759f9e70 | ||
|
|
6d18a86697 | ||
|
|
5fab62345b | ||
|
|
15a84d6eea | ||
|
|
b199ce94c1 | ||
|
|
f0e366a73d | ||
|
|
623d0f3938 | ||
|
|
7946259550 | ||
|
|
015a2fd66c | ||
|
|
77428aa7de | ||
|
|
2761456009 | ||
|
|
6b88740595 | ||
|
|
9fad9a69ec | ||
|
|
9f0bb35d09 | ||
|
|
e792951d24 | ||
|
|
ef33baacba | ||
|
|
9b8676809d | ||
|
|
e45b2ca9cd | ||
|
|
1e368913ab | ||
|
|
f6f03730e3 | ||
|
|
3bd8691ebf | ||
|
|
470270954f | ||
|
|
6b8ebe3f35 | ||
|
|
acf7e54be9 | ||
|
|
73248f7d9f | ||
|
|
0e3c272adc | ||
|
|
d2ec08159d | ||
|
|
12c3da9f5e | ||
|
|
70a67b0c53 | ||
|
|
644398319d | ||
|
|
2c66c47163 |
No files matched your search
@@ -1,3 +1,7 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
data/
|
data/
|
||||||
db/
|
db/
|
||||||
|
|
||||||
|
.env
|
||||||
|
|
||||||
|
*sql.gz
|
||||||
@@ -1,9 +1,30 @@
|
|||||||
# TODO
|
# TODO
|
||||||
|
|
||||||
push offers
|
- accept new offers that are under highest accepted price
|
||||||
redux that bitch
|
|
||||||
|
|
||||||
|
env
|
||||||
|
discogs api key
|
||||||
|
discord api key
|
||||||
|
|
||||||
seller
|
discord bot
|
||||||
eg min 6 items
|
notify on sign up
|
||||||
shipping is 101aud
|
want added
|
||||||
|
offer accepted
|
||||||
|
|
||||||
|
email ppl who signed up
|
||||||
|
say we have updates
|
||||||
|
|
||||||
|
main list filters
|
||||||
|
order by
|
||||||
|
offers only
|
||||||
|
|
||||||
|
sample offer with tooltips
|
||||||
|
|
||||||
|
go over and reorganise event logic
|
||||||
|
should be connection.service ?
|
||||||
|
|
||||||
|
contact options on landing page
|
||||||
|
|
||||||
|
skip unofficial release
|
||||||
|
|
||||||
|
status stepper
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
node_modules/
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
# Environment variables declared in this file are automatically made available to Prisma.
|
|
||||||
# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema
|
|
||||||
|
|
||||||
# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.
|
|
||||||
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings
|
|
||||||
|
|
||||||
DATABASE_URL="postgresql://mcrates:digdeep@0.0.0.0:45432/mcrates?schema=public"
|
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
FROM node:20
|
||||||
|
WORKDIR /opt/crates/api
|
||||||
|
|
||||||
|
# puppeteer deps
|
||||||
|
RUN apt-get update
|
||||||
|
# don't think about it
|
||||||
|
RUN apt-get install -y ca-certificates fonts-liberation libasound2 libatk-bridge2.0-0 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgbm1 libgcc1 libglib2.0-0 libgtk-3-0 libnspr4 libnss3 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 lsb-release wget xdg-utils
|
||||||
|
|
||||||
|
COPY . ./
|
||||||
|
RUN npm i
|
||||||
|
# not sure why doesn't do it itself
|
||||||
|
RUN node node_modules/puppeteer/install.mjs
|
||||||
|
|
||||||
|
CMD ["npm", "start"]
|
||||||
Executable
+1
@@ -0,0 +1 @@
|
|||||||
|
crates-api-7b8d754768-r4f5q 19:40:38.635Z ERROR mcrates: sync error ││ crates-api-7b8d754768-r4f5q TypeError: Cannot read properties of undefined (reading 'status') ││ crates-api-7b8d754768-r4f5q at populateListings (/opt/crates/api/src/events.ts:42:28) ││ crates-api-7b8d754768-r4f5q at updateWantListings (/opt/crates/api/src/events.ts:57:30) ││ crates-api-7b8d754768-r4f5q at EventEmitter.sync (/opt/crates/api/src/events.ts:83:13) ││ crates-api-7b8d754768-r4f5q 19:40:40.707Z INFO mcrates: (event=Sync, wants=192) ││ crates-api-7b8d754768-r4f5q 19:42:14.756Z ERROR mcrates: Navigation timeout of 30000 ms exceeded ││ crates-api-7b8d754768-r4f5q TimeoutError: Navigation timeout of 30000 ms exceeded ││ crates-api-7b8d754768-r4f5q at new Deferred (/opt/crates/api/node_modules/puppeteer-core/src/util/Deferred.ts:73:2 ││ crates-api-7b8d754768-r4f5q at Function.create (/opt/crates/api/node_modules/puppeteer-core/src/util/Deferred.ts:2 ││ crates-api-7b8d754768-r4f5q at new LifecycleWatcher (/opt/crates/api/node_modules/puppeteer-core/src/cdp/Lifecycle ││ crates-api-7b8d754768-r4f5q at CdpFrame.goto (/opt/crates/api/node_modules/puppeteer-core/src/cdp/Frame.ts:159:21) ││ crates-api-7b8d754768-r4f5q at CdpFrame.<anonymous> (/opt/crates/api/node_modules/puppeteer-core/src/util/decorato ││ crates-api-7b8d754768-r4f5q at CdpPage.goto (/opt/crates/api/node_modules/puppeteer-core/src/api/Page.ts:1649:35) ││ crates-api-7b8d754768-r4f5q at Object.listings (/opt/crates/api/src/discogs.ts:80:32) ││ crates-api-7b8d754768-r4f5q at populateListings (/opt/crates/api/src/events.ts:37:30) ││ crates-api-7b8d754768-r4f5q at updateWantListings (/opt/crates/api/src/events.ts:57:30) ││ crates-api-7b8d754768-r4f5q at EventEmitter.sync (/opt/crates/api/src/events.ts:83:13) ││ crates-api-7b8d754768-r4f5q 19:42:14.989Z ERROR mcrates: sync error ││ crates-api-7b8d754768-r4f5q TypeError: Cannot read properties of undefined (reading 'status') ││ crates-api-7b8d754768-r4f5q at populateListings (/opt/crates/api/src/events.ts:42:28) ││ crates-api-7b8d754768-r4f5q at updateWantListings (/opt/crates/api/src/events.ts:57:30) ││ crates-api-7b8d754768-r4f5q at EventEmitter.sync (/opt/crates/api/src/events.ts:83:13) ││ crates-api-7b8d754768-r4f5q 19:42:25.047Z INFO mcrates: (event=Sync, wants=202) ││ crates-api-7b8d754768-r4f5q 19:44:01.988Z ERROR mcrates: Timed out after waiting 30000ms ││ crates-api-7b8d754768-r4f5q TimeoutError: Timed out after waiting 30000ms ││ crates-api-7b8d754768-r4f5q at /opt/crates/api/node_modules/puppeteer-core/src/common/util.ts:302:17 ││ crates-api-7b8d754768-r4f5q at /opt/crates/api/node_modules/puppeteer-core/lib/cjs/third_party/rxjs/rxjs.js:3986:3 ││ crates-api-7b8d754768-r4f5q at OperatorSubscriber2._this._next (/opt/crates/api/node_modules/puppeteer-core/lib/cj ││ crates-api-7b8d754768-r4f5q at OperatorSubscriber2.Subscriber2.next (/opt/crates/api/node_modules/puppeteer-core/l ││ crates-api-7b8d754768-r4f5q at AsyncAction2.<anonymous> (/opt/crates/api/node_modules/puppeteer-core/lib/cjs/third ││ crates-api-7b8d754768-r4f5q at AsyncAction2._execute (/opt/crates/api/node_modules/puppeteer-core/lib/cjs/third_pa ││ crates-api-7b8d754768-r4f5q at AsyncAction2.execute (/opt/crates/api/node_modules/puppeteer-core/lib/cjs/third_par ││ crates-api-7b8d754768-r4f5q at AsyncScheduler2.flush (/opt/crates/api/node_modules/puppeteer-core/lib/cjs/third_pa ││ crates-api-7b8d754768-r4f5q at listOnTimeout (node:internal/timers:581:17) ││ crates-api-7b8d754768-r4f5q at processTimers (node:internal/timers:519:7) ││ crates-api-7b8d754768-r4f5q 19:44:04.209Z ERROR mcrates: sync error ││ crates-api-7b8d754768-r4f5q TypeError: Cannot read properties of undefined (reading 'status') ││ crates-api-7b8d754768-r4f5q at populateListings (/oLine truncated
|
||||||
+7
-3
@@ -24,12 +24,16 @@ async function start() {
|
|||||||
prisma,
|
prisma,
|
||||||
pg,
|
pg,
|
||||||
discogs,
|
discogs,
|
||||||
|
discord,
|
||||||
api,
|
api,
|
||||||
events,
|
events,
|
||||||
} = createServices(log);
|
notify,
|
||||||
|
} = await createServices(log);
|
||||||
|
|
||||||
events.emit(Events.StartupSync);
|
await notify.listen();
|
||||||
// events.emit(Events.ListingsUpdated);
|
await discord.connect();
|
||||||
|
|
||||||
|
// events.emit(Events.Sync);
|
||||||
|
|
||||||
api.listen();
|
api.listen();
|
||||||
}
|
}
|
||||||
|
|||||||
+4097
-226
File diff suppressed because it is too large.
Load diff
+12
-6
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "mates-crates",
|
"name": "crates",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "mocha",
|
"test": "mocha",
|
||||||
"test:watch": "mocha --watch",
|
"test:watch": "mocha --watch",
|
||||||
"migrate": "knex migrate:latest",
|
"migrate": "prisma migrate status",
|
||||||
"migrate:make": "prisma migrate dev --name",
|
"migrate:make": "prisma migrate dev --name",
|
||||||
"start": "ts-node-dev --respawn --transpile-only index.ts | bunyan -o short"
|
"start": "ts-node-dev --respawn --transpile-only index.ts | bunyan -o short"
|
||||||
},
|
},
|
||||||
@@ -16,23 +16,25 @@
|
|||||||
"@types/bcrypt": "^5.0.0",
|
"@types/bcrypt": "^5.0.0",
|
||||||
"@types/bluebird": "^3.5.36",
|
"@types/bluebird": "^3.5.36",
|
||||||
"@types/koa": "^2.13.5",
|
"@types/koa": "^2.13.5",
|
||||||
|
"@types/koa-websocket": "^5.0.7",
|
||||||
"@types/lodash": "^4.14.182",
|
"@types/lodash": "^4.14.182",
|
||||||
"@types/mocha": "^9.1.1",
|
"@types/mocha": "^9.1.1",
|
||||||
"axios": "^0.27.2",
|
"axios": "^0.27.2",
|
||||||
"mocha": "^10.0.0",
|
"mocha": "^10.0.0",
|
||||||
"node-dev": "^7.4.3",
|
"prisma": "^4.5.0",
|
||||||
"prisma": "^4.1.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",
|
||||||
"cheerio": "^1.0.0-rc.12",
|
"cheerio": "^1.0.0-rc.12",
|
||||||
|
"discord.js": "^14.7.1",
|
||||||
|
"esrun": "^3.2.26",
|
||||||
"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",
|
||||||
@@ -40,7 +42,11 @@
|
|||||||
"koa-websocket": "^7.0.0",
|
"koa-websocket": "^7.0.0",
|
||||||
"koa-ws": "^1.0.1",
|
"koa-ws": "^1.0.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"passport-local": "^1.0.0"
|
"passport-local": "^1.0.0",
|
||||||
|
"pg-listen": "^1.7.0",
|
||||||
|
"puppeteer": "^23.3.0",
|
||||||
|
"puppeteer-extra": "^3.3.6",
|
||||||
|
"puppeteer-extra-plugin-stealth": "^2.11.2"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"koa-passport": {
|
"koa-passport": {
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "listings" (
|
||||||
|
"id" BIGINT NOT NULL,
|
||||||
|
"seller" BIGINT NOT NULL,
|
||||||
|
"data" JSONB NOT NULL,
|
||||||
|
"wantId" UUID NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "listings_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "users" (
|
||||||
|
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||||
|
"email" TEXT NOT NULL,
|
||||||
|
"password" TEXT NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "wants" (
|
||||||
|
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||||
|
"release" BIGINT NOT NULL,
|
||||||
|
"data" JSONB NOT NULL,
|
||||||
|
"userId" UUID NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "wants_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "offers" (
|
||||||
|
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||||
|
"status" TEXT NOT NULL,
|
||||||
|
"condition" TEXT NOT NULL DEFAULT '',
|
||||||
|
"item" MONEY NOT NULL,
|
||||||
|
"shipping" MONEY NOT NULL,
|
||||||
|
"total" MONEY NOT NULL,
|
||||||
|
"listingId" BIGINT NOT NULL,
|
||||||
|
"userId" UUID NOT NULL,
|
||||||
|
"wantId" UUID NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "offers_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "listings_seller_idx" ON "listings"("seller");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "users_email_key" ON "users"("email");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "wants_release_idx" ON "wants"("release");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "wants_userId_release_key" ON "wants"("userId", "release");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "offers_listingId_key" ON "offers"("listingId");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "listings" ADD CONSTRAINT "listings_wantId_fkey" FOREIGN KEY ("wantId") REFERENCES "wants"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "wants" ADD CONSTRAINT "wants_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "offers" ADD CONSTRAINT "offers_listingId_fkey" FOREIGN KEY ("listingId") REFERENCES "listings"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "offers" ADD CONSTRAINT "offers_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "offers" ADD CONSTRAINT "offers_wantId_fkey" FOREIGN KEY ("wantId") REFERENCES "wants"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
CREATE OR REPLACE FUNCTION notify_users() RETURNS TRIGGER AS $$
|
||||||
|
DECLARE
|
||||||
|
record RECORD;
|
||||||
|
id UUID;
|
||||||
|
payload JSON;
|
||||||
|
BEGIN
|
||||||
|
IF (TG_OP = 'DELETE') THEN
|
||||||
|
id = OLD.id;
|
||||||
|
ELSE
|
||||||
|
id = NEW.id;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
payload = json_build_object(
|
||||||
|
'table', TG_TABLE_NAME,
|
||||||
|
'action', TG_OP,
|
||||||
|
'id', id
|
||||||
|
);
|
||||||
|
|
||||||
|
PERFORM pg_notify('users.events', payload::text);
|
||||||
|
|
||||||
|
RETURN NULL;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION notify_listings() RETURNS TRIGGER AS $$
|
||||||
|
DECLARE
|
||||||
|
record RECORD;
|
||||||
|
id int8;
|
||||||
|
payload JSON;
|
||||||
|
BEGIN
|
||||||
|
IF (TG_OP = 'DELETE') THEN
|
||||||
|
id = OLD.id;
|
||||||
|
ELSE
|
||||||
|
id = NEW.id;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
payload = json_build_object(
|
||||||
|
'table', TG_TABLE_NAME,
|
||||||
|
'action', TG_OP,
|
||||||
|
'id', id
|
||||||
|
);
|
||||||
|
|
||||||
|
PERFORM pg_notify('listings.events', payload::text);
|
||||||
|
|
||||||
|
RETURN NULL;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION notify_offers() RETURNS TRIGGER AS $$
|
||||||
|
DECLARE
|
||||||
|
record RECORD;
|
||||||
|
id UUID;
|
||||||
|
payload JSON;
|
||||||
|
BEGIN
|
||||||
|
IF (TG_OP = 'DELETE') THEN
|
||||||
|
id = OLD.id;
|
||||||
|
ELSE
|
||||||
|
id = NEW.id;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
payload = json_build_object(
|
||||||
|
'table', TG_TABLE_NAME,
|
||||||
|
'action', TG_OP,
|
||||||
|
'id', id
|
||||||
|
);
|
||||||
|
|
||||||
|
PERFORM pg_notify('offers.events', payload::text);
|
||||||
|
|
||||||
|
RETURN NULL;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION notify_wants() RETURNS TRIGGER AS $$
|
||||||
|
DECLARE
|
||||||
|
record RECORD;
|
||||||
|
id UUID;
|
||||||
|
payload JSON;
|
||||||
|
BEGIN
|
||||||
|
IF (TG_OP = 'DELETE') THEN
|
||||||
|
id = OLD.id;
|
||||||
|
ELSE
|
||||||
|
id = NEW.id;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
payload = json_build_object(
|
||||||
|
'table', TG_TABLE_NAME,
|
||||||
|
'action', TG_OP,
|
||||||
|
'id', id
|
||||||
|
);
|
||||||
|
|
||||||
|
PERFORM pg_notify('wants.events', payload::text);
|
||||||
|
|
||||||
|
RETURN NULL;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE OR REPLACE TRIGGER notify_wants_event
|
||||||
|
AFTER INSERT OR UPDATE OR DELETE ON wants
|
||||||
|
FOR EACH ROW EXECUTE PROCEDURE notify_wants();
|
||||||
|
|
||||||
|
CREATE OR REPLACE TRIGGER notify_listings_event
|
||||||
|
AFTER INSERT OR UPDATE OR DELETE ON listings
|
||||||
|
FOR EACH ROW EXECUTE PROCEDURE notify_listings();
|
||||||
|
|
||||||
|
CREATE OR REPLACE TRIGGER notify_offers_event
|
||||||
|
AFTER INSERT OR UPDATE OR DELETE ON offers
|
||||||
|
FOR EACH ROW EXECUTE PROCEDURE notify_offers();
|
||||||
|
|
||||||
|
CREATE OR REPLACE TRIGGER notify_users_event
|
||||||
|
AFTER INSERT OR UPDATE OR DELETE ON users
|
||||||
|
FOR EACH ROW EXECUTE PROCEDURE notify_users();
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Please do not edit this file manually
|
||||||
|
# It should be added in your version-control system (i.e. Git)
|
||||||
|
provider = "postgresql"
|
||||||
@@ -11,8 +11,13 @@ datasource db {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model Listing {
|
model Listing {
|
||||||
id Int @id
|
|
||||||
seller Int
|
// 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
|
data Json
|
||||||
|
|
||||||
wantId String @db.Uuid
|
wantId String @db.Uuid
|
||||||
@@ -21,7 +26,7 @@ model Listing {
|
|||||||
offers Offer[]
|
offers Offer[]
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @default(now())
|
||||||
|
|
||||||
@@index([seller])
|
@@index([seller])
|
||||||
@@map("listings")
|
@@map("listings")
|
||||||
@@ -33,7 +38,7 @@ model User {
|
|||||||
password String
|
password String
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @default(now())
|
||||||
|
|
||||||
wants Want[]
|
wants Want[]
|
||||||
offers Offer[]
|
offers Offer[]
|
||||||
@@ -43,14 +48,14 @@ model User {
|
|||||||
|
|
||||||
model Want {
|
model Want {
|
||||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||||
release Int
|
release BigInt
|
||||||
data Json
|
data Json
|
||||||
|
|
||||||
userId String @db.Uuid
|
userId String @db.Uuid
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade)
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade)
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @default(now())
|
||||||
|
|
||||||
listings Listing[]
|
listings Listing[]
|
||||||
offers Offer[]
|
offers Offer[]
|
||||||
@@ -65,14 +70,13 @@ 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
|
|
||||||
shipping Decimal @db.Money
|
shipping Decimal @db.Money
|
||||||
total Decimal @db.Money
|
total Decimal @db.Money
|
||||||
comparison Decimal @db.Money
|
|
||||||
|
|
||||||
listingId Int
|
listingId BigInt
|
||||||
listing Listing @relation(fields: [listingId], references: [id])
|
listing Listing @relation(fields: [listingId], references: [id])
|
||||||
|
|
||||||
userId String @db.Uuid
|
userId String @db.Uuid
|
||||||
@@ -81,6 +85,9 @@ model Offer {
|
|||||||
wantId String @db.Uuid
|
wantId String @db.Uuid
|
||||||
want Want @relation(fields: [wantId], references: [id], onDelete: Cascade)
|
want Want @relation(fields: [wantId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @default(now())
|
||||||
|
|
||||||
// only one offer per want for a user
|
// only one offer per want for a user
|
||||||
// @@unique([userId, wantId])
|
// @@unique([userId, wantId])
|
||||||
@@unique([listingId])
|
@@unique([listingId])
|
||||||
|
|||||||
@@ -48,12 +48,4 @@ describe('api', function apiTests() {
|
|||||||
assert(res.headers['set-cookie']);
|
assert(res.headers['set-cookie']);
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('offers', function offerTests() {
|
|
||||||
it('create offers', async function createOffersTest() {
|
|
||||||
const { data: bundles } = await api.get('/bundles');
|
|
||||||
const listingIds = bundles[0].items.map(i => i.id);
|
|
||||||
const { data } = await api.post('/offers', { listingIds, shipping: 50.0, baseShipping: 15.0 });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
+39
-81
@@ -13,17 +13,27 @@ import Bluebird from 'bluebird';
|
|||||||
import type Log from 'bunyan';
|
import type Log from 'bunyan';
|
||||||
import type { Listing, User, Want, Prisma } from '@prisma/client';
|
import type { Listing, User, Want, Prisma } from '@prisma/client';
|
||||||
|
|
||||||
import { Pg } from './pg';
|
import type { Pg } from './pg';
|
||||||
|
import type { Discogs } from './discogs';
|
||||||
|
import type { Offers } from './offers';
|
||||||
|
import type { Rpc } from './rpc';
|
||||||
|
|
||||||
import routerAuth from './routers/router.auth';
|
import routerAuth from './routers/router.auth';
|
||||||
import { Events } from './events';
|
import { Events } from './events';
|
||||||
|
import createRpc, { RpcMessageType } from './rpc';
|
||||||
|
|
||||||
export default function createApi(log: Log, pg: Pg, discogs, offers, events) {
|
export default function createApi(log: Log, pg: Pg, discogs: Discogs, offers: Offers, events) {
|
||||||
const PORT = 41337;
|
const PORT = 41337;
|
||||||
const WS_PORT = 40055;
|
const WS_PORT = 40055;
|
||||||
const SECRET = 'ohja4Eif9eidah3aj5ta2haa4iemooth';
|
const SECRET = 'ohja4Eif9eidah3aj5ta2haa4iemooth';
|
||||||
|
|
||||||
const app = koaWebsocket(new koa());
|
const app = koaWebsocket(new koa());
|
||||||
|
|
||||||
|
// @ts-ignore - ts don't know wtf it talking about
|
||||||
|
BigInt.prototype.toJSON = function bigIntStringify() {
|
||||||
|
return this.toString();
|
||||||
|
};
|
||||||
|
|
||||||
app.use(async (ctx, next) => {
|
app.use(async (ctx, next) => {
|
||||||
try {
|
try {
|
||||||
await next();
|
await next();
|
||||||
@@ -41,6 +51,7 @@ export default function createApi(log: Log, pg: Pg, discogs, offers, events) {
|
|||||||
function listen() {
|
function listen() {
|
||||||
const server = app.listen(PORT);
|
const server = app.listen(PORT);
|
||||||
log.info({ PORT }, 'listening');
|
log.info({ PORT }, 'listening');
|
||||||
|
return server;
|
||||||
}
|
}
|
||||||
|
|
||||||
app.use(cors({ credentials: true }));
|
app.use(cors({ credentials: true }));
|
||||||
@@ -49,7 +60,7 @@ export default function createApi(log: Log, pg: Pg, discogs, offers, events) {
|
|||||||
app.keys = [SECRET];
|
app.keys = [SECRET];
|
||||||
app.use(session(app));
|
app.use(session(app));
|
||||||
|
|
||||||
const { authRouter, passport } = routerAuth(log, pg);
|
const { authRouter, passport } = routerAuth(log, pg, events);
|
||||||
app.use(passport.initialize());
|
app.use(passport.initialize());
|
||||||
app.use(passport.session())
|
app.use(passport.session())
|
||||||
app.use(authRouter.routes());
|
app.use(authRouter.routes());
|
||||||
@@ -58,85 +69,19 @@ export default function createApi(log: Log, pg: Pg, discogs, offers, events) {
|
|||||||
ctx.status = 200;
|
ctx.status = 200;
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get('/acp/bundles', async function bundlesGet(ctx, next) {
|
|
||||||
const bundles = await offers.bundles();
|
|
||||||
ctx.response.body = bundles;
|
|
||||||
})
|
|
||||||
|
|
||||||
router.get('/acp/offers', async function acpOffers(ctx, next) {
|
|
||||||
const offers = await pg.offersAdmin();
|
|
||||||
|
|
||||||
const grouped = groupBy(offers, o => o.listing.data.seller.id);
|
|
||||||
|
|
||||||
const bundles = sortBy(Object.values(grouped).map(offers => {
|
|
||||||
const shipping = offers[0]?.listing.data.seller.shipping;
|
|
||||||
const bundle = {
|
|
||||||
username: offers[0]?.listing.data.seller.username,
|
|
||||||
seller: offers[0]?.listing.data.seller.id,
|
|
||||||
shipsFrom: offers[0]?.listing.data.ships_from,
|
|
||||||
shipping,
|
|
||||||
offers,
|
|
||||||
mentionsAustralia: shipping.toLowerCase().includes('australia'),
|
|
||||||
mentionsWorldwide: shipping.toLowerCase().includes('world'),
|
|
||||||
}
|
|
||||||
|
|
||||||
return bundle;
|
|
||||||
}), bundle => -bundle.offers.length);
|
|
||||||
|
|
||||||
ctx.response.body = bundles;
|
|
||||||
})
|
|
||||||
|
|
||||||
router.post('/offers', async function postOffers(ctx, next) {
|
|
||||||
const { listingIds, shipping, baseShipping } = ctx.request.body;
|
|
||||||
|
|
||||||
ctx.assert(listingIds.length, 400, 'listingIds required');
|
|
||||||
ctx.assert([listingIds, shipping, baseShipping].every(i => i), 400, 'listingIds, shipping, baseShipping required');
|
|
||||||
|
|
||||||
ctx.response.body = await offers.generate(listingIds, shipping, baseShipping);
|
|
||||||
})
|
|
||||||
|
|
||||||
router.post('/offer/accept', async function postOffers(ctx, next) {
|
|
||||||
ctx.assert(ctx.isAuthenticated(), 401, 'log in required');
|
|
||||||
ctx.response.body = await offers.accept(ctx);
|
|
||||||
})
|
|
||||||
|
|
||||||
router.get('/user/offers', async function userOffersGet(ctx, next) {
|
|
||||||
ctx.assert(ctx.isAuthenticated(), 401, 'log in required');
|
|
||||||
const user = ctx.state.user;
|
|
||||||
const offers = await pg.userOffers(user);
|
|
||||||
ctx.response.body = offers;
|
|
||||||
});
|
|
||||||
|
|
||||||
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.WantAdded, want);
|
|
||||||
ctx.response.body = [want];
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/want/discogs', async function wantListPost(ctx, next) {
|
router.post('/want/discogs', async function wantListPost(ctx, next) {
|
||||||
ctx.assert(ctx.isAuthenticated(), 401, 'log in required');
|
ctx.assert(ctx.isAuthenticated(), 401, 'log in required');
|
||||||
const { username } = ctx.request.body;
|
const { username } = ctx.request.body;
|
||||||
|
ctx.assert(username, 'no username supplied');
|
||||||
const user = await pg.userFindId(ctx.state.user.id);
|
const user = await pg.userFindId(ctx.state.user.id);
|
||||||
const wants = await discogs.wantListSync(username, user);
|
const wants = await discogs.wantListSync(username, user);
|
||||||
wants.forEach(w => events.emit(Events.WantAdded, w));
|
events.emit(Events.WantsAdded, wants);
|
||||||
ctx.response.body = wants;
|
ctx.response.body = wants;
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get('/want/all', async function wantGet(ctx, next) {
|
router.post('/want/search', async function wantSearch(ctx, next) {
|
||||||
ctx.assert(ctx.isAuthenticated(), 401, 'log in required');
|
const { query } = ctx.request.body;
|
||||||
const user = ctx.state.user;
|
const results = await discogs.search(query);
|
||||||
const wants = await pg.wantList(user);
|
|
||||||
|
|
||||||
ctx.response.body = wants;
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/want/search', async function wantPost(ctx, next) {
|
|
||||||
const { artist, title } = ctx.request.body;
|
|
||||||
const results = await discogs.search(artist, title);
|
|
||||||
ctx.response.body = results;
|
ctx.response.body = results;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -151,26 +96,39 @@ export default function createApi(log: Log, pg: Pg, discogs, offers, events) {
|
|||||||
|
|
||||||
app.ws.use(passport.initialize());
|
app.ws.use(passport.initialize());
|
||||||
app.ws.use(passport.session());
|
app.ws.use(passport.session());
|
||||||
|
|
||||||
app.ws.use(async (ctx, next) => {
|
app.ws.use(async (ctx, next) => {
|
||||||
|
try {
|
||||||
log.info('new ws connection', { user: ctx.state.user });
|
log.info('new ws connection', { user: ctx.state.user });
|
||||||
ctx.websocket.on('message', async function incoming(message) {
|
|
||||||
log.info(message.toString());
|
// log.child...
|
||||||
|
|
||||||
|
const ws = ctx.websocket;
|
||||||
|
const rpc = createRpc(log, pg, ws, discogs, offers, events, ctx.state.user as User | null);
|
||||||
|
|
||||||
|
ws.on('message', async function incoming(message) {
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(message.toString());
|
const parsed = JSON.parse(message.toString());
|
||||||
log.info(parsed);
|
const response = await rpc.receive(parsed);
|
||||||
// const response = await rpc.receive(parsed);
|
rpc.send(response);
|
||||||
// rpc.send(ws, response);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log.warn(e);
|
log.warn(e);
|
||||||
log.warn(message.toString());
|
log.warn(message.toString());
|
||||||
// rpc.send(ws, [RpcMessageType.Error, e.message || e.msg])
|
rpc.send([RpcMessageType.Error, e.message || e.msg]);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
ctx.websocket.on('close', function onClose() {
|
ws.on('close', function onClose() {
|
||||||
log.info('ws disconnected', { user: ctx.state.user });
|
log.info('ws disconnected', { user: ctx.state.user });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (ctx.state.user) events.emit(Events.Connection, { rpc, id: ctx.state.user.id });
|
||||||
|
await rpc.onConnection();
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
log.error(e);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
+60
-45
@@ -1,23 +1,21 @@
|
|||||||
import fs from 'fs/promises';
|
|
||||||
|
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import cheerio from 'cheerio';
|
import pptr from 'puppeteer-extra'
|
||||||
import Bluebird from 'bluebird';
|
import StealthPlugin from 'puppeteer-extra-plugin-stealth'
|
||||||
|
|
||||||
import type Log from 'bunyan';
|
import type Log from 'bunyan';
|
||||||
import type { PrismaClient, Want, User, Listing } from '@prisma/client';
|
import type { Want, User } from '@prisma/client';
|
||||||
|
|
||||||
import { logAxiosError, wantDetails } from './utils';
|
import { wantDetails } from './utils';
|
||||||
import { Pg } from './pg';
|
import { Pg } from './pg';
|
||||||
|
|
||||||
// margh
|
export default async function createDiscogs(log: Log, pg: Pg) {
|
||||||
// vanderbabe
|
const TOKEN = process.env.DISCOGS_TOKEN;
|
||||||
// jmcdermid1
|
|
||||||
|
|
||||||
export default function createDiscogs(log: Log, pg: Pg) {
|
|
||||||
const TOKEN = 'Discogs token=wRTXuYZGOPsIQgKRGKCAkUkidGtvpSfQgPWkYIpD';
|
|
||||||
const apiBase = 'https://api.discogs.com'
|
const apiBase = 'https://api.discogs.com'
|
||||||
const discogsBase = 'https://www.discogs.com'
|
|
||||||
|
pptr.use(StealthPlugin())
|
||||||
|
const browser = await pptr.launch({ headless: true, args: ['--no-sandbox', '--disable-setuid-sandbox'] })
|
||||||
|
|
||||||
|
log.info('browser created')
|
||||||
|
|
||||||
const api = axios.create({
|
const api = axios.create({
|
||||||
headers: {
|
headers: {
|
||||||
@@ -27,83 +25,98 @@ export default function createDiscogs(log: Log, pg: Pg) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
async function handleApiError(e) {
|
function onSuccess(response) {
|
||||||
logAxiosError(e);
|
return response.data;
|
||||||
if (e.response.status === 429) {
|
|
||||||
throw new Error('rate limited');
|
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
|
function onError(error) {
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
|
api.interceptors.response.use(onSuccess, onError);
|
||||||
|
|
||||||
async function wantListSync(username: string, user: User): Promise<Want[]> {
|
async function wantListSync(username: string, user: User): Promise<Want[]> {
|
||||||
const { data: { wants, pagination }} = await api.get(`${apiBase}/users/${username}/wants`);
|
const res = await api.get(`${apiBase}/users/${username}/wants`);
|
||||||
const releases = await Promise.all(wants.map(want => release(want.id)));
|
const { wants, pagination } = res;
|
||||||
|
|
||||||
|
const releases = await Promise.all(wants.map(want => release(want.id)));
|
||||||
log.info({ wants: wants.length, user: username, userId: user.id }, 'fetched discogs wantlist');
|
log.info({ wants: wants.length, user: username, userId: user.id }, 'fetched discogs wantlist');
|
||||||
|
|
||||||
// TODO filter releases to be vinyl format only
|
// TODO filter releases to be vinyl format only
|
||||||
|
|
||||||
// convert want object to actual release and ignore duplicate errors
|
// convert want object to actual release and ignore duplicate errors
|
||||||
const results = await Promise.allSettled(releases.map(release => pg.wantCreate(release, user)));
|
const results = await Promise.allSettled(releases.map(release => pg.wantCreate(release, user)));
|
||||||
|
log.info(results);
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const additions = results.filter(r => r.status === 'fulfilled').map(r => r.value);
|
const additions = results.filter(r => r.status === 'fulfilled').map(r => r.value);
|
||||||
|
|
||||||
log.info(additions);
|
log.info('want list additions', { additions: additions.length });
|
||||||
|
|
||||||
return additions;
|
return additions;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function release(id: DiscogsRelease): Promise<any> {
|
async function release(id: DiscogsRelease): Promise<any> {
|
||||||
const { data } = await api.get(`${apiBase}/releases/${id}`);
|
const data = await api.get(`${apiBase}/releases/${id}`);
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function search(artist: string, title: string) {
|
async function search(query: string) {
|
||||||
const url = new URL(`${apiBase}/database/search`);
|
const url = new URL(`${apiBase}/database/search`);
|
||||||
url.searchParams.append('artist', artist);
|
url.searchParams.append('query', query);
|
||||||
url.searchParams.append('title', title);
|
|
||||||
url.searchParams.append('type', 'release');
|
url.searchParams.append('type', 'release');
|
||||||
url.searchParams.append('format', 'vinyl');
|
url.searchParams.append('format', 'vinyl');
|
||||||
const { data: { results } } = await api.get(url.href);
|
const res = await api.get(url.href);
|
||||||
|
const { results } = res;
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function inventory(user: string) {
|
async function inventory(user: string) {
|
||||||
const res = await api.get(`${apiBase}/users/${user}/inventory`);
|
const res = await api.get(`${apiBase}/users/${user}/inventory`);
|
||||||
return res.data.listings;
|
return res.listings;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function listings(want: Want) {
|
async function listings(want: Want) {
|
||||||
const res = await api.get(`https://www.discogs.com/sell/release/${want.release}?limit=250`);
|
const page = await browser.newPage()
|
||||||
const $ = cheerio.load(res.data);
|
const url = `https://www.discogs.com/sell/release/${want.release}?limit=250`
|
||||||
const ids = $('.item_description_title[href]')
|
const res = await page.goto(url)
|
||||||
.map(function getListingId() {
|
log.debug({ status: res.status() }, 'status')
|
||||||
const id = Number.parseInt(this.attribs.href.replace('/sell/item/', ''), 10);
|
if (!res.ok()) throw new Error(res.statusText())
|
||||||
return id;
|
const els = await page.$$('.item_description_title[href]')
|
||||||
})
|
const hrefs = await Promise.all(els.map(e => e.evaluate(e => e.href)))
|
||||||
.get();
|
const ids = hrefs
|
||||||
|
.map(h => new URL(h))
|
||||||
|
.map(p => p.pathname.replace('/sell/item/', ''))
|
||||||
|
.map(p => Number.parseInt(p, 10))
|
||||||
|
|
||||||
|
await page.close()
|
||||||
|
|
||||||
|
if (ids.length === 0) log.warn({ res, url, ...wantDetails(want) }, "Want had zero listings")
|
||||||
|
|
||||||
const unlisted = await pg.listingRmUnlisted(want, ids);
|
const unlisted = await pg.listingRmUnlisted(want, ids);
|
||||||
if (unlisted) log.info({ wantId: want.id, unlisted: unlisted }, 'missing listings removed');
|
if (unlisted) log.info({ unlisted: unlisted, ...wantDetails(want) }, 'missing listings removed');
|
||||||
|
|
||||||
// preferentially take listings from sellers if we hit req limits
|
// preferentially take listings from sellers if we hit req limits
|
||||||
|
|
||||||
log.info({ listings: ids.length, ...wantDetails(want)}, 'found listings');
|
// sometimes we get dodgy listings from discogs that throw perm errors
|
||||||
return Bluebird.mapSeries(ids, async id => listing(id, want));
|
// we just want to ignore them
|
||||||
|
// const results = await Promise.allSettled(ids.map(id => listing(id, want)));
|
||||||
|
|
||||||
|
const results = []
|
||||||
|
for await (const id of ids) {
|
||||||
|
const r = await listing(id, want)
|
||||||
|
results.push(r)
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function listing(id, want) {
|
async function listing(id, want) {
|
||||||
const listing = await pg.listingFind(id);
|
const listing = await pg.listingFind(id);
|
||||||
if (listing) return listing;
|
if (listing) return listing;
|
||||||
|
|
||||||
try {
|
|
||||||
log.debug('fetching listing', id);
|
log.debug('fetching listing', id);
|
||||||
const res = await api.get(`${apiBase}/marketplace/listings/${id}`);
|
const discogsListing = await api.get(`${apiBase}/marketplace/listings/${id}`);
|
||||||
log.debug(res.data);
|
log.debug(discogsListing);
|
||||||
return pg.listingUpsert(res.data, want);
|
return pg.listingUpsert(discogsListing, want);
|
||||||
} catch (e) {
|
|
||||||
return handleApiError(e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -115,3 +128,5 @@ export default function createDiscogs(log: Log, pg: Pg) {
|
|||||||
search,
|
search,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type Discogs = ReturnType<typeof createDiscogs>;
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import type Log from 'bunyan';
|
||||||
|
|
||||||
|
import { Client, GatewayIntentBits } from 'discord.js';
|
||||||
|
|
||||||
|
export default function createDiscord(log: Log) {
|
||||||
|
|
||||||
|
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
|
||||||
|
|
||||||
|
const TOKEN = process.env.DISCORD_TOKEN;
|
||||||
|
|
||||||
|
async function connect() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
client.once('ready', () => {
|
||||||
|
log.info({ bot: client.user.tag }, 'discord client logged in');
|
||||||
|
resolve(client);
|
||||||
|
});
|
||||||
|
client.login(TOKEN);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function message(msg) {
|
||||||
|
if (!process.env.DISCORD_ENABLED) return false;
|
||||||
|
log.info({ msg }, 'sending discord message');
|
||||||
|
const channel = client.channels.cache.get('1072490127324090418');
|
||||||
|
// @ts-ignore - wrong
|
||||||
|
const str = JSON.stringify(msg, null, 4);
|
||||||
|
channel.send(`\`\`\`${str}\`\`\``);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
connect,
|
||||||
|
message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Discord = ReturnType<typeof createDiscord>;
|
||||||
+96
-54
@@ -2,85 +2,127 @@ import { EventEmitter } from 'node:events';
|
|||||||
import { setTimeout } from 'timers/promises';
|
import { setTimeout } from 'timers/promises';
|
||||||
|
|
||||||
import type Log from 'bunyan';
|
import type Log from 'bunyan';
|
||||||
import type { Want } from '@prisma/client';
|
import type { Want, Offer } from '@prisma/client';
|
||||||
|
|
||||||
import { Pg } from './pg';
|
import { Pg } from './pg';
|
||||||
|
import { Rpc } from './rpc';
|
||||||
import { wantDetails } from './utils';
|
import { wantDetails } from './utils';
|
||||||
import routerAuth from './routers/router.auth';
|
import routerAuth from './routers/router.auth';
|
||||||
|
|
||||||
export enum Events {
|
export enum Events {
|
||||||
WantAdded = 'WantAdded',
|
WantsAdded = 'WantsAdded',
|
||||||
StartupSync = 'StartupSync',
|
Sync = 'Sync',
|
||||||
|
Connection = 'Connection',
|
||||||
|
|
||||||
ListingsUpdated = 'ListingsUpdated',
|
ListingsUpdated = 'ListingsUpdated',
|
||||||
|
OffersUpdated = 'OffersUpdated',
|
||||||
|
WantsUpdated = 'WantsUpdated',
|
||||||
|
|
||||||
|
DiscordMessage = 'DiscordMessage',
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function createEvents(log: Log, pg: Pg, discogs, offers) {
|
export default function createEvents(log: Log, pg: Pg, discogs, offers, discord) {
|
||||||
const events = new EventEmitter({ captureRejections: true });
|
const events = new EventEmitter({ captureRejections: true });
|
||||||
|
const connections = new Map<UUID, Rpc>();
|
||||||
async function populateListings(want: Want) {
|
|
||||||
try {
|
|
||||||
const listings = await discogs.listings(want);
|
|
||||||
return listings;
|
|
||||||
} catch (e) {
|
|
||||||
if (e.message === 'rate limited') {
|
|
||||||
log.warn('rate limited, trying again in 60s');
|
|
||||||
await setTimeout(60 * 1000);
|
|
||||||
return populateListings(want);
|
|
||||||
}
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
events.on('error', function eventError(e) {
|
events.on('error', function eventError(e) {
|
||||||
log.error('event error', e);
|
log.error('event error', e);
|
||||||
});
|
});
|
||||||
|
|
||||||
events.on(Events.WantAdded, async function wantAdded(want: Want) {
|
const MAX_ATTEMPTS = 3;
|
||||||
log.info({ event: Events.WantAdded, ...wantDetails(want) });
|
const SYNC_EVERY_MS = 60 * 1000
|
||||||
const listings = await populateListings(want);
|
const RATE_LIMIT_WAIT_MS = (attempts) => attempts * 70 * 1000
|
||||||
log.info({ listings: listings.length, ...wantDetails(want) }, 'listings added');
|
async function populateListings(want: Want, attempts = 1) {
|
||||||
events.emit(Events.ListingsUpdated);
|
try {
|
||||||
});
|
const listings = await discogs.listings(want);
|
||||||
|
await pg.wantSynced(want);
|
||||||
|
return listings;
|
||||||
|
} catch (e) {
|
||||||
|
log.error(e)
|
||||||
|
if (e.response.status === 429) {
|
||||||
|
log.warn({ RATE_LIMIT_WAIT_MS: RATE_LIMIT_WAIT_MS(attempts), ...e.response.headers }, 'rate limited, trying again');
|
||||||
|
|
||||||
events.on(Events.StartupSync, async function wantAdded() {
|
await setTimeout(RATE_LIMIT_WAIT_MS(attempts));
|
||||||
const wants = await pg.wantsToSync();
|
if (attempts > MAX_ATTEMPTS) {
|
||||||
log.info({ event: Events.StartupSync, wants: wants.length });
|
log.error(wantDetails(want), 'failed to populate listings')
|
||||||
|
}
|
||||||
|
return populateListings(want, attempts++);
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateWantListings(wants: Want[]) {
|
||||||
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 found');
|
||||||
|
await offers.generate();
|
||||||
|
await notifyUser(want.userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info({ wants: wants.length }, 'finished syncing');
|
log.info({ wants: wants.length }, 'finished syncing');
|
||||||
events.emit(Events.ListingsUpdated);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
events.on(Events.ListingsUpdated, async function listingsUpdated() {
|
|
||||||
const bundles = await offers.bundles();
|
|
||||||
log.info({ event: Events.ListingsUpdated, bundles: bundles.length });
|
|
||||||
|
|
||||||
// const bundle = {
|
|
||||||
// username: b[0]?.data.seller.username,
|
|
||||||
// seller: b[0]?.data.seller.id,
|
|
||||||
// shipsFrom: b[0]?.data.ships_from,
|
|
||||||
// total,
|
|
||||||
// currency: b[0]?.data.price.currency,
|
|
||||||
// shipping: '...',
|
|
||||||
// items,
|
|
||||||
// mentionsAustralia: shipping.toLowerCase().includes('australia'),
|
|
||||||
// mentionsWorldwide: shipping.toLowerCase().includes('world'),
|
|
||||||
// }
|
|
||||||
|
|
||||||
for (const bundle of bundles) {
|
|
||||||
log.info({ seller: bundle.seller, items: bundle.items.length }, 'generating offers...');
|
|
||||||
const listingIds = bundle.items.map(i => i.id);
|
|
||||||
const updatedOffers = await offers.generate(listingIds, 100, 30)
|
|
||||||
log.info({ offers: updatedOffers.length }, 'offers set');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info('offers up to date');
|
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.on(Events.Sync, async function sync() {
|
||||||
|
try {
|
||||||
|
const wants = await pg.wantsToSync();
|
||||||
|
log.info({ event: Events.Sync, wants: wants.length });
|
||||||
|
await updateWantListings(wants);
|
||||||
|
|
||||||
|
if (wants.length === 0) {
|
||||||
|
log.info('all listings up to date');
|
||||||
|
await setTimeout(10000)
|
||||||
|
// forever
|
||||||
|
events.emit(Events.Sync);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
log.error(e, 'sync error')
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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.DiscordMessage, async function onDiscordMessage(msg) {
|
||||||
|
discord.message(msg);
|
||||||
|
})
|
||||||
|
|
||||||
|
// 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.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;
|
return events;
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import createSubscriber from 'pg-listen'
|
||||||
|
import type Log from 'bunyan';
|
||||||
|
|
||||||
|
import { Events } from './events';
|
||||||
|
import { Pg } from './pg';
|
||||||
|
|
||||||
|
export default function createNotify(log: Log, pg: Pg, events) {
|
||||||
|
const OFFERS_CHANNEL = 'offers.events';
|
||||||
|
const USERS_CHANNEL = 'users.events';
|
||||||
|
const LISTINGS_CHANNEL = 'listings.events';
|
||||||
|
const WANTS_CHANNEL = 'wants.events';
|
||||||
|
|
||||||
|
const subscriber = createSubscriber({ connectionString: process.env.DATABASE_URL });
|
||||||
|
|
||||||
|
// pg has a hard limit of 8000B for the notification payload
|
||||||
|
// so we only receive the ids of the updated record
|
||||||
|
// and go fetch the whole thing to figure out who to notify
|
||||||
|
|
||||||
|
async function handleOffers(payload) {
|
||||||
|
const { action, id } = payload;
|
||||||
|
log.debug({ channel: OFFERS_CHANNEL, action, id });
|
||||||
|
if (action === 'DELETE') return null;
|
||||||
|
|
||||||
|
// const offer = await pg.offerFindId(id);
|
||||||
|
// events.emit(Events.OffersUpdated, offer);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleUsers(payload) {
|
||||||
|
const { action, id } = payload;
|
||||||
|
log.debug({ channel: USERS_CHANNEL, action, id });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleListings(payload) {
|
||||||
|
const { action, id } = payload;
|
||||||
|
log.debug({ channel: LISTINGS_CHANNEL, action, id });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleWants(payload) {
|
||||||
|
const { action, id } = payload;
|
||||||
|
// const want = await pg.offerFindId(id);
|
||||||
|
log.debug({ channel: WANTS_CHANNEL, action, id });
|
||||||
|
}
|
||||||
|
|
||||||
|
subscriber.notifications.on(OFFERS_CHANNEL, handleOffers);
|
||||||
|
subscriber.notifications.on(USERS_CHANNEL, handleUsers);
|
||||||
|
subscriber.notifications.on(LISTINGS_CHANNEL, handleListings);
|
||||||
|
subscriber.notifications.on(WANTS_CHANNEL, handleWants);
|
||||||
|
|
||||||
|
async function listen() {
|
||||||
|
await subscriber.connect();
|
||||||
|
await Promise.all([
|
||||||
|
subscriber.listenTo(OFFERS_CHANNEL),
|
||||||
|
subscriber.listenTo(USERS_CHANNEL),
|
||||||
|
subscriber.listenTo(LISTINGS_CHANNEL),
|
||||||
|
subscriber.listenTo(WANTS_CHANNEL),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { listen };
|
||||||
|
}
|
||||||
+92
-51
@@ -1,47 +1,45 @@
|
|||||||
import type Log from 'bunyan';
|
import type Log from 'bunyan';
|
||||||
import type { PrismaClient, Want, User, Listing, Offer, } from '@prisma/client';
|
import type { PrismaClient, Want, User, Listing, Offer, } from '@prisma/client';
|
||||||
|
import assert from 'assert/strict';
|
||||||
import groupBy from 'lodash/groupBy';
|
import groupBy from 'lodash/groupBy';
|
||||||
import sortBy from 'lodash/sortBy';
|
import sortBy from 'lodash/sortBy';
|
||||||
|
import flatMap from 'lodash/flatMap';
|
||||||
import { Context } from 'koa';
|
import { Context } from 'koa';
|
||||||
|
|
||||||
import { logAxiosError } from './utils';
|
import { logAxiosError, wantDetails } from './utils';
|
||||||
import { Pg } from './pg';
|
import { Pg } from './pg';
|
||||||
|
|
||||||
export enum Status {
|
export enum Status {
|
||||||
Pending = 'pending',
|
Complete = 'Complete',
|
||||||
Accepted = 'accepted',
|
Confirmed = 'Confirmed',
|
||||||
Declined = 'declined',
|
Accepted = 'Accepted',
|
||||||
Ordered = 'ordered',
|
Pending = 'Pending',
|
||||||
Complete = 'complete',
|
Declined = 'Declined',
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortOffers(o) {
|
||||||
|
return Object.values(Status).indexOf(o.status);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function createOffers(log: Log, pg: Pg) {
|
export default function createOffers(log: Log, pg: Pg) {
|
||||||
async function generate(listingIds: DiscogsListing[], bulkShipping: number, baseShipping: number) {
|
async function generate() {
|
||||||
log.info('generateOffers', { listingIds, bulkShipping, baseShipping });
|
const listings = flatMap(await pg.interestingListings());
|
||||||
const listings = await pg.listingByIds(listingIds);
|
log.debug('generateOffers', { listings: listings.length });
|
||||||
|
|
||||||
// log.info({ listings }, 'creating offers for listings')
|
const shipping = 7.5; // yolt
|
||||||
|
|
||||||
const shipping = 10 + Math.random(); // yolt
|
|
||||||
|
|
||||||
// const shipping = bulkShipping / listings.length;
|
|
||||||
// how to deal with users who don't want all items in a bundle...
|
|
||||||
|
|
||||||
const offers = listings.map(l => ({
|
const offers = listings.map(l => ({
|
||||||
userId: l.want.user.id,
|
userId: l.want.user.id,
|
||||||
listingId: l.id,
|
listingId: l.id,
|
||||||
wantId: l.want.id,
|
wantId: l.want.id,
|
||||||
|
|
||||||
// sellerId: l.seller,
|
|
||||||
|
|
||||||
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,
|
||||||
|
|
||||||
// without mcrates...
|
|
||||||
comparison: l.data.price.value + baseShipping,
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// we must sort the offers by price,
|
// we must sort the offers by price,
|
||||||
@@ -49,7 +47,12 @@ export default function createOffers(log: Log, pg: Pg) {
|
|||||||
// to deal with this we just ignore duplicates on insert
|
// to deal with this we just ignore duplicates on insert
|
||||||
// and ensure the cheapest one goes in first
|
// and ensure the cheapest one goes in first
|
||||||
const sorted = sortBy(offers, ['item']);
|
const sorted = sortBy(offers, ['item']);
|
||||||
const inserts = await pg.offersCreate(sorted);
|
|
||||||
|
// TODO probably need to take one of each condition of record from a store
|
||||||
|
// eg. they have a VG+ and a M we should make offers for each
|
||||||
|
|
||||||
|
log.trace('generated offers', sorted);
|
||||||
|
const inserts = await pg.offersUpdate(sorted);
|
||||||
|
|
||||||
return inserts;
|
return inserts;
|
||||||
}
|
}
|
||||||
@@ -60,53 +63,91 @@ export default function createOffers(log: Log, pg: Pg) {
|
|||||||
|
|
||||||
// check the user actually has this offer
|
// check the user actually has this offer
|
||||||
const offer = await pg.userOfferFind(ctx.state.user, id);
|
const offer = await pg.userOfferFind(ctx.state.user, id);
|
||||||
return pg.offerSetStatus(offer.id, Status.Accepted);
|
return pg.offerStatusSet([offer.id], Status.Accepted);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function bundles() {
|
async function statusSet(user: User, ids: UUID[], status: Status) {
|
||||||
const bundles = await pg.bundleCandidates();
|
log.info({ status, ids }, 'setting offer status');
|
||||||
|
|
||||||
interface ListingData extends Listing {
|
// const offer = await pg.offerFindId(ids);
|
||||||
data: any,
|
const update = await pg.offerStatusSet(ids, status);
|
||||||
want: { user: User },
|
|
||||||
};
|
|
||||||
|
|
||||||
return bundles.map((b: ListingData[]) => {
|
// forget about this for now
|
||||||
const total = b.reduce((acc, l) => acc + l.data.price.value, 0);
|
// if (status === Status.Complete) {
|
||||||
const items = b.map(l => ({
|
// log.info({ id, status }, 'completed offer, removing want...');
|
||||||
id: l.data.id,
|
// await pg.wantRm(offer.wantId);
|
||||||
description: l.data.release.description,
|
// }
|
||||||
originalPrice: l.data.original_price.value,
|
|
||||||
originalCurrency: l.data.original_price.curr_abbr,
|
|
||||||
price: l.data.price.value,
|
|
||||||
currency: l.data.price.currency,
|
|
||||||
condition: l.data.condition,
|
|
||||||
uri: l.data.uri,
|
|
||||||
user: l.want.user,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const shipping = b[0]?.data.seller.shipping;
|
return update;
|
||||||
|
}
|
||||||
|
|
||||||
|
// converts offers into groups based on seller
|
||||||
|
// includes some bits of info about shipping etc
|
||||||
|
async function acpOffers() {
|
||||||
|
const offers = await pg.offersAdmin();
|
||||||
|
|
||||||
|
const grouped = groupBy(offers, o => o.listing.data.seller.id);
|
||||||
|
|
||||||
|
const sellerOffers = sortBy(Object.values(grouped).map(offers => {
|
||||||
|
const shipping = offers[0]?.listing.data.seller.shipping;
|
||||||
const bundle = {
|
const bundle = {
|
||||||
username: b[0]?.data.seller.username,
|
username: offers[0]?.listing.data.seller.username,
|
||||||
seller: b[0]?.data.seller.id,
|
seller: offers[0]?.listing.data.seller.id,
|
||||||
shipsFrom: b[0]?.data.ships_from,
|
shipsFrom: offers[0]?.listing.data.ships_from,
|
||||||
total,
|
shipping,
|
||||||
currency: b[0]?.data.price.currency,
|
offers: sortBy(offers, sortOffers),
|
||||||
shipping: '...',
|
|
||||||
items,
|
|
||||||
mentionsAustralia: shipping.toLowerCase().includes('australia'),
|
mentionsAustralia: shipping.toLowerCase().includes('australia'),
|
||||||
mentionsWorldwide: shipping.toLowerCase().includes('world'),
|
mentionsWorldwide: shipping.toLowerCase().includes('world'),
|
||||||
}
|
}
|
||||||
|
|
||||||
return bundle;
|
return bundle;
|
||||||
});
|
}), bundle => -bundle.offers.length);
|
||||||
|
|
||||||
|
return sellerOffers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// also groups offers by seller for users
|
||||||
|
// but mostly just munges the structure so the details
|
||||||
|
// are easily available
|
||||||
|
async function userOffers(user: User) {
|
||||||
|
const offers = await pg.userOffers(user);
|
||||||
|
const groups = Object.values(groupBy(offers, (o) => o.wantId))
|
||||||
|
.map(g => {
|
||||||
|
// the offer status is just the most advanced status
|
||||||
|
// based on sort
|
||||||
|
const offers = sortBy(g, sortOffers);
|
||||||
|
const status = offers[0].status;
|
||||||
|
|
||||||
|
// now actually sort them by price
|
||||||
|
offers.sort((a, b) => a.total - b.total);
|
||||||
|
|
||||||
|
const details = offers[0].want.data;
|
||||||
|
|
||||||
|
return {
|
||||||
|
details,
|
||||||
|
offers,
|
||||||
|
status,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function samples() {
|
||||||
|
const user = await pg.userFindEmail('ntr@mnml.gg');
|
||||||
|
const offerGroups = await userOffers(user);
|
||||||
|
const i = Math.floor(Math.random() * offerGroups.length / 2);
|
||||||
|
return offerGroups[i];
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
generate,
|
generate,
|
||||||
accept,
|
accept,
|
||||||
bundles,
|
statusSet,
|
||||||
|
acpOffers,
|
||||||
|
userOffers,
|
||||||
|
samples,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type Offers = ReturnType<typeof createOffers>;
|
||||||
+93
-55
@@ -5,37 +5,14 @@ import * as Bluebird from 'bluebird';
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
|
||||||
import { Status } from './offers';
|
import { Status } from './offers';
|
||||||
|
import { wantDetails } from './utils';
|
||||||
|
|
||||||
export interface UserCreateParams {
|
export interface UserCreateParams {
|
||||||
email: string,
|
email: string,
|
||||||
password: string,
|
password: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Pg {
|
export default function createPg(log: Log, prisma: PrismaClient) {
|
||||||
userFindId: (id: UUID) => Promise<User>,
|
|
||||||
userFindEmail: (email: Email) => Promise<User>,
|
|
||||||
userCreate: (params: UserCreateParams) => Promise<User>,
|
|
||||||
userWants: (user: User) => Promise<Want[]>,
|
|
||||||
userOffers: (user: User) => Promise<Offer[]>,
|
|
||||||
userOfferFind: (user: User, id: UUID) => Promise<Offer>,
|
|
||||||
|
|
||||||
listingUpsert: (listing: Listing, want: Want) => Promise<Listing>,
|
|
||||||
listingFind: (id: DiscogsListing) => Promise<Listing>,
|
|
||||||
listingByIds: (ids: DiscogsListing[]) => Promise<Listing[]>,
|
|
||||||
listingRmUnlisted: (want: Want, ids: DiscogsListing[]) => Promise<number>,
|
|
||||||
|
|
||||||
wantCreate: (discogsWant: any, user: User) => Promise<Want>,
|
|
||||||
wantList: (user: User) => Promise<Want[]>,
|
|
||||||
wantsToSync: () => Promise<Want[]>,
|
|
||||||
|
|
||||||
bundleCandidates: () => Promise<Listing[][]>,
|
|
||||||
|
|
||||||
offersCreate: (offers: Offer[]) => Promise<Offer[]>,
|
|
||||||
offerSetStatus: (id: UUID, status: Status) => Promise<Offer>,
|
|
||||||
offersAdmin: () => Promise<Offer[]>,
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function createPg(log: Log, prisma: PrismaClient): Pg {
|
|
||||||
async function userFindId(id: UUID) {
|
async function userFindId(id: UUID) {
|
||||||
return prisma.user.findUnique({ where: { id }});
|
return prisma.user.findUnique({ where: { id }});
|
||||||
}
|
}
|
||||||
@@ -45,18 +22,37 @@ export default function createPg(log: Log, prisma: PrismaClient): Pg {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function userWants(user: User) {
|
async function userWants(user: User) {
|
||||||
return prisma.want.findMany({ where: { userId: user.id }});
|
const wants = await prisma.want.findMany({
|
||||||
|
where: { userId: user.id },
|
||||||
|
include: { _count: { select: { offers: true }} },
|
||||||
|
})
|
||||||
|
|
||||||
|
return _.sortBy(wants, w => `${w.data.artists_sort} - ${w.data.title}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function userOffers(user: User) {
|
async function userOffers(user: User) {
|
||||||
return prisma.offer.findMany({
|
return prisma.offer.findMany({
|
||||||
where: { userId: user.id },
|
where: {
|
||||||
|
userId: user.id,
|
||||||
|
status: { not: Status.Declined }
|
||||||
|
},
|
||||||
include: {
|
include: {
|
||||||
want: true,
|
want: true,
|
||||||
},
|
},
|
||||||
orderBy: {
|
orderBy: [
|
||||||
total: 'asc',
|
{ updatedAt: 'asc' },
|
||||||
},
|
{ total: 'asc' },
|
||||||
|
{ listingId: 'asc' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function offerFindId(id: UUID) {
|
||||||
|
return prisma.offer.findFirstOrThrow({
|
||||||
|
where: { id },
|
||||||
|
include: {
|
||||||
|
want: true,
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,6 +82,9 @@ export default function createPg(log: Log, prisma: PrismaClient): Pg {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// userUpdate({ email: 'jen@vdk.com', password: 'jen' });
|
||||||
|
// userUpdate({ email: 'james@mcd', password: 'james' });
|
||||||
|
|
||||||
async function userUpdate(params: UserCreateParams) {
|
async function userUpdate(params: UserCreateParams) {
|
||||||
const hashRounds = 10;
|
const hashRounds = 10;
|
||||||
const password = await bcrypt.hash(params.password, hashRounds);
|
const password = await bcrypt.hash(params.password, hashRounds);
|
||||||
@@ -101,6 +100,12 @@ export default function createPg(log: Log, prisma: PrismaClient): Pg {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function wantFindId(id: UUID) {
|
||||||
|
return prisma.want.findFirstOrThrow({
|
||||||
|
where: { id },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function wantList(user: User) {
|
async function wantList(user: User) {
|
||||||
return prisma.want.findMany({
|
return prisma.want.findMany({
|
||||||
where: { userId: user.id },
|
where: { userId: user.id },
|
||||||
@@ -108,7 +113,18 @@ export default function createPg(log: Log, prisma: PrismaClient): Pg {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function wantsToSync() {
|
async function wantsToSync() {
|
||||||
return prisma.want.findMany({});
|
return prisma.want.findMany({
|
||||||
|
orderBy: { updatedAt: 'asc' },
|
||||||
|
where: {
|
||||||
|
updatedAt: { lte: new Date(Date.now() - 1000 * 60 * 15) }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function wantRm(id: UUID) {
|
||||||
|
await prisma.want.delete({
|
||||||
|
where: { id },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function wantCreate(discogsRelease, user) {
|
async function wantCreate(discogsRelease, user) {
|
||||||
@@ -123,14 +139,21 @@ export default function createPg(log: Log, prisma: PrismaClient): Pg {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function wantSynced(want: Want) {
|
||||||
|
return prisma.want.update({
|
||||||
|
where: { id: want.id },
|
||||||
|
data: { updatedAt: new Date() },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function listingRmUnlisted(want: Want, currentListingIds: DiscogsListing[]) {
|
async function listingRmUnlisted(want: Want, currentListingIds: DiscogsListing[]) {
|
||||||
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
|
// delete all the offers where we don't want to keep them
|
||||||
// so delete those offers first
|
// for historical reasons
|
||||||
const { count: offersRemoved } = await prisma.offer.deleteMany({
|
const { count: offersRemoved } = await prisma.offer.deleteMany({
|
||||||
where: {
|
where: {
|
||||||
wantId: want.id,
|
wantId: want.id,
|
||||||
@@ -138,12 +161,12 @@ export default function createPg(log: Log, prisma: PrismaClient): Pg {
|
|||||||
notIn: currentListingIds,
|
notIn: currentListingIds,
|
||||||
},
|
},
|
||||||
status: {
|
status: {
|
||||||
in: openOfferStatuses,
|
notIn: protectedOfferStatuses,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (offersRemoved > 0) log.info({ offersRemoved }, 'open offers removed');
|
if (offersRemoved > 0) log.info({ offersRemoved, ...wantDetails(want) }, 'pending offers removed');
|
||||||
|
|
||||||
// now we have to filter listingIds with confirmed offers
|
// now we have to filter listingIds with confirmed offers
|
||||||
// and get their listingIds to filter out
|
// and get their listingIds to filter out
|
||||||
@@ -151,26 +174,27 @@ 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: {
|
||||||
listingId: true,
|
listingId: true,
|
||||||
}
|
},
|
||||||
})).map(r => r.listingId);
|
})).map(r => r.listingId);
|
||||||
const removalIds = _.without(currentListingIds, ...confirmedListingIds);
|
const removalIds = currentListingIds.filter(id => confirmedListingIds.includes(id));
|
||||||
|
|
||||||
|
if (removalIds.length) log.info({ removalIds });
|
||||||
|
|
||||||
// finally we delete listings that aren't related
|
// finally we delete listings that aren't related
|
||||||
// to a historical offer
|
// to a historical offer
|
||||||
const { count } = await prisma.listing.deleteMany({
|
const { count } = await prisma.listing.deleteMany({
|
||||||
where: {
|
where: {
|
||||||
wantId: want.id,
|
id: { in: removalIds },
|
||||||
id: {
|
},
|
||||||
notIn: currentListingIds,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (count) log.info({ count }, 'second stage open offers removed');
|
||||||
|
|
||||||
return count;
|
return count;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -218,8 +242,12 @@ export default function createPg(log: Log, prisma: PrismaClient): Pg {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function bundleCandidates(): Promise<Listing[][]> {
|
async function interestingListings() {
|
||||||
|
// this basically just needs to return the listingIds to generate offers
|
||||||
|
// but for ease of abstraction we get the listings themselves
|
||||||
|
// grouped by seller
|
||||||
const candidates = await prisma.$queryRaw`
|
const candidates = await prisma.$queryRaw`
|
||||||
|
WITH groups AS (
|
||||||
select
|
select
|
||||||
seller, count(distinct("wantId")), array_agg(id) as listings
|
seller, count(distinct("wantId")), array_agg(id) as listings
|
||||||
from
|
from
|
||||||
@@ -230,7 +258,10 @@ export default function createPg(log: Log, prisma: PrismaClient): Pg {
|
|||||||
seller
|
seller
|
||||||
order by
|
order by
|
||||||
count desc
|
count desc
|
||||||
limit 5;
|
)
|
||||||
|
select *
|
||||||
|
from groups
|
||||||
|
where count >= 5;
|
||||||
` as BundleCandidate[];
|
` as BundleCandidate[];
|
||||||
|
|
||||||
return Promise.all(candidates.map(async c => prisma.listing.findMany({
|
return Promise.all(candidates.map(async c => prisma.listing.findMany({
|
||||||
@@ -246,25 +277,26 @@ export default function createPg(log: Log, prisma: PrismaClient): Pg {
|
|||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function offersCreate(offers: Offer[]) {
|
async function offersUpdate(offers) {
|
||||||
return await prisma.$transaction(
|
return await prisma.$transaction(
|
||||||
offers.map(offer => prisma.offer.upsert({
|
offers.map(offer => prisma.offer.upsert({
|
||||||
where: {
|
where: {
|
||||||
listingId: offer.listingId,
|
listingId: offer.listingId,
|
||||||
},
|
},
|
||||||
create: offer,
|
create: offer,
|
||||||
update: _.omit(offer, ['status']),
|
update: _.omit(offer, ['status', 'id']),
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function offerSetStatus(id: UUID, status: Status) {
|
async function offerStatusSet(ids: UUID[], status: Status) {
|
||||||
return prisma.offer.update({
|
return prisma.offer.updateMany({
|
||||||
where: { id: id },
|
where: { id: { in: ids } },
|
||||||
data: { status },
|
data: { status },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// effectively all offers
|
||||||
async function offersAdmin() {
|
async function offersAdmin() {
|
||||||
return prisma.offer.findMany({
|
return prisma.offer.findMany({
|
||||||
include: {
|
include: {
|
||||||
@@ -286,15 +318,21 @@ export default function createPg(log: Log, prisma: PrismaClient): Pg {
|
|||||||
wantCreate,
|
wantCreate,
|
||||||
wantList,
|
wantList,
|
||||||
wantsToSync,
|
wantsToSync,
|
||||||
|
wantSynced,
|
||||||
|
wantRm,
|
||||||
|
|
||||||
listingUpsert,
|
listingUpsert,
|
||||||
listingFind,
|
listingFind,
|
||||||
listingRmUnlisted,
|
listingRmUnlisted,
|
||||||
listingByIds,
|
listingByIds,
|
||||||
|
|
||||||
bundleCandidates,
|
interestingListings,
|
||||||
|
|
||||||
offersCreate,
|
offersUpdate,
|
||||||
offerSetStatus,
|
offerStatusSet,
|
||||||
offersAdmin,
|
offersAdmin,
|
||||||
|
offerFindId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type Pg = ReturnType<typeof createPg>;
|
||||||
@@ -4,8 +4,9 @@ import { Strategy as LocalStrategy } from 'passport-local';
|
|||||||
import Router from '@koa/router'
|
import Router from '@koa/router'
|
||||||
|
|
||||||
import type { Pg } from './../pg';
|
import type { Pg } from './../pg';
|
||||||
|
import { Events } from './../events';
|
||||||
|
|
||||||
export default function authRouter(log, pg: Pg) {
|
export default function authRouter(log, pg: Pg, events) {
|
||||||
const passport = new KoaPassport();
|
const passport = new KoaPassport();
|
||||||
|
|
||||||
async function verifyPassword(user, password) {
|
async function verifyPassword(user, password) {
|
||||||
@@ -69,6 +70,7 @@ export default function authRouter(log, pg: Pg) {
|
|||||||
id: user.id,
|
id: user.id,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
};
|
};
|
||||||
|
events.emit(Events.DiscordMessage, { msg: 'user register', email: user.email });
|
||||||
});
|
});
|
||||||
|
|
||||||
return { authRouter, passport };
|
return { authRouter, passport };
|
||||||
|
|||||||
+258
@@ -0,0 +1,258 @@
|
|||||||
|
import assert from 'assert/strict';
|
||||||
|
|
||||||
|
import type Log from 'bunyan';
|
||||||
|
|
||||||
|
import type { Want, Offer, User } from '@prisma/client';
|
||||||
|
|
||||||
|
import type { Pg } from './pg';
|
||||||
|
import type { Discogs } from './discogs';
|
||||||
|
import type { Offers } from './offers';
|
||||||
|
|
||||||
|
import { Status } from './offers';
|
||||||
|
import { isAdmin } from './utils';
|
||||||
|
import { Events } from './events';
|
||||||
|
|
||||||
|
export type Rpc = ReturnType<typeof createRpc>;
|
||||||
|
|
||||||
|
export enum RpcMessageType {
|
||||||
|
Ping = 'Ping',
|
||||||
|
Pong = 'Pong',
|
||||||
|
|
||||||
|
// points out user is anon
|
||||||
|
WhoDis = 'WhoDis',
|
||||||
|
|
||||||
|
Bundles = 'Bundles',
|
||||||
|
Offers = 'Offers',
|
||||||
|
List = 'List',
|
||||||
|
Wants = 'Wants',
|
||||||
|
User = 'User',
|
||||||
|
|
||||||
|
Error = 'Error',
|
||||||
|
|
||||||
|
OfferSamples = 'OfferSamples',
|
||||||
|
OfferStatusSet = 'OfferStatusSet',
|
||||||
|
|
||||||
|
AcpOfferStatusSet = 'AcpOfferStatusSet',
|
||||||
|
AcpOffers = 'AcpOffers',
|
||||||
|
AcpListingsSync = 'AcpListingsSync',
|
||||||
|
|
||||||
|
WantRm = 'WantRm',
|
||||||
|
WantAdd = 'WantAdd',
|
||||||
|
}
|
||||||
|
|
||||||
|
type ErrorMessage = [RpcMessageType.Error, string];
|
||||||
|
type PongMessage = [RpcMessageType.Pong, []];
|
||||||
|
type PingMessage = [RpcMessageType.Ping, []];
|
||||||
|
type WhoDisMessage = [RpcMessageType.WhoDis, []];
|
||||||
|
type UserMessage = [RpcMessageType.User, User];
|
||||||
|
type WantsMessage = [RpcMessageType.Wants, Want[]];
|
||||||
|
type OffersMessage = [RpcMessageType.Offers, Offer[]];
|
||||||
|
type OfferStatusSetMessage = [RpcMessageType.OfferStatusSet, Offer[]];
|
||||||
|
type OfferSamplesMessage = [RpcMessageType.OfferSamples, Offer[]];
|
||||||
|
|
||||||
|
type AcpOfferStatusSetMessage = [RpcMessageType.AcpOfferStatusSet, Offer[]];
|
||||||
|
type AcpOffersMessage = [RpcMessageType.AcpOffers, Offer[]];
|
||||||
|
type AcpListingsSyncMessage = [RpcMessageType.AcpListingsSync, []];
|
||||||
|
|
||||||
|
export type RpcMessage =
|
||||||
|
PingMessage |
|
||||||
|
PongMessage |
|
||||||
|
WhoDisMessage |
|
||||||
|
|
||||||
|
UserMessage |
|
||||||
|
|
||||||
|
WantsMessage |
|
||||||
|
|
||||||
|
OffersMessage |
|
||||||
|
OfferSamplesMessage |
|
||||||
|
OfferStatusSetMessage |
|
||||||
|
|
||||||
|
AcpOfferStatusSetMessage |
|
||||||
|
AcpOffersMessage |
|
||||||
|
AcpListingsSyncMessage |
|
||||||
|
|
||||||
|
ErrorMessage;
|
||||||
|
|
||||||
|
|
||||||
|
type OfferStatusSetParams = {
|
||||||
|
ids: UUID[],
|
||||||
|
status: Status,
|
||||||
|
};
|
||||||
|
|
||||||
|
type WantRmParams = {
|
||||||
|
id: UUID,
|
||||||
|
};
|
||||||
|
|
||||||
|
type WantAddParams = {
|
||||||
|
releaseId: number,
|
||||||
|
};
|
||||||
|
|
||||||
|
function checkMsgType(msgType: RpcMessageType): msgType is RpcMessageType {
|
||||||
|
if (RpcMessageType[msgType]) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// rpc is created per connection from the api
|
||||||
|
export default function createRpc(log: Log, pg: Pg, ws, discogs: Discogs, offers: Offers, events, user: User | null) {
|
||||||
|
async function ping(): Promise<PongMessage> {
|
||||||
|
return [RpcMessageType.Pong, []];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function userOffers(): Promise<OffersMessage> {
|
||||||
|
return [RpcMessageType.Offers, await offers.userOffers(user)];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function userWants(): Promise<WantsMessage> {
|
||||||
|
return [RpcMessageType.Wants, await pg.userWants(user)];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function offerStatusSet(params: OfferStatusSetParams): Promise<OffersMessage> {
|
||||||
|
const { ids, status } = params;
|
||||||
|
assert(ids, 'no ids supplied');
|
||||||
|
assert(Status[status], 'unknown status');
|
||||||
|
|
||||||
|
const offer = await offers.statusSet(user, ids, status);
|
||||||
|
const userOffers = await offers.userOffers(user);
|
||||||
|
|
||||||
|
events.emit(Events.DiscordMessage, { msg: 'user offer event', user: user.email, offer })
|
||||||
|
|
||||||
|
return [RpcMessageType.Offers, userOffers]
|
||||||
|
}
|
||||||
|
|
||||||
|
async function wantRm(params: WantRmParams): Promise<WantsMessage> {
|
||||||
|
const { id } = params;
|
||||||
|
assert(id, 'no id supplied');
|
||||||
|
|
||||||
|
await pg.wantRm(id);
|
||||||
|
|
||||||
|
const wants = await pg.userWants(user);
|
||||||
|
return [RpcMessageType.Wants, wants];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function wantAdd(params: WantAddParams): Promise<WantsMessage> {
|
||||||
|
const { releaseId } = params;
|
||||||
|
assert(releaseId, 'no id supplied');
|
||||||
|
|
||||||
|
const release = await discogs.release(releaseId);
|
||||||
|
const want = await pg.wantCreate(release, user);
|
||||||
|
events.emit(Events.WantsAdded, [want]);
|
||||||
|
|
||||||
|
const wants = await pg.userWants(user);
|
||||||
|
return [RpcMessageType.Wants, wants];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function acpOfferStatusSet(params: OfferStatusSetParams): Promise<AcpOffersMessage> {
|
||||||
|
const { id, status } = params;
|
||||||
|
assert(isAdmin(user), 'not authorised');
|
||||||
|
assert(id, 'no id supplied');
|
||||||
|
assert(Status[status], 'unknown status');
|
||||||
|
|
||||||
|
const offer = await offers.statusSet(user, id, status);
|
||||||
|
|
||||||
|
return [RpcMessageType.AcpOffers, await offers.acpOffers()]
|
||||||
|
}
|
||||||
|
|
||||||
|
async function acpOffers(): Promise<AcpOffersMessage> {
|
||||||
|
assert(isAdmin(user), 'not authorised');
|
||||||
|
return [RpcMessageType.AcpOffers, await offers.acpOffers()];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function acpListingsSync(): Promise<AcpListingsSyncMessage> {
|
||||||
|
assert(isAdmin(user), 'not authorised');
|
||||||
|
events.emit(Events.Sync);
|
||||||
|
return [RpcMessageType.AcpListingsSync, []];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function receive(message: any): Promise<RpcMessage> {
|
||||||
|
const [msgType, params] = message;
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
log.debug('anon rpc call');
|
||||||
|
return [RpcMessageType.WhoDis, []];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msgType !== RpcMessageType.Ping) log.info({ msgType, params }, 'rpc message');
|
||||||
|
|
||||||
|
// // this sucks but can't figure out how to convince typescript to agree yet
|
||||||
|
if (checkMsgType(msgType)) {
|
||||||
|
switch (msgType) {
|
||||||
|
case RpcMessageType.Ping:
|
||||||
|
return ping();
|
||||||
|
|
||||||
|
case RpcMessageType.Offers:
|
||||||
|
return userOffers();
|
||||||
|
case RpcMessageType.Wants:
|
||||||
|
return userWants();
|
||||||
|
case RpcMessageType.OfferStatusSet:
|
||||||
|
return offerStatusSet(params);
|
||||||
|
|
||||||
|
case RpcMessageType.WantRm:
|
||||||
|
return wantRm(params);
|
||||||
|
case RpcMessageType.WantAdd:
|
||||||
|
return wantAdd(params);
|
||||||
|
|
||||||
|
case RpcMessageType.AcpOfferStatusSet:
|
||||||
|
return acpOfferStatusSet(params);
|
||||||
|
case RpcMessageType.AcpOffers:
|
||||||
|
return acpOffers();
|
||||||
|
case RpcMessageType.AcpListingsSync:
|
||||||
|
return acpListingsSync();
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error(`Message type ${msgType} is known but unhandled`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error(`Unknown message type ${msgType}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Invalid parameters for message type ${msgType}: ${JSON.stringify(message)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function anonConnection() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async function adminConnection() {
|
||||||
|
// don't need it, requested on route
|
||||||
|
// send(await acpOffers());
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onConnection() {
|
||||||
|
if (!user) return anonConnection();
|
||||||
|
|
||||||
|
const userMsg: UserMessage = [RpcMessageType.User, user];
|
||||||
|
send(userMsg);
|
||||||
|
|
||||||
|
await pushUserOffers();
|
||||||
|
await pushUserWants();
|
||||||
|
|
||||||
|
if (isAdmin(user)) return adminConnection();
|
||||||
|
}
|
||||||
|
|
||||||
|
function send(msg: RpcMessage) {
|
||||||
|
ws.send(JSON.stringify(msg, (key, value) =>
|
||||||
|
typeof value === 'bigint'
|
||||||
|
? value.toString()
|
||||||
|
: value // return everything else unchanged
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pushUserOffers() {
|
||||||
|
send(await userOffers());
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pushUserWants() {
|
||||||
|
send(await userWants());
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
receive,
|
||||||
|
send,
|
||||||
|
onConnection,
|
||||||
|
|
||||||
|
pushUserOffers,
|
||||||
|
pushUserWants,
|
||||||
|
}
|
||||||
|
}
|
||||||
+12
-6
@@ -1,24 +1,30 @@
|
|||||||
import { PrismaClient } from '@prisma/client'
|
import { PrismaClient } from '@prisma/client'
|
||||||
|
|
||||||
import createDiscogs from './discogs';
|
|
||||||
import createOffers from './offers';
|
|
||||||
import createLog from './log';
|
|
||||||
import createApi from './api';
|
import createApi from './api';
|
||||||
|
import createDiscogs from './discogs';
|
||||||
import createEvents from './events';
|
import createEvents from './events';
|
||||||
|
import createLog from './log';
|
||||||
|
import createNotify from './notify';
|
||||||
|
import createOffers from './offers';
|
||||||
import createPg from './pg';
|
import createPg from './pg';
|
||||||
|
import createDiscord from './discord';
|
||||||
|
|
||||||
export default function creates(log) {
|
export default async function createServices(log) {
|
||||||
const prisma = new PrismaClient();
|
const prisma = new PrismaClient();
|
||||||
|
const discord = createDiscord(log);
|
||||||
const pg = createPg(log, prisma);
|
const pg = createPg(log, prisma);
|
||||||
const discogs = createDiscogs(log, pg);
|
const discogs = await createDiscogs(log, pg);
|
||||||
const offers = createOffers(log, pg);
|
const offers = createOffers(log, pg);
|
||||||
const events = createEvents(log, pg, discogs, offers);
|
const events = createEvents(log, pg, discogs, offers, discord);
|
||||||
|
const notify = createNotify(log, pg, events);
|
||||||
const api = createApi(log, pg, discogs, offers, events);
|
const api = createApi(log, pg, discogs, offers, events);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
api,
|
api,
|
||||||
discogs,
|
discogs,
|
||||||
|
discord,
|
||||||
events,
|
events,
|
||||||
|
notify,
|
||||||
pg,
|
pg,
|
||||||
prisma,
|
prisma,
|
||||||
};
|
};
|
||||||
|
|||||||
+9
-1
@@ -1,4 +1,4 @@
|
|||||||
import type { Want } from '@prisma/client';
|
import type { Want, User } from '@prisma/client';
|
||||||
|
|
||||||
export function logAxiosError(e) {
|
export function logAxiosError(e) {
|
||||||
console.warn({
|
console.warn({
|
||||||
@@ -17,6 +17,8 @@ export function logAxiosError(e) {
|
|||||||
export function wantDetails(want: Want) {
|
export function wantDetails(want: Want) {
|
||||||
const data = want.data as any;
|
const data = want.data as any;
|
||||||
|
|
||||||
|
if (!data) console.warn(want);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: data.title,
|
title: data.title,
|
||||||
release: data.id,
|
release: data.id,
|
||||||
@@ -24,3 +26,9 @@ export function wantDetails(want: Want) {
|
|||||||
id: want.id
|
id: want.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isAdmin(user: User) {
|
||||||
|
return user.email === 'ntr@mnml.gg';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function offerDetails() {}
|
||||||
+11
-7
@@ -8,23 +8,27 @@ module.exports = {
|
|||||||
"build/"
|
"build/"
|
||||||
],
|
],
|
||||||
rules: {
|
rules: {
|
||||||
|
'arrow-body-style': [0],
|
||||||
|
|
||||||
|
'max-len': ['error', 120],
|
||||||
|
|
||||||
|
'no-console': [0],
|
||||||
|
'no-multi-spaces': [0],
|
||||||
// prevents stupid complaints a la
|
// prevents stupid complaints a la
|
||||||
// (req) {
|
// (req) {
|
||||||
// req.something = x;
|
// req.something = x;
|
||||||
// }
|
// }
|
||||||
'no-param-reassign': [2, { props: false }],
|
'no-param-reassign': [2, { props: false }],
|
||||||
'no-multi-spaces': [0],
|
|
||||||
'max-len': ['error', 120],
|
|
||||||
'import/no-extraneous-dependencies': [0],
|
|
||||||
'prefer-arrow-callback': [0],
|
|
||||||
'arrow-body-style': [0],
|
|
||||||
'no-return-assign': 0,
|
'no-return-assign': 0,
|
||||||
'no-console': [0],
|
|
||||||
|
'prefer-arrow-callback': [0],
|
||||||
|
|
||||||
// i like loops
|
// i like loops
|
||||||
'no-plusplus': [0],
|
'no-plusplus': [0],
|
||||||
|
|
||||||
'no-await-in-loop': [0],
|
'no-await-in-loop': [0],
|
||||||
|
|
||||||
indent: ['error', 4],
|
indent: ['error', 4],
|
||||||
|
|
||||||
'keyword-spacing': ['error'],
|
'keyword-spacing': ['error'],
|
||||||
'key-spacing': ['error'],
|
'key-spacing': ['error'],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# build step
|
||||||
|
FROM node:16 AS build-env
|
||||||
|
WORKDIR /opt/crates/client
|
||||||
|
|
||||||
|
COPY . ./
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# static server
|
||||||
|
FROM nginx
|
||||||
|
WORKDIR /srv/app
|
||||||
|
COPY --from=build-env /opt/crates/client/build /srv/app
|
||||||
|
COPY ./client.nginx.conf /etc/nginx/nginx.conf
|
||||||
Executable
+8
@@ -0,0 +1,8 @@
|
|||||||
|
# build step
|
||||||
|
FROM node:16 AS build-env
|
||||||
|
WORKDIR /opt/crates/client
|
||||||
|
|
||||||
|
COPY . ./
|
||||||
|
RUN npm i
|
||||||
|
|
||||||
|
CMD ["npm", "start"]
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
events {
|
||||||
|
worker_connections 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
http {
|
||||||
|
include mime.types;
|
||||||
|
sendfile on;
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 8080;
|
||||||
|
listen [::]:8080;
|
||||||
|
|
||||||
|
autoindex off;
|
||||||
|
|
||||||
|
root /srv/app;
|
||||||
|
|
||||||
|
gzip on;
|
||||||
|
gzip_disable "msie6";
|
||||||
|
|
||||||
|
gzip_vary on;
|
||||||
|
gzip_proxied any;
|
||||||
|
gzip_comp_level 6;
|
||||||
|
gzip_buffers 16 8k;
|
||||||
|
gzip_http_version 1.1;
|
||||||
|
gzip_min_length 256;
|
||||||
|
gzip_types
|
||||||
|
application/atom+xml
|
||||||
|
application/geo+json
|
||||||
|
application/javascript
|
||||||
|
application/x-javascript
|
||||||
|
application/json
|
||||||
|
application/ld+json
|
||||||
|
application/manifest+json
|
||||||
|
application/rdf+xml
|
||||||
|
application/rss+xml
|
||||||
|
application/xhtml+xml
|
||||||
|
application/xml
|
||||||
|
font/eot
|
||||||
|
font/otf
|
||||||
|
font/ttf
|
||||||
|
image/svg+xml
|
||||||
|
text/css
|
||||||
|
text/javascript
|
||||||
|
text/plain
|
||||||
|
text/xml;
|
||||||
|
|
||||||
|
error_page 404 /;
|
||||||
|
}
|
||||||
|
}
|
||||||
+37
-20
@@ -10,17 +10,19 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@reduxjs/toolkit": "^1.8.5",
|
"@reduxjs/toolkit": "^1.8.5",
|
||||||
|
"izitoast": "^1.4.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
|
"logrocket": "^3.0.1",
|
||||||
"preact": "^10.10.0",
|
"preact": "^10.10.0",
|
||||||
"preact-render-to-string": "^5.2.1",
|
"preact-render-to-string": "^5.2.1",
|
||||||
"preact-router": "^3.2.1",
|
"preact-router": "^3.2.1",
|
||||||
"react-feather": "^2.0.10",
|
"react-feather": "^2.0.10",
|
||||||
"react-redux": "^8.0.2",
|
"react-redux": "^8.0.2",
|
||||||
"redux": "^4.2.0",
|
"redux": "^4.2.0"
|
||||||
"unistore": "^3.5.2"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/plugin-transform-react-jsx-source": "^7.18.6",
|
"@babel/plugin-transform-react-jsx-source": "^7.18.6",
|
||||||
|
"@types/lodash": "^4.17.10",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.30.6",
|
"@typescript-eslint/eslint-plugin": "^5.30.6",
|
||||||
"@typescript-eslint/parser": "^5.30.6",
|
"@typescript-eslint/parser": "^5.30.6",
|
||||||
"eslint": "^8.20.0",
|
"eslint": "^8.20.0",
|
||||||
@@ -3008,6 +3010,13 @@
|
|||||||
"integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
|
"integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/lodash": {
|
||||||
|
"version": "4.17.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.10.tgz",
|
||||||
|
"integrity": "sha512-YpS0zzoduEhuOWjAotS6A5AVCva7X4lVlYLF0FYHAY9sdraBfnatttHItlWeZdGhuEkf+OzMNg2ZYAx8t+52uQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/mime": {
|
"node_modules/@types/mime": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.0.tgz",
|
||||||
@@ -10844,6 +10853,11 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/izitoast": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/izitoast/-/izitoast-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-Oc1X2wiQtPp39i5VpIjf3GJf5sfCtHKXZ5szx7RareyEeFLUlcEW0FSfBni28+Ul6KNKZRKzhVuWzSP4Xngh0w=="
|
||||||
|
},
|
||||||
"node_modules/jake": {
|
"node_modules/jake": {
|
||||||
"version": "10.8.5",
|
"version": "10.8.5",
|
||||||
"resolved": "https://registry.npmjs.org/jake/-/jake-10.8.5.tgz",
|
"resolved": "https://registry.npmjs.org/jake/-/jake-10.8.5.tgz",
|
||||||
@@ -11959,6 +11973,11 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/logrocket": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/logrocket/-/logrocket-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-jOWG+jEzobKVxGytzZ+4KGm2kiMQMRTHab2uDWQyVZcHfEF38BlCH1yjQVY4LCmuQUwZitP9biMzJZnyUQ0dtQ=="
|
||||||
|
},
|
||||||
"node_modules/loose-envify": {
|
"node_modules/loose-envify": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||||
@@ -19059,19 +19078,6 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/unistore": {
|
|
||||||
"version": "3.5.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/unistore/-/unistore-3.5.2.tgz",
|
|
||||||
"integrity": "sha512-2Aa4eX0Ua1umyiI3Eai6Li+wXYOHgaDBGOPB3Hvw7PAVuD30TAyh5kS4yNKb2fLDbQgizvPhKQRcYnOdfsm4VQ==",
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"preact": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"react": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/universalify": {
|
"node_modules/universalify": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
|
||||||
@@ -23439,6 +23445,12 @@
|
|||||||
"integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
|
"integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"@types/lodash": {
|
||||||
|
"version": "4.17.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.10.tgz",
|
||||||
|
"integrity": "sha512-YpS0zzoduEhuOWjAotS6A5AVCva7X4lVlYLF0FYHAY9sdraBfnatttHItlWeZdGhuEkf+OzMNg2ZYAx8t+52uQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"@types/mime": {
|
"@types/mime": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.0.tgz",
|
||||||
@@ -29574,6 +29586,11 @@
|
|||||||
"istanbul-lib-report": "^3.0.0"
|
"istanbul-lib-report": "^3.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"izitoast": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/izitoast/-/izitoast-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-Oc1X2wiQtPp39i5VpIjf3GJf5sfCtHKXZ5szx7RareyEeFLUlcEW0FSfBni28+Ul6KNKZRKzhVuWzSP4Xngh0w=="
|
||||||
|
},
|
||||||
"jake": {
|
"jake": {
|
||||||
"version": "10.8.5",
|
"version": "10.8.5",
|
||||||
"resolved": "https://registry.npmjs.org/jake/-/jake-10.8.5.tgz",
|
"resolved": "https://registry.npmjs.org/jake/-/jake-10.8.5.tgz",
|
||||||
@@ -30448,6 +30465,11 @@
|
|||||||
"is-unicode-supported": "^0.1.0"
|
"is-unicode-supported": "^0.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"logrocket": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/logrocket/-/logrocket-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-jOWG+jEzobKVxGytzZ+4KGm2kiMQMRTHab2uDWQyVZcHfEF38BlCH1yjQVY4LCmuQUwZitP9biMzJZnyUQ0dtQ=="
|
||||||
|
},
|
||||||
"loose-envify": {
|
"loose-envify": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||||
@@ -35978,11 +36000,6 @@
|
|||||||
"crypto-random-string": "^2.0.0"
|
"crypto-random-string": "^2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"unistore": {
|
|
||||||
"version": "3.5.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/unistore/-/unistore-3.5.2.tgz",
|
|
||||||
"integrity": "sha512-2Aa4eX0Ua1umyiI3Eai6Li+wXYOHgaDBGOPB3Hvw7PAVuD30TAyh5kS4yNKb2fLDbQgizvPhKQRcYnOdfsm4VQ=="
|
|
||||||
},
|
|
||||||
"universalify": {
|
"universalify": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
|
||||||
|
|||||||
Regular → Executable
+4
-1
@@ -6,11 +6,13 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "preact build --no-prerender",
|
"build": "preact build --no-prerender",
|
||||||
"start": "preact --no-clear watch",
|
"start": "preact --no-clear watch",
|
||||||
"lint": "eslint src"
|
"lint": "eslint --fix src"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@reduxjs/toolkit": "^1.8.5",
|
"@reduxjs/toolkit": "^1.8.5",
|
||||||
|
"izitoast": "^1.4.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
|
"logrocket": "^3.0.1",
|
||||||
"preact": "^10.10.0",
|
"preact": "^10.10.0",
|
||||||
"preact-render-to-string": "^5.2.1",
|
"preact-render-to-string": "^5.2.1",
|
||||||
"preact-router": "^3.2.1",
|
"preact-router": "^3.2.1",
|
||||||
@@ -20,6 +22,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/plugin-transform-react-jsx-source": "^7.18.6",
|
"@babel/plugin-transform-react-jsx-source": "^7.18.6",
|
||||||
|
"@types/lodash": "^4.17.10",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.30.6",
|
"@typescript-eslint/eslint-plugin": "^5.30.6",
|
||||||
"@typescript-eslint/parser": "^5.30.6",
|
"@typescript-eslint/parser": "^5.30.6",
|
||||||
"eslint": "^8.20.0",
|
"eslint": "^8.20.0",
|
||||||
|
|||||||
+48
-28
@@ -1,52 +1,72 @@
|
|||||||
import { h } from 'preact';
|
import { h } from 'preact';
|
||||||
import { useState, useEffect } from 'preact/hooks';
|
import { useEffect } from 'preact/hooks';
|
||||||
import { Route, Router, route } from 'preact-router';
|
import { useDispatch, Provider } from 'react-redux'
|
||||||
import { Provider } from 'react-redux'
|
import { Route, Router, } from 'preact-router';
|
||||||
|
import LogRocket from 'logrocket';
|
||||||
|
|
||||||
import createWs from './ws';
|
import createWs from './ws';
|
||||||
import http from './http';
|
import store, { wsState } from './store'
|
||||||
|
|
||||||
import Header from './components/header';
|
import Nav from './components/nav';
|
||||||
|
|
||||||
import Home from './routes/home';
|
import Main from './routes/main';
|
||||||
import Bundles from './routes/bundles';
|
import Acp from './routes/acp';
|
||||||
import Admin from './routes/acp';
|
|
||||||
import Login from './routes/login';
|
import Login from './routes/login';
|
||||||
import Search from './routes/search';
|
import Search from './routes/search';
|
||||||
import List from './routes/list';
|
import List from './routes/list';
|
||||||
|
import Info from './routes/info';
|
||||||
|
|
||||||
import store, { userState, wsState } from './store'
|
if (window.location.host === 'crates.ntwl.xyz') LogRocket.init('yh0dy3/mcrates');
|
||||||
|
|
||||||
const ws = createWs(store);
|
|
||||||
ws.connect();
|
|
||||||
|
|
||||||
async function init() {
|
// ██ ██ ███████ █████ ██████ ███████ ███████ ██████ ██████ █████ ██████ ██ ██
|
||||||
const [error, user] = await http.getLogin();
|
// ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
|
||||||
if (error) return route('/login');
|
// ██ █ ██ █████ ███████ ██████ █████ ███████ ██ ██ ██████ ███████ ██ █████
|
||||||
route('/bundles');
|
// ██ ███ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
|
||||||
store.dispatch(userState.actions.set(user));
|
// ███ ███ ███████ ██ ██ ██ ██ ███████ ███████ ██████ ██████ ██ ██ ██████ ██ ██
|
||||||
store.dispatch(wsState.actions.set(ws));
|
|
||||||
}
|
|
||||||
|
|
||||||
init();
|
|
||||||
|
|
||||||
const App = () => {
|
const Mcrates = () => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
// i have no idea why this is needed,
|
||||||
|
// but it makes sure the user changes actually rerender
|
||||||
|
useEffect(() => {
|
||||||
|
const ws = createWs(dispatch);
|
||||||
|
ws.connect();
|
||||||
|
dispatch(wsState.actions.set(ws));
|
||||||
|
}, [dispatch])
|
||||||
|
|
||||||
|
function routeChange(e) {
|
||||||
|
const { ws } = store.getState();
|
||||||
|
if (!ws) return null;
|
||||||
|
if (e.url === '/acp') return ws.sendAcpOffers();
|
||||||
|
if (e.url === '/') return ws.sendOffers();
|
||||||
|
if (e.url === '/list') return ws.sendWants();
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Provider store={store}>
|
<div id="mcrates">
|
||||||
<main id="app">
|
<Nav />
|
||||||
<Header />
|
<Router onChange={routeChange}>
|
||||||
<Router>
|
<Route path="/" component={Main} />
|
||||||
<Route path="/" component={Home} />
|
|
||||||
<Route path="/login" component={Login} mode="login" />
|
<Route path="/login" component={Login} mode="login" />
|
||||||
<Route path="/register" component={Login} mode="register" />
|
<Route path="/register" component={Login} mode="register" />
|
||||||
<Route path="/search" component={Search} />
|
<Route path="/search" component={Search} />
|
||||||
<Route path="/list" component={List} />
|
<Route path="/list" component={List} />
|
||||||
|
{/*<Route path="/info" component={Info} />*/}
|
||||||
|
|
||||||
{/* ACP */}
|
{/* ACP */}
|
||||||
<Route path="/bundles" component={Bundles} />
|
<Route path="/acp" component={Acp} />
|
||||||
<Route path="/offers/acp" component={Admin} />
|
|
||||||
</Router>
|
</Router>
|
||||||
</main>
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const App = () => {
|
||||||
|
return (
|
||||||
|
<Provider store={store}>
|
||||||
|
<Mcrates />
|
||||||
</Provider>
|
</Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,337 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
sodipodi:docname="swirl.fix.svg"
|
||||||
|
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"
|
||||||
|
id="svg8"
|
||||||
|
version="1.1"
|
||||||
|
viewBox="0 0 96.148266 82.95547"
|
||||||
|
height="313.53247"
|
||||||
|
width="363.39502">
|
||||||
|
<defs
|
||||||
|
id="defs2">
|
||||||
|
<inkscape:path-effect
|
||||||
|
effect="perspective-envelope"
|
||||||
|
up_left_point="-158.75,85.333336"
|
||||||
|
up_right_point="159.45,85.333336"
|
||||||
|
down_left_point="-158.75,111.79167"
|
||||||
|
down_right_point="158.75,111.79167"
|
||||||
|
id="path-effect6876"
|
||||||
|
is_visible="true"
|
||||||
|
deform_type="perspective"
|
||||||
|
horizontal_mirror="false"
|
||||||
|
vertical_mirror="false"
|
||||||
|
overflow_perspective="false" />
|
||||||
|
</defs>
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="base"
|
||||||
|
pagecolor="#ffebcd"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:zoom="14.481547"
|
||||||
|
inkscape:cx="181.99653"
|
||||||
|
inkscape:cy="193.62635"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
inkscape:current-layer="layer1"
|
||||||
|
showgrid="false"
|
||||||
|
showborder="true"
|
||||||
|
inkscape:window-width="2556"
|
||||||
|
inkscape:window-height="1402"
|
||||||
|
inkscape:window-x="2560"
|
||||||
|
inkscape:window-y="34"
|
||||||
|
inkscape:window-maximized="0"
|
||||||
|
units="px"
|
||||||
|
fit-margin-top="8"
|
||||||
|
fit-margin-right="8"
|
||||||
|
fit-margin-bottom="8"
|
||||||
|
fit-margin-left="8"
|
||||||
|
inkscape:pagecheckerboard="true">
|
||||||
|
<inkscape:grid
|
||||||
|
type="xygrid"
|
||||||
|
id="grid815"
|
||||||
|
spacingx="2.6458333"
|
||||||
|
spacingy="2.6458333"
|
||||||
|
originx="-36.831378"
|
||||||
|
originy="-89.812155" />
|
||||||
|
</sodipodi:namedview>
|
||||||
|
<metadata
|
||||||
|
id="metadata5">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:title></dc:title>
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1"
|
||||||
|
style="display:inline"
|
||||||
|
transform="translate(-36.831378,-124.23238)">
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path10242"
|
||||||
|
d="m 39.087803,177.13337 v -22.9279 l 45.818557,27.79778 45.81855,-27.71837 v 22.892 l -45.818559,27.75426 z"
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:0.39687499;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path10244"
|
||||||
|
d="M 39.087803,154.20547 84.906351,126.48711 130.72491,154.28488 84.906351,182.03914 39.087794,154.20547"
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:0.39687499;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path10396"
|
||||||
|
d="m 84.90636,182.03914 v 22.892"
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:0.39687499;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||||
|
<path
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:0.39687499;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
d="M 43.6823,155.2907 84.251848,129.27328 126.12932,153.20325 85.560615,179.25297 43.6823,155.2907"
|
||||||
|
id="path11658"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path11660"
|
||||||
|
d="m 47.84146,156.15882 35.887132,-24.36344 38.240578,20.54299 -35.88561,24.39248 -38.2421,-20.57203"
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:0.39687499;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||||
|
<path
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:0.39687499;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
d="m 51.603832,156.84186 31.713373,-22.76505 34.888705,17.58143 -31.711318,22.79115 -34.89076,-17.60753"
|
||||||
|
id="path11662"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path11664"
|
||||||
|
d="M 55.004869,157.36773 83.000788,136.13907 114.80407,151.13498 86.810601,172.38708 55.00486,157.36773"
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:0.39687499;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||||
|
<path
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:0.39687499;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
d="M 58.077109,157.76072 82.76466,138.00191 111.7311,150.74435 87.046302,170.52419 58.0771,157.76072"
|
||||||
|
id="path11666"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path11668"
|
||||||
|
d="M 60.850409,158.0419 82.596088,139.68344 108.95716,150.46527 87.214448,168.84259 60.850409,158.0419"
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:0.39687499;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||||
|
<path
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:0.39687499;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
d="m 63.352121,158.22952 19.131893,-17.02926 23.970846,9.07928 -19.128774,17.04616 -23.973965,-9.09618"
|
||||||
|
id="path11670"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path11672"
|
||||||
|
d="m 65.607289,158.33937 16.811604,-15.77181 21.780277,7.60381 -16.808406,15.78696 -21.783475,-7.61896"
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:0.39687499;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||||
|
<path
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:0.39687499;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
d="m 67.638806,158.38507 14.753693,-14.58584 19.774661,6.32796 -14.750438,14.59939 -19.777916,-6.34151"
|
||||||
|
id="path11674"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path11676"
|
||||||
|
d="m 69.467606,158.37833 12.930142,-13.47036 17.940192,5.22734 -12.926902,13.48245 -17.943432,-5.23943"
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:0.39687499;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||||
|
<path
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:0.39687499;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
d="m 71.112789,158.32915 11.315792,-12.42379 16.263793,4.28033 -11.312595,12.43461 -16.26699,-4.29115"
|
||||||
|
id="path11678"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path11680"
|
||||||
|
d="m 72.591771,158.24614 9.888034,-11.44417 14.733241,3.46781 -9.884899,11.45383 -14.736376,-3.47747"
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:0.39687499;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||||
|
<path
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:0.39687499;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
d="m 73.920439,158.1366 8.626548,-10.52918 13.33708,2.77287 -8.62352,10.53779 -13.340117,-2.78148"
|
||||||
|
id="path11682"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path11684"
|
||||||
|
d="m 75.113233,158.00675 7.513182,-9.67626 12.064591,2.18051 -7.510251,9.68394 -12.067531,-2.18819"
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:0.39687499;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||||
|
<path
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:0.39687499;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
d="m 76.183315,157.86181 6.531587,-8.88266 10.905765,1.67756 -6.528763,8.88949 -10.908598,-1.68439"
|
||||||
|
id="path11686"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path11688"
|
||||||
|
d="m 77.142637,157.70619 5.667137,-8.14552 9.851348,1.25235 -5.664445,8.15161 -9.854048,-1.25844"
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:0.39687499;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||||
|
<path
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:0.39687499;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
d="m 78.002061,157.54358 4.906768,-7.46198 8.892683,0.89464 -4.904201,7.46738 -8.89525,-0.90004"
|
||||||
|
id="path11690"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path11694"
|
||||||
|
d="m 78.771436,157.37702 4.238774,-6.82908 8.02175,0.5954 -4.236341,6.83388 -8.024183,-0.6002"
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:0.39687499;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||||
|
<path
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:0.39687499;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
d="m 79.459703,157.20905 3.652706,-6.24394 7.231124,0.34668 -3.650397,6.2482 -7.233433,-0.35094"
|
||||||
|
id="path11696"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path11698"
|
||||||
|
d="m 80.074972,157.04169 3.1392,-5.70366 6.51395,0.14155 -3.137033,5.70744 -6.516117,-0.14533"
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:0.39687499;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||||
|
<path
|
||||||
|
style="opacity:1;vector-effect:none;fill:#f4f1d0;fill-opacity:1;stroke:#f4f1d0;stroke-width:0.39687499;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
d="m 80.624568,156.87663 2.689946,-5.2055 5.863887,-0.0261 -2.687912,5.20886 -5.86593,0.0228"
|
||||||
|
id="path11700"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<g
|
||||||
|
style="stroke:#f4f1d0;stroke-width:4.46899033;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="g9298"
|
||||||
|
transform="matrix(0.08880641,0,0,0.08880641,-71.602691,3.991392)">
|
||||||
|
<path
|
||||||
|
id="path5442"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
d="m 1888.0249,2040.4803 -52.9167,32.054 v 34.3699 l 52.9167,-32.054"
|
||||||
|
style="vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:4.46899033;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||||
|
<path
|
||||||
|
id="path5444"
|
||||||
|
sodipodi:nodetypes="cc"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
d="m 1925.0665,2035.2276 29.1042,-0.4447"
|
||||||
|
style="vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:4.46899033;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||||
|
<path
|
||||||
|
id="path5446"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
d="m 1927.7124,2033.6249 -26.4584,16.027"
|
||||||
|
style="vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:4.46899033;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||||
|
<path
|
||||||
|
id="path5448"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
d="m 1901.254,2066.8368 v -34.3699 l 39.6875,-24.0404 13.2292,9.1714 -26.4583,16.027"
|
||||||
|
style="vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:4.46899033;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||||
|
<path
|
||||||
|
id="path5450"
|
||||||
|
style="vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:4.46899033;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
d="m 1967.3999,2026.7694 26.4583,-50.3968 26.4583,18.3429"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<path
|
||||||
|
id="path5452"
|
||||||
|
style="vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:4.46899033;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
d="m 1980.629,2001.571 26.4584,-16.027"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<path
|
||||||
|
id="path5454"
|
||||||
|
style="vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:4.46899033;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
d="m 2020.3166,1960.3456 52.9167,-32.0539"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<path
|
||||||
|
id="path5456"
|
||||||
|
style="vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:4.46899033;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
d="m 2046.7749,1944.3187 v 34.3699"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<path
|
||||||
|
id="path5458"
|
||||||
|
style="vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:4.46899033;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
d="m 2086.4624,1954.6481 v -34.3699 l 52.9167,-32.0539"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<path
|
||||||
|
id="path5460"
|
||||||
|
style="vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:4.46899033;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
d="m 2086.4624,1937.4632 26.4584,-16.027"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<path
|
||||||
|
id="path5462"
|
||||||
|
style="vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:4.46899033;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
d="m 2086.4624,1954.6481 52.9167,-32.0539"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<path
|
||||||
|
id="path5464"
|
||||||
|
style="vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:4.46899033;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
d="m 2205.5249,1848.157 -52.9166,32.0538 v 17.185 l 52.9166,-32.0539 v 17.185 l -52.9166,32.0538"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
style="stroke:#f4f1d0;stroke-width:3.91797233;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="g9259"
|
||||||
|
transform="matrix(0.08880641,0.05387819,0,0.11554215,6.1924204,89.232392)">
|
||||||
|
<g
|
||||||
|
style="stroke:#f4f1d0;stroke-width:3.91797233;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
transform="translate(13.215028,6.61458)"
|
||||||
|
id="g9255">
|
||||||
|
<path
|
||||||
|
style="vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:3.91797233;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
d="m 463.02082,495.4375 v -26.45833 l 26.45834,13.22916 26.45833,-13.22916 v 26.45833"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path9237" />
|
||||||
|
<path
|
||||||
|
style="vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:3.91797233;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
d="m 529.16665,495.4375 26.45834,-26.45833 26.45833,26.45833"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path9239" />
|
||||||
|
<path
|
||||||
|
style="vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:3.91797233;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
d="m 542.39582,482.20833 h 26.45833"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path9241" />
|
||||||
|
<path
|
||||||
|
style="vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:3.91797233;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
d="m 582.08333,468.97917 h 52.91666"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path9243" />
|
||||||
|
<path
|
||||||
|
style="vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:3.91797233;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
d="M 608.54166,468.97917 V 495.4375"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path9245" />
|
||||||
|
<path
|
||||||
|
style="vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:3.91797233;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
d="m 648.22916,495.4375 v -26.45833 h 52.91667"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path9247" />
|
||||||
|
<path
|
||||||
|
style="vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:3.91797233;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
d="m 648.22916,482.20833 h 26.45833"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path9249" />
|
||||||
|
<path
|
||||||
|
style="vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:3.91797233;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
d="m 648.22916,495.4375 h 52.91667"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path9251" />
|
||||||
|
<path
|
||||||
|
style="vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:3.91797233;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
d="m 767.29166,468.97917 h -52.91667 v 13.22916 h 52.91667 v 13.22917 h -52.91667"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path9253" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer2"
|
||||||
|
inkscape:label="Box"
|
||||||
|
transform="translate(-36.831378,-124.23238)" />
|
||||||
|
<g
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer3"
|
||||||
|
inkscape:label="Text"
|
||||||
|
transform="translate(-36.831378,-124.23238)" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 22 KiB |
@@ -0,0 +1,541 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
sodipodi:docname="swirl.svg"
|
||||||
|
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"
|
||||||
|
id="svg8"
|
||||||
|
version="1.1"
|
||||||
|
viewBox="0 0 94.568563 81.393395"
|
||||||
|
height="307.62857"
|
||||||
|
width="357.4245">
|
||||||
|
<defs
|
||||||
|
id="defs2">
|
||||||
|
<inkscape:path-effect
|
||||||
|
effect="perspective-envelope"
|
||||||
|
up_left_point="-158.75,85.333336"
|
||||||
|
up_right_point="159.45,85.333336"
|
||||||
|
down_left_point="-158.75,111.79167"
|
||||||
|
down_right_point="158.75,111.79167"
|
||||||
|
id="path-effect6876"
|
||||||
|
is_visible="true"
|
||||||
|
deform_type="perspective"
|
||||||
|
horizontal_mirror="false"
|
||||||
|
vertical_mirror="false"
|
||||||
|
overflow_perspective="false" />
|
||||||
|
</defs>
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="base"
|
||||||
|
pagecolor="#ffebcd"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:zoom="11.313708"
|
||||||
|
inkscape:cx="234.71776"
|
||||||
|
inkscape:cy="105.70791"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
inkscape:current-layer="layer3"
|
||||||
|
showgrid="false"
|
||||||
|
showborder="true"
|
||||||
|
inkscape:window-width="2556"
|
||||||
|
inkscape:window-height="1402"
|
||||||
|
inkscape:window-x="2560"
|
||||||
|
inkscape:window-y="34"
|
||||||
|
inkscape:window-maximized="0"
|
||||||
|
inkscape:pagecheckerboard="true"
|
||||||
|
units="px"
|
||||||
|
fit-margin-top="5"
|
||||||
|
fit-margin-left="5"
|
||||||
|
fit-margin-right="5"
|
||||||
|
fit-margin-bottom="5">
|
||||||
|
<inkscape:grid
|
||||||
|
type="xygrid"
|
||||||
|
id="grid815"
|
||||||
|
spacingx="2.6458333"
|
||||||
|
spacingy="2.6458333"
|
||||||
|
originx="-118.41853"
|
||||||
|
originy="-209.93335" />
|
||||||
|
</sodipodi:namedview>
|
||||||
|
<metadata
|
||||||
|
id="metadata5">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:title />
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<g
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer3"
|
||||||
|
inkscape:label="Text"
|
||||||
|
transform="translate(-58.547806,-111.30139)">
|
||||||
|
<g
|
||||||
|
style="display:inline;stroke:#f4f1d0;stroke-opacity:1;stroke-width:4.47492335;stroke-miterlimit:4;stroke-dasharray:none"
|
||||||
|
id="g12059"
|
||||||
|
transform="matrix(0.08868867,0,0,0.08868867,-50.468219,-9.5066841)">
|
||||||
|
<g
|
||||||
|
id="g10220"
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:3.92624803;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
transform="matrix(1,-0.60574282,0,1.2990197,876.00795,2035.6967)">
|
||||||
|
<path
|
||||||
|
style="vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:3.92624803;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
d="m 1005.4167,475.69732 h -52.91672 v 26.45834 h 52.91672"
|
||||||
|
id="path10196"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<path
|
||||||
|
style="vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:3.92624803;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
d="m 1042.4583,488.92649 29.1042,13.22917"
|
||||||
|
id="path10198"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
sodipodi:nodetypes="cc" />
|
||||||
|
<path
|
||||||
|
style="vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:3.92624803;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
d="m 1045.1042,488.92649 h -26.4584"
|
||||||
|
id="path10200"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<path
|
||||||
|
style="vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:3.92624803;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
d="m 1018.6458,502.15566 v -26.45834 h 39.6875 l 13.2292,13.22917 h -26.4583"
|
||||||
|
id="path10202"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path10204"
|
||||||
|
d="m 1084.7917,502.15566 26.4583,-26.45833 26.4583,26.45833"
|
||||||
|
style="vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:3.92624803;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path10206"
|
||||||
|
d="m 1098.0208,488.92649 h 26.4584"
|
||||||
|
style="vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:3.92624803;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path10208"
|
||||||
|
d="m 1150.9375,475.69732 h 52.9167"
|
||||||
|
style="vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:3.92624803;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path10210"
|
||||||
|
d="m 1177.3958,475.69732 v 26.45834"
|
||||||
|
style="vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:3.92624803;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path10212"
|
||||||
|
d="M 1217.0833,502.15566 V 475.69732 H 1270"
|
||||||
|
style="vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:3.92624803;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path10214"
|
||||||
|
d="m 1217.0833,488.92649 h 26.4584"
|
||||||
|
style="vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:3.92624803;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path10216"
|
||||||
|
d="M 1217.0833,502.15566 H 1270"
|
||||||
|
style="vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:3.92624803;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path10218"
|
||||||
|
d="m 1336.1458,475.69732 h -52.9166 v 13.22917 h 52.9166 v 13.22917 h -52.9166"
|
||||||
|
style="vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:3.92624803;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
transform="matrix(1,0.60669255,0,1.3010563,1504.3934,1849.0405)"
|
||||||
|
id="g10240"
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:3.92317387;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill">
|
||||||
|
<path
|
||||||
|
style="vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:3.92317387;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
d="M -158.75,111.79167 V 85.333336 l 26.45834,13.229164 26.45833,-13.229164 v 26.458334"
|
||||||
|
id="path10222"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<path
|
||||||
|
style="vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:3.92317387;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
d="M -92.604166,111.79167 -66.145832,85.333336 -39.6875,111.79167"
|
||||||
|
id="path10224"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<path
|
||||||
|
style="vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:3.92317387;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
d="m -79.374999,98.5625 h 26.458333"
|
||||||
|
id="path10226"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<path
|
||||||
|
style="vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:3.92317387;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
d="M -26.458333,85.333336 H 26.458333"
|
||||||
|
id="path10228"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<path
|
||||||
|
style="vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:3.92317387;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
d="M 0,85.333336 V 111.79167"
|
||||||
|
id="path10230"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<path
|
||||||
|
style="vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:3.92317387;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
d="M 39.6875,111.79167 V 85.333336 h 52.916666"
|
||||||
|
id="path10232"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<path
|
||||||
|
style="vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:3.92317387;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
d="M 39.6875,98.5625 H 66.145832"
|
||||||
|
id="path10234"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<path
|
||||||
|
style="vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:3.92317387;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
d="M 39.6875,111.79167 H 92.604166"
|
||||||
|
id="path10236"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<path
|
||||||
|
style="vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:3.92317387;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
d="M 158.75,85.333336 H 105.83333 V 98.5625 H 158.75 v 13.22917 h -52.91667"
|
||||||
|
id="path10238"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
</g>
|
||||||
|
<path
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:4.47492335;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
d="m 1246.4246,1949.6563 v -258.1784 l 515.9375,313.0154 515.9374,-312.1212 v 257.7742 l -515.9375,312.5254 z"
|
||||||
|
id="path10242"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<path
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:4.47492335;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
d="m 1246.4246,1691.4779 515.9374,-312.1212 515.9375,313.0154 -515.9375,312.5254 -515.9375,-313.4196"
|
||||||
|
id="path10244"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<path
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:4.47492335;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
d="m 1762.3621,2004.8975 v 257.7742"
|
||||||
|
id="path10396"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path11658"
|
||||||
|
d="m 1298.1607,1703.6981 456.8313,-292.9679 471.5591,269.4622 -456.8218,293.3316 -471.5686,-269.8259"
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:4.47492335;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||||
|
<path
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:4.47492335;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
d="m 1344.9947,1713.4735 404.1052,-274.3433 430.6061,231.3233 -404.0881,274.6703 -430.6232,-231.6503"
|
||||||
|
id="path11660"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path11662"
|
||||||
|
d="m 1387.3607,1721.1648 357.1068,-256.3447 392.8625,197.9748 -357.0837,256.6386 -392.8856,-198.2687"
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:4.47492335;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||||
|
<path
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:4.47492335;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
d="m 1425.6579,1727.0864 315.2466,-239.0443 358.1192,168.8607 -315.219,239.3082 -358.1469,-169.1246"
|
||||||
|
id="path11664"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path11666"
|
||||||
|
d="m 1460.2527,1731.5116 277.9929,-222.4931 326.1751,143.4856 -277.9619,222.7299 -326.2062,-143.7224"
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:4.47492335;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||||
|
<path
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:4.47492335;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
d="m 1491.4813,1734.6778 244.8661,-206.7245 296.8375,121.4083 -244.8327,206.9368 -296.8709,-121.6206"
|
||||||
|
id="path11668"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path11670"
|
||||||
|
d="m 1519.6517,1736.7905 215.4337,-191.7571 269.9225,102.2367 -215.3986,191.9474 -269.9576,-102.427"
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:4.47492335;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||||
|
<path
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:4.47492335;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
d="m 1545.0459,1738.0275 189.3062,-177.5977 245.2557,85.6223 -189.2702,177.7682 -245.2917,-85.7928"
|
||||||
|
id="path11672"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path11674"
|
||||||
|
d="m 1567.9217,1738.5421 166.1332,-164.2431 222.6716,71.2556 -166.0966,164.3957 -222.7082,-71.4082"
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:4.47492335;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||||
|
<path
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:4.47492335;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
d="m 1588.5148,1738.4661 145.5992,-151.6822 202.0146,58.8621 -145.5627,151.8185 -202.0511,-58.9984"
|
||||||
|
id="path11676"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path11678"
|
||||||
|
d="m 1607.0403,1737.9124 127.4209,-139.8975 183.1376,48.1985 -127.3849,140.0192 -183.1736,-48.3202"
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:4.47492335;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||||
|
<path
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:4.47492335;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
d="m 1623.6943,1736.9776 111.3437,-128.8665 165.9029,39.0492 -111.3084,128.9752 -165.9382,-39.1579"
|
||||||
|
id="path11680"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path11682"
|
||||||
|
d="m 1638.6557,1735.7442 97.1388,-118.5633 150.1815,31.2237 -97.1047,118.6603 -150.2157,-31.3207"
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:4.47492335;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||||
|
<path
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:4.47492335;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
d="m 1652.0871,1734.282 84.6018,-108.959 135.8527,24.5535 -84.5688,109.0455 -135.8858,-24.64"
|
||||||
|
id="path11684"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path11686"
|
||||||
|
d="m 1664.1367,1732.6499 73.5486,-100.0227 122.8038,18.8901 -73.5168,100.0996 -122.8357,-18.967"
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:4.47492335;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||||
|
<path
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:4.47492335;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
d="m 1674.9391,1730.8976 63.8145,-91.7223 110.9306,14.1021 -63.7842,91.7907 -110.961,-14.1705"
|
||||||
|
id="path11688"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path11690"
|
||||||
|
d="m 1684.6166,1729.0665 55.2524,-84.0253 100.1356,10.0741 -55.2235,84.0861 -100.1645,-10.1349"
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:4.47492335;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||||
|
<path
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:4.47492335;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
d="m 1693.2801,1727.191 47.7305,-76.8986 90.3285,6.7045 -47.7031,76.9526 -90.3559,-6.7585"
|
||||||
|
id="path11694"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path11696"
|
||||||
|
d="m 1701.0303,1725.2995 41.1311,-70.3095 81.4257,3.9038 -41.1051,70.3575 -81.4517,-3.9518"
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:4.47492335;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||||
|
<path
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:4.47492335;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
d="m 1707.9585,1723.415 35.3488,-64.2258 73.35,1.5939 -35.3244,64.2684 -73.3744,-1.6365"
|
||||||
|
id="path11698"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path11700"
|
||||||
|
d="m 1714.1472,1721.5563 30.29,-58.6162 66.03,-0.2941 -30.2671,58.654 -66.053,0.2563"
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:4.47492335;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||||
|
<path
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:4.47492335;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
d="m 1719.6718,1719.7364 25.8707,-53.45 59.3996,-1.8202 -25.849,53.4835 -59.4213,1.7867"
|
||||||
|
id="path11702"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path11704"
|
||||||
|
d="m 1724.5994,1717.9689 22.0164,-48.698 53.3985,-3.0369 -21.9962,48.7276 -53.4187,3.0073"
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:4.47492335;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||||
|
<path
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:4.47492335;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
d="m 1728.9909,1716.2627 18.6609,-44.3319 47.9706,-3.9903 -18.6419,44.358 -47.9895,3.9642"
|
||||||
|
id="path11706"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path11708"
|
||||||
|
d="m 1732.9019,1714.6241 15.7446,-40.3246 43.0646,-4.7202 -15.7268,40.3477 -43.0824,4.6971"
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:4.47492335;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||||
|
<path
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:4.47492335;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
d="m 1736.3819,1713.0578 13.2153,-36.6507 38.6337,-5.2613 -13.1988,36.6711 -38.6502,5.2409"
|
||||||
|
id="path11710"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path11712"
|
||||||
|
d="m 1739.4759,1711.5666 11.0263,-33.2856 34.6344,-5.6438 -11.0109,33.3036 -34.6498,5.6258"
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:4.47492335;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||||
|
<path
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:4.47492335;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
d="m 1742.2244,1710.1521 9.1361,-30.2065 31.0273,-5.8937 -9.1217,30.2224 -31.0417,5.8778"
|
||||||
|
id="path11714"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path11716"
|
||||||
|
d="m 1744.6639,1708.8147 7.5081,-27.3918 27.7762,-6.0334 -7.4948,27.4057 -27.7895,6.0195"
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:4.47492335;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||||
|
<path
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:4.47492335;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
d="m 1746.8272,1707.554 6.1096,-24.8211 24.8479,-6.0826 -6.0973,24.8333 -24.8602,6.0704"
|
||||||
|
id="path11718"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path11720"
|
||||||
|
d="m 1748.7438,1706.3688 4.912,-22.4753 22.2122,-6.0578 -4.9007,22.4859 -22.2235,6.0472"
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:4.47492335;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||||
|
<path
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:4.47492335;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
d="m 1750.4402,1705.2573 3.8898,-20.3366 19.8413,-5.9734 -3.8793,20.3458 -19.8518,5.9642"
|
||||||
|
id="path11722"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path11724"
|
||||||
|
d="m 1751.9404,1704.2174 3.0205,-18.3884 17.7103,-5.8416 -3.0109,18.3963 -17.7199,5.8337"
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:4.47492335;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||||
|
<path
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:4.47492335;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
d="m 1753.2654,1703.2464 2.2844,-16.615 15.7962,-5.673 -2.2755,16.622 -15.8051,5.666"
|
||||||
|
id="path11726"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path11728"
|
||||||
|
d="m 1754.4347,1702.3415 1.6639,-15.0021 14.078,-5.4761 -1.6557,15.0082 -14.0862,5.47"
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:4.47492335;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||||
|
<path
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:4.47492335;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
d="m 1755.4655,1701.4999 1.1434,-13.5365 12.5369,-5.2585 -1.136,13.5418 -12.5443,5.2532"
|
||||||
|
id="path11730"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path11732"
|
||||||
|
d="m 1756.3731,1700.7184 0.7097,-12.2056 11.1555,-5.0263 -0.703,12.2101 -11.1622,5.0218"
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:4.47492335;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||||
|
<path
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:4.47492335;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
d="m 1757.1711,1699.9939 0.351,-10.9979 9.9181,-4.785 -0.3448,11.0019 -9.9243,4.781"
|
||||||
|
id="path11734"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path11736"
|
||||||
|
d="m 1757.872,1699.3233 0.057,-9.9029 8.8105,-4.5387 -0.051,9.9064 -8.8162,4.5352"
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:4.47492335;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||||
|
<path
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:4.47492335;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
d="m 1758.4869,1698.7034 -0.1823,-8.9109 7.8197,-4.2909 0.1874,8.9139 -7.825,4.2879"
|
||||||
|
id="path11738"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path11740"
|
||||||
|
d="m 1759.0257,1698.1313 -0.3739,-8.0128 6.9342,-4.0447 0.3786,8.0154 -6.9392,4.0421"
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:4.47492335;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||||
|
<path
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:4.47492335;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
d="m 1759.4969,1697.6039 -0.5251,-7.2002 6.1433,-3.8024 0.5296,7.2025 -6.1479,3.8001"
|
||||||
|
id="path11742"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path11744"
|
||||||
|
d="m 1759.9085,1697.1183 -0.6422,-6.4656 5.4376,-3.5657 0.6462,6.4676 -5.4418,3.5637"
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:4.47492335;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||||
|
<path
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:4.47492335;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
d="m 1760.2674,1696.6718 -0.73,-5.802 4.8081,-3.3361 0.7338,5.8036 -4.812,3.3345"
|
||||||
|
id="path11746"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path11748"
|
||||||
|
d="m 1760.5797,1696.2616 -0.7935,-5.2028 4.2473,-3.1148 0.7969,5.2042 -4.2507,3.1134"
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:4.47492335;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||||
|
<path
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:4.47492335;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
d="m 1760.8512,1695.8851 -0.8365,-4.6622 3.7478,-2.9024 0.8396,4.6635 -3.751,2.9011"
|
||||||
|
id="path11750"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path11754"
|
||||||
|
d="m 1761.0865,1695.5401 -0.8624,-4.1749 3.3034,-2.6996 0.8653,4.176 -3.3064,2.6985"
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:4.47492335;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||||
|
<path
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:4.47492335;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
d="m 1761.2901,1695.2241 -0.8743,-3.7358 2.9084,-2.5067 0.877,3.7368 -2.9112,2.5057"
|
||||||
|
id="path11756"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path11758"
|
||||||
|
d="m 1761.4657,1694.935 -0.8745,-3.3404 2.5576,-2.3239 0.877,3.3414 -2.5602,2.3229"
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:4.47492335;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||||
|
<path
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:4.47492335;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
d="m 1761.617,1694.6708 -0.8655,-2.9848 2.2463,-2.151 0.8678,2.9856 -2.2488,2.1502"
|
||||||
|
id="path11760"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path11762"
|
||||||
|
d="m 1761.7469,1694.4295 -0.849,-2.665 1.9703,-1.9881 0.8511,2.6656 -1.9725,1.9875"
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:4.47492335;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||||
|
<path
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:4.47492335;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
d="m 1761.8581,1694.2094 -0.8266,-2.3777 1.7257,-1.8351 0.8286,2.3782 -1.7278,1.8346"
|
||||||
|
id="path11764"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path11766"
|
||||||
|
d="m 1761.9529,1694.0088 -0.7998,-2.1198 1.5093,-1.6917 0.8016,2.1201 -1.5112,1.6914"
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:4.47492335;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||||
|
<path
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:4.47492335;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
d="m 1762.0335,1693.826 -0.7696,-1.8883 1.3181,-1.5575 0.7712,1.8885 -1.3198,1.5573"
|
||||||
|
id="path11928"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path11930"
|
||||||
|
d="m 1762.1017,1693.6596 -0.7369,-1.6807 1.1492,-1.4323 0.7383,1.6808 -1.1507,1.4323"
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:4.47492335;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||||
|
<path
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:4.47492335;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
d="m 1762.1591,1693.5084 -0.7025,-1.4948 1.0002,-1.3157 0.7037,1.4948 -1.0015,1.3157"
|
||||||
|
id="path11932"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path11934"
|
||||||
|
d="m 1762.2072,1693.371 -0.6672,-1.3283 0.8688,-1.2074 0.6683,1.3283 -0.8699,1.2074"
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:4.47492335;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||||
|
<path
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:4.47492335;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
d="m 1762.2472,1693.2462 -0.6315,-1.1794 0.7532,-1.1067 0.6325,1.1794 -0.7542,1.1067"
|
||||||
|
id="path11936"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path11938"
|
||||||
|
d="m 1762.2802,1693.133 -0.5958,-1.0463 0.6516,-1.0134 0.5967,1.0463 -0.6525,1.0134"
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:4.47492335;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||||
|
<path
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:4.47492335;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
d="m 1762.3073,1693.0304 -0.5607,-0.9274 0.5624,-0.927 0.5615,0.9273 -0.5632,0.9271"
|
||||||
|
id="path11940"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path11942"
|
||||||
|
d="m 1762.3292,1692.9374 -0.5262,-0.8212 0.4842,-0.8472 0.5269,0.8211 -0.4849,0.8473"
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:4.47492335;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||||
|
<path
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:4.47492335;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
d="m 1762.3467,1692.8533 -0.4926,-0.7265 0.4156,-0.7736 0.4933,0.7263 -0.4163,0.7738"
|
||||||
|
id="path11944"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path11946"
|
||||||
|
d="m 1762.3606,1692.7772 -0.4604,-0.6421 0.3557,-0.7058 0.461,0.6419 -0.3563,0.706"
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:4.47492335;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill" />
|
||||||
|
<path
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f4f1d0;stroke-width:4.47492335;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
d="m 1762.3713,1692.7084 -0.4294,-0.5669 0.3034,-0.6434 0.4299,0.5667 -0.3039,0.6436"
|
||||||
|
id="path11948"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 41 KiB |
@@ -1,35 +0,0 @@
|
|||||||
import { h } from 'preact';
|
|
||||||
import { ExternalLink, Check } from 'react-feather';
|
|
||||||
import { useState } from 'preact/hooks';
|
|
||||||
|
|
||||||
import http from '../http';
|
|
||||||
|
|
||||||
export default function Offer(offer) {
|
|
||||||
const { want: { data: details } } = offer;
|
|
||||||
|
|
||||||
const [accepted, setAccepted] = useState(offer.status !== 'pending');
|
|
||||||
|
|
||||||
async function onClick() {
|
|
||||||
const [error] = await http.postOfferAccept(offer.id);
|
|
||||||
if (error) return false;
|
|
||||||
setAccepted(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
const classes = `${accepted ? 'accepted' : ''}`;
|
|
||||||
const btnText = accepted
|
|
||||||
? offer.status
|
|
||||||
: 'Accept';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div class="offer">
|
|
||||||
<section>
|
|
||||||
<h2>{details.artists_sort} - {details.title} <a href={details.uri} target="_blank"><ExternalLink /></a></h2>
|
|
||||||
<h5 class='alt-text' >{details.year}</h5>
|
|
||||||
<h5 class='alt-text'>{details.formats[0].descriptions.join(' ')}</h5>
|
|
||||||
<h5 class='number'>${offer.total}</h5>
|
|
||||||
</section>
|
|
||||||
<img src={details.thumb} />
|
|
||||||
<button onClick={onClick} disabled={accepted} class={classes} >{btnText}</button>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
import { h } from 'preact';
|
|
||||||
import { Square, CheckSquare } from 'react-feather';
|
|
||||||
import { useState, useReducer } from 'preact/hooks';
|
|
||||||
|
|
||||||
import http from '../http';
|
|
||||||
|
|
||||||
function Item(item, state, dispatch) {
|
|
||||||
const icon = state.checkedIds.includes(item.id)
|
|
||||||
? <CheckSquare onClick={() => dispatch({ id: item.id})} />
|
|
||||||
: <Square onClick={() => dispatch({ id: item.id})} />;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={item.id}>
|
|
||||||
<a href={item.uri} target="_blank" >
|
|
||||||
<h4>{item.description}</h4>
|
|
||||||
</a>
|
|
||||||
<h5 class='alt-text'>{item.price.toFixed(2)} {item.currency}</h5>
|
|
||||||
{icon}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const reducer = (state, action) => {
|
|
||||||
if (state.checkedIds.includes(action.id)) {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
checkedIds: state.checkedIds.filter(id => id !== action.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
checkedIds: [
|
|
||||||
...state.checkedIds,
|
|
||||||
action.id
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Bundle(bundle) {
|
|
||||||
const initialState = { checkedIds: bundle.items.map(i => i.id) }
|
|
||||||
const [state, dispatch] = useReducer(reducer, initialState)
|
|
||||||
|
|
||||||
const [shipping, setShipping] = useState(0);
|
|
||||||
const [baseShipping, setBaseShipping] = useState(0);
|
|
||||||
|
|
||||||
const items = bundle.items.map(item => Item(item, state, dispatch));
|
|
||||||
|
|
||||||
async function submit(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
const [error, offers] = await http.postOffers(state.checkedIds, shipping, baseShipping);
|
|
||||||
console.log(error, offers);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div class="bundle">
|
|
||||||
<section>
|
|
||||||
<h2>{bundle.username} - {bundle.seller}</h2>
|
|
||||||
<h3>{bundle.items.length} items - {bundle.total.toFixed(2)} {bundle.currency}</h3>
|
|
||||||
<h4>Ships: {bundle.mentionsAustralia && 'AU'} {bundle.mentionsWorldwide && 'WORLD'}</h4>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section>
|
|
||||||
<form action="">
|
|
||||||
<div class="item">
|
|
||||||
<label for="shipping">Full Shipping Cost</label>
|
|
||||||
<input type="number" name="shipping" id="shipping" value={shipping} onChange={(e) => setShipping(Number.parseFloat((e.target as HTMLInputElement).value))} step="0.01" min="0" required />
|
|
||||||
</div>
|
|
||||||
<div class="item">
|
|
||||||
<label for="baseShipping">Base Shipping Cost</label>
|
|
||||||
<input type="number" name="baseShipping" id="baseShipping" value={baseShipping} onChange={(e) => setBaseShipping(Number.parseFloat((e.target as HTMLInputElement).value))} step="0.01" min="0" required />
|
|
||||||
</div>
|
|
||||||
<div class="item">
|
|
||||||
<input type="submit" value="Generate Offers" onClick={(e) => submit(e)} />
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<div class='items'>
|
|
||||||
{items}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,19 +1,44 @@
|
|||||||
import { h } from 'preact';
|
import { h } from 'preact';
|
||||||
import { route } from 'preact-router';
|
import { route } from 'preact-router';
|
||||||
import { useSelector, useDispatch } from 'react-redux'
|
import { useEffect } from 'preact/hooks';
|
||||||
|
import { useSelector } from 'react-redux'
|
||||||
|
import LogRocket from 'logrocket';
|
||||||
|
|
||||||
import http from './../http';
|
import http from './../http';
|
||||||
import { State, userState } from './../store';
|
import { State } from './../store';
|
||||||
|
|
||||||
const Header = () => {
|
import Swirl from './swirl';
|
||||||
const dispatch = useDispatch()
|
|
||||||
|
function showAdmin(user) {
|
||||||
|
return user && user.email === 'ntr@mnml.gg';
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_ROUTE = '/';
|
||||||
|
|
||||||
|
const Nav = () => {
|
||||||
const user = useSelector((state: State) => state.user);
|
const user = useSelector((state: State) => state.user);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!user) {
|
||||||
|
route('/login');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
route(DEFAULT_ROUTE)
|
||||||
|
|
||||||
|
if (window.location.host === 'crates.ntwl.xyz') {
|
||||||
|
LogRocket.identify(user.id, {
|
||||||
|
name: user.email,
|
||||||
|
email: user.email,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}, [user])
|
||||||
|
|
||||||
async function logoutClick(e) {
|
async function logoutClick(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
await http.postLogout();
|
await http.postLogout();
|
||||||
route('/login');
|
window.location.reload();
|
||||||
dispatch(userState.actions.set(null));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const nav = () => (
|
const nav = () => (
|
||||||
@@ -33,14 +58,13 @@ const Header = () => {
|
|||||||
|
|
||||||
const adminNav = () => (
|
const adminNav = () => (
|
||||||
<nav>
|
<nav>
|
||||||
<a href="/bundles">Bundles</a>
|
<a href='/acp'>Offers ACP</a>
|
||||||
<a href='/offers/acp'>Offers ACP</a>
|
|
||||||
</nav>
|
</nav>
|
||||||
)
|
)
|
||||||
|
|
||||||
const userNav = () => (
|
const userNav = () => (
|
||||||
<nav>
|
<nav>
|
||||||
<a href="/account">Account</a>
|
<a href="/info">Info</a>
|
||||||
<a href="/auth/logout" onClick={logoutClick}>Logout</a>
|
<a href="/auth/logout" onClick={logoutClick}>Logout</a>
|
||||||
</nav>
|
</nav>
|
||||||
)
|
)
|
||||||
@@ -48,10 +72,10 @@ const Header = () => {
|
|||||||
return (
|
return (
|
||||||
<header>
|
<header>
|
||||||
{user ? nav() : anonNav()}
|
{user ? nav() : anonNav()}
|
||||||
{user ? adminNav() : null}
|
{showAdmin(user) ? adminNav() : <div />}
|
||||||
{user ? userNav() : null}
|
{user ? userNav() : null}
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Header;
|
export default Nav;
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
import { h } from 'preact';
|
||||||
|
import { ExternalLink, Check } from 'react-feather';
|
||||||
|
import { useState, useCallback } from 'preact/hooks';
|
||||||
|
import _ from 'lodash'
|
||||||
|
|
||||||
|
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">
|
||||||
|
<div class='number'>${offer.total}</div>
|
||||||
|
<div class='number'>{offer.condition}</div>
|
||||||
|
<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';
|
||||||
|
|
||||||
|
const title = accepted
|
||||||
|
? 'Click to cancel'
|
||||||
|
: 'Click to accept this offer';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button disabled={confirmed} title={title} onClick={() => setStatus()} class={classes}>${offer.total} {offer.condition}</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function OfferConditionSlider([condition, offers], i, groupId, rangeMin, rangeMax) {
|
||||||
|
const step = 'any'
|
||||||
|
const id = `${groupId}-${i}`
|
||||||
|
const markersId = `${id}-markers`
|
||||||
|
const steps = offers.map(o => o.total)
|
||||||
|
const initialValue = _.last(offers.filter(o => o.status === 'Accepted'))?.total || 0
|
||||||
|
|
||||||
|
const ws = useSelector((state: State) => state.ws);
|
||||||
|
const [acceptedPrice, setAcceptedPrice] = useState(initialValue)
|
||||||
|
|
||||||
|
const sendOffers = useCallback(_.debounce(function send(v) {
|
||||||
|
const accepted = offers.filter(o => Number(o.total) <= v).map(o => o.id)
|
||||||
|
const pending = offers.filter(o => Number(o.total) > v).map(o => o.id)
|
||||||
|
ws.sendOfferStatusSet(accepted, 'Accepted')
|
||||||
|
ws.sendOfferStatusSet(pending, 'Pending')
|
||||||
|
}, 1000), [])
|
||||||
|
|
||||||
|
function onChange(target) {
|
||||||
|
const highest = () => {
|
||||||
|
return _.findLast(steps, s => target.value >= Number(s)) || 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const v = highest()
|
||||||
|
target.value = v
|
||||||
|
setAcceptedPrice(v)
|
||||||
|
sendOffers(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
const acceptedPriceLabel = acceptedPrice ? `$${acceptedPrice}` : '-'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={id}>
|
||||||
|
<label for={id}>{condition}</label><br />
|
||||||
|
<label for={id}>{acceptedPriceLabel}</label><br />
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
id={id}
|
||||||
|
name={id}
|
||||||
|
list={markersId}
|
||||||
|
step={step}
|
||||||
|
min={rangeMin}
|
||||||
|
max={rangeMax}
|
||||||
|
value={acceptedPrice}
|
||||||
|
onChange={e => onChange((e.target as HTMLInputElement)) } />
|
||||||
|
|
||||||
|
<datalist id={markersId}>
|
||||||
|
{offers.map(o => <option value={o.total}></option>)}
|
||||||
|
</datalist>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default function OfferGroup(group, i) {
|
||||||
|
const ws = useSelector((state: State) => state.ws);
|
||||||
|
|
||||||
|
const format = group.details.formats[0].text ? `${group.details.formats[0].text} - ` : '';
|
||||||
|
const descriptions = group.details.formats.map(f => f.descriptions.join(' ')).join(' ')
|
||||||
|
const detailText = `${format} ${descriptions}`;
|
||||||
|
|
||||||
|
const conditions = Object.entries(_.groupBy(group.offers, 'condition'))
|
||||||
|
const rangeMin = 0
|
||||||
|
const rangeMax = _.max(group.offers.map(o => Number(o.total)))
|
||||||
|
|
||||||
|
const SHOW_BUTTONS = false
|
||||||
|
const buttons = SHOW_BUTTONS ? (
|
||||||
|
<div class="lines">
|
||||||
|
{group.offers.map(o => OfferPriceBtn(o))}
|
||||||
|
</div>
|
||||||
|
) : null
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="offer" key={i} id={group.details.id}>
|
||||||
|
<section>
|
||||||
|
<h3>{group.details.artists_sort} - {group.details.title} <a href={group.details.uri} target="_blank" rel="noreferrer"><ExternalLink /></a></h3>
|
||||||
|
<div class='alt-text details'>
|
||||||
|
{group.details.year} - {detailText}
|
||||||
|
</div>
|
||||||
|
<div class='alt-text details'>Status: {group.status}</div>
|
||||||
|
</section>
|
||||||
|
<img src={group.details.thumb} />
|
||||||
|
<div>
|
||||||
|
{conditions.map((c, i) => OfferConditionSlider(c, i, group.details.id, rangeMin, rangeMax))}
|
||||||
|
</div>
|
||||||
|
{ buttons }
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -2,35 +2,42 @@ import { h } from 'preact';
|
|||||||
import { ExternalLink, Check } from 'react-feather';
|
import { ExternalLink, Check } from 'react-feather';
|
||||||
import { useState } from 'preact/hooks';
|
import { useState } from 'preact/hooks';
|
||||||
|
|
||||||
|
import { useSelector, useDispatch } from 'react-redux'
|
||||||
|
import { State } from './../store';
|
||||||
|
|
||||||
import http from '../http';
|
import http from '../http';
|
||||||
|
|
||||||
export default function Offer(offer) {
|
export default function Offer(offer) {
|
||||||
|
const ws = useSelector((state: State) => state.ws);
|
||||||
|
|
||||||
const { want: { data: details } } = offer;
|
const { want: { data: details } } = offer;
|
||||||
|
|
||||||
const [accepted, setAccepted] = useState(offer.status !== 'pending');
|
function setStatus(status) {
|
||||||
|
ws.sendOfferStatusSet(offer.id, status);
|
||||||
async function onClick() {
|
|
||||||
const [error] = await http.postOfferAccept(offer.id);
|
|
||||||
if (error) return false;
|
|
||||||
setAccepted(true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const accepted = offer.status === 'Accepted';
|
||||||
const classes = `${accepted ? 'accepted' : ''}`;
|
const classes = `${accepted ? 'accepted' : ''}`;
|
||||||
const btnText = accepted
|
const btnText = accepted
|
||||||
? offer.status
|
? offer.status
|
||||||
: 'Accept';
|
: 'Accept';
|
||||||
|
|
||||||
|
const negBtn = accepted
|
||||||
|
? <button onClick={() => setStatus('Pending')} class='neg'>Cancel</button>
|
||||||
|
: <button onClick={() => setStatus('Declined')} class='neg'>Decline</button>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="offer">
|
<div class="offer">
|
||||||
<section>
|
<section>
|
||||||
<h2>{details.artists_sort} - {details.title} <a href={details.uri} target="_blank"><ExternalLink /></a></h2>
|
<h3>{details.artists_sort} - {details.title} <a href={details.uri} target="_blank" rel="noreferrer"><ExternalLink /></a></h3>
|
||||||
<h5 class='alt-text' >{details.year}</h5>
|
<div class='alt-text' >{details.year}</div>
|
||||||
<h5 class='alt-text'>{details.formats[0].descriptions.join(' ')}</h5>
|
<div class='alt-text'>{details.formats[0].descriptions.join(' ')}</div>
|
||||||
<h5 class='number'>mcrates price: ${offer.total}</h5>
|
<div class='number'>mcrates price: ${offer.total}</div>
|
||||||
<h5 class='number'>shipped by itself: ${offer.comparison}</h5>
|
<div class='number'>shipped by itself: ${offer.comparison}</div>
|
||||||
</section>
|
</section>
|
||||||
<img src={details.thumb} />
|
<img src={details.thumb} />
|
||||||
<button onClick={onClick} disabled={accepted} class={classes} >{btnText}</button>
|
<button onClick={() => setStatus('Accepted')} disabled={accepted} class={classes} >{btnText}</button>
|
||||||
|
{negBtn}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { h } from 'preact';
|
||||||
|
|
||||||
|
export default function Swirl() {
|
||||||
|
return <div class='swirl' />;
|
||||||
|
}
|
||||||
@@ -1,17 +1,40 @@
|
|||||||
import { h } from 'preact';
|
import { h } from 'preact';
|
||||||
|
import { useState } from 'preact/hooks';
|
||||||
|
import { ExternalLink } from 'react-feather';
|
||||||
|
|
||||||
|
import { useSelector } from 'react-redux'
|
||||||
|
import { State } from './../store'
|
||||||
|
|
||||||
function Want(want) {
|
function Want(want) {
|
||||||
console.log(want);
|
|
||||||
const r = want.data;
|
const r = want.data;
|
||||||
|
|
||||||
|
const ws = useSelector((state: State) => state.ws);
|
||||||
|
const [confirm, setConfirm] = useState(false);
|
||||||
|
|
||||||
|
function onClick() {
|
||||||
|
if (confirm) {
|
||||||
|
setConfirm(false);
|
||||||
|
return ws.sendWantRm(want.id);
|
||||||
|
}
|
||||||
|
setTimeout(() => setConfirm(false), 2500);
|
||||||
|
return setConfirm(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = confirm
|
||||||
|
? 'confirm'
|
||||||
|
: 'remove';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<article key={r.id}>
|
<article key={r.id}>
|
||||||
<div>
|
<div>
|
||||||
<h3>{r.artists_sort} - {r.title}</h3>
|
<h3>{r.artists_sort} - {r.title} <a href={r.uri} target="_blank" rel="noreferrer"><ExternalLink /></a></h3>
|
||||||
<h5 class='alt-text' >{r.year}</h5>
|
|
||||||
<h5 class='alt-text'>{r.formats[0].descriptions.join(' ')}</h5>
|
|
||||||
</div>
|
</div>
|
||||||
<img src={r.thumb} />
|
<img src={r.thumb} />
|
||||||
|
<div class='details'>
|
||||||
|
<div class='alt'>{r.year} - {r.formats[0].descriptions.join(' ')}</div>
|
||||||
|
<div class='alt'>{want._count.offers} offers</div>
|
||||||
|
</div>
|
||||||
|
<button onClick={onClick} >{text}</button>
|
||||||
</article>
|
</article>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-43
@@ -32,34 +32,18 @@ function onError(error) {
|
|||||||
api.interceptors.response.use(onSuccess, onError);
|
api.interceptors.response.use(onSuccess, onError);
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
getBundles: async function getBundles() {
|
// TODO move to rpc
|
||||||
return api.get('/acp/bundles');
|
postWantSearch: async function postWantSearch(query) {
|
||||||
},
|
return api.post('/want/search', { query });
|
||||||
|
|
||||||
getOffers: async function getOffers() {
|
|
||||||
return api.get('/user/offers');
|
|
||||||
},
|
|
||||||
|
|
||||||
getOffersAcp: async function getOffersAcp() {
|
|
||||||
return api.get('/acp/offers');
|
|
||||||
},
|
|
||||||
|
|
||||||
getWants: async function getWants() {
|
|
||||||
return api.get('/want/all');
|
|
||||||
},
|
|
||||||
|
|
||||||
postWantSearch: async function postWantSearch(artist, title) {
|
|
||||||
return api.post('/want/search', { artist, title });
|
|
||||||
},
|
|
||||||
|
|
||||||
postWant: async function postWant(releaseId) {
|
|
||||||
return api.post('/want', { releaseId });
|
|
||||||
},
|
},
|
||||||
|
|
||||||
postDiscogs: async function postDiscogs(username) {
|
postDiscogs: async function postDiscogs(username) {
|
||||||
return api.post('/want/discogs', { username });
|
return api.post('/want/discogs', { username });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// --------------------------------
|
||||||
|
// require as http to set cookies
|
||||||
|
|
||||||
getLogin: async function getLogin() {
|
getLogin: async function getLogin() {
|
||||||
return api.get('/auth/login')
|
return api.get('/auth/login')
|
||||||
},
|
},
|
||||||
@@ -82,27 +66,6 @@ const actions = {
|
|||||||
password,
|
password,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
postOffers: async function postOffers(listingIds, shipping, baseShipping) {
|
|
||||||
return api.post('/offers', {
|
|
||||||
listingIds,
|
|
||||||
shipping,
|
|
||||||
baseShipping,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
postOfferAccept: async function postOffers(id) {
|
|
||||||
return api.post('/offer/accept', {
|
|
||||||
id,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
postOfferStatus: async function postOfferStatus(id, status) {
|
|
||||||
return api.post('/acp/offer/status', {
|
|
||||||
id,
|
|
||||||
status,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default actions;
|
export default actions;
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
|
import 'izitoast/dist/css/iziToast.min.css';
|
||||||
import './style/index.less';
|
import './style/index.less';
|
||||||
|
|
||||||
import App from './app';
|
import App from './app';
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
+38
-28
@@ -1,30 +1,46 @@
|
|||||||
import { h } from 'preact';
|
import { h } from 'preact';
|
||||||
import { useState, useEffect } from 'preact/hooks';
|
import { useState, useEffect } from 'preact/hooks';
|
||||||
import { connect } from 'unistore/preact'
|
|
||||||
|
|
||||||
import http from './../http';
|
import http from './../http';
|
||||||
import AcpOffer from './../components/acp.offer';
|
|
||||||
|
|
||||||
// hmmm
|
import { useSelector, useDispatch } from 'react-redux'
|
||||||
// bundle...
|
import { State } from './../store';
|
||||||
import { Status } from './../../../api/src/offers'
|
|
||||||
|
enum Status {
|
||||||
|
Accepted = 'Accepted',
|
||||||
|
Pending = 'Pending',
|
||||||
|
Declined = 'Declined',
|
||||||
|
Confirmed = 'Confirmed',
|
||||||
|
Complete = 'Complete',
|
||||||
|
}
|
||||||
|
|
||||||
|
const POSITIVE_STATUS = [Status.Accepted, Status.Confirmed, Status.Complete];
|
||||||
|
|
||||||
function Item({ item }) {
|
function Item({ item }) {
|
||||||
|
const ws = useSelector((state: State) => state.ws);
|
||||||
|
console.log(item);
|
||||||
|
|
||||||
const statuses = Object.entries(Status).map(([label, value]) => (
|
const statuses = Object.entries(Status).map(([label, value]) => (
|
||||||
<option value={value}>{label}</option>
|
<option selected={item.status === value} value={value}>{value}</option>
|
||||||
));
|
));
|
||||||
|
|
||||||
function onChange(e) {
|
function onChange(e) {
|
||||||
console.log(item.id, e.target.value);
|
ws.sendAcpOfferStatusSet(item.id, e.target.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const bold = POSITIVE_STATUS.includes(item.status)
|
||||||
|
? 'bold'
|
||||||
|
: '';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={item.id}>
|
<div key={item.id} class={bold}>
|
||||||
<a href={item.listing.data.uri} target="_blank" >
|
<a href={item.listing.data.uri} target="_blank" rel="noreferrer" >
|
||||||
<h4>{item.listing.data.release.description}</h4>
|
<h4>{item.listing.data.release.description}</h4>
|
||||||
</a>
|
</a>
|
||||||
<h5 class='alt-text'>{item.total}</h5>
|
<h5>{item.user.email}</h5>
|
||||||
|
<h5 class='alt-text'>{item.total} - {item.condition}</h5>
|
||||||
<h5>{item.status}</h5>
|
<h5>{item.status}</h5>
|
||||||
|
<label>Set Status:</label>
|
||||||
<select onChange={onChange}>
|
<select onChange={onChange}>
|
||||||
{statuses}
|
{statuses}
|
||||||
</select>
|
</select>
|
||||||
@@ -33,11 +49,13 @@ function Item({ item }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function SellerGroup(group) {
|
function SellerGroup(group) {
|
||||||
|
const ready = group.offers.filter(item => POSITIVE_STATUS.includes(item.status)).length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="bundle">
|
<div class="bundle">
|
||||||
<section>
|
<section>
|
||||||
<h2>{group.username} - {group.seller}</h2>
|
<h2>{group.username} - {group.seller}</h2>
|
||||||
<h3>{group.offers.length} offers</h3>
|
<h3>{ready} / {group.offers.length} offers</h3>
|
||||||
<h4>Ships: {group.mentionsAustralia && 'AU'} {group.mentionsWorldwide && 'WORLD'}</h4>
|
<h4>Ships: {group.mentionsAustralia && 'AU'} {group.mentionsWorldwide && 'WORLD'}</h4>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -49,30 +67,22 @@ function SellerGroup(group) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function Home() {
|
function Home() {
|
||||||
const [offers, setOffers] = useState([]);
|
const offers = useSelector((state: State) => state.acpOffers);
|
||||||
|
const ws = useSelector((state: State) => state.ws);
|
||||||
|
|
||||||
useEffect(() => {
|
function sync() {
|
||||||
async function getOffers() {
|
ws.sendAcpListingsSync();
|
||||||
const [error, offers] = await http.getOffersAcp();
|
|
||||||
if (error) return console.error(error);
|
|
||||||
setOffers(offers);
|
|
||||||
}
|
|
||||||
getOffers();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (!offers.length) {
|
|
||||||
return (
|
|
||||||
<div class={'offers'}>
|
|
||||||
loading...
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div class='acp'>
|
||||||
|
<button onClick={sync}> sync listings </button>
|
||||||
|
<button onClick={sync}> generate offers </button>
|
||||||
|
<hr />
|
||||||
<div class='bundles'>
|
<div class='bundles'>
|
||||||
{offers.map(SellerGroup)}
|
{offers.map(SellerGroup)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
import { h } from 'preact';
|
|
||||||
import { useState, useEffect } from 'preact/hooks';
|
|
||||||
|
|
||||||
import http from './../http';
|
|
||||||
import Bundle from './../components/bundle';
|
|
||||||
|
|
||||||
function Bundles() {
|
|
||||||
const [bundles, setBundles] = useState([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
async function getBundles() {
|
|
||||||
const [error, bundles] = await http.getBundles();
|
|
||||||
if (error) return console.error(error);
|
|
||||||
setBundles(bundles);
|
|
||||||
}
|
|
||||||
getBundles();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (!bundles.length) {
|
|
||||||
return (
|
|
||||||
<div class={'bundles'}>
|
|
||||||
loading...
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div class={'bundles'}>
|
|
||||||
{bundles.map(Bundle)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Bundles;
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import { h } from 'preact';
|
|
||||||
import { useState, useEffect } from 'preact/hooks';
|
|
||||||
import { route } from 'preact-router';
|
|
||||||
import { useSelector, useDispatch } from 'react-redux'
|
|
||||||
|
|
||||||
import http from './../http';
|
|
||||||
import { State, offersState } from './../store';
|
|
||||||
|
|
||||||
import Offer from './../components/offer';
|
|
||||||
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div class={'offers'}>
|
|
||||||
{offers.map(Offer)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Home;
|
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import { h } from 'preact';
|
||||||
|
|
||||||
|
import Swirl from './../components/swirl';
|
||||||
|
|
||||||
|
const Info = () => {
|
||||||
|
return (
|
||||||
|
<div class='info'>
|
||||||
|
<Swirl />
|
||||||
|
<div class='words'>
|
||||||
|
<section>
|
||||||
|
<h1>Info</h1>
|
||||||
|
<p> Thanks for signing up! </p>
|
||||||
|
<p> Mates Crates is a record importing service in alpha. </p>
|
||||||
|
<p> Its purpose is to reduce the cost of shipping vinyl records to Melbourne, Australia. </p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<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> (this can take a few minutes time!) </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>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Offers</h2>
|
||||||
|
<p> Not all items on your <span class='alt'>List</span> will have offers. However, as new users join the site the number of offers we can make increases, so be sure to tell your friends!</p>
|
||||||
|
<p> Think of offers like an expression of interest, they are not confirmed until they are set to <span class='alt-text'>Confirmed</span> status. </p>
|
||||||
|
<p> After that they will be shipped to Melbourne and ready for pick up. </p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Pick Up</h2>
|
||||||
|
<p> Use the contact options to get in touch. </p>
|
||||||
|
<p> Payment is cash on delivery. </p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>FAQ</h2>
|
||||||
|
<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> Friends </h3>
|
||||||
|
<p> Please invite other collectors to the site. More users means offers can become cheaper and shipped sooner. </p>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
<section class='contact'>
|
||||||
|
<h2>Contact</h2>
|
||||||
|
<p><a href='mailto:ntr@mnml.gg?subject=Mates%20Crates%20Enquiry'>Email</a></p>
|
||||||
|
<p><a href='https://discord.gg/QfjY8WNQyx'>Discord</a></p>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Info;
|
||||||
+12
-20
@@ -1,31 +1,23 @@
|
|||||||
import { h } from 'preact';
|
import { h } from 'preact';
|
||||||
import { useState, useEffect } from 'preact/hooks';
|
|
||||||
|
|
||||||
// import style from './style.less';
|
import { useSelector } from 'react-redux'
|
||||||
import http from './../http';
|
import { route } from 'preact-router';
|
||||||
|
|
||||||
|
import { State } from './../store'
|
||||||
|
import * as toast from './../toast';
|
||||||
|
|
||||||
import Want from './../components/want';
|
import Want from './../components/want';
|
||||||
|
|
||||||
interface Props {
|
const List = () => {
|
||||||
user: any,
|
const wants = useSelector((state: State) => state.wants);
|
||||||
wants: any[],
|
|
||||||
getUserOffers: (userId: string) => void,
|
|
||||||
}
|
|
||||||
|
|
||||||
const List = ({ user }: Props) => {
|
if (!wants) {
|
||||||
const [wants, setOffers] = useState([]);
|
return <div> loading... </div>;
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
async function getWants() {
|
|
||||||
const [error, wants] = await http.getWants();
|
|
||||||
if (error) return console.error(error);
|
|
||||||
setOffers(wants);
|
|
||||||
}
|
}
|
||||||
getWants();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
|
|
||||||
if (!wants.length) {
|
if (!wants.length) {
|
||||||
return <div> loading... </div>;
|
toast.info('List Empty', 'You have no items in your list, add some with search.');
|
||||||
|
route('/search');
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
+33
-21
@@ -5,34 +5,45 @@ import { useSelector, useDispatch } from 'react-redux'
|
|||||||
|
|
||||||
import http from './../http';
|
import http from './../http';
|
||||||
import { State, userState } from './../store';
|
import { State, userState } from './../store';
|
||||||
|
import Swirl from './../components/swirl';
|
||||||
|
|
||||||
const Login = ({ mode }) => {
|
const Login = ({ mode }) => {
|
||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
const user = useSelector((state: State) => state.user);
|
const user = useSelector((state: State) => state.user);
|
||||||
|
const ws = useSelector((state: State) => state.ws);
|
||||||
|
|
||||||
const [email, setEmail] = useState('')
|
|
||||||
const [password, setPassword] = useState('');
|
|
||||||
const [confirmPassword, setConfirmPassword] = useState('');
|
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
|
|
||||||
async function postLogin() {
|
function gotoRegister() {
|
||||||
|
return route('/register');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function postLogin(email, password) {
|
||||||
const [error, user] = await http.postLogin(email, password);
|
const [error, user] = await http.postLogin(email, password);
|
||||||
if (error) return setError(error);
|
if (error) return setError(error);
|
||||||
route('/');
|
route('/');
|
||||||
dispatch(userState.actions.set(user));
|
dispatch(userState.actions.set(user));
|
||||||
|
// reconnect ws to get logged in methods
|
||||||
|
ws.connect();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function postRegister() {
|
async function postRegister(email, password, confirm) {
|
||||||
|
if (password !== confirm) return setError('passwords do not match');
|
||||||
|
|
||||||
const [error, user] = await http.postRegister(email, password);
|
const [error, user] = await http.postRegister(email, password);
|
||||||
if (error) return setError(error);
|
if (error) return setError(error);
|
||||||
route('/search');
|
route('/search');
|
||||||
dispatch(userState.actions.set(user));
|
dispatch(userState.actions.set(user));
|
||||||
|
// reconnect ws to get logged in methods
|
||||||
|
ws.connect();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSubmit(e) {
|
function handleSubmit(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (mode === 'login') return postLogin()
|
|
||||||
if (mode === 'register') return postRegister();
|
console.log(e.target.email, e.target.password, e.target.confirm);
|
||||||
|
if (mode === 'login') return postLogin(e.target.email.value, e.target.password.value);
|
||||||
|
if (mode === 'register') return postRegister(e.target.email.value, e.target.password.value, e.target.confirm.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
const emailRef = useRef<HTMLInputElement>()
|
const emailRef = useRef<HTMLInputElement>()
|
||||||
@@ -48,21 +59,21 @@ const Login = ({ mode }) => {
|
|||||||
if (mode === 'register') return (
|
if (mode === 'register') return (
|
||||||
<div class="login">
|
<div class="login">
|
||||||
{errorEl}
|
{errorEl}
|
||||||
<form action="">
|
<form action="" onSubmit={handleSubmit}>
|
||||||
<div class="item">
|
<div class="item">
|
||||||
<label for="email">email</label>
|
<label for="email">email</label>
|
||||||
<input type="email" name="email" id="email" ref={emailRef} value={email} required onInput={(e) => setEmail((e.target as HTMLInputElement).value)} />
|
<input type="email" name="email" id="email" ref={emailRef} required />
|
||||||
</div>
|
</div>
|
||||||
<div class="item">
|
<div class="item">
|
||||||
<label for="password">password</label>
|
<label for="password">password</label>
|
||||||
<input type="password" name="password" id="password" value={password} required onInput={(e) => setPassword((e.target as HTMLInputElement).value)} />
|
<input type="password" name="password" id="password" required />
|
||||||
</div>
|
</div>
|
||||||
<div class="item">
|
<div class="item">
|
||||||
<label for="password">confirm password</label>
|
<label for="password">confirm password</label>
|
||||||
<input type="password" name="password" id="password" value={confirmPassword} required onInput={(e) => setConfirmPassword((e.target as HTMLInputElement).value)} />
|
<input type="password" name="confirm" id="confirm" required />
|
||||||
</div>
|
</div>
|
||||||
<div class="item">
|
<div class="item">
|
||||||
<input type="submit" value="register" onClick={handleSubmit} />
|
<input type="submit" value="register" />
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -72,19 +83,20 @@ const Login = ({ mode }) => {
|
|||||||
return (
|
return (
|
||||||
<div class="login">
|
<div class="login">
|
||||||
{errorEl}
|
{errorEl}
|
||||||
<form action="">
|
<form action="" onSubmit={handleSubmit}>
|
||||||
<div class="item">
|
<div class="item">
|
||||||
<label for="email">email</label>
|
<label for="email">email</label>
|
||||||
<input type="email" name="email" id="email" ref={emailRef} value={email} required onInput={(e) => setEmail((e.target as HTMLInputElement).value)} />
|
<input type="email" name="email" id="email" ref={emailRef} />
|
||||||
</div>
|
</div>
|
||||||
<div class="item">
|
<div class="item">
|
||||||
<label for="password">password</label>
|
<label for="password">password</label>
|
||||||
<input type="password" name="password" id="password" value={password} required onInput={(e) => setPassword((e.target as HTMLInputElement).value)} />
|
<input type="password" name="password" id="password" />
|
||||||
</div>
|
</div>
|
||||||
<div class="item">
|
<div class="item">
|
||||||
<input type="submit" value="login" onClick={handleSubmit} />
|
<input type="submit" value="login" />
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
<button onClick={gotoRegister}> create account </button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -93,11 +105,11 @@ function LoginPage({ mode }) {
|
|||||||
return (
|
return (
|
||||||
<div class="landing">
|
<div class="landing">
|
||||||
<Login mode={mode} />
|
<Login mode={mode} />
|
||||||
<div></div>
|
<Swirl />
|
||||||
<div class="info">
|
<div class='words'>
|
||||||
<p>Mates Crates is a record importing service.</p>
|
<p>record importing service</p>
|
||||||
<p>We bundle orders together from thousands of online stores to reduce the cost of shipping vinyl.</p>
|
<p>we bundle orders together from thousands of online stores to reduce the cost of shipping vinyl</p>
|
||||||
<p>Based in Preston.</p>
|
<p>then we get together and play them</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
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) => {
|
||||||
|
if (!want._count.offers) return null
|
||||||
|
const href = `#${want.release}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class=''>
|
||||||
|
<a href={href}>{want.data.artists_sort} - {want.data.title}</a>
|
||||||
|
<span>{want._count.offers}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function Home() {
|
||||||
|
const offers = useSelector((state: State) => state.offers);
|
||||||
|
const wants = useSelector((state: State) => state.wants);
|
||||||
|
|
||||||
|
if (offers && offers.length === 0) {
|
||||||
|
toast.info('no offers', 'You have no offers yet, search for some more items.');
|
||||||
|
route('/search');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!offers || !wants) return <div>loading...</div>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main >
|
||||||
|
<nav>
|
||||||
|
<div class='header'>
|
||||||
|
<span>Record</span>
|
||||||
|
<span>Offers</span>
|
||||||
|
</div>
|
||||||
|
{wants.map(GroupNav)}
|
||||||
|
</nav>
|
||||||
|
<div class={'offers'}>
|
||||||
|
{offers.map(OfferGroup)}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Home;
|
||||||
@@ -1,28 +1,31 @@
|
|||||||
import { h } from 'preact';
|
import { h } from 'preact';
|
||||||
import { useState, useRef, useEffect } from 'preact/hooks';
|
import { useState, useRef, useEffect } from 'preact/hooks';
|
||||||
import { ExternalLink, Check } from 'react-feather';
|
import { ExternalLink, Check } from 'react-feather';
|
||||||
|
import { useSelector } from 'react-redux'
|
||||||
|
|
||||||
|
import { State } from './../store';
|
||||||
|
import { info, error as toastError } from './../toast';
|
||||||
|
|
||||||
import http from '../http';
|
import http from '../http';
|
||||||
|
|
||||||
function Result(r) {
|
function Result(r, wants, ws) {
|
||||||
const [added, setAdded] = useState(false)
|
|
||||||
|
|
||||||
async function onClick() {
|
async function onClick() {
|
||||||
const [error, want] = await http.postWant(r.id);
|
ws.sendWantAdd(r.id);
|
||||||
if (error) return false;
|
|
||||||
setAdded(true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const classes = `${added ? 'added' : ''}`;
|
// releaseIds are bigints on server
|
||||||
const btnText = added
|
const wanted = wants.some(w => BigInt(w.release) === BigInt(r.id));
|
||||||
|
|
||||||
|
const classes = `${wanted ? 'added' : ''}`;
|
||||||
|
const btnText = wanted
|
||||||
? <Check />
|
? <Check />
|
||||||
: '+';
|
: '+';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<article key={r.id}>
|
<article key={r.id}>
|
||||||
<button onClick={onClick} disabled={added} class={classes} >{btnText}</button>
|
<button onClick={onClick} disabled={wanted} class={classes} >{btnText}</button>
|
||||||
<div>
|
<div>
|
||||||
<h3>{r.title} <a href={`https://discogs.com${r.uri}`} target="_blank"><ExternalLink /></a></h3>
|
<h3>{r.title} <a href={`https://discogs.com${r.uri}`} target="_blank" rel="noreferrer"><ExternalLink /></a></h3>
|
||||||
<h5 class='alt-text' >{r.year}</h5>
|
<h5 class='alt-text' >{r.year}</h5>
|
||||||
<h5 class='alt-text'>{r.country}</h5>
|
<h5 class='alt-text'>{r.country}</h5>
|
||||||
<h5 class='alt-text'>{r.format.join(' ')}</h5>
|
<h5 class='alt-text'>{r.format.join(' ')}</h5>
|
||||||
@@ -33,35 +36,47 @@ function Result(r) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const User = () => {
|
const User = () => {
|
||||||
const [artist, setArtist] = useState('')
|
const [query, setQuery] = useState('')
|
||||||
const [title, setTitle] = useState('');
|
|
||||||
const [username, setUsername] = useState('');
|
const [username, setUsername] = useState('');
|
||||||
const [results, setResults] = useState([]);
|
const [results, setResults] = useState([]);
|
||||||
const [infoText, setInfoText] = useState('');
|
|
||||||
|
const wants = useSelector((state: State) => state.wants);
|
||||||
|
const ws = useSelector((state: State) => state.ws);
|
||||||
|
|
||||||
async function fetchResults() {
|
async function fetchResults() {
|
||||||
const [error, results] = await http.postWantSearch(artist, title);
|
const [error, results] = await http.postWantSearch(query);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
setResults([]);
|
||||||
|
toastError('Error', error);
|
||||||
|
}
|
||||||
|
|
||||||
if (results.length === 0) {
|
if (results.length === 0) {
|
||||||
setResults([]);
|
setResults([]);
|
||||||
return setInfoText('No results');
|
return info('No Results', 'No results returned from search');
|
||||||
}
|
}
|
||||||
|
|
||||||
setInfoText('');
|
|
||||||
setResults(results);
|
setResults(results);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function submitWantlist() {
|
async function submitWantlist() {
|
||||||
const [error, wants] = await http.postDiscogs(username);
|
const [error, wants] = await http.postDiscogs(username);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
setResults([]);
|
||||||
|
toastError('Error', error);
|
||||||
|
}
|
||||||
|
|
||||||
if (wants.length === 0) {
|
if (wants.length === 0) {
|
||||||
setResults([]);
|
setResults([]);
|
||||||
return setInfoText('No new additions');
|
return info('Success', 'No new additions');
|
||||||
}
|
}
|
||||||
|
|
||||||
setInfoText(`Added ${wants.length} items to your list.`);
|
info('Success', `Added ${wants.length} items to your list.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const searchRef = useRef<HTMLInputElement>()
|
const searchRef = useRef<HTMLInputElement>();
|
||||||
|
const resultsRef = useRef<HTMLDivElement>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
searchRef.current.focus()
|
searchRef.current.focus()
|
||||||
@@ -78,21 +93,15 @@ const User = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const topEl = results.length
|
const topEl = results.length
|
||||||
? <button onClick={() => window.scrollTo(0, 0)}> top </button>
|
? <button onClick={() => resultsRef.current.scrollTo(0, 0)}> top </button>
|
||||||
: null;
|
|
||||||
|
|
||||||
const infoEl = infoText
|
|
||||||
? <article><div></div><div>{infoText}</div></article>
|
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class='search'>
|
<div class='search'>
|
||||||
<div>
|
<div>
|
||||||
<form action="" method="get">
|
<form action="" method="get">
|
||||||
<label for="artist">Artist</label>
|
<label for="query">Query</label>
|
||||||
<input type="text" name="artist" ref={searchRef} placeholder="Ben Klock" id="artist" value={artist} onChange={e => setArtist((e.target as HTMLInputElement).value) } required />
|
<input type="text" name="query" ref={searchRef} placeholder="ben klock subzero" id="query" value={query} onChange={e => setQuery((e.target as HTMLInputElement).value) } required />
|
||||||
<label for="title">Title</label>
|
|
||||||
<input type="text" name="title" placeholder="Subzero" id="title" value={title} onChange={e => setTitle((e.target as HTMLInputElement).value)} required />
|
|
||||||
<input type="submit" value="Search" onClick={handleSubmit} />
|
<input type="submit" value="Search" onClick={handleSubmit} />
|
||||||
</form>
|
</form>
|
||||||
<hr />
|
<hr />
|
||||||
@@ -100,16 +109,14 @@ const User = () => {
|
|||||||
<form action="" method="get">
|
<form action="" method="get">
|
||||||
<label for="Discogs">Discogs Username</label>
|
<label for="Discogs">Discogs Username</label>
|
||||||
<input type="text" name="username" placeholder="Discogs Username" id="username" value={username} onChange={e => setUsername((e.target as HTMLInputElement).value) } required />
|
<input type="text" name="username" placeholder="Discogs Username" id="username" value={username} onChange={e => setUsername((e.target as HTMLInputElement).value) } required />
|
||||||
<input type="submit" value="Sync" onClick={handleDiscogsSubmit} />
|
<input type="submit" value="Sync Wantlist" onClick={handleDiscogsSubmit} />
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
<p>Data provided by Discogs.</p>
|
<p>Data provided by Discogs.</p>
|
||||||
</div>
|
</div>
|
||||||
<div />
|
<div class='results' ref={resultsRef}>
|
||||||
<div>
|
{results.map(r => Result(r, wants, ws))}
|
||||||
{infoEl}
|
|
||||||
{results.map(Result)}
|
|
||||||
{topEl}
|
{topEl}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+25
-2
@@ -11,12 +11,28 @@ export const userState = createSlice({
|
|||||||
|
|
||||||
export const offersState = createSlice({
|
export const offersState = createSlice({
|
||||||
name: 'offers',
|
name: 'offers',
|
||||||
|
initialState: null,
|
||||||
|
reducers: {
|
||||||
|
set: (state, action) => action.payload
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const acpOffersState = createSlice({
|
||||||
|
name: 'acpOffers',
|
||||||
initialState: [],
|
initialState: [],
|
||||||
reducers: {
|
reducers: {
|
||||||
set: (state, action) => action.payload
|
set: (state, action) => action.payload
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const wantsState = createSlice({
|
||||||
|
name: 'wants',
|
||||||
|
initialState: null,
|
||||||
|
reducers: {
|
||||||
|
set: (state, action) => action.payload
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export const wsState = createSlice({
|
export const wsState = createSlice({
|
||||||
name: 'ws',
|
name: 'ws',
|
||||||
initialState: null,
|
initialState: null,
|
||||||
@@ -26,12 +42,19 @@ export const wsState = createSlice({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const reducer = combineReducers({
|
const reducer = combineReducers({
|
||||||
user: userState.reducer,
|
|
||||||
offers: offersState.reducer,
|
offers: offersState.reducer,
|
||||||
|
wants: wantsState.reducer,
|
||||||
ws: wsState.reducer,
|
ws: wsState.reducer,
|
||||||
|
user: userState.reducer,
|
||||||
|
|
||||||
|
acpOffers: acpOffersState.reducer,
|
||||||
});
|
});
|
||||||
|
|
||||||
const store = createStore(reducer);
|
const store = createStore(
|
||||||
|
reducer,
|
||||||
|
// @ts-ignore -- devtools window exists on dev
|
||||||
|
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
|
||||||
|
);
|
||||||
|
|
||||||
export default store;
|
export default store;
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
+72
-231
@@ -8,7 +8,7 @@ html {
|
|||||||
box-sizing: inherit;
|
box-sizing: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
body, h1, h2, h3, h4, h5, h6, p, ol, ul {
|
body, h4, h5, h6, p, ol, ul {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
@@ -23,32 +23,6 @@ img {
|
|||||||
height: auto;
|
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
|
// stuff from UX laws
|
||||||
html {
|
html {
|
||||||
color:var(--base-font-color);
|
color:var(--base-font-color);
|
||||||
@@ -56,7 +30,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,6 +39,31 @@ html {
|
|||||||
--breakpoint-medium: 35em;
|
--breakpoint-medium: 35em;
|
||||||
--breakpoint-large: 56em;
|
--breakpoint-large: 56em;
|
||||||
|
|
||||||
|
--small-font-size: 0.75em;
|
||||||
|
--base-font-size: 1em;
|
||||||
|
--smedium-font-size: 1.333em;
|
||||||
|
--medium-font-size: 1.777em;
|
||||||
|
--large-font-size: 2.369em;
|
||||||
|
--xlarge-font-size: 3.157em;
|
||||||
|
--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);
|
||||||
|
|
||||||
|
--base-line-height: 1.4;
|
||||||
|
--header-line-height: 1.2;
|
||||||
|
|
||||||
|
--base-border-color: var(--secondary-font-color);
|
||||||
|
--base-border: 2px solid var(--base-border-color);
|
||||||
|
--hover-border: 2px solid var(--base-font-color);
|
||||||
|
--focus-border: 2px solid var(--base-font-color);
|
||||||
|
--base-border-radius: .2rem;
|
||||||
|
--base-focus: 2px solid var(--base-accent-color);
|
||||||
|
--base-outline-offset: 0.25em;
|
||||||
|
|
||||||
|
--min-font-size: var(--base-font-size);
|
||||||
|
--max-font-size: var(--large-font-size);
|
||||||
|
|
||||||
--color-dark-blue: #101014;
|
--color-dark-blue: #101014;
|
||||||
--color-mid-blue: #16171d;
|
--color-mid-blue: #16171d;
|
||||||
--color-light-blue: #1f2128;
|
--color-light-blue: #1f2128;
|
||||||
@@ -73,45 +72,27 @@ html {
|
|||||||
--color-accent-blue: #5E9ED6;
|
--color-accent-blue: #5E9ED6;
|
||||||
|
|
||||||
--color-blue-gray: #506485;
|
--color-blue-gray: #506485;
|
||||||
--color-green: #537a34;
|
|
||||||
|
--color-blue: #475977;
|
||||||
|
--color-green: #476d4a;
|
||||||
|
--color-red: #ae4442;
|
||||||
|
|
||||||
|
--color-accepted: var(--color-blue);
|
||||||
|
--color-confirmed: var(--color-green);
|
||||||
|
|
||||||
--base-accent-color: var(--color-accent-blue);
|
--base-accent-color: var(--color-accent-blue);
|
||||||
--fg-color: var(--color-off-white);
|
--fg-color: var(--color-off-white);
|
||||||
--hover-link-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;
|
--background-0: var(--color-dark-blue);
|
||||||
--base-font-size: 1em;
|
--background-1: var(--color-mid-blue);
|
||||||
--smedium-font-size: 1.333em;
|
--background-2: var(--color-light-blue);
|
||||||
--medium-font-size: 1.777em;
|
|
||||||
--large-font-size: 2.369em;
|
|
||||||
--xlarge-font-size: 3.157em;
|
|
||||||
--base-font-family: 'Raleway', Arial, Sans-Serif;
|
|
||||||
--secondary-font-family: 'Noto Sans Mono', monospace;
|
|
||||||
--base-font-color: var(--color-off-white);
|
|
||||||
--secondary-font-color: var(--color-mid-grey);
|
|
||||||
|
|
||||||
--base-line-height: 1.4;
|
--spacing: 1rem;
|
||||||
--header-line-height: 1.2;
|
--spacing-half: calc(var(--spacing)/2);
|
||||||
|
--spacing-double: calc(var(--spacing)*2);
|
||||||
|
|
||||||
--min-font-size: var(--base-font-size);
|
--utility-bar-header-height: calc(var(--spacing) * 1.5);
|
||||||
--max-font-size: var(--large-font-size);
|
|
||||||
|
|
||||||
--base-border-color: var(--secondary-font-color);
|
|
||||||
--base-border: 1px solid var(--base-border-color);
|
|
||||||
--hover-border: 1px solid var(--base-font-color);
|
|
||||||
--focus-border: 1px solid var(--base-font-color);
|
|
||||||
--base-border-radius: .2rem;
|
|
||||||
--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);
|
|
||||||
|
|
||||||
--utility-bar-header-height: calc(var(--base-spacing) * 1.5);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
button, input, optgroup, select, textarea {
|
button, input, optgroup, select, textarea {
|
||||||
@@ -125,6 +106,11 @@ button, input, optgroup, select, textarea {
|
|||||||
line-height:var(--base-line-height);
|
line-height:var(--base-line-height);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button, [type=submit] {
|
||||||
|
padding: var(--spacing-half);
|
||||||
|
background: var(--background-1);
|
||||||
|
}
|
||||||
|
|
||||||
input:hover, textarea:hover {
|
input:hover, textarea:hover {
|
||||||
color:var(--base-font-color);
|
color:var(--base-font-color);
|
||||||
border:var(--hover-border)
|
border:var(--hover-border)
|
||||||
@@ -136,11 +122,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] {
|
||||||
@@ -150,125 +136,47 @@ button, [type=button], [type=reset], [type=submit] {
|
|||||||
|
|
||||||
font-family: var(--secondary-font-family);
|
font-family: var(--secondary-font-family);
|
||||||
font-size: var(--small-font-size);
|
font-size: var(--small-font-size);
|
||||||
|
// font-weight: bold;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: var(--tertiary-background-color);
|
background: var(--background-2);
|
||||||
border-color: transparent;
|
border-color: transparent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ok actual styles
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
margin-top:0;
|
||||||
body {
|
line-height:var(--header-line-height);
|
||||||
padding-top: calc(var(--base-spacing)/2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
main {
|
h1 {
|
||||||
padding: 0 5% 0 5%;
|
margin-bottom:var(--spacing);
|
||||||
|
font-size:var(--large-font-size)
|
||||||
}
|
}
|
||||||
|
|
||||||
button, [type=submit] {
|
h2 {
|
||||||
padding: var(--base-spacing-half);
|
margin-bottom:var(--spacing);
|
||||||
background: var(--secondary-background-color);
|
font-size:var(--medium-font-size);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.landing {
|
h3 {
|
||||||
display: grid;
|
margin-bottom:calc(var(--spacing)/2);
|
||||||
grid-template-columns: 1fr 0.5fr 2fr;
|
font-size:var(--smedium-font-size)
|
||||||
|
|
||||||
.login {
|
|
||||||
button {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.info {
|
|
||||||
p {
|
|
||||||
margin-bottom: var(--base-spacing);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.alt-text {
|
h4 {
|
||||||
font-family: var(--secondary-font-family);
|
margin-bottom:calc(var(--spacing)/3);
|
||||||
// font-size: var(--small-font-size);
|
font-size:var(--base-font-size)
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
article {
|
p {
|
||||||
background-color: var(--secondary-background-color);
|
margin:0 0 var(--spacing)
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.search {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 0.5fr 2fr;
|
|
||||||
|
|
||||||
article {
|
|
||||||
display: grid;
|
|
||||||
padding: var(--base-spacing);
|
|
||||||
margin-bottom: var(--base-spacing);
|
|
||||||
grid-template-columns: 1fr 4fr 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
margin-right: var(--base-spacing);
|
|
||||||
background: var(--tertiary-background-color);
|
|
||||||
border-color: var(--secondary-background-color);
|
|
||||||
|
|
||||||
&.added {
|
|
||||||
background: var(--color-green);
|
|
||||||
cursor: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.list {
|
|
||||||
display: flex;
|
|
||||||
flex-flow: row wrap;
|
|
||||||
|
|
||||||
article {
|
|
||||||
flex: 1 1 20%;
|
|
||||||
padding: var(--base-spacing);
|
|
||||||
margin: var(--base-spacing-half);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
form {
|
|
||||||
margin-bottom: var(--base-spacing);
|
|
||||||
|
|
||||||
label, input {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
input, button {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
input {
|
|
||||||
padding: var(--base-spacing-half);
|
|
||||||
margin-bottom: var(--base-spacing);
|
|
||||||
}
|
|
||||||
|
|
||||||
label {
|
|
||||||
font-family: var(--secondary-font-family);
|
|
||||||
font-size: var(--small-font-size);
|
|
||||||
text-transform: uppercase;
|
|
||||||
margin-bottom: var(--base-spacing-half);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
// padding-left: var(--base-spacing);
|
|
||||||
margin-right: var(--base-spacing);
|
|
||||||
|
|
||||||
color: var(--secondary-font-color);
|
color: var(--secondary-font-color);
|
||||||
font-family: var(--secondary-font-family);
|
font-family: var(--secondary-font-family);
|
||||||
font-size: var(--small-font-size);
|
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
@@ -276,78 +184,11 @@ a {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.alt-text, .alt {
|
||||||
header {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr min-content;
|
|
||||||
padding-bottom: var(--base-spacing);
|
|
||||||
|
|
||||||
a {
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.bundles {
|
|
||||||
flex-flow: row;
|
|
||||||
|
|
||||||
.bundle {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 2fr 1fr;
|
|
||||||
margin-bottom: var(--base-spacing-double);
|
|
||||||
|
|
||||||
.items {
|
|
||||||
grid-column: 1 / -1;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(5, 1fr);
|
|
||||||
align-items: stretch;
|
|
||||||
|
|
||||||
div {
|
|
||||||
margin-right: var(--base-spacing);
|
|
||||||
margin-bottom: var(--base-spacing);
|
|
||||||
|
|
||||||
display: grid;
|
|
||||||
grid-template-rows: 1fr 1fr 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.offers {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
|
|
||||||
.offer {
|
|
||||||
display: grid;
|
|
||||||
grid-template-areas:
|
|
||||||
"details img"
|
|
||||||
"btn btn";
|
|
||||||
|
|
||||||
padding: var(--base-spacing);
|
|
||||||
margin: var(--base-spacing-half);
|
|
||||||
background: var(--secondary-background-color);
|
|
||||||
|
|
||||||
section {
|
|
||||||
grid-area: details;
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
grid-area: img;
|
|
||||||
justify-self: end;
|
|
||||||
margin-left: var(--base-spacing-half);
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
grid-area: btn;
|
|
||||||
|
|
||||||
&.accepted {
|
|
||||||
background: var(--color-accent-blue);
|
|
||||||
cursor: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.number {
|
|
||||||
font-family: var(--secondary-font-family);
|
font-family: var(--secondary-font-family);
|
||||||
|
// font-size: var(--small-font-size);
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@import 'fonts.less';
|
||||||
|
@import 'styles.less';
|
||||||
@@ -0,0 +1,422 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
overflow: scroll;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
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;
|
||||||
|
overflow: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
.words {
|
||||||
|
grid-area: words;
|
||||||
|
overflow: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.acp {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type='range']{ accent-color: var(--color-accepted); }
|
||||||
|
|
||||||
|
@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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>crates - record import service</title>
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
|
|
||||||
|
<% preact.headEnd %>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<% preact.bodyEnd %>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import toast from 'izitoast';
|
||||||
|
|
||||||
|
export function info(title, message) {
|
||||||
|
toast.info({
|
||||||
|
title,
|
||||||
|
message: message || '',
|
||||||
|
position: 'bottomCenter',
|
||||||
|
color: '#1f2128',
|
||||||
|
titleColor: '#F4F1D0',
|
||||||
|
messageColor: '#F4F1D0',
|
||||||
|
iconColor: '#F4F1D0',
|
||||||
|
icon: '',
|
||||||
|
theme: 'dark',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function error(title, message) {
|
||||||
|
toast.error({
|
||||||
|
title,
|
||||||
|
message: message || '',
|
||||||
|
position: 'bottomCenter',
|
||||||
|
color: '#ae4442',
|
||||||
|
titleColor: '#F4F1D0',
|
||||||
|
messageColor: '#F4F1D0',
|
||||||
|
iconColor: '#F4F1D0',
|
||||||
|
icon: '',
|
||||||
|
theme: 'dark',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
+74
-8
@@ -1,6 +1,11 @@
|
|||||||
|
import throttle from 'lodash/throttle';
|
||||||
|
|
||||||
|
import { wantsState, userState, offersState, acpOffersState } from './store'
|
||||||
|
import * as toast from './toast';
|
||||||
|
|
||||||
const SOCKET_URL = `${window.location.protocol === 'https:' ? 'wss://' : 'ws://'}${window.location.host}/api/ws`;
|
const SOCKET_URL = `${window.location.protocol === 'https:' ? 'wss://' : 'ws://'}${window.location.host}/api/ws`;
|
||||||
|
|
||||||
export default function createSocket(store) {
|
export default function createSocket(dispatch) {
|
||||||
let ws;
|
let ws;
|
||||||
|
|
||||||
// -------------
|
// -------------
|
||||||
@@ -24,20 +29,66 @@ export default function createSocket(store) {
|
|||||||
// setTimeout(sendPing, 50);
|
// setTimeout(sendPing, 50);
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendSourcesAvailable() {
|
function sendOfferStatusSet(ids, status) {
|
||||||
send(['SourcesAvailable', {}]);
|
send(['OfferStatusSet', { ids, status }]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendWantRm(id) {
|
||||||
|
send(['WantRm', { id }]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendWantAdd(releaseId) {
|
||||||
|
send(['WantAdd', { releaseId }]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendOffers() {
|
||||||
|
send(['Offers', {}]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendWants() {
|
||||||
|
send(['Wants', {}]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendAcpOffers() {
|
||||||
|
send(['AcpOffers', {}]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendAcpOfferStatusSet(id, status) {
|
||||||
|
send(['AcpOfferStatusSet', { id, status }]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendAcpListingsSync() {
|
||||||
|
send(['AcpListingsSync', {}]);
|
||||||
}
|
}
|
||||||
|
|
||||||
let pongTimeout;
|
let pongTimeout;
|
||||||
function onPong() {
|
function onPong() {
|
||||||
pongTimeout = setTimeout(sendPing, 1000);
|
pongTimeout = setTimeout(sendPing, 5000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------
|
// -------------
|
||||||
// Incoming
|
// Incoming
|
||||||
// -------------
|
// -------------
|
||||||
function onSourcesAvailable(clouds) {
|
function onUser(user) {
|
||||||
// store.dispatch(http.LIBRARY_RECEIVE_POINTCLOUDS, clouds);
|
dispatch(userState.actions.set(user));
|
||||||
|
}
|
||||||
|
|
||||||
|
function onWants(wants) {
|
||||||
|
dispatch(wantsState.actions.set(wants));
|
||||||
|
}
|
||||||
|
|
||||||
|
const offersNotification = throttle((offers) => {
|
||||||
|
if (!offers.length) return false;
|
||||||
|
toast.info('Offers Updated', 'New offers available.')
|
||||||
|
}, 60 * 1000);
|
||||||
|
|
||||||
|
function onOffers(offers) {
|
||||||
|
offersNotification(offers);
|
||||||
|
dispatch(offersState.actions.set(offers));
|
||||||
|
}
|
||||||
|
|
||||||
|
function onAcpOffers(offers) {
|
||||||
|
dispatch(acpOffersState.actions.set(offers));
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------
|
// -------------
|
||||||
@@ -46,7 +97,11 @@ export default function createSocket(store) {
|
|||||||
// when the server sends a reply it will have one of these message types
|
// when the server sends a reply it will have one of these message types
|
||||||
// this object wraps the reply types to a function
|
// this object wraps the reply types to a function
|
||||||
const handlers = {
|
const handlers = {
|
||||||
SourcesAvailable: onSourcesAvailable,
|
Wants: onWants,
|
||||||
|
Offers: onOffers,
|
||||||
|
User: onUser,
|
||||||
|
|
||||||
|
AcpOffers: onAcpOffers,
|
||||||
|
|
||||||
Pong: onPong,
|
Pong: onPong,
|
||||||
Error: errHandler,
|
Error: errHandler,
|
||||||
@@ -71,6 +126,8 @@ export default function createSocket(store) {
|
|||||||
|
|
||||||
// Connection opened
|
// Connection opened
|
||||||
function onOpen() {
|
function onOpen() {
|
||||||
|
console.log('websocket connected', SOCKET_URL)
|
||||||
|
toast.info('connected', 'connected to server');
|
||||||
sendPing();
|
sendPing();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,6 +165,15 @@ export default function createSocket(store) {
|
|||||||
return {
|
return {
|
||||||
connect,
|
connect,
|
||||||
|
|
||||||
sendSourcesAvailable,
|
sendOfferStatusSet,
|
||||||
|
sendWantRm,
|
||||||
|
sendWantAdd,
|
||||||
|
|
||||||
|
sendOffers,
|
||||||
|
sendWants,
|
||||||
|
|
||||||
|
sendAcpOffers,
|
||||||
|
sendAcpOfferStatusSet,
|
||||||
|
sendAcpListingsSync,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
version: "3.3"
|
||||||
|
|
||||||
|
services:
|
||||||
|
client:
|
||||||
|
image: registry.ntwl.xyz/crates-client
|
||||||
|
build:
|
||||||
|
context: ./client
|
||||||
|
dockerfile: ./client.Dockerfile
|
||||||
|
|
||||||
|
api:
|
||||||
|
image: registry.ntwl.xyz/crates-api
|
||||||
|
build:
|
||||||
|
context: ./api
|
||||||
|
dockerfile: ./api.Dockerfile
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
version: "3.3"
|
|
||||||
|
|
||||||
services:
|
|
||||||
pg:
|
|
||||||
image: postgres:latest
|
|
||||||
environment:
|
|
||||||
POSTGRES_USER: 'mcrates'
|
|
||||||
POSTGRES_DB: 'mcrates'
|
|
||||||
POSTGRES_PASSWORD: 'digdeep'
|
|
||||||
|
|
||||||
# allow prisma client to push changes from outside docker etc
|
|
||||||
# ufw blocks this from being publicly accessible
|
|
||||||
ports:
|
|
||||||
- '45432:5432'
|
|
||||||
volumes:
|
|
||||||
- "./db:/var/lib/postgresql/data"
|
|
||||||
|
|
||||||
api:
|
|
||||||
image: node:16
|
|
||||||
working_dir: /opt/mcrates/api
|
|
||||||
entrypoint: npm start
|
|
||||||
environment:
|
|
||||||
DATABASE_URL: 'postgresql://mcrates:digdeep@pg:5432/mcrates?schema=public'
|
|
||||||
volumes:
|
|
||||||
- "./api:/opt/mcrates/api"
|
|
||||||
|
|
||||||
client:
|
|
||||||
image: node:16
|
|
||||||
working_dir: /opt/mcrates/client
|
|
||||||
entrypoint: npm start
|
|
||||||
volumes:
|
|
||||||
- "./client:/opt/mcrates/client"
|
|
||||||
|
|
||||||
nginx:
|
|
||||||
image: nginx:latest
|
|
||||||
volumes:
|
|
||||||
- './nginx/matescrates.au.nginx.conf:/etc/nginx/conf.d/default.conf'
|
|
||||||
- '/etc/letsencrypt:/etc/letsencrypt'
|
|
||||||
ports:
|
|
||||||
- '80:80'
|
|
||||||
- '443:443'
|
|
||||||
@@ -4,18 +4,18 @@ services:
|
|||||||
pg:
|
pg:
|
||||||
image: postgres:latest
|
image: postgres:latest
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_USER: 'mcrates'
|
POSTGRES_USER: 'crates'
|
||||||
POSTGRES_DB: 'test'
|
POSTGRES_DB: 'test'
|
||||||
POSTGRES_PASSWORD: 'digdeep'
|
POSTGRES_PASSWORD: 'digdeep'
|
||||||
|
|
||||||
api:
|
api:
|
||||||
image: node:16
|
image: node:16
|
||||||
working_dir: /opt/mcrates/api
|
working_dir: /opt/crates/api
|
||||||
entrypoint: npm run test:watch
|
entrypoint: npm run test:watch
|
||||||
depends_on:
|
depends_on:
|
||||||
- pg
|
- pg
|
||||||
environment:
|
environment:
|
||||||
DATABASE_URL: "postgresql://mcrates:digdeep@pg:5432/test?schema=public"
|
DATABASE_URL: "postgresql://crates:digdeep@pg:5432/test?schema=public"
|
||||||
volumes:
|
volumes:
|
||||||
- "./api:/opt/mcrates/api"
|
- "./api:/opt/crates/api"
|
||||||
|
|
||||||
+33
-13
@@ -4,34 +4,54 @@ services:
|
|||||||
pg:
|
pg:
|
||||||
image: postgres:latest
|
image: postgres:latest
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_USER: 'mcrates'
|
POSTGRES_USER: '${POSTGRES_USER}'
|
||||||
POSTGRES_DB: 'mcrates'
|
POSTGRES_DB: '${POSTGRES_DB}'
|
||||||
POSTGRES_PASSWORD: 'digdeep'
|
POSTGRES_PASSWORD: '${POSTGRES_PASSWORD}'
|
||||||
|
volumes:
|
||||||
|
# so ez
|
||||||
|
# insane
|
||||||
|
- "./pg:/docker-entrypoint-initdb.d"
|
||||||
ports:
|
ports:
|
||||||
- '45432:5432'
|
- '45432:5432'
|
||||||
volumes:
|
|
||||||
- "./db:/var/lib/postgresql/data"
|
|
||||||
|
|
||||||
api:
|
api:
|
||||||
image: node:16
|
image: crates/api
|
||||||
working_dir: /opt/mcrates/api
|
build:
|
||||||
|
context: ./api
|
||||||
|
dockerfile: ./api.Dockerfile
|
||||||
|
working_dir: /opt/crates/api
|
||||||
depends_on:
|
depends_on:
|
||||||
- 'pg'
|
- 'pg'
|
||||||
entrypoint: npm start
|
command: npm start
|
||||||
environment:
|
environment:
|
||||||
DATABASE_URL: 'postgresql://mcrates:digdeep@pg:5432/mcrates?schema=public'
|
DATABASE_URL: 'postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:5432/${POSTGRES_DB}?schema=public'
|
||||||
|
DISCORD_TOKEN: '${DISCORD_TOKEN}'
|
||||||
|
DISCOGS_TOKEN: '${DISCOGS_TOKEN}'
|
||||||
|
CHOKIDAR_USEPOLLING: 1
|
||||||
volumes:
|
volumes:
|
||||||
- "./api:/opt/mcrates/api"
|
- "./api:/opt/crates/api"
|
||||||
|
- "/opt/crates/api/node_modules"
|
||||||
|
|
||||||
|
migrate:
|
||||||
|
restart: on-failure:3
|
||||||
|
extends: api
|
||||||
|
command: npm run migrate status
|
||||||
|
|
||||||
client:
|
client:
|
||||||
image: node:16
|
image: node:16
|
||||||
working_dir: /opt/mcrates/client
|
working_dir: /opt/crates/client
|
||||||
|
build:
|
||||||
|
context: ./client
|
||||||
|
dockerfile: ./client.dev.Dockerfile
|
||||||
entrypoint: npm start
|
entrypoint: npm start
|
||||||
ports:
|
ports:
|
||||||
- '8080:8080' # fwd the hmr websocket
|
- '8080:8080' # fwd the hmr websocket
|
||||||
|
environment:
|
||||||
|
CHOKIDAR_USEPOLLING: 1
|
||||||
volumes:
|
volumes:
|
||||||
- "./client:/opt/mcrates/client"
|
- "./client:/opt/crates/client"
|
||||||
- "./api:/opt/mcrates/api"
|
- "./api:/opt/crates/api"
|
||||||
|
- "/opt/crates/client/node_modules"
|
||||||
|
|
||||||
nginx:
|
nginx:
|
||||||
image: nginx:latest
|
image: nginx:latest
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
upstream mcrates_api {
|
upstream crates_api {
|
||||||
server api:41337;
|
server api:41337;
|
||||||
}
|
}
|
||||||
|
|
||||||
upstream mcrates_client {
|
upstream crates_client {
|
||||||
server client:8080;
|
server client:8080;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -13,12 +13,12 @@ map $http_upgrade $connection_upgrade {
|
|||||||
|
|
||||||
server {
|
server {
|
||||||
location / {
|
location / {
|
||||||
proxy_pass http://mcrates_client;
|
proxy_pass http://crates_client;
|
||||||
proxy_read_timeout 600s;
|
proxy_read_timeout 600s;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /api/ws {
|
location /api/ws {
|
||||||
proxy_pass http://mcrates_api;
|
proxy_pass http://crates_api;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
proxy_set_header Connection $connection_upgrade;
|
proxy_set_header Connection $connection_upgrade;
|
||||||
@@ -28,7 +28,7 @@ server {
|
|||||||
|
|
||||||
location /api/ {
|
location /api/ {
|
||||||
# trailing slash means api will receive url WITHOUT /api
|
# trailing slash means api will receive url WITHOUT /api
|
||||||
proxy_pass http://mcrates_api/;
|
proxy_pass http://crates_api/;
|
||||||
proxy_read_timeout 600s;
|
proxy_read_timeout 600s;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,20 +2,11 @@ upstream mcrates_api {
|
|||||||
server api:41337;
|
server api:41337;
|
||||||
}
|
}
|
||||||
|
|
||||||
upstream mcrates_client {
|
|
||||||
server client:8080;
|
|
||||||
}
|
|
||||||
|
|
||||||
# upstream mcrates_ws {
|
|
||||||
# server 127.0.0.1:40055;
|
|
||||||
# }
|
|
||||||
|
|
||||||
map $http_upgrade $connection_upgrade {
|
map $http_upgrade $connection_upgrade {
|
||||||
default upgrade;
|
default upgrade;
|
||||||
'' close;
|
'' close;
|
||||||
}
|
}
|
||||||
|
|
||||||
# DEV
|
|
||||||
server {
|
server {
|
||||||
server_name matescrates.au;
|
server_name matescrates.au;
|
||||||
|
|
||||||
@@ -26,17 +17,19 @@ server {
|
|||||||
gzip_types text/plain text/css text/xml application/json application/javascript application/xml+rss application/atom+xml image/svg+xml;
|
gzip_types text/plain text/css text/xml application/json application/javascript application/xml+rss application/atom+xml image/svg+xml;
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
proxy_pass http://mcrates_client;
|
root /srv/matescrates;
|
||||||
proxy_read_timeout 600s;
|
index index.html;
|
||||||
|
try_files $uri $uri/ index.html;
|
||||||
}
|
}
|
||||||
|
|
||||||
# location /api/ws {
|
location /api/ws {
|
||||||
# proxy_pass http://mcrates_ws;
|
proxy_pass http://mcrates_api;
|
||||||
# proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
# proxy_set_header Upgrade $http_upgrade;
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
# proxy_set_header Connection $connection_upgrade;
|
proxy_set_header Connection $connection_upgrade;
|
||||||
# proxy_read_timeout 600s;
|
proxy_set_header Host $host;
|
||||||
# }
|
proxy_read_timeout 600s;
|
||||||
|
}
|
||||||
|
|
||||||
location /api/ {
|
location /api/ {
|
||||||
# trailing slash means api will receive url WITHOUT /api
|
# trailing slash means api will receive url WITHOUT /api
|
||||||
@@ -44,6 +37,8 @@ server {
|
|||||||
proxy_read_timeout 600s;
|
proxy_read_timeout 600s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
error_page 404 /;
|
||||||
|
|
||||||
listen [::]:443 ssl ipv6only=on; # managed by Certbot
|
listen [::]:443 ssl ipv6only=on; # managed by Certbot
|
||||||
listen 443 ssl; # managed by Certbot
|
listen 443 ssl; # managed by Certbot
|
||||||
ssl_certificate /etc/letsencrypt/live/matescrates.au/fullchain.pem; # managed by Certbot
|
ssl_certificate /etc/letsencrypt/live/matescrates.au/fullchain.pem; # managed by Certbot
|
||||||
|
|||||||
Reference in new issue
Block a user