11 Commits
Author SHA1 Message Date
ntr 5b0c7c05ca navbar and notifications 2023-01-17 23:44:16 +11:00
ntr a97560c1c7 fix search and list to work with overflow scrolling 2023-01-15 12:24:15 +11:00
ntr 00cb224fb3 font change 2023-01-15 12:07:47 +11:00
ntr 6c3cac7381 navbar wip 2023-01-11 23:22:15 +11:00
ntr b91f877045 notify 2022-12-27 15:37:46 +11:00
ntr b86a4216cf comments 2022-12-27 13:07:07 +11:00
ntr 57fefdc9e2 more notify 2022-12-27 13:01:42 +11:00
ntr 358958c127 notify wip 2022-12-27 01:05:56 +11:00
ntr c205ae81e2 format notes 2022-12-26 19:34:04 +11:00
ntr 0cec6414ae open offers fix 2022-12-26 19:02:26 +11:00
ntr 43173a833d ask for updates on route changes 2022-12-21 23:43:49 +11:00
29 changed files with 1367 additions and 573 deletions

No files matched your search

+6 -9
View File
@@ -1,7 +1,11 @@
# TODO
contact options on landing page
push offers
move want add to rpc
add last synced time to want
go over and reorganise event logic
contact options on landing page
skip unofficial release
@@ -9,10 +13,3 @@ status stepper
queue up wantlist adds to gracefully handle rate limiting
push offers
listen / notify
on offers
notify admins
+2 -2
View File
@@ -26,10 +26,10 @@ async function start() {
discogs,
api,
events,
notify,
} = createServices(log);
// events.emit(Events.Sync);
// events.emit(Events.ListingsUpdated);
await notify.listen();
api.listen();
}
+336 -4
View File
@@ -23,7 +23,8 @@
"koa-websocket": "^7.0.0",
"koa-ws": "^1.0.1",
"lodash": "^4.17.21",
"passport-local": "^1.0.0"
"passport-local": "^1.0.0",
"pg-listen": "^1.7.0"
},
"devDependencies": {
"@types/bcrypt": "^5.0.0",
@@ -697,6 +698,15 @@
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"dev": true
},
"node_modules/buffer-writer": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz",
"integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==",
"peer": true,
"engines": {
"node": ">=4"
}
},
"node_modules/bufferutil": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-1.1.0.tgz",
@@ -2892,6 +2902,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/packet-reader": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz",
"integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==",
"peer": true
},
"node_modules/parse5": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.0.0.tgz",
@@ -2991,6 +3007,129 @@
"resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz",
"integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg=="
},
"node_modules/pg": {
"version": "8.8.0",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.8.0.tgz",
"integrity": "sha512-UXYN0ziKj+AeNNP7VDMwrehpACThH7LUl/p8TDFpEUuSejCUIwGSfxpHsPvtM6/WXFy6SU4E5RG4IJV/TZAGjw==",
"peer": true,
"dependencies": {
"buffer-writer": "2.0.0",
"packet-reader": "1.0.0",
"pg-connection-string": "^2.5.0",
"pg-pool": "^3.5.2",
"pg-protocol": "^1.5.0",
"pg-types": "^2.1.0",
"pgpass": "1.x"
},
"engines": {
"node": ">= 8.0.0"
},
"peerDependencies": {
"pg-native": ">=3.0.1"
},
"peerDependenciesMeta": {
"pg-native": {
"optional": true
}
}
},
"node_modules/pg-connection-string": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.5.0.tgz",
"integrity": "sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ==",
"peer": true
},
"node_modules/pg-format": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/pg-format/-/pg-format-1.0.4.tgz",
"integrity": "sha512-YyKEF78pEA6wwTAqOUaHIN/rWpfzzIuMh9KdAhc3rSLQ/7zkRFcCgYBAEGatDstLyZw4g0s9SNICmaTGnBVeyw==",
"engines": {
"node": ">=4.0"
}
},
"node_modules/pg-int8": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
"peer": true,
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/pg-listen": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/pg-listen/-/pg-listen-1.7.0.tgz",
"integrity": "sha512-MKDwKLm4ryhy7iq1yw1K1MvUzBdTkaT16HZToddX9QaT8XSdt3Kins5mYH6DLECGFzFWG09VdXvWOIYogjXrsg==",
"dependencies": {
"debug": "^4.1.1",
"pg-format": "^1.0.4",
"typed-emitter": "^0.1.0"
},
"peerDependencies": {
"pg": "7.x || 8.x"
}
},
"node_modules/pg-listen/node_modules/debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"dependencies": {
"ms": "2.1.2"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/pg-listen/node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/pg-pool": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.5.2.tgz",
"integrity": "sha512-His3Fh17Z4eg7oANLob6ZvH8xIVen3phEZh2QuyrIl4dQSDVEabNducv6ysROKpDNPSD+12tONZVWfSgMvDD9w==",
"peer": true,
"peerDependencies": {
"pg": ">=8.0"
}
},
"node_modules/pg-protocol": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.5.0.tgz",
"integrity": "sha512-muRttij7H8TqRNu/DxrAJQITO4Ac7RmX3Klyr/9mJEOBeIpgnF8f9jAfRz5d3XwQZl5qBjF9gLsUtMPJE0vezQ==",
"peer": true
},
"node_modules/pg-types": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
"integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
"peer": true,
"dependencies": {
"pg-int8": "1.0.1",
"postgres-array": "~2.0.0",
"postgres-bytea": "~1.0.0",
"postgres-date": "~1.0.4",
"postgres-interval": "^1.1.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/pgpass": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
"integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
"peer": true,
"dependencies": {
"split2": "^4.1.0"
}
},
"node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
@@ -3003,6 +3142,45 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/postgres-array": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
"peer": true,
"engines": {
"node": ">=4"
}
},
"node_modules/postgres-bytea": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz",
"integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/postgres-date": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
"integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/postgres-interval": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
"integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
"peer": true,
"dependencies": {
"xtend": "^4.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/prisma": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/prisma/-/prisma-4.5.0.tgz",
@@ -3234,6 +3412,15 @@
"source-map": "^0.6.0"
}
},
"node_modules/split2": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/split2/-/split2-4.1.0.tgz",
"integrity": "sha512-VBiJxFkxiXRlUIeyMQi8s4hgvKCSjtknJv/LVYbrgALPwf5zSKmEwV9Lst25AkvMDnvxODugjdl6KZgwKM1WYQ==",
"peer": true,
"engines": {
"node": ">= 10.x"
}
},
"node_modules/statuses": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
@@ -3483,6 +3670,11 @@
"node": ">= 0.6"
}
},
"node_modules/typed-emitter": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/typed-emitter/-/typed-emitter-0.1.0.tgz",
"integrity": "sha512-Tfay0l6gJMP5rkil8CzGbLthukn+9BN/VXWcABVFPjOoelJ+koW8BuPZYk+h/L+lEeIp1fSzVRiWRPIjKVjPdg=="
},
"node_modules/typescript": {
"version": "4.7.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz",
@@ -3636,7 +3828,6 @@
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
"dev": true,
"engines": {
"node": ">=0.4"
}
@@ -4276,6 +4467,12 @@
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"dev": true
},
"buffer-writer": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz",
"integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==",
"peer": true
},
"bufferutil": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-1.1.0.tgz",
@@ -5891,6 +6088,12 @@
"p-limit": "^3.0.2"
}
},
"packet-reader": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz",
"integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==",
"peer": true
},
"parse5": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.0.0.tgz",
@@ -5962,12 +6165,131 @@
"resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz",
"integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg=="
},
"pg": {
"version": "8.8.0",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.8.0.tgz",
"integrity": "sha512-UXYN0ziKj+AeNNP7VDMwrehpACThH7LUl/p8TDFpEUuSejCUIwGSfxpHsPvtM6/WXFy6SU4E5RG4IJV/TZAGjw==",
"peer": true,
"requires": {
"buffer-writer": "2.0.0",
"packet-reader": "1.0.0",
"pg-connection-string": "^2.5.0",
"pg-pool": "^3.5.2",
"pg-protocol": "^1.5.0",
"pg-types": "^2.1.0",
"pgpass": "1.x"
}
},
"pg-connection-string": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.5.0.tgz",
"integrity": "sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ==",
"peer": true
},
"pg-format": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/pg-format/-/pg-format-1.0.4.tgz",
"integrity": "sha512-YyKEF78pEA6wwTAqOUaHIN/rWpfzzIuMh9KdAhc3rSLQ/7zkRFcCgYBAEGatDstLyZw4g0s9SNICmaTGnBVeyw=="
},
"pg-int8": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
"peer": true
},
"pg-listen": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/pg-listen/-/pg-listen-1.7.0.tgz",
"integrity": "sha512-MKDwKLm4ryhy7iq1yw1K1MvUzBdTkaT16HZToddX9QaT8XSdt3Kins5mYH6DLECGFzFWG09VdXvWOIYogjXrsg==",
"requires": {
"debug": "^4.1.1",
"pg-format": "^1.0.4",
"typed-emitter": "^0.1.0"
},
"dependencies": {
"debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"requires": {
"ms": "2.1.2"
}
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
}
}
},
"pg-pool": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.5.2.tgz",
"integrity": "sha512-His3Fh17Z4eg7oANLob6ZvH8xIVen3phEZh2QuyrIl4dQSDVEabNducv6ysROKpDNPSD+12tONZVWfSgMvDD9w==",
"peer": true,
"requires": {}
},
"pg-protocol": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.5.0.tgz",
"integrity": "sha512-muRttij7H8TqRNu/DxrAJQITO4Ac7RmX3Klyr/9mJEOBeIpgnF8f9jAfRz5d3XwQZl5qBjF9gLsUtMPJE0vezQ==",
"peer": true
},
"pg-types": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
"integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
"peer": true,
"requires": {
"pg-int8": "1.0.1",
"postgres-array": "~2.0.0",
"postgres-bytea": "~1.0.0",
"postgres-date": "~1.0.4",
"postgres-interval": "^1.1.0"
}
},
"pgpass": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
"integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
"peer": true,
"requires": {
"split2": "^4.1.0"
}
},
"picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true
},
"postgres-array": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
"peer": true
},
"postgres-bytea": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz",
"integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==",
"peer": true
},
"postgres-date": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
"integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
"peer": true
},
"postgres-interval": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
"integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
"peer": true,
"requires": {
"xtend": "^4.0.0"
}
},
"prisma": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/prisma/-/prisma-4.5.0.tgz",
@@ -6138,6 +6460,12 @@
"source-map": "^0.6.0"
}
},
"split2": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/split2/-/split2-4.1.0.tgz",
"integrity": "sha512-VBiJxFkxiXRlUIeyMQi8s4hgvKCSjtknJv/LVYbrgALPwf5zSKmEwV9Lst25AkvMDnvxODugjdl6KZgwKM1WYQ==",
"peer": true
},
"statuses": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
@@ -6304,6 +6632,11 @@
"mime-types": "~2.1.24"
}
},
"typed-emitter": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/typed-emitter/-/typed-emitter-0.1.0.tgz",
"integrity": "sha512-Tfay0l6gJMP5rkil8CzGbLthukn+9BN/VXWcABVFPjOoelJ+koW8BuPZYk+h/L+lEeIp1fSzVRiWRPIjKVjPdg=="
},
"typescript": {
"version": "4.7.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz",
@@ -6427,8 +6760,7 @@
"xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
"dev": true
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="
},
"y18n": {
"version": "5.0.8",
+2 -1
View File
@@ -41,7 +41,8 @@
"koa-websocket": "^7.0.0",
"koa-ws": "^1.0.1",
"lodash": "^4.17.21",
"passport-local": "^1.0.0"
"passport-local": "^1.0.0",
"pg-listen": "^1.7.0"
},
"overrides": {
"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"
+5
View File
@@ -11,6 +11,11 @@ datasource db {
}
model Listing {
// example listingId 2148851036
// works fine as a JS number
// but JS numbers are cast to int4 for pg by prisma
// becomes -2146116260
id BigInt @id
seller BigInt
data Json
+2 -11
View File
@@ -63,16 +63,6 @@ export default function createApi(log: Log, pg: Pg, discogs: Discogs, offers: Of
ctx.status = 200;
});
router.post('/want', async function wantPost(ctx, next) {
ctx.assert(ctx.isAuthenticated(), 401, 'log in required');
const { releaseId } = ctx.request.body;
const user = await pg.userFindId(ctx.state.user.id);
const release = await discogs.release(releaseId);
const want = await pg.wantCreate(release, user);
events.emit(Events.WantsAdded, [want]);
ctx.response.body = [want];
});
router.post('/want/discogs', async function wantListPost(ctx, next) {
ctx.assert(ctx.isAuthenticated(), 401, 'log in required');
const { username } = ctx.request.body;
@@ -109,7 +99,6 @@ export default function createApi(log: Log, pg: Pg, discogs: Discogs, offers: Of
const ws = ctx.websocket;
const rpc = createRpc(log, pg, ws, discogs, offers, events, ctx.state.user as User | null);
if (ctx.state.user) events.emit(Events.Connection, { ws, id: ctx.state.user.id });
ws.on('message', async function incoming(message) {
try {
const parsed = JSON.parse(message.toString());
@@ -127,7 +116,9 @@ export default function createApi(log: Log, pg: Pg, discogs: Discogs, offers: Of
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);
}
+1 -1
View File
@@ -48,7 +48,7 @@ export default function createDiscogs(log: Log, pg: Pg) {
// @ts-ignore
const additions = results.filter(r => r.status === 'fulfilled').map(r => r.value);
log.info('want list additions', additions);
log.info('want list additions', { additions: additions.length });
return additions;
}
+29 -6
View File
@@ -2,22 +2,26 @@ import { EventEmitter } from 'node:events';
import { setTimeout } from 'timers/promises';
import type Log from 'bunyan';
import type { Want } from '@prisma/client';
import type { Want, Offer } from '@prisma/client';
import { Pg } from './pg';
import { Rpc } from './rpc';
import { wantDetails } from './utils';
import routerAuth from './routers/router.auth';
export enum Events {
WantsAdded = 'WantsAdded',
Sync = 'Sync',
ListingsUpdated = 'ListingsUpdated',
Connection = 'Connection',
ListingsUpdated = 'ListingsUpdated',
OffersUpdated = 'OffersUpdated',
WantsUpdated = 'WantsUpdated',
}
export default function createEvents(log: Log, pg: Pg, discogs, offers) {
const events = new EventEmitter({ captureRejections: true });
const connections = new Map();
const connections = new Map<UUID, Rpc>();
async function populateListings(want: Want) {
try {
@@ -40,7 +44,7 @@ export default function createEvents(log: Log, pg: Pg, discogs, offers) {
async function updateWantListings(wants: Want[]) {
for (const want of wants) {
const listings = await populateListings(want);
log.info({ listings: listings.length, ...wantDetails(want) }, 'listings added');
log.info({ listings: listings.length, ...wantDetails(want) }, 'listings found');
events.emit(Events.ListingsUpdated);
}
@@ -64,11 +68,30 @@ export default function createEvents(log: Log, pg: Pg, discogs, offers) {
events.on(Events.ListingsUpdated, async function listingsUpdated() {
const updatedOffers = await offers.generate()
log.debug({ offers: updatedOffers.length }, 'offers up to date');
for (const [id, rpc] of connections) {
rpc.pushUserOffers();
}
});
events.on(Events.Connection, async function onConnection({ ws, id }) {
connections.set(id, ws);
events.on(Events.Connection, async function onConnection({ rpc, id }) {
log.debug('new rpc connection', { id, rpc });
connections.set(id, rpc);
})
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;
}
+60
View File
@@ -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 };
}
+17 -11
View File
@@ -24,6 +24,7 @@ export default function createPg(log: Log, prisma: PrismaClient) {
return prisma.want.findMany({
where: { userId: user.id },
include: { _count: { select: { offers: true }} },
orderBy: { offers: { _count: 'desc' } }
});
}
@@ -97,6 +98,12 @@ export default function createPg(log: Log, prisma: PrismaClient) {
});
}
async function wantFindId(id: UUID) {
return prisma.want.findFirstOrThrow({
where: { id },
});
}
async function wantList(user: User) {
return prisma.want.findMany({
where: { userId: user.id },
@@ -131,8 +138,8 @@ export default function createPg(log: Log, prisma: PrismaClient) {
const protectedOfferStatuses = [Status.Confirmed, Status.Complete];
// can't delete a listing with an open offer
// so delete those offers first
// delete all the offers where we don't want to keep them
// for historical reasons
const { count: offersRemoved } = await prisma.offer.deleteMany({
where: {
wantId: want.id,
@@ -145,7 +152,7 @@ export default function createPg(log: Log, prisma: PrismaClient) {
}
});
if (offersRemoved > 0) log.info({ offersRemoved }, 'open offers removed');
if (offersRemoved > 0) log.info({ offersRemoved }, 'pending offers removed');
// now we have to filter listingIds with confirmed offers
// and get their listingIds to filter out
@@ -154,23 +161,22 @@ export default function createPg(log: Log, prisma: PrismaClient) {
wantId: want.id,
status: {
in: protectedOfferStatuses,
}
},
},
select: {
listingId: true,
}
},
})).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
// to a historical offer
const { count } = await prisma.listing.deleteMany({
where: {
wantId: want.id,
id: {
notIn: currentListingIds,
}
}
id: { in: removalIds },
},
});
log.info({ count }, 'second stage open offers removed');
+102 -77
View File
@@ -12,6 +12,81 @@ 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 = {
id: UUID,
status: Status,
};
type WantRmParams = {
id: UUID,
};
type WantAddParams = {
releaseId: number,
};
function checkMsgType(msgType: RpcMessageType): msgType is RpcMessageType {
if (RpcMessageType[msgType]) {
return true;
@@ -54,6 +129,18 @@ export default function createRpc(log: Log, pg: Pg, ws, discogs: Discogs, offers
return [RpcMessageType.Wants, wants];
}
async function wantAdd(params: WantAddParams): Promise<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');
@@ -101,6 +188,8 @@ export default function createRpc(log: Log, pg: Pg, ws, discogs: Discogs, offers
case RpcMessageType.WantRm:
return wantRm(params);
case RpcMessageType.WantAdd:
return wantAdd(params);
case RpcMessageType.AcpOfferStatusSet:
return acpOfferStatusSet(params);
@@ -134,13 +223,8 @@ export default function createRpc(log: Log, pg: Pg, ws, discogs: Discogs, offers
const userMsg: UserMessage = [RpcMessageType.User, user];
send(userMsg);
const userOffers = await offers.userOffers(user);
const offersMsg: OffersMessage = [RpcMessageType.Offers, userOffers];
send(offersMsg);
const wants = await pg.userWants(user);
const wantsMsg: WantsMessage = [RpcMessageType.Wants, wants];
send(wantsMsg);
await pushUserOffers();
await pushUserWants();
if (isAdmin(user)) return adminConnection();
}
@@ -153,79 +237,20 @@ export default function createRpc(log: Log, pg: Pg, ws, discogs: Discogs, offers
))
}
async function pushUserOffers() {
send(await userOffers());
}
async function pushUserWants() {
send(await userWants());
}
return {
receive,
send,
onConnection,
pushUserOffers,
pushUserWants,
}
}
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',
}
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 = {
id: UUID,
status: Status,
};
type WantRmParams = {
id: UUID,
};
+4 -1
View File
@@ -6,19 +6,22 @@ import createLog from './log';
import createApi from './api';
import createEvents from './events';
import createPg from './pg';
import createNotify from './notify';
export default function creates(log) {
export default function createServices(log) {
const prisma = new PrismaClient();
const pg = createPg(log, prisma);
const discogs = createDiscogs(log, pg);
const offers = createOffers(log, pg);
const events = createEvents(log, pg, discogs, offers);
const notify = createNotify(log, pg, events);
const api = createApi(log, pg, discogs, offers, events);
return {
api,
discogs,
events,
notify,
pg,
prisma,
};
+8 -6
View File
@@ -9,7 +9,7 @@ import store, { wsState } from './store'
import Nav from './components/nav';
import Offers from './routes/offers';
import Main from './routes/main';
import Acp from './routes/acp';
import Login from './routes/login';
import Search from './routes/search';
@@ -18,7 +18,7 @@ import Info from './routes/info';
if (window.location.host === 'matescrates.au') LogRocket.init('yh0dy3/mcrates');
const Main = () => {
const Mcrates = () => {
const dispatch = useDispatch();
// i have no idea why this is needed,
@@ -33,13 +33,15 @@ const Main = () => {
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 (
<main id="app">
<div id="mcrates">
<Nav />
<Router onChange={routeChange}>
<Route path="/" component={Offers} />
<Route path="/" component={Main} />
<Route path="/login" component={Login} mode="login" />
<Route path="/register" component={Login} mode="register" />
<Route path="/search" component={Search} />
@@ -49,14 +51,14 @@ const Main = () => {
{/* ACP */}
<Route path="/acp" component={Acp} />
</Router>
</main>
</div>
);
};
const App = () => {
return (
<Provider store={store}>
<Main />
<Mcrates />
</Provider>
);
}
+11 -3
View File
@@ -49,8 +49,12 @@ function OfferPriceBtn(offer) {
? offer.status
: 'Accept';
const title = accepted
? 'Click to cancel'
: 'Click to accept this offer';
return (
<button disabled={confirmed} onClick={() => setStatus()} class={classes}>${offer.total} {offer.condition}</button>
<button disabled={confirmed} title={title} onClick={() => setStatus()} class={classes}>${offer.total} {offer.condition}</button>
)
}
@@ -58,12 +62,16 @@ function OfferPriceBtn(offer) {
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}`;
return (
<div class="offer" key={i}>
<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"><ExternalLink /></a></h3>
<div class='alt-text details'>
{group.details.year} - {group.details.formats.map(f => f.descriptions.join(' ')).join(' ')}
{group.details.year} - {detailText}
</div>
<div class='alt-text details'>Status: {group.status}</div>
</section>
+6 -5
View File
@@ -1,12 +1,10 @@
import { h } from 'preact';
import { useState, useCallback } from 'preact/hooks';
import { useState } from 'preact/hooks';
import { ExternalLink } from 'react-feather';
import { useSelector } from 'react-redux'
import { State } from './../store'
import http from './../http';
function Want(want) {
const r = want.data;
@@ -14,10 +12,13 @@ function Want(want) {
const [confirm, setConfirm] = useState(false);
function onClick() {
if (confirm) return ws.sendWantRm(want.id);
if (confirm) {
setConfirm(false);
return ws.sendWantRm(want.id);
}
setTimeout(() => setConfirm(false), 2500);
return setConfirm(true);
};
}
const text = confirm
? 'confirm'
-13
View File
@@ -33,15 +33,10 @@ api.interceptors.response.use(onSuccess, onError);
const actions = {
// TODO move to rpc
postWantSearch: async function postWantSearch(query) {
return api.post('/want/search', { query });
},
postWant: async function postWant(releaseId) {
return api.post('/want', { releaseId });
},
postDiscogs: async function postDiscogs(username) {
return api.post('/want/discogs', { username });
},
@@ -71,14 +66,6 @@ const actions = {
password,
});
},
postOffers: async function postOffers(listingIds, shipping, baseShipping) {
return api.post('/offers', {
listingIds,
shipping,
baseShipping,
});
},
};
export default actions;
-1
View File
@@ -10,7 +10,6 @@ import Want from './../components/want';
const List = () => {
const wants = useSelector((state: State) => state.wants);
const user = useSelector((state: State) => state.user);
if (!wants) {
return <div> loading... </div>;
+47
View File
@@ -0,0 +1,47 @@
import { h } from 'preact';
import { route } from 'preact-router';
import { useSelector } from 'react-redux'
import { State } from './../store';
import * as toast from './../toast';
import OfferGroup from './../components/offer.group';
const GroupNav = (want) => {
const href = want._count.offers ? `#${want.release}` : null;
return (
<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;
-27
View File
@@ -1,27 +0,0 @@
import { h } from 'preact';
import { route } from 'preact-router';
import { useSelector } from 'react-redux'
import { State } from './../store';
import * as toast from './../toast';
import OfferGroup from './../components/offer.group';
function Home() {
const offers = useSelector((state: State) => state.offers);
if (offers && offers.length === 0) {
toast.info('no offers', 'You have no offers yet, search for some more items.');
route('/search');
}
if (!offers) return <div>loading...</div>;
return (
<div class={'offers'}>
{offers.map(OfferGroup)}
</div>
);
}
export default Home;
+13 -14
View File
@@ -7,25 +7,22 @@ import { State } from './../store';
import http from '../http';
function Result(r, wants) {
const [added, setAdded] = useState(false)
function Result(r, wants, ws) {
async function onClick() {
const [error, want] = await http.postWant(r.id);
if (error) return false;
setAdded(true);
ws.sendWantAdd(r.id);
}
const wanted = wants.some(w => w.release === r.id);
// releaseIds are bigints on server
const wanted = wants.some(w => BigInt(w.release) === BigInt(r.id));
const classes = `${added || wanted ? 'added' : ''}`;
const btnText = added
const classes = `${wanted ? 'added' : ''}`;
const btnText = wanted
? <Check />
: '+';
return (
<article key={r.id}>
<button onClick={onClick} disabled={added} class={classes} >{btnText}</button>
<button onClick={onClick} disabled={wanted} class={classes} >{btnText}</button>
<div>
<h3>{r.title} <a href={`https://discogs.com${r.uri}`} target="_blank"><ExternalLink /></a></h3>
<h5 class='alt-text' >{r.year}</h5>
@@ -45,6 +42,7 @@ const User = () => {
const [infoText, setInfoText] = useState('');
const wants = useSelector((state: State) => state.wants);
const ws = useSelector((state: State) => state.ws);
async function fetchResults() {
const [error, results] = await http.postWantSearch(query);
@@ -68,7 +66,8 @@ const User = () => {
setInfoText(`Added ${wants.length} items to your list.`);
}
const searchRef = useRef<HTMLInputElement>()
const searchRef = useRef<HTMLInputElement>();
const resultsRef = useRef<HTMLDivElement>();
useEffect(() => {
searchRef.current.focus()
@@ -85,7 +84,7 @@ const User = () => {
}
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
@@ -111,9 +110,9 @@ const User = () => {
<hr />
<p>Data provided by Discogs.</p>
</div>
<div class='results'>
<div class='results' ref={resultsRef}>
{infoEl}
{results.map(r => Result(r, wants))}
{results.map(r => Result(r, wants, ws))}
{topEl}
</div>
</div>
+87
View File
@@ -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;
}
+11 -379
View File
@@ -23,32 +23,6 @@ img {
height: auto;
}
/* latin */
@font-face {
font-family: 'Raleway';
font-style: normal;
font-weight: 400;
src: url(https://fonts.gstatic.com/s/raleway/v28/1Ptxg8zYS_SKggPN4iEgvnHyvveLxVvaorCIPrE.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: 'Source Code Pro';
font-style: normal;
font-weight: 400;
src: url(https://fonts.gstatic.com/s/sourcecodepro/v21/HI_diYsKILxRpg3hIP6sJ7fM7PqPMcMnZFqUwX28DMyQtMlrTA.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: 'Noto Sans Mono';
font-style: normal;
font-weight: 400;
font-stretch: normal;
src: url(https://fonts.gstatic.com/s/notosansmono/v20/BngrUXNETWXI6LwhGYvaxZikqZqK6fBq6kPvUce2oAZcdthSBUsYck4-_FNJ093dVQ.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
// stuff from UX laws
html {
color:var(--base-font-color);
@@ -71,7 +45,7 @@ html {
--medium-font-size: 1.777em;
--large-font-size: 2.369em;
--xlarge-font-size: 3.157em;
--base-font-family: 'Raleway', Arial, Sans-Serif;
--base-font-family: 'Barlow', Arial, Sans-Serif;
--secondary-font-family: 'Noto Sans Mono', monospace;
--base-font-color: var(--color-off-white);
--secondary-font-color: var(--color-mid-grey);
@@ -131,6 +105,11 @@ button, input, optgroup, select, textarea {
line-height:var(--base-line-height);
}
button, [type=submit] {
padding: var(--spacing-half);
background: var(--background-1);
}
input:hover, textarea:hover {
color:var(--base-font-color);
border:var(--hover-border)
@@ -156,7 +135,7 @@ button, [type=button], [type=reset], [type=submit] {
font-family: var(--secondary-font-family);
font-size: var(--small-font-size);
font-weight: bold;
// font-weight: bold;
text-transform: uppercase;
&:hover {
@@ -194,180 +173,6 @@ p {
margin:0 0 var(--spacing)
}
// ok actual styles
body {
padding-top: calc(var(--spacing)/2);
}
main {
padding: 0 var(--spacing-double) 0 var(--spacing-double);
}
button, [type=submit] {
padding: var(--spacing-half);
background: var(--background-1);
}
.landing {
display: grid;
grid-template-columns: 1fr 2fr;
grid-gap: calc(var(--spacing-double) * 2);
grid-template-areas:
"login swirl"
"login words";
.login {
grid-area: login;
button {
width: 100%;
}
}
.swirl {
grid-area: swirl;
height: 15rem;
}
.words {
grid-area: words;
text-align: center;
}
}
.info {
display: grid;
grid-template-areas:
"words swirl"
"words contact";
grid-template-columns: 1fr 1fr;
grid-template-rows: min-content 1fr;
section {
margin-bottom: var(--spacing);
}
.contact {
grid-area: contact;
text-align: center;
}
.words {
grid-area: words;
}
.swirl {
grid-area: swirl;
height: 15rem;
}
}
.alt-text, .alt {
font-family: var(--secondary-font-family);
// font-size: var(--small-font-size);
text-transform: uppercase;
}
article {
background-color: var(--background-1);
button {
border-color: var(--background-2);
}
}
.search {
display: grid;
grid-template-columns: 1fr 2fr;
.results {
display: grid;
padding-left: var(--spacing);
grid-gap: var(--spacing);
article {
display: grid;
padding: var(--spacing);
grid-template-columns: 1fr 4fr min-content;
}
}
button {
margin-right: var(--spacing);
background: var(--background-2);
border-color: var(--background-1);
&.added {
background: var(--color-green);
cursor: auto;
}
}
}
.list {
display: grid;
grid-template-columns:
1fr 1fr 1fr;
grid-gap: var(--spacing);
article {
display: grid;
// grid-template-rows:
// 1fr min-content min-content;
grid-template-areas:
"title img"
"details details"
"remove remove";
button {
margin-top: var(--spacing-half);
grid-area: remove
}
.details {
grid-area: details;
}
padding: var(--spacing);
img {
justify-self: end;
}
}
}
form {
margin-bottom: var(--spacing);
label, input {
display: block;
}
input, button {
width: 100%;
}
input {
padding: var(--spacing-half);
margin-bottom: var(--spacing);
}
label {
font-family: var(--secondary-font-family);
font-size: var(--small-font-size);
text-transform: uppercase;
margin-bottom: var(--spacing-half);
}
}
a {
color: var(--secondary-font-color);
font-family: var(--secondary-font-family);
@@ -378,184 +183,11 @@ a {
}
}
nav {
a {
font-size: var(--small-font-size);
margin-right: var(--spacing);
}
}
header {
display: grid;
grid-template-columns: 1fr 1fr min-content;
padding-bottom: var(--spacing);
a {
white-space: nowrap;
}
}
.bundles {
flex-flow: row;
.bundle {
margin-bottom: var(--spacing-double);
section {
margin-bottom: var(--spacing);
}
.items {
display: grid;
grid-template-columns: repeat(5, 1fr);
div {
margin-right: var(--spacing);
margin-bottom: var(--spacing);
display: grid;
&.bold {
font-weight: bold;
background: var(--background-2);
}
}
}
}
}
.offers {
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: var(--spacing);
.offer {
display: grid;
grid-template-columns: 1fr min-content;
grid-template-rows: min-content min-content;
grid-template-areas:
"details img"
"lines lines";
padding: var(--spacing);
background: var(--background-1);
.details {
font-size: var(--small-font-size);
}
.status {
margin-top: var(--spacing-half);
}
section {
grid-area: details;
}
img {
grid-area: img;
justify-self: end;
margin-left: var(--spacing-half);
}
.lines {
grid-area: lines;
display: flex;
flex-wrap: wrap;
column-gap: var(--spacing-half);
button {
margin-top: var(--spacing-half);
border-color: var(--background-2);
&.accepted {
background: var(--color-accepted);
}
&.confirmed {
cursor: auto;
background: var(--color-confirmed);
}
&.neg {
margin-left: var(--spacing-half);
}
}
}
}
}
.number {
.alt-text, .alt {
font-family: var(--secondary-font-family);
// font-size: var(--small-font-size);
text-transform: uppercase;
}
.swirl {
background-image: url("./../assets/swirl.fix.svg");
background-size: contain;
background-repeat: no-repeat;
background-position: center;
margin-bottom: var(--spacing);
}
@media (max-width:900px) {
.landing {
grid-template-columns: 1fr;
grid-template-areas:
"swirl"
"login"
"words";
}
.list {
grid-template-columns: 1fr;
}
.search {
grid-template-columns: 1fr;
}
.offers {
grid-template-columns: 1fr;
}
.info {
grid-template-columns: 1fr;
// is necessary for some reason
grid-template-areas:
"swirl"
"contact"
"words";
}
main {
padding: 0 var(--spacing) 0 var(--spacing);
}
}
@media (max-width:1200px) {
img {
width: 5rem;
}
}
// ------------
// toasts
.iziToast {
border-radius: 0;
border-color: var(--base-border-color);
.iziToast-body {
font-family: var(--base-font-family);
font-size: var(--base-font-size);
}
.iziToast-title {
font-family: var(--secondary-font-family);
text-transform: uppercase;
}
}
@import 'fonts.less';
@import 'styles.less';
+410
View File
@@ -0,0 +1,410 @@
body {
padding-top: calc(var(--spacing)/2);
overflow: hidden;
}
#mcrates {
padding: 0 var(--spacing-double) var(--spacing) var(--spacing-double);
display: grid;
grid-template-areas:
"header header"
"main main"
"footer footer";
height: 100vh;
width: 100vw;
display: grid;
grid-template-columns: 1fr;
grid-template-rows: min-content 1fr min-content;
}
.landing {
display: grid;
grid-template-columns: 1fr 2fr;
grid-gap: calc(var(--spacing-double) * 2);
grid-template-areas:
"login swirl"
"login words";
.login {
grid-area: login;
button {
width: 100%;
}
}
.swirl {
grid-area: swirl;
height: 15rem;
}
.words {
grid-area: words;
text-align: center;
}
}
.info {
display: grid;
grid-template-areas:
"words swirl"
"words contact";
grid-template-columns: 1fr 1fr;
grid-template-rows: min-content 1fr;
section {
margin-bottom: var(--spacing);
}
.contact {
grid-area: contact;
text-align: center;
}
.words {
grid-area: words;
}
.swirl {
grid-area: swirl;
height: 15rem;
}
}
article {
background-color: var(--background-1);
button {
border-color: var(--background-2);
}
}
.search {
display: grid;
grid-template-columns: 1fr 2fr;
overflow: hidden;
.results {
display: grid;
padding-left: var(--spacing);
grid-gap: var(--spacing);
article {
display: grid;
padding: var(--spacing);
grid-template-columns: 1fr 4fr min-content;
}
overflow: scroll;
}
button {
margin-right: var(--spacing);
background: var(--background-2);
border-color: var(--background-1);
&.added {
background: var(--color-green);
cursor: auto;
}
}
}
.list {
display: grid;
grid-template-columns:
1fr 1fr 1fr;
grid-gap: var(--spacing);
overflow: scroll;
article {
display: grid;
grid-template-areas:
"title img"
"details details"
"remove remove";
button {
margin-top: var(--spacing-half);
grid-area: remove
}
.details {
grid-area: details;
}
padding: var(--spacing);
img {
justify-self: end;
}
}
}
form {
margin-bottom: var(--spacing);
label, input {
display: block;
}
input, button {
width: 100%;
}
input {
padding: var(--spacing-half);
margin-bottom: var(--spacing);
}
label {
font-family: var(--secondary-font-family);
font-size: var(--small-font-size);
text-transform: uppercase;
margin-bottom: var(--spacing-half);
}
}
header {
grid-area: header;
display: grid;
grid-template-columns: 1fr 1fr min-content;
border-bottom: var(--base-border);
padding-bottom: var(--spacing-half);
margin-bottom: var(--spacing-half);
a {
font-size: var(--small-font-size);
margin-right: var(--spacing);
}
a {
white-space: nowrap;
}
}
main {
grid-area: main;
display: grid;
grid-template-areas:
"nav content";
grid-template-columns:
1fr 1fr;
column-gap: var(--spacing);
overflow: hidden;
nav {
// duck scrollbars
padding-right: var(--spacing-half);
grid-area: nav;
div {
display: grid;
grid-template-columns:
1fr min-content min-content;
column-gap: var(--spacing-half);
a {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
overflow: scroll;
}
.offers {
grid-area: content;
overflow: scroll;
}
}
.bundles {
flex-flow: row;
.bundle {
margin-bottom: var(--spacing-double);
section {
margin-bottom: var(--spacing);
}
.items {
display: grid;
grid-template-columns: repeat(5, 1fr);
div {
margin-right: var(--spacing);
margin-bottom: var(--spacing);
display: grid;
&.bold {
font-weight: bold;
background: var(--background-2);
}
}
}
}
}
.offers {
display: grid;
// grid-template-columns: 1fr 1fr;
grid-gap: var(--spacing);
.offer {
display: grid;
grid-template-columns: 1fr min-content;
grid-template-rows: min-content min-content;
grid-template-areas:
"details img"
"lines lines";
padding: var(--spacing);
background: var(--background-1);
.details {
font-size: var(--small-font-size);
}
.status {
margin-top: var(--spacing-half);
}
section {
grid-area: details;
}
img {
grid-area: img;
justify-self: end;
margin-left: var(--spacing-half);
}
.lines {
grid-area: lines;
display: flex;
flex-wrap: wrap;
column-gap: var(--spacing-half);
button {
margin-top: var(--spacing-half);
border-color: var(--background-2);
&.accepted {
background: var(--color-accepted);
}
&.confirmed {
cursor: auto;
background: var(--color-confirmed);
}
&.neg {
margin-left: var(--spacing-half);
}
}
}
}
}
.number {
font-family: var(--secondary-font-family);
text-transform: uppercase;
}
.swirl {
background-image: url("./../assets/swirl.fix.svg");
background-size: contain;
background-repeat: no-repeat;
background-position: center;
margin-bottom: var(--spacing);
}
@media (max-width:900px) {
.landing {
grid-template-columns: 1fr;
grid-template-areas:
"swirl"
"login"
"words";
}
.list {
grid-template-columns: 1fr;
}
.search {
grid-template-columns: 1fr;
}
.offers {
grid-template-columns: 1fr;
}
.info {
grid-template-columns: 1fr;
// is necessary for some reason
grid-template-areas:
"swirl"
"contact"
"words";
}
#mcrates {
padding: 0 var(--spacing) 0 var(--spacing);
}
main {
grid-template-columns: 1fr;
grid-template-rows: 1fr 3fr;
grid-gap: var(--spacing-half);
grid-template-areas:
"nav"
"content";
}
}
@media (max-width:1200px) {
img {
width: 5rem;
}
}
// ------------
// toasts
.iziToast {
border-radius: 0;
border-color: var(--base-border-color);
.iziToast-body {
font-family: var(--base-font-family);
font-size: var(--base-font-size);
}
.iziToast-title {
font-family: var(--secondary-font-family);
text-transform: uppercase;
}
}
+1
View File
@@ -6,6 +6,7 @@
<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>
+1 -1
View File
@@ -3,7 +3,7 @@ import toast from 'izitoast';
export function info(title, message) {
toast.info({
title,
message,
message: message || '',
position: 'bottomCenter',
color: '#1f2128',
titleColor: '#F4F1D0',
+11 -1
View File
@@ -1,4 +1,7 @@
import { wantsState, wsState, userState, offersState, acpOffersState } from './store'
import debounce from 'lodash/debounce';
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`;
@@ -34,6 +37,10 @@ export default function createSocket(dispatch) {
send(['WantRm', { id }]);
}
function sendWantAdd(releaseId) {
send(['WantAdd', { releaseId }]);
}
function sendOffers() {
send(['Offers', {}]);
}
@@ -114,6 +121,7 @@ export default function createSocket(dispatch) {
// Connection opened
function onOpen() {
console.log('websocket connected', SOCKET_URL)
toast.info('connected', 'connected to server');
sendPing();
}
@@ -153,8 +161,10 @@ export default function createSocket(dispatch) {
sendOfferStatusSet,
sendWantRm,
sendWantAdd,
sendOffers,
sendWants,
sendAcpOffers,
sendAcpOfferStatusSet,