33 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
ntr 0e785e0c8c bigint 2022-12-21 23:32:57 +11:00
ntr 6b73831b51 expand offers 2022-12-21 22:02:24 +11:00
ntr f0623cb74b toasts 2022-12-02 11:09:16 +11:00
ntr 2f8c883d75 Merge branch 'master' of ssh://git.mnml.gg:40022/home/ntr/mcrates into sample-offers 2022-12-02 09:22:25 +11:00
ntr 7262d6b37b additional typing 2022-12-02 09:18:45 +11:00
ntr dfc783c936 acp listings sync 2022-11-28 10:46:19 +11:00
ntr 631942bb41 clean up wants flow and http usage 2022-11-28 10:31:37 +11:00
ntr e74ab36313 fix offers comparison 2022-11-28 10:10:37 +11:00
ntr 5115dc5f8e create account btn 2022-11-28 09:58:34 +11:00
ntr 9e854f5d81 properly reconnect on register 2022-11-28 09:52:45 +11:00
ntr 5a19bf62fd adminconnection 2022-11-27 15:08:59 +11:00
ntr e185d04039 fix login form 2022-11-27 14:48:15 +11:00
ntr f726c960cd style changes and offer loading routing 2022-11-26 13:11:29 +11:00
ntr 18d5f26326 mobile styles 2022-11-26 11:06:57 +11:00
ntr a0cd49b411 discogs and offers inferred types 2022-11-24 09:35:05 +11:00
ntr f546d76912 comments n stuff 2022-11-23 17:27:38 +11:00
ntr 28759f9e70 completely ditch this bundles intermediate data type 2022-11-10 19:46:53 +11:00
ntr 6d18a86697 search just uses query 2022-11-10 17:36:52 +11:00
ntr 5fab62345b logo 2022-10-31 23:27:03 +11:00
ntr 15a84d6eea offer groups with status 2022-10-29 12:42:01 +11:00
ntr b199ce94c1 group status 2022-10-29 11:57:20 +11:00
ntr f0e366a73d styles 2022-10-24 19:23:06 +11:00
41 changed files with 2585 additions and 693 deletions

No files matched your search

+9 -8
View File
@@ -1,14 +1,15 @@
# TODO
push offers
add last synced time to want
go over and reorganise event logic
contact options on landing page
skip unofficial release
status stepper
queue up wantlist adds to gracefully handle rate limiting
push offers
cover off admin routes and commands
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.StartupSync);
// events.emit(Events.ListingsUpdated);
await notify.listen();
api.listen();
}
+377 -4
View File
@@ -23,12 +23,14 @@
"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",
"@types/bluebird": "^3.5.36",
"@types/koa": "^2.13.5",
"@types/koa-websocket": "^5.0.7",
"@types/lodash": "^4.14.182",
"@types/mocha": "^9.1.1",
"axios": "^0.27.2",
@@ -325,6 +327,17 @@
"@types/koa": "*"
}
},
"node_modules/@types/koa-websocket": {
"version": "5.0.7",
"resolved": "https://registry.npmjs.org/@types/koa-websocket/-/koa-websocket-5.0.7.tgz",
"integrity": "sha512-zv5F0iaV9zk3iXLzvskUZKgX35RW7F7UNX3tK1FNQUn2nierQWlY2aM0dBTYs0kLVLRtBhsxcUdiKzDp9JPOAw==",
"dev": true,
"dependencies": {
"@types/koa": "*",
"@types/koa-compose": "*",
"@types/ws": "*"
}
},
"node_modules/@types/lodash": {
"version": "4.14.182",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.182.tgz",
@@ -382,6 +395,15 @@
"integrity": "sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==",
"dev": true
},
"node_modules/@types/ws": {
"version": "8.5.3",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz",
"integrity": "sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@ungap/promise-all-settled": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz",
@@ -676,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",
@@ -2871,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",
@@ -2970,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",
@@ -2982,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",
@@ -3213,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",
@@ -3462,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",
@@ -3615,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"
}
@@ -3961,6 +4173,17 @@
"@types/koa": "*"
}
},
"@types/koa-websocket": {
"version": "5.0.7",
"resolved": "https://registry.npmjs.org/@types/koa-websocket/-/koa-websocket-5.0.7.tgz",
"integrity": "sha512-zv5F0iaV9zk3iXLzvskUZKgX35RW7F7UNX3tK1FNQUn2nierQWlY2aM0dBTYs0kLVLRtBhsxcUdiKzDp9JPOAw==",
"dev": true,
"requires": {
"@types/koa": "*",
"@types/koa-compose": "*",
"@types/ws": "*"
}
},
"@types/lodash": {
"version": "4.14.182",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.182.tgz",
@@ -4018,6 +4241,15 @@
"integrity": "sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==",
"dev": true
},
"@types/ws": {
"version": "8.5.3",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz",
"integrity": "sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@ungap/promise-all-settled": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz",
@@ -4235,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",
@@ -5850,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",
@@ -5921,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",
@@ -6097,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",
@@ -6263,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",
@@ -6386,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",
+3 -1
View File
@@ -16,6 +16,7 @@
"@types/bcrypt": "^5.0.0",
"@types/bluebird": "^3.5.36",
"@types/koa": "^2.13.5",
"@types/koa-websocket": "^5.0.7",
"@types/lodash": "^4.14.182",
"@types/mocha": "^9.1.1",
"axios": "^0.27.2",
@@ -40,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"
+12 -6
View File
@@ -11,8 +11,13 @@ datasource db {
}
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
wantId String @db.Uuid
@@ -43,7 +48,7 @@ model User {
model Want {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
release Int
release BigInt
data Json
userId String @db.Uuid
@@ -68,12 +73,10 @@ model Offer {
condition String @default("")
item Decimal @db.Money
// markup Decimal @db.Money
shipping Decimal @db.Money
total Decimal @db.Money
comparison Decimal @db.Money
listingId Int
listingId BigInt
listing Listing @relation(fields: [listingId], references: [id])
userId String @db.Uuid
@@ -82,6 +85,9 @@ model Offer {
wantId String @db.Uuid
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
// @@unique([userId, wantId])
@@unique([listingId])
+36 -51
View File
@@ -13,12 +13,16 @@ import Bluebird from 'bluebird';
import type Log from 'bunyan';
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 { 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 WS_PORT = 40055;
const SECRET = 'ohja4Eif9eidah3aj5ta2haa4iemooth';
@@ -59,23 +63,6 @@ export default function createApi(log: Log, pg: Pg, discogs, offers, events) {
ctx.status = 200;
});
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.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;
@@ -85,17 +72,9 @@ export default function createApi(log: Log, pg: Pg, discogs, offers, events) {
ctx.response.body = wants;
});
router.get('/want/all', async function wantGet(ctx, next) {
ctx.assert(ctx.isAuthenticated(), 401, 'log in required');
const user = ctx.state.user;
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);
router.post('/want/search', async function wantSearch(ctx, next) {
const { query } = ctx.request.body;
const results = await discogs.search(query);
ctx.response.body = results;
});
@@ -110,33 +89,39 @@ export default function createApi(log: Log, pg: Pg, discogs, offers, events) {
app.ws.use(passport.initialize());
app.ws.use(passport.session());
app.ws.use(async (ctx, next) => {
log.info('new ws connection', { user: ctx.state.user });
try {
log.info('new ws connection', { user: ctx.state.user });
// log.child...
// log.child...
const ws = ctx.websocket;
const rpc = createRpc(log, pg, ctx.state.user, ws, discogs, offers);
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());
const response = await rpc.receive(parsed);
rpc.send(response);
} catch (e) {
log.warn(e);
log.warn(message.toString());
rpc.send([RpcMessageType.Error, e.message || e.msg]);
}
return true;
});
ws.on('message', async function incoming(message) {
try {
const parsed = JSON.parse(message.toString());
const response = await rpc.receive(parsed);
rpc.send(response);
} catch (e) {
log.warn(e);
log.warn(message.toString());
rpc.send([RpcMessageType.Error, e.message || e.msg]);
}
return true;
});
ws.on('close', function onClose() {
log.info('ws disconnected', { user: ctx.state.user });
});
ws.on('close', function onClose() {
log.info('ws disconnected', { user: ctx.state.user });
});
await rpc.onConnection();
if (ctx.state.user) events.emit(Events.Connection, { rpc, id: ctx.state.user.id });
await rpc.onConnection();
} catch (e) {
log.error(e);
}
});
return {
+5 -4
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(additions);
log.info('want list additions', { additions: additions.length });
return additions;
}
@@ -58,10 +58,9 @@ export default function createDiscogs(log: Log, pg: Pg) {
return data;
}
async function search(artist: string, title: string) {
async function search(query: string) {
const url = new URL(`${apiBase}/database/search`);
url.searchParams.append('artist', artist);
url.searchParams.append('title', title);
url.searchParams.append('query', query);
url.searchParams.append('type', 'release');
url.searchParams.append('format', 'vinyl');
const { data: { results } } = await api.get(url.href);
@@ -113,3 +112,5 @@ export default function createDiscogs(log: Log, pg: Pg) {
search,
};
}
export type Discogs = ReturnType<typeof createDiscogs>;
+33 -18
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',
StartupSync = 'StartupSync',
ListingsUpdated = 'ListingsUpdated',
Sync = 'Sync',
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);
}
@@ -51,32 +55,43 @@ export default function createEvents(log: Log, pg: Pg, discogs, offers) {
log.info({ event: Events.WantsAdded, wants: wants.map(w => wantDetails(w)) }, 'starting listings population');
await updateWantListings(wants);
log.info({ event: Events.WantsAdded, wants: wants.map(w => wantDetails(w)) }, 'finished populating listings');
events.emit(Events.ListingsUpdated);
});
events.on(Events.StartupSync, async function startupSync() {
events.on(Events.Sync, async function sync() {
const wants = await pg.wantsToSync();
log.info({ event: Events.StartupSync, wants: wants.length });
log.info({ event: Events.Sync, wants: wants.length });
await updateWantListings(wants);
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 updatedOffers = await offers.generate()
log.debug({ offers: updatedOffers.length }, 'offers up to date');
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');
for (const [id, rpc] of connections) {
rpc.pushUserOffers();
}
log.info('offers up to date');
});
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 };
}
+45 -56
View File
@@ -3,6 +3,7 @@ import type { PrismaClient, Want, User, Listing, Offer, } from '@prisma/client';
import assert from 'assert/strict';
import groupBy from 'lodash/groupBy';
import sortBy from 'lodash/sortBy';
import flatMap from 'lodash/flatMap';
import { Context } from 'koa';
import { logAxiosError, wantDetails } from './utils';
@@ -21,24 +22,17 @@ function sortOffers(o) {
}
export default function createOffers(log: Log, pg: Pg) {
async function generate(listingIds: DiscogsListing[], bulkShipping: number, baseShipping: number) {
log.info('generateOffers', { listingIds, bulkShipping, baseShipping });
const listings = await pg.listingByIds(listingIds);
// log.info({ listings }, 'creating offers for listings')
async function generate() {
const listings = flatMap(await pg.interestingListings());
log.debug('generateOffers', { listings: listings.length });
const shipping = 7.5; // 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 => ({
userId: l.want.user.id,
listingId: l.id,
wantId: l.want.id,
// sellerId: l.seller,
status: Status.Pending,
condition: l.data.condition,
@@ -46,9 +40,6 @@ export default function createOffers(log: Log, pg: Pg) {
item: 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,
@@ -56,7 +47,12 @@ export default function createOffers(log: Log, pg: Pg) {
// to deal with this we just ignore duplicates on insert
// and ensure the cheapest one goes in first
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;
}
@@ -84,46 +80,8 @@ export default function createOffers(log: Log, pg: Pg) {
return update;
}
async function bundles() {
const bundles = await pg.bundleCandidates();
interface ListingData extends Listing {
data: any,
want: { user: User },
};
return bundles.map((b: ListingData[]) => {
const total = b.reduce((acc, l) => acc + l.data.price.value, 0);
const items = b.map(l => ({
id: l.data.id,
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;
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'),
}
return bundle;
});
}
// converts offers into groups based on seller
// includes some bits of info about shipping etc
async function acpOffers() {
const offers = await pg.offersAdmin();
@@ -147,17 +105,48 @@ export default function createOffers(log: Log, pg: Pg) {
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);
return groupBy(offers, (o) => o.wantId);
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 {
generate,
accept,
bundles,
statusSet,
acpOffers,
userOffers,
samples,
};
}
export type Offers = ReturnType<typeof createOffers>;
+55 -58
View File
@@ -11,33 +11,7 @@ export interface UserCreateParams {
password: string,
}
export interface Pg {
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[]>,
wantRm: (id: UUID) => Promise<void>,
bundleCandidates: () => Promise<Listing[][]>,
offersCreate: (offers: Offer[]) => Promise<Offer[]>,
offerStatusSet: (id: UUID, status: Status) => Promise<Offer>,
offersAdmin: () => Promise<Offer[]>,
offerFindId: (id: UUID) => Promise<Offer>,
}
export default function createPg(log: Log, prisma: PrismaClient): Pg {
export default function createPg(log: Log, prisma: PrismaClient) {
async function userFindId(id: UUID) {
return prisma.user.findUnique({ where: { id }});
}
@@ -47,7 +21,11 @@ export default function createPg(log: Log, prisma: PrismaClient): Pg {
}
async function userWants(user: User) {
return prisma.want.findMany({ where: { userId: user.id }});
return prisma.want.findMany({
where: { userId: user.id },
include: { _count: { select: { offers: true }} },
orderBy: { offers: { _count: 'desc' } }
});
}
async function userOffers(user: User) {
@@ -59,9 +37,11 @@ export default function createPg(log: Log, prisma: PrismaClient): Pg {
include: {
want: true,
},
orderBy: {
total: 'asc',
},
orderBy: [
{ updatedAt: 'asc' },
{ total: 'asc' },
{ listingId: 'asc' },
],
});
}
@@ -118,6 +98,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) {
return prisma.want.findMany({
where: { userId: user.id },
@@ -152,8 +138,8 @@ export default function createPg(log: Log, prisma: PrismaClient): Pg {
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,
@@ -166,7 +152,7 @@ export default function createPg(log: Log, prisma: PrismaClient): Pg {
}
});
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
@@ -175,25 +161,26 @@ export default function createPg(log: Log, prisma: PrismaClient): Pg {
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');
return count;
});
}
@@ -241,19 +228,26 @@ 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`
select
seller, count(distinct("wantId")), array_agg(id) as listings
from
listings
where
seller not in (3709203)
group by
seller
order by
count desc
limit 5;
WITH groups AS (
select
seller, count(distinct("wantId")), array_agg(id) as listings
from
listings
where
seller not in (3709203)
group by
seller
order by
count desc
)
select *
from groups
where count >= 5;
` as BundleCandidate[];
return Promise.all(candidates.map(async c => prisma.listing.findMany({
@@ -269,14 +263,14 @@ export default function createPg(log: Log, prisma: PrismaClient): Pg {
})));
}
async function offersCreate(offers: Offer[]) {
async function offersUpdate(offers) {
return await prisma.$transaction(
offers.map(offer => prisma.offer.upsert({
where: {
listingId: offer.listingId,
},
create: offer,
update: _.omit(offer, ['status']),
update: _.omit(offer, ['status', 'id']),
}))
);
}
@@ -288,6 +282,7 @@ export default function createPg(log: Log, prisma: PrismaClient): Pg {
});
}
// effectively all offers
async function offersAdmin() {
return prisma.offer.findMany({
include: {
@@ -316,11 +311,13 @@ export default function createPg(log: Log, prisma: PrismaClient): Pg {
listingRmUnlisted,
listingByIds,
bundleCandidates,
interestingListings,
offersCreate,
offersUpdate,
offerStatusSet,
offersAdmin,
offerFindId,
};
}
export type Pg = ReturnType<typeof createPg>;
+129 -69
View File
@@ -1,14 +1,91 @@
import { EventEmitter } from 'node:events';
import { setTimeout } from 'timers/promises';
import assert from 'assert/strict';
import type Log from 'bunyan';
import type { Want, Offer, User } from '@prisma/client';
import { Pg } from './pg';
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 = {
id: UUID,
status: Status,
};
type WantRmParams = {
id: UUID,
};
type WantAddParams = {
releaseId: number,
};
function checkMsgType(msgType: RpcMessageType): msgType is RpcMessageType {
if (RpcMessageType[msgType]) {
@@ -18,7 +95,7 @@ function checkMsgType(msgType: RpcMessageType): msgType is RpcMessageType {
}
// rpc is created per connection from the api
export default function createRpc(log: Log, pg: Pg, user: User, ws, discogs, offers) {
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, []];
}
@@ -52,6 +129,18 @@ export default function createRpc(log: Log, pg: Pg, user: User, ws, discogs, off
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');
@@ -68,6 +157,12 @@ export default function createRpc(log: Log, pg: Pg, user: User, ws, discogs, off
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;
@@ -93,11 +188,15 @@ export default function createRpc(log: Log, pg: Pg, user: User, ws, discogs, off
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`);
@@ -109,13 +208,25 @@ export default function createRpc(log: Log, pg: Pg, user: User, ws, discogs, off
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);
const wants = await pg.userWants(user);
const wantsMsg: WantsMessage = [RpcMessageType.Wants, wants];
send(wantsMsg);
await pushUserOffers();
await pushUserWants();
if (isAdmin(user)) return adminConnection();
}
function send(msg: RpcMessage) {
@@ -126,71 +237,20 @@ export default function createRpc(log: Log, pg: Pg, user: User, ws, discogs, off
))
}
async function pushUserOffers() {
send(await userOffers());
}
async function pushUserWants() {
send(await userWants());
}
return {
receive,
send,
onConnection,
pushUserOffers,
pushUserWants,
}
}
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',
OfferStatusSet = 'OfferStatusSet',
AcpOfferStatusSet = 'AcpOfferStatusSet',
AcpOffers = 'AcpOffers',
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 AcpOfferStatusSetMessage = [RpcMessageType.AcpOfferStatusSet, Offer[]];
type AcpOffersMessage = [RpcMessageType.AcpOffers, Offer[]];
export type RpcMessage =
PingMessage |
PongMessage |
WhoDisMessage |
UserMessage |
WantsMessage |
OffersMessage |
OfferStatusSetMessage |
AcpOfferStatusSetMessage |
AcpOffersMessage |
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,
};
+11
View File
@@ -10,6 +10,7 @@
"license": "MIT",
"dependencies": {
"@reduxjs/toolkit": "^1.8.5",
"izitoast": "^1.4.0",
"lodash": "^4.17.21",
"logrocket": "^3.0.1",
"preact": "^10.10.0",
@@ -10844,6 +10845,11 @@
"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": {
"version": "10.8.5",
"resolved": "https://registry.npmjs.org/jake/-/jake-10.8.5.tgz",
@@ -29566,6 +29572,11 @@
"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": {
"version": "10.8.5",
"resolved": "https://registry.npmjs.org/jake/-/jake-10.8.5.tgz",
+1
View File
@@ -10,6 +10,7 @@
},
"dependencies": {
"@reduxjs/toolkit": "^1.8.5",
"izitoast": "^1.4.0",
"lodash": "^4.17.21",
"logrocket": "^3.0.1",
"preact": "^10.10.0",
+9 -7
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,8 +18,8 @@ import Info from './routes/info';
if (window.location.host === 'matescrates.au') LogRocket.init('yh0dy3/mcrates');
const Main = () => {
const dispatch = useDispatch()
const Mcrates = () => {
const dispatch = useDispatch();
// i have no idea why this is needed,
// but it makes sure the user changes actually rerender
@@ -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>
);
}
+337
View File
@@ -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

+541
View File
@@ -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

+2
View File
@@ -7,6 +7,8 @@ import LogRocket from 'logrocket';
import http from './../http';
import { State } from './../store';
import Swirl from './swirl';
function showAdmin(user) {
return user && user.email === 'ntr@mnml.gg';
}
@@ -26,7 +26,8 @@ function OfferLine(offer) {
return (
<div class="line">
<span class='number'>${offer.total}</span>
<div class='number'>${offer.total}</div>
<div class='number'>{offer.condition}</div>
<button onClick={() => setStatus('Accepted')} disabled={accepted} class={classes} >{btnText}</button>
{negBtn}
</div>
@@ -48,27 +49,35 @@ 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>
)
}
export default function WantOffers(wantOffers) {
export default function OfferGroup(group, i) {
const ws = useSelector((state: State) => state.ws);
const details = wantOffers[0].want.data;
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">
<div class="offer" key={i} id={group.details.id}>
<section>
<h3>{details.artists_sort} - {details.title} <a href={details.uri} target="_blank"><ExternalLink /></a></h3>
<div class='alt-text' >{details.year}</div>
<div class='alt-text'>{details.formats.map(f => f.descriptions.join(' ')).join(' ')}</div>
<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} - {detailText}
</div>
<div class='alt-text details'>Status: {group.status}</div>
</section>
<img src={details.thumb} />
<img src={group.details.thumb} />
<div class="lines">
{wantOffers.map(o => OfferPriceBtn(o))}
{group.offers.map(o => OfferPriceBtn(o))}
</div>
</div>
)
+5
View File
@@ -0,0 +1,5 @@
import { h } from 'preact';
export default function Swirl() {
return <div class='swirl' />;
}
+8 -7
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'
@@ -30,8 +31,8 @@ function Want(want) {
</div>
<img src={r.thumb} />
<div class='details'>
<div class='alt'>{r.year}</div>
<div class='alt'>{r.formats[0].descriptions.join(' ')}</div>
<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>
+6 -30
View File
@@ -32,34 +32,18 @@ function onError(error) {
api.interceptors.response.use(onSuccess, onError);
const actions = {
getBundles: async function getBundles() {
return api.get('/acp/bundles');
},
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 });
// TODO move to rpc
postWantSearch: async function postWantSearch(query) {
return api.post('/want/search', { query });
},
postDiscogs: async function postDiscogs(username) {
return api.post('/want/discogs', { username });
},
// --------------------------------
// require as http to set cookies
getLogin: async function getLogin() {
return api.get('/auth/login')
},
@@ -82,14 +66,6 @@ const actions = {
password,
});
},
postOffers: async function postOffers(listingIds, shipping, baseShipping) {
return api.post('/offers', {
listingIds,
shipping,
baseShipping,
});
},
};
export default actions;
+2
View File
@@ -1,4 +1,6 @@
import 'izitoast/dist/css/iziToast.min.css';
import './style/index.less';
import App from './app';
export default App;
+9 -12
View File
@@ -70,21 +70,18 @@ function Home() {
const offers = useSelector((state: State) => state.acpOffers);
const ws = useSelector((state: State) => state.ws);
// useEffect(() => {
// ws.sendAcpOffers();
// }, [offers, ws]);
if (!offers.length) {
return (
<div class={'offers'}>
loading...
</div>
);
function sync() {
ws.sendAcpListingsSync();
}
return (
<div class='bundles'>
{offers.map(SellerGroup)}
<div>
<button onClick={sync}> sync listings </button>
<button onClick={sync}> generate offers </button>
<hr />
<div class='bundles'>
{offers.map(SellerGroup)}
</div>
</div>
);
}
+3
View File
@@ -1,8 +1,11 @@
import { h } from 'preact';
import Swirl from './../components/swirl';
const Info = () => {
return (
<div class='info'>
<Swirl />
<div class='words'>
<section>
<h1>Info</h1>
+10 -8
View File
@@ -1,23 +1,25 @@
import { h } from 'preact';
import { useSelector } from 'react-redux'
import { route } from 'preact-router';
import { State } from './../store'
import * as toast from './../toast';
import Want from './../components/want';
interface Props {
user: any,
wants: any[],
getUserOffers: (userId: string) => void,
}
const List = ({ user }: Props) => {
const List = () => {
const wants = useSelector((state: State) => state.wants);
if (!wants.length) {
if (!wants) {
return <div> loading... </div>;
}
if (!wants.length) {
toast.info('List Empty', 'You have no items in your list, add some with search.');
route('/search');
}
return (
<div class="list">
{wants.map(Want)}
+28 -19
View File
@@ -5,18 +5,20 @@ import { useSelector, useDispatch } from 'react-redux'
import http from './../http';
import { State, userState } from './../store';
import Swirl from './../components/swirl';
const Login = ({ mode }) => {
const dispatch = useDispatch()
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('');
async function postLogin() {
function gotoRegister() {
return route('/register');
}
async function postLogin(email, password) {
const [error, user] = await http.postLogin(email, password);
if (error) return setError(error);
route('/');
@@ -25,17 +27,23 @@ const Login = ({ mode }) => {
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);
if (error) return setError(error);
route('/search');
dispatch(userState.actions.set(user));
// reconnect ws to get logged in methods
ws.connect();
}
function handleSubmit(e) {
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>()
@@ -51,21 +59,21 @@ const Login = ({ mode }) => {
if (mode === 'register') return (
<div class="login">
{errorEl}
<form action="">
<form action="" onSubmit={handleSubmit}>
<div class="item">
<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 class="item">
<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 class="item">
<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 class="item">
<input type="submit" value="register" onClick={handleSubmit} />
<input type="submit" value="register" />
</div>
</form>
</div>
@@ -75,19 +83,20 @@ const Login = ({ mode }) => {
return (
<div class="login">
{errorEl}
<form action="">
<form action="" onSubmit={handleSubmit}>
<div class="item">
<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 class="item">
<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 class="item">
<input type="submit" value="login" onClick={handleSubmit} />
<input type="submit" value="login"/>
</div>
</form>
<button onClick={gotoRegister}> create account </button>
</div>
);
};
@@ -96,11 +105,11 @@ function LoginPage({ mode }) {
return (
<div class="landing">
<Login mode={mode} />
<div></div>
<div>
<Swirl />
<div class='words'>
<p>Mates Crates is a record importing service.</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>Records can then be picked up from Preston.</p>
</div>
</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;
-30
View File
@@ -1,30 +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 WantOffers from './../components/want.offers';
function Home() {
const dispatch = useDispatch()
const offers = useSelector((state: State) => state.offers);
// if (!offers.length) {
// return (
// <div class={'offers'}>
// loading...
// </div>
// );
// }
return (
<div class={'offers'}>
{Object.values(offers).map(WantOffers)}
</div>
);
}
export default Home;
+24 -21
View File
@@ -1,26 +1,28 @@
import { h } from 'preact';
import { useState, useRef, useEffect } from 'preact/hooks';
import { ExternalLink, Check } from 'react-feather';
import { useSelector } from 'react-redux'
import { State } from './../store';
import http from '../http';
function Result(r) {
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 classes = `${added ? 'added' : ''}`;
const btnText = added
// releaseIds are bigints on server
const wanted = wants.some(w => BigInt(w.release) === BigInt(r.id));
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>
@@ -33,14 +35,17 @@ function Result(r) {
}
const User = () => {
const [artist, setArtist] = useState('')
const [query, setQuery] = useState('')
const [title, setTitle] = useState('');
const [username, setUsername] = 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() {
const [error, results] = await http.postWantSearch(artist, title);
const [error, results] = await http.postWantSearch(query);
if (results.length === 0) {
setResults([]);
@@ -61,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()
@@ -78,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
@@ -89,10 +95,8 @@ const User = () => {
<div class='search'>
<div>
<form action="" method="get">
<label for="artist">Artist</label>
<input type="text" name="artist" ref={searchRef} placeholder="Ben Klock" id="artist" value={artist} onChange={e => setArtist((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 />
<label for="query">Query</label>
<input type="text" name="query" ref={searchRef} placeholder="ben klock subzero" id="query" value={query} onChange={e => setQuery((e.target as HTMLInputElement).value) } required />
<input type="submit" value="Search" onClick={handleSubmit} />
</form>
<hr />
@@ -100,16 +104,15 @@ const User = () => {
<form action="" method="get">
<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="submit" value="Sync" onClick={handleDiscogsSubmit} />
<input type="submit" value="Sync Wantlist" onClick={handleDiscogsSubmit} />
</form>
</div>
<hr />
<p>Data provided by Discogs.</p>
</div>
<div />
<div>
<div class='results' ref={resultsRef}>
{infoEl}
{results.map(Result)}
{results.map(r => Result(r, wants, ws))}
{topEl}
</div>
</div>
+3 -2
View File
@@ -11,7 +11,7 @@ export const userState = createSlice({
export const offersState = createSlice({
name: 'offers',
initialState: [],
initialState: null,
reducers: {
set: (state, action) => action.payload
},
@@ -27,7 +27,7 @@ export const acpOffersState = createSlice({
export const wantsState = createSlice({
name: 'wants',
initialState: [],
initialState: null,
reducers: {
set: (state, action) => action.payload
},
@@ -52,6 +52,7 @@ const reducer = combineReducers({
const store = createStore(
reducer,
// @ts-ignore -- devtools window exists on dev
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);
+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;
}
+12 -265
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,142 +173,6 @@ p {
margin:0 0 var(--spacing)
}
// ok actual styles
body {
padding-top: calc(var(--spacing)/2);
}
main {
padding: 0 5% 0 5%;
}
button, [type=submit] {
padding: var(--spacing-half);
background: var(--background-1);
}
.landing {
display: grid;
grid-template-columns: 1fr 0.5fr 2fr;
.login {
button {
width: 100%;
}
}
}
.info {
display: grid;
grid-template-columns: 1fr 1fr;
section {
margin-bottom: var(--spacing);
}
.contact {
text-align: center;
}
}
.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 0.5fr 2fr;
article {
display: grid;
padding: var(--spacing);
margin-bottom: var(--spacing);
grid-template-columns: 1fr 4fr 1fr;
}
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-row-gap: var(--spacing);
grid-column-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);
}
}
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);
@@ -340,107 +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;
.offer {
display: grid;
grid-template-columns: 1fr min-content;
grid-template-rows: 1fr min-content;
grid-template-areas:
"details img"
"lines lines";
padding: var(--spacing);
margin: var(--spacing-half);
background: var(--background-1);
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;
}
@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;
}
}
+15
View File
@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>matescrates.au - 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>
+15
View File
@@ -0,0 +1,15 @@
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',
});
}
+16 -4
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', {}]);
}
@@ -50,6 +57,10 @@ export default function createSocket(dispatch) {
send(['AcpOfferStatusSet', { id, status }]);
}
function sendAcpListingsSync() {
send(['AcpListingsSync', {}]);
}
let pongTimeout;
function onPong() {
pongTimeout = setTimeout(sendPing, 5000);
@@ -110,10 +121,8 @@ export default function createSocket(dispatch) {
// Connection opened
function onOpen() {
console.log('websocket connected', SOCKET_URL)
toast.info('connected', 'connected to server');
sendPing();
sendAcpOffers();
sendOffers();
sendWants();
}
function onError(event) {
@@ -152,10 +161,13 @@ export default function createSocket(dispatch) {
sendOfferStatusSet,
sendWantRm,
sendWantAdd,
sendOffers,
sendWants,
sendAcpOffers,
sendAcpOfferStatusSet,
sendAcpListingsSync,
};
}