Compare commits
9 Commits
feat/csv-p
...
feat/holog
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6299c07b87 | ||
| 171ce294f4 | |||
|
|
023cd87319 | ||
|
|
04ea65eeeb | ||
|
|
9d9524e654 | ||
|
|
bc99be51ea | ||
|
|
b06e24d382 | ||
|
|
7b4e06733f | ||
|
|
f72d479c1d |
109
package-lock.json
generated
@@ -121,6 +121,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@azure-rest/core-client/-/core-client-2.5.1.tgz",
|
||||
"integrity": "sha512-EHaOXW0RYDKS5CFffnixdyRPak5ytiCtU7uXDcP/uiY+A6jFRwNGzzJBiznkCzvi5EYpY+YWinieqHb0oY916A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@azure/abort-controller": "^2.1.2",
|
||||
"@azure/core-auth": "^1.10.0",
|
||||
@@ -138,6 +139,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz",
|
||||
"integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
@@ -150,6 +152,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.10.1.tgz",
|
||||
"integrity": "sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@azure/abort-controller": "^2.1.2",
|
||||
"@azure/core-util": "^1.13.0",
|
||||
@@ -183,6 +186,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@azure/core-http-compat/-/core-http-compat-2.3.2.tgz",
|
||||
"integrity": "sha512-Tf6ltdKzOJEgxZeWLCjMxrxbodB/ZeCbzzA1A2qHbhzAjzjHoBVSUeSl/baT/oHAxhc4qdqVaDKnc2+iE932gw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@azure/abort-controller": "^2.1.2"
|
||||
},
|
||||
@@ -199,6 +203,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-2.7.2.tgz",
|
||||
"integrity": "sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@azure/abort-controller": "^2.0.0",
|
||||
"@azure/core-util": "^1.2.0",
|
||||
@@ -214,6 +219,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@azure/core-paging/-/core-paging-1.6.2.tgz",
|
||||
"integrity": "sha512-YKWi9YuCU04B55h25cnOYZHxXYtEvQEbKST5vqRga7hWY9ydd3FZHdeQF8pyh+acWZvppw13M/LMGx0LABUVMA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
@@ -245,6 +251,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.3.1.tgz",
|
||||
"integrity": "sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
@@ -257,6 +264,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.13.1.tgz",
|
||||
"integrity": "sha512-XPArKLzsvl0Hf0CaGyKHUyVgF7oDnhKoP85Xv6M4StF/1AhfORhZudHtOyf2s+FcbuQ9dPRAjB8J2KvRRMUK2A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@azure/abort-controller": "^2.1.2",
|
||||
"@typespec/ts-http-runtime": "^0.3.0",
|
||||
@@ -294,6 +302,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@azure/keyvault-common/-/keyvault-common-2.0.0.tgz",
|
||||
"integrity": "sha512-wRLVaroQtOqfg60cxkzUkGKrKMsCP6uYXAOomOIysSMyt1/YM0eUn9LqieAWM8DLcU4+07Fio2YGpPeqUbpP9w==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@azure/abort-controller": "^2.0.0",
|
||||
"@azure/core-auth": "^1.3.0",
|
||||
@@ -313,6 +322,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@azure/keyvault-keys/-/keyvault-keys-4.10.0.tgz",
|
||||
"integrity": "sha512-eDT7iXoBTRZ2n3fLiftuGJFD+yjkiB1GNqzU2KbY1TLYeXeSPVTVgn2eJ5vmRTZ11978jy2Kg2wI7xa9Tyr8ag==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@azure-rest/core-client": "^2.3.3",
|
||||
"@azure/abort-controller": "^2.1.2",
|
||||
@@ -336,6 +346,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.3.0.tgz",
|
||||
"integrity": "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typespec/ts-http-runtime": "^0.3.0",
|
||||
"tslib": "^2.6.2"
|
||||
@@ -349,6 +360,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-4.29.0.tgz",
|
||||
"integrity": "sha512-/f3eHkSNUTl6DLQHm+bKecjBKcRQxbd/XLx8lvSYp8Nl/HRyPuIPOijt9Dt0sH50/SxOwQ62RnFCmFlGK+bR/w==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@azure/msal-common": "15.15.0"
|
||||
},
|
||||
@@ -361,6 +373,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-15.15.0.tgz",
|
||||
"integrity": "sha512-/n+bN0AKlVa+AOcETkJSKj38+bvFs78BaP4rNtv3MJCmPH0YrHiskMRe74OhyZ5DZjGISlFyxqvf9/4QVEi2tw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.8.0"
|
||||
}
|
||||
@@ -370,6 +383,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-3.8.8.tgz",
|
||||
"integrity": "sha512-+f1VrJH1iI517t4zgmuhqORja0bL6LDQXfBqkjuMmfTYXTQQnh1EvwwxO3UbKLT05N0obF72SRHFrC1RBDv5Gg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@azure/msal-common": "15.15.0",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
@@ -1460,7 +1474,8 @@
|
||||
"version": "5.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@js-joda/core/-/core-5.7.0.tgz",
|
||||
"integrity": "sha512-WBu4ULVVxySLLzK1Ppq+OdfP+adRS4ntmDQT915rzDJ++i95gc2jZkM5B6LWEAwN3lGXpfie3yPABozdD3K3Vg==",
|
||||
"license": "BSD-3-Clause"
|
||||
"license": "BSD-3-Clause",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@js-temporal/polyfill": {
|
||||
"version": "0.5.1",
|
||||
@@ -1788,7 +1803,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
|
||||
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/popperjs"
|
||||
@@ -2234,7 +2248,8 @@
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@tediousjs/connection-string/-/connection-string-0.5.0.tgz",
|
||||
"integrity": "sha512-7qSgZbincDDDFyRweCIEvZULFAw5iz/DeunhvuxpL31nfntX3P4Yd4HkHBRg9H8CdqY1e5WFN1PZIz/REL9MVQ==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@types/bootstrap": {
|
||||
"version": "5.2.10",
|
||||
@@ -2311,7 +2326,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.4.0.tgz",
|
||||
"integrity": "sha512-9wLpoeWuBlcbBpOY3XmzSTG3oscB6xjBEEtn+pYXTfhyXhIxC5FsBer2KTopBlvKEiW9l13po9fq+SJY/5lkhw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~7.18.0"
|
||||
}
|
||||
@@ -2322,7 +2336,6 @@
|
||||
"integrity": "sha512-gT+oueVQkqnj6ajGJXblFR4iavIXWsGAFCk3dP4Kki5+a9R4NMt0JARdk6s8cUKcfUoqP5dAtDSLU8xYUTFV+Q==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"pg-protocol": "*",
|
||||
@@ -2334,6 +2347,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.23.tgz",
|
||||
"integrity": "sha512-wwXrtQvbMHxCbBgjHaMGEmImFTQxxpfMOR/ZoQnXxB1woqkUbdLGFDgauo00Py9IudiaqSeiBiulSV9i6XIPig==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
@@ -2349,6 +2363,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.4.tgz",
|
||||
"integrity": "sha512-CI0NhTrz4EBaa0U+HaaUZrJhPoso8sG7ZFya8uQoBA57fjzrjRSv87ekCjLZOFExN+gXE/z0xuN2QfH4H2HrLQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"http-proxy-agent": "^7.0.0",
|
||||
"https-proxy-agent": "^7.0.0",
|
||||
@@ -2369,6 +2384,7 @@
|
||||
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
|
||||
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"event-target-shim": "^5.0.0"
|
||||
},
|
||||
@@ -2393,6 +2409,7 @@
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
|
||||
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
@@ -2526,7 +2543,6 @@
|
||||
"resolved": "https://registry.npmjs.org/astro/-/astro-5.18.1.tgz",
|
||||
"integrity": "sha512-m4VWilWZ+Xt6NPoYzC4CgGZim/zQUO7WFL0RHCH0AiEavF1153iC3+me2atDvXpf/yX4PyGUeD8wZLq1cirT3g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@astrojs/compiler": "^2.13.0",
|
||||
"@astrojs/internal-helpers": "0.7.6",
|
||||
@@ -2668,13 +2684,15 @@
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/bl": {
|
||||
"version": "6.1.6",
|
||||
"resolved": "https://registry.npmjs.org/bl/-/bl-6.1.6.tgz",
|
||||
"integrity": "sha512-jLsPgN/YSvPUg9UX0Kd73CXpm2Psg9FxMeCSXnk3WBO3CMT10JMwijubhGfHCnFu6TPn1ei3b975dxv7K2pWVg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/readable-stream": "^4.0.0",
|
||||
"buffer": "^6.0.3",
|
||||
@@ -2748,6 +2766,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.2.1"
|
||||
@@ -2757,13 +2776,15 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
||||
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
|
||||
"license": "BSD-3-Clause"
|
||||
"license": "BSD-3-Clause",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/bundle-name": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz",
|
||||
"integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"run-applescript": "^7.0.0"
|
||||
},
|
||||
@@ -3139,6 +3160,7 @@
|
||||
"resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz",
|
||||
"integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"bundle-name": "^4.1.0",
|
||||
"default-browser-id": "^5.0.0"
|
||||
@@ -3155,6 +3177,7 @@
|
||||
"resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz",
|
||||
"integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
@@ -3167,6 +3190,7 @@
|
||||
"resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz",
|
||||
"integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -4019,6 +4043,7 @@
|
||||
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
|
||||
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
@@ -4189,6 +4214,7 @@
|
||||
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
|
||||
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
@@ -4204,6 +4230,7 @@
|
||||
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
|
||||
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.8.x"
|
||||
}
|
||||
@@ -4722,6 +4749,7 @@
|
||||
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
|
||||
"integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"agent-base": "^7.1.0",
|
||||
"debug": "^4.3.4"
|
||||
@@ -4735,6 +4763,7 @@
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
|
||||
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"agent-base": "^7.1.2",
|
||||
"debug": "4"
|
||||
@@ -4748,6 +4777,7 @@
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
|
||||
"integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
},
|
||||
@@ -4777,7 +4807,8 @@
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "BSD-3-Clause"
|
||||
"license": "BSD-3-Clause",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/immutable": {
|
||||
"version": "5.1.5",
|
||||
@@ -4915,7 +4946,8 @@
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/js-md4/-/js-md4-0.3.2.tgz",
|
||||
"integrity": "sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "4.1.1",
|
||||
@@ -4941,6 +4973,7 @@
|
||||
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
|
||||
"integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"jws": "^4.0.1",
|
||||
"lodash.includes": "^4.3.0",
|
||||
@@ -4963,6 +4996,7 @@
|
||||
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
|
||||
"integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"buffer-equal-constant-time": "^1.0.1",
|
||||
"ecdsa-sig-formatter": "1.0.11",
|
||||
@@ -4974,6 +5008,7 @@
|
||||
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
|
||||
"integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"jwa": "^2.0.1",
|
||||
"safe-buffer": "^5.0.1"
|
||||
@@ -4992,43 +5027,50 @@
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
||||
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/lodash.isboolean": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
|
||||
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/lodash.isinteger": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
|
||||
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/lodash.isnumber": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
|
||||
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/lodash.isplainobject": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
||||
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/lodash.isstring": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
|
||||
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/lodash.once": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
|
||||
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/loglevel": {
|
||||
"version": "1.9.2",
|
||||
@@ -5940,6 +5982,7 @@
|
||||
"resolved": "https://registry.npmjs.org/mssql/-/mssql-11.0.1.tgz",
|
||||
"integrity": "sha512-KlGNsugoT90enKlR8/G36H0kTxPthDhmtNUCwEHvgRza5Cjpjoj+P2X6eMpFUDN7pFrJZsKadL4x990G8RBE1w==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@tediousjs/connection-string": "^0.5.0",
|
||||
"commander": "^11.0.0",
|
||||
@@ -5960,6 +6003,7 @@
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
},
|
||||
@@ -5972,6 +6016,7 @@
|
||||
"resolved": "https://registry.npmjs.org/tedious/-/tedious-18.6.2.tgz",
|
||||
"integrity": "sha512-g7jC56o3MzLkE3lHkaFe2ZdOVFBahq5bsB60/M4NYUbocw/MCrS89IOEQUFr+ba6pb8ZHczZ/VqCyYeYq0xBAg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@azure/core-auth": "^1.7.2",
|
||||
"@azure/identity": "^4.2.1",
|
||||
@@ -6025,7 +6070,8 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/native-duplexpair/-/native-duplexpair-1.0.0.tgz",
|
||||
"integrity": "sha512-E7QQoM+3jvNtlmyfqRZ0/U75VFgCls+fSkbml2MpgWkWyz3ox8Y58gNhfuziuQYGNNQAbFZJQck55LHCnCK6CA==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/neotraverse": {
|
||||
"version": "0.6.18",
|
||||
@@ -6140,6 +6186,7 @@
|
||||
"resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz",
|
||||
"integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"default-browser": "^5.2.1",
|
||||
"define-lazy-prop": "^3.0.0",
|
||||
@@ -6237,7 +6284,6 @@
|
||||
"resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz",
|
||||
"integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"pg-connection-string": "^2.12.0",
|
||||
"pg-pool": "^3.13.0",
|
||||
@@ -6445,6 +6491,7 @@
|
||||
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
|
||||
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 0.6.0"
|
||||
}
|
||||
@@ -6508,6 +6555,7 @@
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz",
|
||||
"integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"abort-controller": "^3.0.0",
|
||||
"buffer": "^6.0.3",
|
||||
@@ -6773,14 +6821,14 @@
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
|
||||
"integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz",
|
||||
"integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.8"
|
||||
},
|
||||
@@ -6825,6 +6873,7 @@
|
||||
"resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz",
|
||||
"integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
@@ -6850,20 +6899,21 @@
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/sass": {
|
||||
"version": "1.98.0",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.98.0.tgz",
|
||||
"integrity": "sha512-+4N/u9dZ4PrgzGgPlKnaaRQx64RO0JBKs9sDhQ2pLgN6JQZ25uPQZKQYaBJU48Kd5BxgXoJ4e09Dq7nMcOUW3A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"chokidar": "^4.0.0",
|
||||
"immutable": "^5.1.5",
|
||||
@@ -7049,7 +7099,8 @@
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
|
||||
"integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
|
||||
"license": "BSD-3-Clause"
|
||||
"license": "BSD-3-Clause",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/standardwebhooks": {
|
||||
"version": "1.0.0",
|
||||
@@ -7087,6 +7138,7 @@
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.2.0"
|
||||
}
|
||||
@@ -7180,6 +7232,7 @@
|
||||
"resolved": "https://registry.npmjs.org/tarn/-/tarn-3.0.2.tgz",
|
||||
"integrity": "sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
@@ -7189,6 +7242,7 @@
|
||||
"resolved": "https://registry.npmjs.org/tedious/-/tedious-19.2.1.tgz",
|
||||
"integrity": "sha512-pk1Q16Yl62iocuQB+RWbg6rFUFkIyzqOFQ6NfysCltRvQqKwfurgj8v/f2X+CKvDhSL4IJ0cCOfCHDg9PWEEYA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@azure/core-auth": "^1.7.2",
|
||||
"@azure/identity": "^4.2.1",
|
||||
@@ -7297,7 +7351,6 @@
|
||||
"integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "~0.27.0",
|
||||
"get-tsconfig": "^4.7.5"
|
||||
@@ -7329,7 +7382,6 @@
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -7670,6 +7722,7 @@
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
@@ -8322,6 +8375,7 @@
|
||||
"resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz",
|
||||
"integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"is-wsl": "^3.1.0"
|
||||
},
|
||||
@@ -8400,7 +8454,6 @@
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
|
||||
BIN
public/holofoils/ancient.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
public/holofoils/angular.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
public/holofoils/cosmos-bottom-trans.png
Normal file
|
After Width: | Height: | Size: 128 KiB |
BIN
public/holofoils/cosmos-bottom.png
Normal file
|
After Width: | Height: | Size: 128 KiB |
BIN
public/holofoils/cosmos-middle-trans.png
Normal file
|
After Width: | Height: | Size: 107 KiB |
BIN
public/holofoils/cosmos-middle.gif
Normal file
|
After Width: | Height: | Size: 78 KiB |
BIN
public/holofoils/cosmos-middle.png
Normal file
|
After Width: | Height: | Size: 107 KiB |
BIN
public/holofoils/cosmos-top-trans.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
public/holofoils/cosmos-top.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
public/holofoils/cosmos.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
public/holofoils/crossover.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
public/holofoils/galaxy-source.png
Normal file
|
After Width: | Height: | Size: 561 KiB |
BIN
public/holofoils/galaxy.jpg
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
public/holofoils/geometric.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
public/holofoils/glitter.png
Normal file
|
After Width: | Height: | Size: 111 KiB |
BIN
public/holofoils/grain.webp
Normal file
|
After Width: | Height: | Size: 58 KiB |
BIN
public/holofoils/illusion-mask.png
Normal file
|
After Width: | Height: | Size: 94 KiB |
BIN
public/holofoils/illusion.png
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
public/holofoils/illusion2.png
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
public/holofoils/metal.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
public/holofoils/rainbow.jpg
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
public/holofoils/stylish.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
public/holofoils/stylish2.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
public/holofoils/trainerbg.jpg
Normal file
|
After Width: | Height: | Size: 259 KiB |
BIN
public/holofoils/trainerbg.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
public/holofoils/vmaxbg.jpg
Normal file
|
After Width: | Height: | Size: 74 KiB |
BIN
public/holofoils/wave.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
@@ -1,6 +1,9 @@
|
||||
import chalk from 'chalk';
|
||||
import { client } from '../src/db/typesense.ts';
|
||||
import type { DBInstance } from '../src/db/index.ts';
|
||||
import fs from "node:fs/promises";
|
||||
import { sql } from 'drizzle-orm'
|
||||
|
||||
|
||||
const DollarToInt = (dollar: any) => {
|
||||
if (dollar === null) return null;
|
||||
@@ -8,6 +11,31 @@ const DollarToInt = (dollar: any) => {
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const Sleep = (ms: number) => {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
|
||||
export const FileExists = async (path: string): Promise<boolean> => {
|
||||
try {
|
||||
await fs.access(path);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const GetNumberOrNull = (value: any): number | null => {
|
||||
const number = Number(value); // Attempt to convert the value to a number
|
||||
if (Number.isNaN(number)) {
|
||||
return null; // Return null if the result is NaN
|
||||
}
|
||||
return number; // Otherwise, return the number
|
||||
}
|
||||
|
||||
|
||||
// Delete and recreate the 'cards' index
|
||||
export const createCardCollection = async () => {
|
||||
try {
|
||||
@@ -101,4 +129,49 @@ export const upsertSkuCollection = async (db:DBInstance) => {
|
||||
marketPrice: DollarToInt(sku.marketPrice),
|
||||
})), { action: 'upsert' });
|
||||
console.log(chalk.green('Collection "skus" indexed successfully.'));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export const UpdateVariants = async (db:DBInstance) => {
|
||||
const updates = await db.execute(sql`update cards as c
|
||||
set
|
||||
product_name = a.product_name, product_line_name = a.product_line_name, product_url_name = a.product_url_name, rarity_name = a.rarity_name,
|
||||
sealed = a.sealed, set_id = a.set_id, card_type = a.card_type, energy_type = a.energy_type, number = a.number, artist = a.artist
|
||||
from (
|
||||
select t.product_id, b.variant,
|
||||
coalesce(o.product_name, regexp_replace(regexp_replace(coalesce(nullif(t.product_name, ''), t.product_url_name),' \\\\(.*\\\\)',''),' - .*$','')) as product_name,
|
||||
coalesce(o.product_line_name, t.product_line_name) as product_line_name, coalesce(o.product_url_name, t.product_url_name) as product_url_name,
|
||||
coalesce(o.rarity_name, t.rarity_name) as rarity_name, coalesce(o.sealed, t.sealed) as sealed, coalesce(o.set_id, t.set_id) as set_id,
|
||||
coalesce(o.card_type, t.card_type) as card_type, coalesce(o.energy_type, t.energy_type) as energy_type,
|
||||
coalesce(o.number, t.number) as number, coalesce(o.artist, t.artist) as artist
|
||||
from tcg_cards t
|
||||
join (select distinct product_id, variant from skus) b on t.product_id = b.product_id
|
||||
left join tcg_overrides o on t.product_id = o.product_id
|
||||
) a
|
||||
where c.product_id = a.product_id and c.variant = a.variant and
|
||||
(
|
||||
c.product_name is distinct from a.product_name or c.product_line_name is distinct from a.product_line_name or
|
||||
c.product_url_name is distinct from a.product_url_name or c.rarity_name is distinct from a.rarity_name or
|
||||
c.sealed is distinct from a.sealed or c.set_id is distinct from a.set_id or c.card_type is distinct from a.card_type or
|
||||
c.energy_type is distinct from a.energy_type or c."number" is distinct from a."number" or c.artist is distinct from a.artist
|
||||
)
|
||||
`);
|
||||
console.log(`Updated ${updates.rowCount} rows in cards table`);
|
||||
|
||||
const inserts = await db.execute(sql`insert into cards (product_id, variant, product_name, product_line_name, product_url_name, rarity_name, sealed, set_id, card_type, energy_type, "number", artist)
|
||||
select t.product_id, b.variant,
|
||||
coalesce(o.product_name, regexp_replace(regexp_replace(coalesce(nullif(t.product_name, ''), t.product_url_name),' \\\\(.*\\\\)',''),' - .*$','')) as product_name,
|
||||
coalesce(o.product_line_name, t.product_line_name) as product_line_name, coalesce(o.product_url_name, t.product_url_name) as product_url_name, coalesce(o.rarity_name, t.rarity_name) as rarity_name,
|
||||
coalesce(o.sealed, t.sealed) as sealed, coalesce(o.set_id, t.set_id) as set_id, coalesce(o.card_type, t.card_type) as card_type,
|
||||
coalesce(o.energy_type, t.energy_type) as energy_type, coalesce(o.number, t.number) as number, coalesce(o.artist, t.artist) as artist
|
||||
from tcg_cards t
|
||||
join (select distinct product_id, variant from skus) b on t.product_id = b.product_id
|
||||
left join tcg_overrides o on t.product_id = o.product_id
|
||||
where not exists (select 1 from cards where product_id=t.product_id and variant=b.variant)
|
||||
`);
|
||||
console.log(`Inserted ${inserts.rowCount} rows into cards table`);
|
||||
|
||||
}
|
||||
@@ -5,10 +5,11 @@ import { db, ClosePool } from '../src/db/index.ts';
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import chalk from 'chalk';
|
||||
import * as helper from './pokemon-helper.ts';
|
||||
//import util from 'util';
|
||||
|
||||
|
||||
async function syncTcgplayer() {
|
||||
async function syncTcgplayer(cardSets:string[] = []) {
|
||||
|
||||
const productLines = [ "pokemon", "pokemon-japan" ];
|
||||
|
||||
@@ -29,36 +30,21 @@ async function syncTcgplayer() {
|
||||
|
||||
const setNames = data.results[0].aggregations.setName;
|
||||
for (const setName of setNames) {
|
||||
console.log(chalk.blue(`Syncing product line "${productLine}" with setName "${setName.urlValue}"...`));
|
||||
await syncProductLine(productLine, "setName", setName.urlValue);
|
||||
let processSet = true;
|
||||
if (cardSets.length > 0) {
|
||||
processSet = cardSets.some(set => setName.value.toLowerCase().includes(set.toLowerCase()));
|
||||
}
|
||||
if (processSet) {
|
||||
console.log(chalk.blue(`Syncing product line "${productLine}" with setName "${setName.urlValue}"...`));
|
||||
await syncProductLine(productLine, "setName", setName.urlValue);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
console.log(chalk.green('✓ All TCGPlayer data synchronized successfully!'));
|
||||
}
|
||||
|
||||
function sleep(ms: number) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function fileExists(path: string): Promise<boolean> {
|
||||
try {
|
||||
await fs.access(path);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function getNumberOrNull(value: any): number | null {
|
||||
const number = Number(value); // Attempt to convert the value to a number
|
||||
if (Number.isNaN(number)) {
|
||||
return null; // Return null if the result is NaN
|
||||
}
|
||||
return number; // Otherwise, return the number
|
||||
}
|
||||
|
||||
async function syncProductLine(productLine: string, field: string, fieldValue: string) {
|
||||
let start = 0;
|
||||
@@ -123,7 +109,7 @@ async function syncProductLine(productLine: string, field: string, fieldValue: s
|
||||
for (const item of data.results[0].results) {
|
||||
|
||||
// Check if productId already exists and skip if it does (to avoid hitting the API too much)
|
||||
if (allProductIds.has(item.productId)) {
|
||||
if (allProductIds.size > 0 && allProductIds.has(item.productId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -163,7 +149,7 @@ async function syncProductLine(productLine: string, field: string, fieldValue: s
|
||||
cardTypeB: item.customAttributes.cardTypeB || null,
|
||||
energyType: detailData.customAttributes.energyType?.[0] || null,
|
||||
flavorText: detailData.customAttributes.flavorText || null,
|
||||
hp: getNumberOrNull(item.customAttributes.hp),
|
||||
hp: helper.GetNumberOrNull(item.customAttributes.hp),
|
||||
number: detailData.customAttributes.number || '',
|
||||
releaseDate: detailData.customAttributes.releaseDate ? new Date(detailData.customAttributes.releaseDate) : null,
|
||||
resistance: item.customAttributes.resistance || null,
|
||||
@@ -201,7 +187,7 @@ async function syncProductLine(productLine: string, field: string, fieldValue: s
|
||||
cardTypeB: item.customAttributes.cardTypeB || null,
|
||||
energyType: detailData.customAttributes.energyType?.[0] || null,
|
||||
flavorText: detailData.customAttributes.flavorText || null,
|
||||
hp: getNumberOrNull(item.customAttributes.hp),
|
||||
hp: helper.GetNumberOrNull(item.customAttributes.hp),
|
||||
number: detailData.customAttributes.number || '',
|
||||
releaseDate: detailData.customAttributes.releaseDate ? new Date(detailData.customAttributes.releaseDate) : null,
|
||||
resistance: item.customAttributes.resistance || null,
|
||||
@@ -218,7 +204,9 @@ async function syncProductLine(productLine: string, field: string, fieldValue: s
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
console.log(`item: ${item.setId}\tdetail: ${detailData.setId}`);
|
||||
console.log(`item: ${item.setCode}\tdetail: ${detailData.setCode}`);
|
||||
console.log(`item: ${item.setName}\tdetail: ${detailData.setName}`);
|
||||
// set is...
|
||||
await db.insert(schema.sets).values({
|
||||
setId: detailData.setId,
|
||||
@@ -255,7 +243,7 @@ async function syncProductLine(productLine: string, field: string, fieldValue: s
|
||||
|
||||
// get image if it doesn't already exist
|
||||
const imagePath = path.join(process.cwd(), 'public', 'cards', `${item.productId}.jpg`);
|
||||
if (!await fileExists(imagePath)) {
|
||||
if (!await helper.FileExists(imagePath)) {
|
||||
const imageResponse = await fetch(`https://tcgplayer-cdn.tcgplayer.com/product/${item.productId}_in_1000x1000.jpg`);
|
||||
if (imageResponse.ok) {
|
||||
const buffer = await imageResponse.arrayBuffer();
|
||||
@@ -267,7 +255,7 @@ async function syncProductLine(productLine: string, field: string, fieldValue: s
|
||||
}
|
||||
|
||||
// be nice to the API and not send too many requests in a short time
|
||||
await sleep(300);
|
||||
await helper.Sleep(300);
|
||||
|
||||
}
|
||||
|
||||
@@ -277,8 +265,21 @@ async function syncProductLine(productLine: string, field: string, fieldValue: s
|
||||
|
||||
// clear the log file
|
||||
await fs.rm('missing_images.log', { force: true });
|
||||
let allProductIds = new Set();
|
||||
|
||||
const allProductIds = new Set(await db.select({ productId: schema.cards.productId }).from(schema.cards).then(rows => rows.map(row => row.productId)));
|
||||
const args = process.argv.slice(2);
|
||||
if (args.length === 0) {
|
||||
allProductIds = new Set(await db.select({ productId: schema.cards.productId }).from(schema.cards).then(rows => rows.map(row => row.productId)));
|
||||
await syncTcgplayer();
|
||||
}
|
||||
else {
|
||||
await syncTcgplayer(args);
|
||||
}
|
||||
|
||||
// update the card table with new/updated variants
|
||||
await helper.UpdateVariants(db);
|
||||
|
||||
// index the card updates
|
||||
helper.upsertCardCollection(db);
|
||||
|
||||
await syncTcgplayer();
|
||||
await ClosePool();
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import chalk from 'chalk';
|
||||
import { db, ClosePool } from '../src/db/index.ts';
|
||||
import * as Indexing from './indexing.ts';
|
||||
import * as Indexing from './pokemon-helper.ts';
|
||||
|
||||
|
||||
await Indexing.createCardCollection();
|
||||
await Indexing.createSkuCollection();
|
||||
//await Indexing.createCardCollection();
|
||||
//await Indexing.createSkuCollection();
|
||||
await Indexing.upsertCardCollection(db);
|
||||
await Indexing.upsertSkuCollection(db);
|
||||
await ClosePool();
|
||||
|
||||
@@ -3,15 +3,11 @@ import 'dotenv/config';
|
||||
import chalk from 'chalk';
|
||||
import { db, ClosePool } from '../src/db/index.ts';
|
||||
import { sql, inArray, eq } from 'drizzle-orm';
|
||||
import { skus, processingSkus, priceHistory } from '../src/db/schema.ts';
|
||||
import { skus, processingSkus, priceHistory, salesHistory } from '../src/db/schema.ts';
|
||||
import { toSnakeCase } from 'drizzle-orm/casing';
|
||||
import * as Indexing from './indexing.ts';
|
||||
import * as helper from './pokemon-helper.ts';
|
||||
|
||||
|
||||
function sleep(ms: number) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function resetProcessingTable() {
|
||||
// Use sql.raw to execute the TRUNCATE TABLE statement
|
||||
await db.execute(sql.raw('TRUNCATE TABLE pokemon.processing_skus;'));
|
||||
@@ -21,6 +17,7 @@ async function resetProcessingTable() {
|
||||
async function syncPrices() {
|
||||
const batchSize = 1000;
|
||||
// const skuIndex = client.collections('skus');
|
||||
const updatedCards = new Set<number>();
|
||||
|
||||
await resetProcessingTable();
|
||||
console.log(chalk.green('Processing table reset and populated with current SKUs.'));
|
||||
@@ -60,7 +57,7 @@ async function syncPrices() {
|
||||
// remove skus from the 'working' processingSkus table
|
||||
await db.delete(processingSkus).where(inArray(processingSkus.skuId, skuIds));
|
||||
// be nice to the API and not send too many requests in a short time
|
||||
await sleep(200);
|
||||
await helper.Sleep(200);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -103,21 +100,63 @@ async function syncPrices() {
|
||||
});
|
||||
console.log(chalk.cyan(`${skuRows.length} history rows added.`));
|
||||
}
|
||||
for (const productId of skuRows.filter(row => row.calculatedAt != null).map(row => row.productId)) {
|
||||
updatedCards.add(productId);
|
||||
}
|
||||
}
|
||||
|
||||
// remove skus from the 'working' processingSkus table
|
||||
await db.delete(processingSkus).where(inArray(processingSkus.skuId, skuIds));
|
||||
|
||||
// be nice to the API and not send too many requests in a short time
|
||||
await sleep(200);
|
||||
await helper.Sleep(200);
|
||||
}
|
||||
|
||||
return updatedCards;
|
||||
}
|
||||
|
||||
const updateLatestSales = async (updatedCards: Set<number>) => {
|
||||
for (const productId of updatedCards.values()) {
|
||||
console.log(`Getting sale history for ${productId}`)
|
||||
const salesResponse = await fetch(`https://mpapi.tcgplayer.com/v2/product/${productId}/latestsales`,{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36'
|
||||
},
|
||||
body: JSON.stringify({ conditions:[], languages:[1], limit:25, listType:"All", variants:[] }),
|
||||
});
|
||||
if (!salesResponse.ok) {
|
||||
console.error('Error fetching sale history:', salesResponse.statusText);
|
||||
process.exit(1);
|
||||
}
|
||||
const salesData = await salesResponse.json();
|
||||
for (const sale of salesData.data) {
|
||||
const skuData = await db.query.skus.findFirst({ where: { productId: productId, variant: sale.variant, condition: sale.condition } });
|
||||
if (skuData) {
|
||||
await db.insert(salesHistory).values({
|
||||
skuId: skuData.skuId,
|
||||
orderDate: new Date(sale.orderDate),
|
||||
title: sale.title,
|
||||
customListingId: sale.customListingId,
|
||||
language: sale.language,
|
||||
listingType: sale.listingType,
|
||||
purchasePrice: sale.purchasePrice,
|
||||
quantity: sale.quantity,
|
||||
shippingPrice: sale.shippingPrice
|
||||
}).onConflictDoNothing();
|
||||
}
|
||||
}
|
||||
await helper.Sleep(500);
|
||||
}
|
||||
}
|
||||
|
||||
const start = Date.now();
|
||||
await syncPrices();
|
||||
await Indexing.upsertSkuCollection(db);
|
||||
const updatedCards = await syncPrices();
|
||||
await helper.upsertSkuCollection(db);
|
||||
//console.log(updatedCards);
|
||||
//console.log(updatedCards.size);
|
||||
//await updateLatestSales(updatedCards);
|
||||
await ClosePool();
|
||||
const end = Date.now();
|
||||
const duration = (end - start) / 1000;
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
import 'dotenv/config';
|
||||
import { db, ClosePool } from '../src/db/index.ts';
|
||||
import { sql } from 'drizzle-orm'
|
||||
|
||||
async function syncVariants() {
|
||||
const updates = await db.execute(sql`update cards as c
|
||||
set
|
||||
product_name = a.product_name, product_line_name = a.product_line_name, product_url_name = a.product_url_name, rarity_name = a.rarity_name,
|
||||
sealed = a.sealed, set_id = a.set_id, card_type = a.card_type, energy_type = a.energy_type, number = a.number, artist = a.artist
|
||||
from (
|
||||
select t.product_id, b.variant,
|
||||
coalesce(o.product_name, regexp_replace(regexp_replace(coalesce(nullif(t.product_name, ''), t.product_url_name),' \\\\(.*\\\\)',''),' - .*$','')) as product_name,
|
||||
coalesce(o.product_line_name, t.product_line_name) as product_line_name, coalesce(o.product_url_name, t.product_url_name) as product_url_name,
|
||||
coalesce(o.rarity_name, t.rarity_name) as rarity_name, coalesce(o.sealed, t.sealed) as sealed, coalesce(o.set_id, t.set_id) as set_id,
|
||||
coalesce(o.card_type, t.card_type) as card_type, coalesce(o.energy_type, t.energy_type) as energy_type,
|
||||
coalesce(o.number, t.number) as number, coalesce(o.artist, t.artist) as artist
|
||||
from tcg_cards t
|
||||
join (select distinct product_id, variant from skus) b on t.product_id = b.product_id
|
||||
left join tcg_overrides o on t.product_id = o.product_id
|
||||
) a
|
||||
where c.product_id = a.product_id and c.variant = a.variant and
|
||||
(
|
||||
c.product_name is distinct from a.product_name or c.product_line_name is distinct from a.product_line_name or
|
||||
c.product_url_name is distinct from a.product_url_name or c.rarity_name is distinct from a.rarity_name or
|
||||
c.sealed is distinct from a.sealed or c.set_id is distinct from a.set_id or c.card_type is distinct from a.card_type or
|
||||
c.energy_type is distinct from a.energy_type or c."number" is distinct from a."number" or c.artist is distinct from a.artist
|
||||
)
|
||||
`);
|
||||
console.log(`Updated ${updates.rowCount} rows in cards table`);
|
||||
|
||||
const inserts = await db.execute(sql`insert into cards (product_id, variant, product_name, product_line_name, product_url_name, rarity_name, sealed, set_id, card_type, energy_type, "number", artist)
|
||||
select t.product_id, b.variant,
|
||||
coalesce(o.product_name, regexp_replace(regexp_replace(coalesce(nullif(t.product_name, ''), t.product_url_name),' \\\\(.*\\\\)',''),' - .*$','')) as product_name,
|
||||
coalesce(o.product_line_name, t.product_line_name) as product_line_name, coalesce(o.product_url_name, t.product_url_name) as product_url_name, coalesce(o.rarity_name, t.rarity_name) as rarity_name,
|
||||
coalesce(o.sealed, t.sealed) as sealed, coalesce(o.set_id, t.set_id) as set_id, coalesce(o.card_type, t.card_type) as card_type,
|
||||
coalesce(o.energy_type, t.energy_type) as energy_type, coalesce(o.number, t.number) as number, coalesce(o.artist, t.artist) as artist
|
||||
from tcg_cards t
|
||||
join (select distinct product_id, variant from skus) b on t.product_id = b.product_id
|
||||
left join tcg_overrides o on t.product_id = o.product_id
|
||||
where not exists (select 1 from cards where product_id=t.product_id and variant=b.variant)
|
||||
`);
|
||||
console.log(`Inserted ${inserts.rowCount} rows into cards table`);
|
||||
|
||||
}
|
||||
|
||||
await syncVariants();
|
||||
await ClosePool();
|
||||
@@ -22,7 +22,7 @@
|
||||
@import 'bootstrap/scss/alert';
|
||||
@import 'bootstrap/scss/badge';
|
||||
// @import 'bootstrap/scss/breadcrumb';
|
||||
// @import 'bootstrap/scss/button-group';
|
||||
@import 'bootstrap/scss/button-group';
|
||||
@import 'bootstrap/scss/buttons';
|
||||
@import 'bootstrap/scss/card';
|
||||
// @import 'bootstrap/scss/carousel';
|
||||
|
||||
2115
src/assets/css/_card.scss
Normal file
685
src/assets/css/_holofoil-integration.scss
Normal file
@@ -0,0 +1,685 @@
|
||||
// =============================================================================
|
||||
// HOLOFOIL INTEGRATION
|
||||
// =============================================================================
|
||||
//
|
||||
// Three effect zones, determined by rarity and variant:
|
||||
//
|
||||
// NONE — no effect at all
|
||||
// variant = Normal (or no recognised rarity/variant)
|
||||
//
|
||||
// INVERSE — effect on borders only (everything except the art window)
|
||||
// variant = Reverse Holofoil
|
||||
// rarity = Prism Rare
|
||||
//
|
||||
// ART WINDOW — effect clipped to the artwork area only
|
||||
// rarity = Rare | Amazing Rare | Classic Collection | Holo Rare
|
||||
// variant = Holofoil | 1st Edition Holofoil
|
||||
//
|
||||
// FULL CARD — effect over the entire card
|
||||
// rarity = Ultra Rare | Character Rare | Illustration Rare |
|
||||
// Special Illustration Rare | Double Rare | Hyper Rare |
|
||||
// Mega Rare | Mega Attack Rare | ACE Spec Rare | ACE Rare |
|
||||
// Art Rare | Special Art Rare | Black White Rare |
|
||||
// Character Super Rare | Mega Ultra Rare | Rare BREAK |
|
||||
// Secret Rare | Shiny Holo Rare | Shiny Rare |
|
||||
// Shiny Secret Rare | Shiny Ultra Rare
|
||||
//
|
||||
// =============================================================================
|
||||
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// 1. CSS CUSTOM PROPERTIES — set on every wrapper element
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
.image-grow,
|
||||
.card-image-wrap {
|
||||
|
||||
// Pointer tracking — updated by holofoil-init.js on mousemove
|
||||
--pointer-x: 50%;
|
||||
--pointer-y: 50%;
|
||||
--background-x: 50%;
|
||||
--background-y: 50%;
|
||||
--pointer-from-center: 0;
|
||||
--pointer-from-top: 0.5;
|
||||
--pointer-from-left: 0.5;
|
||||
--card-opacity: 0;
|
||||
--card-scale: 1;
|
||||
|
||||
// Card geometry — matches Bootstrap's rounded-4 (--bs-border-radius-xl)
|
||||
--card-radius: var(--bs-border-radius-xl, 0.375rem);
|
||||
|
||||
// Art window clip — original poke-holo values, correct for standard TCG card scans
|
||||
// inset(top right bottom left): top=9.85%, sides=8%, bottom=52.85% (art bottom at 47.15%)
|
||||
--clip-art: inset(9.85% 8% 52.85% 8%);
|
||||
|
||||
// Sunpillar palette
|
||||
--sunpillar-1: hsl(2, 100%, 73%);
|
||||
--sunpillar-2: hsl(53, 100%, 69%);
|
||||
--sunpillar-3: hsl(93, 100%, 69%);
|
||||
--sunpillar-4: hsl(176, 100%, 76%);
|
||||
--sunpillar-5: hsl(228, 100%, 74%);
|
||||
--sunpillar-6: hsl(283, 100%, 73%);
|
||||
--sunpillar-clr-1: var(--sunpillar-1);
|
||||
--sunpillar-clr-2: var(--sunpillar-2);
|
||||
--sunpillar-clr-3: var(--sunpillar-3);
|
||||
--sunpillar-clr-4: var(--sunpillar-4);
|
||||
--sunpillar-clr-5: var(--sunpillar-5);
|
||||
--sunpillar-clr-6: var(--sunpillar-6);
|
||||
|
||||
// Colour tokens
|
||||
--red: #f80e35;
|
||||
--yellow: #eedf10;
|
||||
--green: #21e985;
|
||||
--blue: #0dbde9;
|
||||
--violet: #c929f1;
|
||||
|
||||
// Glow
|
||||
--card-glow: hsl(175, 100%, 90%);
|
||||
|
||||
// Texture assets
|
||||
--grain: url('/holofoils/grain.webp');
|
||||
--glitter: url('/holofoils/glitter.png');
|
||||
--glittersize: 25%;
|
||||
--foil: none;
|
||||
|
||||
// Energy glow overrides
|
||||
&[data-energy="Water"] { --card-glow: hsl(192, 97%, 60%); }
|
||||
&[data-energy="Fire"] { --card-glow: hsl(9, 81%, 59%); }
|
||||
&[data-energy="Grass"] { --card-glow: hsl(96, 81%, 65%); }
|
||||
&[data-energy="Lightning"] { --card-glow: hsl(54, 87%, 63%); }
|
||||
&[data-energy="Psychic"] { --card-glow: hsl(281, 62%, 58%); }
|
||||
&[data-energy="Fighting"] { --card-glow: rgb(145, 90, 39); }
|
||||
&[data-energy="Darkness"] { --card-glow: hsl(189, 77%, 27%); }
|
||||
&[data-energy="Metal"] { --card-glow: hsl(184, 20%, 70%); }
|
||||
&[data-energy="Dragon"] { --card-glow: hsl(51, 60%, 35%); }
|
||||
&[data-energy="Fairy"] { --card-glow: hsl(323, 100%, 89%); }
|
||||
|
||||
// Hover activates opacity; JS updates pointer vars
|
||||
&:hover,
|
||||
&[data-holo-active] { --card-opacity: 0.2; }
|
||||
|
||||
display: block; // ensure wrapper is a block-level containing block
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
border-radius: var(--card-radius);
|
||||
}
|
||||
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// 2. HOLO-SHINE AND HOLO-GLARE BASE STRUCTURE
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
.holo-shine,
|
||||
.holo-glare {
|
||||
pointer-events: none;
|
||||
display: block;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: var(--card-radius);
|
||||
// NO overflow:hidden — it interferes with clip-path on the element itself
|
||||
will-change: transform, opacity, background-image, background-size,
|
||||
background-position, background-blend-mode, filter;
|
||||
}
|
||||
|
||||
// The img inside has mb-2 but the wrapper already has the right size from
|
||||
// aspect-ratio on .card-image — zero the margin so img fills wrapper flush.
|
||||
.image-grow > img,
|
||||
.card-image-wrap > img {
|
||||
display: block;
|
||||
margin-bottom: 0 !important;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.holo-shine {
|
||||
z-index: 3;
|
||||
mix-blend-mode: color-dodge;
|
||||
opacity: var(--card-opacity);
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: var(--card-radius);
|
||||
// Sunpillar palette shift for ::before depth layer
|
||||
--sunpillar-clr-1: var(--sunpillar-5);
|
||||
--sunpillar-clr-2: var(--sunpillar-6);
|
||||
--sunpillar-clr-3: var(--sunpillar-1);
|
||||
--sunpillar-clr-4: var(--sunpillar-2);
|
||||
--sunpillar-clr-5: var(--sunpillar-3);
|
||||
--sunpillar-clr-6: var(--sunpillar-4);
|
||||
}
|
||||
|
||||
&::after {
|
||||
// Second palette shift for uppermost pseudo layer
|
||||
--sunpillar-clr-1: var(--sunpillar-6);
|
||||
--sunpillar-clr-2: var(--sunpillar-1);
|
||||
--sunpillar-clr-3: var(--sunpillar-2);
|
||||
--sunpillar-clr-4: var(--sunpillar-3);
|
||||
--sunpillar-clr-5: var(--sunpillar-4);
|
||||
--sunpillar-clr-6: var(--sunpillar-5);
|
||||
}
|
||||
}
|
||||
|
||||
.holo-glare {
|
||||
z-index: 4;
|
||||
mix-blend-mode: overlay;
|
||||
opacity: var(--card-opacity);
|
||||
background-image: radial-gradient(
|
||||
farthest-corner circle at var(--pointer-x) var(--pointer-y),
|
||||
hsla(0, 0%, 100%, 0.8) 10%,
|
||||
hsla(0, 0%, 100%, 0.65) 20%,
|
||||
hsla(0, 0%, 0%, 0.5) 90%
|
||||
);
|
||||
|
||||
// Grain texture on ::before — soft-light blend adds physical substrate feel
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image: var(--grain);
|
||||
background-size: 33%;
|
||||
background-repeat: repeat;
|
||||
mix-blend-mode: soft-light;
|
||||
opacity: 0.15;
|
||||
}
|
||||
|
||||
// Glitter texture on ::after — overlay blend adds sparkle points
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image: var(--glitter);
|
||||
background-size: var(--glittersize) var(--glittersize);
|
||||
background-repeat: repeat;
|
||||
background-position:
|
||||
calc(50% - ((5px * 2) * var(--pointer-from-left)) + 5px)
|
||||
calc(50% - ((5px * 2) * var(--pointer-from-top)) + 5px);
|
||||
mix-blend-mode: overlay;
|
||||
opacity: calc(var(--card-opacity) * 0.6);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// 3. ZONE HELPERS — reusable effect mixin
|
||||
// The standard prismatic effect, applied at different clip regions below.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
// Standard shine background (used by ART WINDOW and FULL CARD zones)
|
||||
@mixin prismatic-shine {
|
||||
background-image:
|
||||
var(--grain),
|
||||
repeating-linear-gradient(110deg,
|
||||
var(--violet), var(--blue), var(--green), var(--yellow), var(--red),
|
||||
var(--violet), var(--blue), var(--green), var(--yellow), var(--red),
|
||||
var(--violet), var(--blue), var(--green), var(--yellow), var(--red)
|
||||
);
|
||||
background-position:
|
||||
center center,
|
||||
calc(((50% - var(--background-x)) * 2.6) + 50%)
|
||||
calc(((50% - var(--background-y)) * 3.5) + 50%);
|
||||
background-size: 33%, 400% 400%;
|
||||
background-repeat: repeat, no-repeat;
|
||||
background-blend-mode: soft-light, normal;
|
||||
filter: brightness(.8) contrast(.85) saturate(.75);
|
||||
}
|
||||
|
||||
@mixin prismatic-glare {
|
||||
opacity: calc(var(--card-opacity) * 0.4);
|
||||
filter: brightness(0.8) contrast(1.5);
|
||||
}
|
||||
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// 4. ZONE 0 — NORMAL: no effect
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
.image-grow[data-variant="Normal" i],
|
||||
.card-image-wrap[data-variant="Normal" i] {
|
||||
.holo-shine,
|
||||
.holo-glare { display: none !important; }
|
||||
}
|
||||
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// 5. ZONE 1 — ART WINDOW EFFECT
|
||||
// rarity: Rare, Amazing Rare, Classic Collection, Holo Rare
|
||||
// variant: Holofoil, 1st Edition Holofoil
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
.image-grow[data-rarity="Rare" i]:not([data-variant="Reverse Holofoil" i]):not([data-variant="Normal" i]),
|
||||
.card-image-wrap[data-rarity="Rare" i]:not([data-variant="Reverse Holofoil" i]):not([data-variant="Normal" i]),
|
||||
.image-grow[data-rarity="Amazing Rare" i]:not([data-variant="Reverse Holofoil" i]),
|
||||
.card-image-wrap[data-rarity="Amazing Rare" i]:not([data-variant="Reverse Holofoil" i]),
|
||||
.image-grow[data-rarity="Classic Collection" i]:not([data-variant="Reverse Holofoil" i]),
|
||||
.card-image-wrap[data-rarity="Classic Collection" i]:not([data-variant="Reverse Holofoil" i]),
|
||||
.image-grow[data-rarity="Holo Rare" i]:not([data-variant="Reverse Holofoil" i]),
|
||||
.card-image-wrap[data-rarity="Holo Rare" i]:not([data-variant="Reverse Holofoil" i]),
|
||||
.image-grow[data-variant="Holofoil" i]:not([data-variant="Reverse Holofoil" i]),
|
||||
.card-image-wrap[data-variant="Holofoil" i]:not([data-variant="Reverse Holofoil" i]),
|
||||
.image-grow[data-variant="1st Edition Holofoil" i],
|
||||
.card-image-wrap[data-variant="1st Edition Holofoil" i] {
|
||||
|
||||
.holo-shine {
|
||||
clip-path: var(--clip-art);
|
||||
@include prismatic-shine;
|
||||
}
|
||||
|
||||
.holo-glare {
|
||||
clip-path: var(--clip-art);
|
||||
@include prismatic-glare;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// 5b. ZONE 1 BORDER ADDITION — Holofoil + 1st Edition Holofoil
|
||||
//
|
||||
// Real holofoil cards have the foil stamp on both the art window AND the card
|
||||
// border. The element carries the art window clip; ::before carries the border
|
||||
// clip via the same zero-width tunnel polygon as Zone 3.
|
||||
// ::before inherits background-image/size/position from the parent via inherit.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
.image-grow[data-variant="Holofoil" i]:not([data-variant="Reverse Holofoil" i]),
|
||||
.card-image-wrap[data-variant="Holofoil" i]:not([data-variant="Reverse Holofoil" i]),
|
||||
.image-grow[data-variant="1st Edition Holofoil" i],
|
||||
.card-image-wrap[data-variant="1st Edition Holofoil" i] {
|
||||
|
||||
.holo-shine {
|
||||
&::before {
|
||||
background-image: inherit;
|
||||
background-size: inherit;
|
||||
background-position: inherit;
|
||||
background-repeat: inherit;
|
||||
background-blend-mode: inherit;
|
||||
filter: inherit;
|
||||
mix-blend-mode: color-dodge;
|
||||
clip-path: polygon(
|
||||
0% 0%, 0% 100%, 100% 100%, 100% 0%, 0% 0%,
|
||||
8% 9.85%, 92% 9.85%, 92% 47.15%, 8% 47.15%, 8% 9.85%
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
.holo-glare {
|
||||
&::before {
|
||||
clip-path: polygon(
|
||||
0% 0%, 0% 100%, 100% 100%, 100% 0%, 0% 0%,
|
||||
8% 9.85%, 92% 9.85%, 92% 47.15%, 8% 47.15%, 8% 9.85%
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// 6. ZONE 2 — FULL CARD EFFECT
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
.image-grow[data-rarity="Ultra Rare" i]:not([data-variant="Reverse Holofoil" i]),
|
||||
.card-image-wrap[data-rarity="Ultra Rare" i]:not([data-variant="Reverse Holofoil" i]),
|
||||
.image-grow[data-rarity="Character Rare" i]:not([data-variant="Reverse Holofoil" i]),
|
||||
.card-image-wrap[data-rarity="Character Rare" i]:not([data-variant="Reverse Holofoil" i]),
|
||||
.image-grow[data-rarity="Illustration Rare" i]:not([data-variant="Reverse Holofoil" i]),
|
||||
.card-image-wrap[data-rarity="Illustration Rare" i]:not([data-variant="Reverse Holofoil" i]),
|
||||
.image-grow[data-rarity="Special Illustration Rare" i]:not([data-variant="Reverse Holofoil" i]),
|
||||
.card-image-wrap[data-rarity="Special Illustration Rare" i]:not([data-variant="Reverse Holofoil" i]),
|
||||
.image-grow[data-rarity="Double Rare" i]:not([data-variant="Reverse Holofoil" i]),
|
||||
.card-image-wrap[data-rarity="Double Rare" i]:not([data-variant="Reverse Holofoil" i]),
|
||||
.image-grow[data-rarity="Hyper Rare" i]:not([data-variant="Reverse Holofoil" i]),
|
||||
.card-image-wrap[data-rarity="Hyper Rare" i]:not([data-variant="Reverse Holofoil" i]),
|
||||
.image-grow[data-rarity="Mega Rare" i]:not([data-variant="Reverse Holofoil" i]),
|
||||
.card-image-wrap[data-rarity="Mega Rare" i]:not([data-variant="Reverse Holofoil" i]),
|
||||
.image-grow[data-rarity="Mega Attack Rare" i]:not([data-variant="Reverse Holofoil" i]),
|
||||
.card-image-wrap[data-rarity="Mega Attack Rare" i]:not([data-variant="Reverse Holofoil" i]),
|
||||
.image-grow[data-rarity="ACE Spec Rare" i]:not([data-variant="Reverse Holofoil" i]),
|
||||
.card-image-wrap[data-rarity="ACE Spec Rare" i]:not([data-variant="Reverse Holofoil" i]),
|
||||
.image-grow[data-rarity="ACE Rare" i]:not([data-variant="Reverse Holofoil" i]),
|
||||
.card-image-wrap[data-rarity="ACE Rare" i]:not([data-variant="Reverse Holofoil" i]),
|
||||
.image-grow[data-rarity="Art Rare" i]:not([data-variant="Reverse Holofoil" i]),
|
||||
.card-image-wrap[data-rarity="Art Rare" i]:not([data-variant="Reverse Holofoil" i]),
|
||||
.image-grow[data-rarity="Special Art Rare" i]:not([data-variant="Reverse Holofoil" i]),
|
||||
.card-image-wrap[data-rarity="Special Art Rare" i]:not([data-variant="Reverse Holofoil" i]),
|
||||
.image-grow[data-rarity="Black White Rare" i]:not([data-variant="Reverse Holofoil" i]),
|
||||
.card-image-wrap[data-rarity="Black White Rare" i]:not([data-variant="Reverse Holofoil" i]),
|
||||
.image-grow[data-rarity="Character Super Rare" i]:not([data-variant="Reverse Holofoil" i]),
|
||||
.card-image-wrap[data-rarity="Character Super Rare" i]:not([data-variant="Reverse Holofoil" i]),
|
||||
.image-grow[data-rarity="Mega Ultra Rare" i]:not([data-variant="Reverse Holofoil" i]),
|
||||
.card-image-wrap[data-rarity="Mega Ultra Rare" i]:not([data-variant="Reverse Holofoil" i]),
|
||||
.image-grow[data-rarity="Rare BREAK" i]:not([data-variant="Reverse Holofoil" i]),
|
||||
.card-image-wrap[data-rarity="Rare BREAK" i]:not([data-variant="Reverse Holofoil" i]),
|
||||
.image-grow[data-rarity="Secret Rare" i]:not([data-variant="Reverse Holofoil" i]),
|
||||
.card-image-wrap[data-rarity="Secret Rare" i]:not([data-variant="Reverse Holofoil" i]),
|
||||
.image-grow[data-rarity="Shiny Holo Rare" i]:not([data-variant="Reverse Holofoil" i]),
|
||||
.card-image-wrap[data-rarity="Shiny Holo Rare" i]:not([data-variant="Reverse Holofoil" i]),
|
||||
.image-grow[data-rarity="Shiny Rare" i]:not([data-variant="Reverse Holofoil" i]),
|
||||
.card-image-wrap[data-rarity="Shiny Rare" i]:not([data-variant="Reverse Holofoil" i]),
|
||||
.image-grow[data-rarity="Shiny Secret Rare" i]:not([data-variant="Reverse Holofoil" i]),
|
||||
.card-image-wrap[data-rarity="Shiny Secret Rare" i]:not([data-variant="Reverse Holofoil" i]),
|
||||
.image-grow[data-rarity="Shiny Ultra Rare" i]:not([data-variant="Reverse Holofoil" i]),
|
||||
.card-image-wrap[data-rarity="Shiny Ultra Rare" i]:not([data-variant="Reverse Holofoil" i]) {
|
||||
|
||||
.holo-shine {
|
||||
clip-path: none;
|
||||
@include prismatic-shine;
|
||||
}
|
||||
|
||||
.holo-glare {
|
||||
clip-path: none;
|
||||
@include prismatic-glare;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// 7. ZONE 3 — INVERSE (borders only): Reverse Holofoil + Prism Rare
|
||||
//
|
||||
// Applies the effect to everything EXCEPT the art window.
|
||||
// Uses the "zero-width tunnel" technique from css-tricks.com/cutting-inner-part-element-using-clip-path/
|
||||
// Outer rectangle drawn anticlockwise, closes back to 0% 0%, then the inner
|
||||
// art window rectangle is drawn clockwise — nonzero winding treats the inner
|
||||
// shape as a hole, leaving the art window transparent.
|
||||
//
|
||||
// Outer (anticlockwise): 0 0 → 0 100% → 100% 100% → 100% 0 → 0 0
|
||||
// Inner art window (clockwise): 8% 9.85% → 92% 9.85% → 92% 47.15% → 8% 47.15%
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
.image-grow[data-variant="Reverse Holofoil" i],
|
||||
.card-image-wrap[data-variant="Reverse Holofoil" i],
|
||||
.image-grow[data-rarity="Prism Rare" i],
|
||||
.card-image-wrap[data-rarity="Prism Rare" i] {
|
||||
|
||||
// Energy colour tint — multiply blend darkens the card toward --card-glow.
|
||||
// z-index 2 puts it above the card image (z-index 1) but below holo-shine (3).
|
||||
// Opacity tied to --card-opacity so it appears/disappears with the hover effect.
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: var(--card-radius);
|
||||
background: var(--card-glow);
|
||||
mix-blend-mode: multiply;
|
||||
opacity: calc(var(--card-opacity) * 0.5);
|
||||
z-index: 2;
|
||||
pointer-events: none;
|
||||
clip-path: polygon(
|
||||
0% 0%, 0% 100%, 100% 100%, 100% 0%, 0% 0%,
|
||||
8% 9.85%, 92% 9.85%, 92% 47.15%, 8% 47.15%, 8% 9.85%
|
||||
);
|
||||
}
|
||||
|
||||
.holo-shine {
|
||||
// Energy-aware gradient — weaves --card-glow (set per data-energy on the
|
||||
// wrapper) into the prismatic colour sequence so each energy type gets a
|
||||
// tinted shimmer: Grass = green, Fire = orange, Water = cyan, etc.
|
||||
background-image:
|
||||
var(--grain),
|
||||
repeating-linear-gradient(110deg,
|
||||
var(--card-glow),
|
||||
var(--blue),
|
||||
var(--card-glow),
|
||||
var(--green),
|
||||
var(--yellow),
|
||||
var(--card-glow),
|
||||
var(--red),
|
||||
var(--violet),
|
||||
var(--card-glow)
|
||||
);
|
||||
background-position:
|
||||
center center,
|
||||
calc(((50% - var(--background-x)) * 2.6) + 50%)
|
||||
calc(((50% - var(--background-y)) * 3.5) + 50%);
|
||||
background-size: 33%, 400% 400%;
|
||||
background-repeat: repeat, no-repeat;
|
||||
background-blend-mode: soft-light, normal;
|
||||
filter: brightness(1.0) contrast(1.0) saturate(1.4);
|
||||
clip-path: polygon(
|
||||
0% 0%, 0% 100%, 100% 100%, 100% 0%, 0% 0%,
|
||||
8% 9.85%, 92% 9.85%, 92% 47.15%, 8% 47.15%, 8% 9.85%
|
||||
) !important;
|
||||
}
|
||||
|
||||
.holo-glare {
|
||||
@include prismatic-glare;
|
||||
// Hot-spot tinted with energy colour to match the shine treatment
|
||||
background-image: radial-gradient(
|
||||
farthest-corner circle at var(--pointer-x) var(--pointer-y),
|
||||
var(--card-glow) 0%,
|
||||
hsla(0, 0%, 100%, 0.3) 20%,
|
||||
hsla(0, 0%, 0%, 0.5) 90%
|
||||
);
|
||||
clip-path: polygon(
|
||||
0% 0%, 0% 100%, 100% 100%, 100% 0%, 0% 0%,
|
||||
8% 9.85%, 92% 9.85%, 92% 47.15%, 8% 47.15%, 8% 9.85%
|
||||
) !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// 8. MODAL ANIMATION
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
// Note: --card-opacity is intentionally NOT registered via @property.
|
||||
// Registering it as <number> makes it interpolatable, causing the browser
|
||||
// to smoothly transition it when JS sets it via inline style — creating the
|
||||
// unwanted slow fade-in. Without registration it changes instantly.
|
||||
@property --pointer-x { syntax: '<percentage>'; inherits: true; initial-value: 50%; }
|
||||
@property --pointer-y { syntax: '<percentage>'; inherits: true; initial-value: 50%; }
|
||||
@property --background-x { syntax: '<percentage>'; inherits: true; initial-value: 50%; }
|
||||
@property --background-y { syntax: '<percentage>'; inherits: true; initial-value: 50%; }
|
||||
@property --pointer-from-center { syntax: '<number>'; inherits: true; initial-value: 0; }
|
||||
@property --pointer-from-left { syntax: '<number>'; inherits: true; initial-value: 0.5; }
|
||||
@property --pointer-from-top { syntax: '<number>'; inherits: true; initial-value: 0.5; }
|
||||
|
||||
@keyframes holo-modal-opacity {
|
||||
0% { opacity: 0; }
|
||||
4% { opacity: 0; }
|
||||
8% { opacity: 0.35; }
|
||||
85% { opacity: 0.35; }
|
||||
90%, 100%{ opacity: 0; }
|
||||
}
|
||||
|
||||
@keyframes holo-modal-position {
|
||||
0% {
|
||||
--pointer-x: 50%; --pointer-y: 50%;
|
||||
--background-x: 50%; --background-y: 50%;
|
||||
--pointer-from-center: 0; --pointer-from-left: 0.5; --pointer-from-top: 0.5;
|
||||
}
|
||||
8% {
|
||||
--pointer-x: 25%; --pointer-y: 15%;
|
||||
--background-x: 38%; --background-y: 28%;
|
||||
--pointer-from-center: 0.85; --pointer-from-left: 0.25; --pointer-from-top: 0.15;
|
||||
}
|
||||
25% {
|
||||
--pointer-x: 70%; --pointer-y: 30%;
|
||||
--background-x: 64%; --background-y: 34%;
|
||||
--pointer-from-center: 0.9; --pointer-from-left: 0.70; --pointer-from-top: 0.30;
|
||||
}
|
||||
45% {
|
||||
--pointer-x: 80%; --pointer-y: 70%;
|
||||
--background-x: 74%; --background-y: 68%;
|
||||
--pointer-from-center: 0.88; --pointer-from-left: 0.80; --pointer-from-top: 0.70;
|
||||
}
|
||||
65% {
|
||||
--pointer-x: 35%; --pointer-y: 80%;
|
||||
--background-x: 38%; --background-y: 76%;
|
||||
--pointer-from-center: 0.8; --pointer-from-left: 0.35; --pointer-from-top: 0.80;
|
||||
}
|
||||
85% {
|
||||
--pointer-x: 25%; --pointer-y: 15%;
|
||||
--background-x: 38%; --background-y: 28%;
|
||||
--pointer-from-center: 0.85;
|
||||
}
|
||||
100% {
|
||||
--pointer-x: 50%; --pointer-y: 50%;
|
||||
--background-x: 50%; --background-y: 50%;
|
||||
--pointer-from-center: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.card-image-wrap.holo-modal-mode {
|
||||
// Animate pointer vars on the wrapper so CSS custom props interpolate
|
||||
animation: holo-modal-position 60s ease-in-out infinite;
|
||||
animation-delay: var(--shimmer-delay, -2s);
|
||||
|
||||
.holo-shine,
|
||||
.holo-glare {
|
||||
// Animate opacity directly — no @property needed, native interpolation
|
||||
animation: holo-modal-opacity 60s ease-in-out infinite;
|
||||
animation-delay: var(--shimmer-delay, -2s);
|
||||
}
|
||||
|
||||
&[data-holo-active] {
|
||||
animation-play-state: paused;
|
||||
.holo-shine { opacity: 0.20; }
|
||||
.holo-glare { opacity: calc(0.20 * 0.4); }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// 9. MOBILE / TOUCH — static holofoil overlay, no JS tracking
|
||||
//
|
||||
// @media (hover: none) targets touchscreens only.
|
||||
// Technique from joshdance.com/100/Day50: two rainbow gradients at opposing
|
||||
// fixed positions interact via blend modes to create a static holographic sheen.
|
||||
// Where the two gradient bands cross, the additive blending creates bright
|
||||
// rainbow intersections that read as a light-catch effect — no pointer needed.
|
||||
//
|
||||
// Implementation:
|
||||
// - .holo-shine gets the two-gradient stack at fixed diagonal positions
|
||||
// - ::before moves in the opposite direction (negative position) so the
|
||||
// crossing point creates the characteristic holofoil bright intersection
|
||||
// - opacity is always-on at a low value — no hover event needed
|
||||
// - will-change reset to auto — no GPU layer reservation needed
|
||||
// - Glitter hidden — parallax position is meaningless without tracking
|
||||
// - No CSS animation on touch — pure static CSS, zero JS involvement
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
@media (hover: none) {
|
||||
|
||||
.holo-shine,
|
||||
.holo-glare {
|
||||
will-change: auto;
|
||||
}
|
||||
|
||||
// Disable any animation on modal cards on touch — static treatment handles it
|
||||
.card-image-wrap.holo-modal-mode {
|
||||
animation: none;
|
||||
|
||||
.holo-shine,
|
||||
.holo-glare { animation: none; }
|
||||
}
|
||||
|
||||
// Suppress glitter — parallax position is meaningless without pointer tracking
|
||||
.holo-glare::after { display: none; }
|
||||
|
||||
// ── Static holofoil overlay for all effect zones on touch ─────────────────
|
||||
// Override the JS-driven background-position values with fixed diagonals.
|
||||
// The ::before pseudo moves in the opposite direction to create crossing bands.
|
||||
|
||||
.image-grow,
|
||||
.card-image-wrap {
|
||||
|
||||
// Zone 1 — art window
|
||||
&[data-rarity="Rare" i]:not([data-variant="Reverse Holofoil" i]):not([data-variant="Normal" i]),
|
||||
&[data-rarity="Amazing Rare" i]:not([data-variant="Reverse Holofoil" i]),
|
||||
&[data-rarity="Classic Collection" i]:not([data-variant="Reverse Holofoil" i]),
|
||||
&[data-rarity="Holo Rare" i]:not([data-variant="Reverse Holofoil" i]),
|
||||
&[data-variant="Holofoil" i]:not([data-variant="Reverse Holofoil" i]),
|
||||
&[data-variant="1st Edition Holofoil" i],
|
||||
// Zone 2 — full card
|
||||
&[data-rarity="Ultra Rare" i]:not([data-variant="Reverse Holofoil" i]),
|
||||
&[data-rarity="Character Rare" i]:not([data-variant="Reverse Holofoil" i]),
|
||||
&[data-rarity="Illustration Rare" i]:not([data-variant="Reverse Holofoil" i]),
|
||||
&[data-rarity="Special Illustration Rare" i]:not([data-variant="Reverse Holofoil" i]),
|
||||
&[data-rarity="Double Rare" i]:not([data-variant="Reverse Holofoil" i]),
|
||||
&[data-rarity="Hyper Rare" i]:not([data-variant="Reverse Holofoil" i]),
|
||||
&[data-rarity="Mega Rare" i]:not([data-variant="Reverse Holofoil" i]),
|
||||
&[data-rarity="Mega Attack Rare" i]:not([data-variant="Reverse Holofoil" i]),
|
||||
&[data-rarity="ACE Spec Rare" i]:not([data-variant="Reverse Holofoil" i]),
|
||||
&[data-rarity="ACE Rare" i]:not([data-variant="Reverse Holofoil" i]),
|
||||
&[data-rarity="Art Rare" i]:not([data-variant="Reverse Holofoil" i]),
|
||||
&[data-rarity="Special Art Rare" i]:not([data-variant="Reverse Holofoil" i]),
|
||||
&[data-rarity="Black White Rare" i]:not([data-variant="Reverse Holofoil" i]),
|
||||
&[data-rarity="Character Super Rare" i]:not([data-variant="Reverse Holofoil" i]),
|
||||
&[data-rarity="Mega Ultra Rare" i]:not([data-variant="Reverse Holofoil" i]),
|
||||
&[data-rarity="Rare BREAK" i]:not([data-variant="Reverse Holofoil" i]),
|
||||
&[data-rarity="Secret Rare" i]:not([data-variant="Reverse Holofoil" i]),
|
||||
&[data-rarity="Shiny Holo Rare" i]:not([data-variant="Reverse Holofoil" i]),
|
||||
&[data-rarity="Shiny Rare" i]:not([data-variant="Reverse Holofoil" i]),
|
||||
&[data-rarity="Shiny Secret Rare" i]:not([data-variant="Reverse Holofoil" i]),
|
||||
&[data-rarity="Shiny Ultra Rare" i]:not([data-variant="Reverse Holofoil" i]),
|
||||
// Zone 3 — inverse (Reverse Holofoil + Prism Rare)
|
||||
// Energy colour woven in via --card-glow, same as desktop Zone 3
|
||||
&[data-variant="Reverse Holofoil" i],
|
||||
&[data-rarity="Prism Rare" i] {
|
||||
|
||||
// Energy colour multiply tint — kept subtle on mobile
|
||||
&::after {
|
||||
opacity: 0.04;
|
||||
}
|
||||
|
||||
.holo-shine {
|
||||
background-image:
|
||||
var(--grain),
|
||||
repeating-linear-gradient(110deg,
|
||||
var(--card-glow),
|
||||
var(--blue),
|
||||
var(--card-glow),
|
||||
var(--green),
|
||||
var(--yellow),
|
||||
var(--card-glow),
|
||||
var(--red),
|
||||
var(--violet),
|
||||
var(--card-glow)
|
||||
);
|
||||
background-size: 33%, 400% 400%;
|
||||
background-repeat: repeat, no-repeat;
|
||||
background-blend-mode: soft-light, normal;
|
||||
background-position: center, 38% 25%;
|
||||
filter: brightness(1.0) contrast(1.1) saturate(1.0);
|
||||
opacity: 0.35;
|
||||
|
||||
&::before {
|
||||
background-image:
|
||||
repeating-linear-gradient(110deg,
|
||||
var(--card-glow),
|
||||
var(--blue),
|
||||
var(--card-glow),
|
||||
var(--green),
|
||||
var(--yellow),
|
||||
var(--card-glow),
|
||||
var(--red),
|
||||
var(--violet),
|
||||
var(--card-glow)
|
||||
);
|
||||
background-size: 400% 400%;
|
||||
background-position: 62% 75%;
|
||||
mix-blend-mode: color-dodge;
|
||||
opacity: 0.18;
|
||||
filter: brightness(1.0) contrast(1.1) saturate(1.0);
|
||||
}
|
||||
|
||||
&::after { display: none; }
|
||||
}
|
||||
|
||||
.holo-glare {
|
||||
opacity: 0.15;
|
||||
background-image: radial-gradient(
|
||||
farthest-corner circle at 35% 25%,
|
||||
var(--card-glow) 0%,
|
||||
hsla(0, 0%, 100%, 0.2) 30%,
|
||||
hsla(0, 0%, 0%, 0.3) 90%
|
||||
);
|
||||
filter: brightness(0.8) contrast(1.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,9 @@ $container-max-widths: (
|
||||
|
||||
@import "_bootstrap";
|
||||
|
||||
// ── Holofoil ──────────────────────────────────────────────────────────────
|
||||
@import "_holofoil-integration"; // also pulls in _card.scss
|
||||
|
||||
/* --------------------------------------------------
|
||||
Root Variables
|
||||
-------------------------------------------------- */
|
||||
@@ -292,7 +295,7 @@ $tiers: (
|
||||
.card-image {
|
||||
aspect-ratio: 23 / 32;
|
||||
object-fit: cover;
|
||||
z-index: 998;
|
||||
z-index: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -360,6 +363,7 @@ $tiers: (
|
||||
bottom: 5vh;
|
||||
right: 5vw;
|
||||
display: none;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.top-icon svg {
|
||||
@@ -400,6 +404,7 @@ $tiers: (
|
||||
|
||||
.price-row {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
margin-top: -1.25rem;
|
||||
border-radius: 0.33rem;
|
||||
background: linear-gradient(
|
||||
|
||||
260
src/assets/js/holofoil-init.js
Normal file
@@ -0,0 +1,260 @@
|
||||
/**
|
||||
* holofoil-init.js
|
||||
* Instruments .image-grow and .card-image-wrap with the holofoil effect system.
|
||||
*/
|
||||
|
||||
(function HolofoilSystem() {
|
||||
|
||||
'use strict';
|
||||
|
||||
// Variants that receive NO effect
|
||||
const NO_EFFECT_VARIANTS = new Set(['normal']);
|
||||
|
||||
// Variants that always receive an effect regardless of rarity
|
||||
const HOLO_VARIANTS = new Set([
|
||||
'reverse holofoil',
|
||||
'holofoil',
|
||||
'1st edition holofoil',
|
||||
]);
|
||||
|
||||
// Rarities that receive an effect
|
||||
const HOLO_RARITIES = new Set([
|
||||
// Art window zone
|
||||
'rare',
|
||||
'amazing rare',
|
||||
'classic collection',
|
||||
'holo rare',
|
||||
// Full card zone
|
||||
'ultra rare',
|
||||
'character rare',
|
||||
'illustration rare',
|
||||
'special illustration rare',
|
||||
'double rare',
|
||||
'hyper rare',
|
||||
'mega rare',
|
||||
'mega attack rare',
|
||||
'ace spec rare',
|
||||
'ace rare',
|
||||
'art rare',
|
||||
'special art rare',
|
||||
'black white rare',
|
||||
'character super rare',
|
||||
'mega ultra rare',
|
||||
'rare break',
|
||||
'secret rare',
|
||||
'shiny holo rare',
|
||||
'shiny rare',
|
||||
'shiny secret rare',
|
||||
'shiny ultra rare',
|
||||
// Inverse zone
|
||||
'prism rare',
|
||||
]);
|
||||
|
||||
const ALL_WRAPPERS_SEL = '.image-grow, .card-image-wrap';
|
||||
|
||||
const rand = (min, max) => parseFloat((Math.random() * (max - min) + min).toFixed(2));
|
||||
const clamp01 = n => Math.max(0, Math.min(1, n));
|
||||
|
||||
function pointerVars(x, y, rect) {
|
||||
const fromLeft = clamp01((x - rect.left) / rect.width);
|
||||
const fromTop = clamp01((y - rect.top) / rect.height);
|
||||
const fromCenter = clamp01(Math.sqrt((fromLeft - 0.5) ** 2 + (fromTop - 0.5) ** 2) * 2);
|
||||
return {
|
||||
px: fromLeft * 100,
|
||||
py: fromTop * 100,
|
||||
fromLeft,
|
||||
fromTop,
|
||||
fromCenter,
|
||||
bgX: 50 + (fromLeft - 0.5) * -30,
|
||||
bgY: 50 + (fromTop - 0.5) * -30,
|
||||
};
|
||||
}
|
||||
|
||||
function applyPointerVars(el, v) {
|
||||
el.style.setProperty('--pointer-x', v.px.toFixed(1) + '%');
|
||||
el.style.setProperty('--pointer-y', v.py.toFixed(1) + '%');
|
||||
el.style.setProperty('--pointer-from-left', v.fromLeft.toFixed(3));
|
||||
el.style.setProperty('--pointer-from-top', v.fromTop.toFixed(3));
|
||||
el.style.setProperty('--pointer-from-center', v.fromCenter.toFixed(3));
|
||||
el.style.setProperty('--background-x', v.bgX.toFixed(1) + '%');
|
||||
el.style.setProperty('--background-y', v.bgY.toFixed(1) + '%');
|
||||
}
|
||||
|
||||
function shouldHaveEffect(el) {
|
||||
if (el.dataset.default === 'true') return false;
|
||||
// Also check if the card image itself is the default fallback
|
||||
const img = el.querySelector('img');
|
||||
if (img && img.src && img.src.endsWith('/cards/default.jpg')) return false;
|
||||
const variant = (el.dataset.variant || '').toLowerCase().trim();
|
||||
const rarity = (el.dataset.rarity || '').toLowerCase().trim();
|
||||
if (NO_EFFECT_VARIANTS.has(variant)) return false;
|
||||
if (HOLO_VARIANTS.has(variant)) return true;
|
||||
if (HOLO_RARITIES.has(rarity)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function injectChildren(el) {
|
||||
if (el.querySelector('.holo-shine')) return;
|
||||
const shine = document.createElement('div');
|
||||
shine.className = 'holo-shine';
|
||||
const glare = document.createElement('div');
|
||||
glare.className = 'holo-glare';
|
||||
el.appendChild(shine);
|
||||
el.appendChild(glare);
|
||||
}
|
||||
|
||||
function watchForDefault(el) {
|
||||
if (el.dataset.default === 'true') return;
|
||||
var observer = new MutationObserver(function() {
|
||||
if (el.dataset.default === 'true') {
|
||||
var shine = el.querySelector('.holo-shine');
|
||||
var glare = el.querySelector('.holo-glare');
|
||||
if (shine) shine.style.display = 'none';
|
||||
if (glare) glare.style.display = 'none';
|
||||
observer.disconnect();
|
||||
}
|
||||
});
|
||||
observer.observe(el, { attributes: true, attributeFilter: ['data-default'] });
|
||||
}
|
||||
|
||||
const canHover = window.matchMedia('(hover: hover)').matches;
|
||||
|
||||
function stamp(el) {
|
||||
if (el.dataset.holoInit) return;
|
||||
if (!shouldHaveEffect(el)) {
|
||||
el.dataset.holoInit = 'skip';
|
||||
return;
|
||||
}
|
||||
injectChildren(el);
|
||||
if (el.classList.contains('card-image-wrap')) {
|
||||
if (canHover) {
|
||||
// Desktop: use hover + pointer tracking, same as grid cards.
|
||||
// No animation — CSS :hover rule controls --card-opacity directly.
|
||||
el.classList.remove('holo-modal-mode');
|
||||
} else {
|
||||
// Touch: use the autonomous CSS animation sweep.
|
||||
el.classList.add('holo-modal-mode');
|
||||
el.style.setProperty('--shimmer-delay', rand(-8, 0) + 's');
|
||||
}
|
||||
}
|
||||
watchForDefault(el);
|
||||
el.dataset.holoInit = '1';
|
||||
}
|
||||
|
||||
const pointerState = new WeakMap();
|
||||
|
||||
function onPointerEnter(e) {
|
||||
const el = e.currentTarget;
|
||||
if (el.dataset.holoInit !== '1') return;
|
||||
el.dataset.holoActive = '1';
|
||||
// Inline style wins over CSS immediately — @property not registered for
|
||||
// --card-opacity so no interpolation. All calc() multipliers in child
|
||||
// rules (glare * 0.4, glitter * 0.6) work correctly from this single var.
|
||||
el.style.setProperty('--card-opacity', '0.2');
|
||||
if (!pointerState.has(el)) pointerState.set(el, { rafId: null });
|
||||
}
|
||||
|
||||
function onPointerMove(e) {
|
||||
const el = e.currentTarget;
|
||||
if (el.dataset.holoInit !== '1') return;
|
||||
const state = pointerState.get(el);
|
||||
if (!state) return;
|
||||
if (state.rafId) cancelAnimationFrame(state.rafId);
|
||||
state.rafId = requestAnimationFrame(function() {
|
||||
applyPointerVars(el, pointerVars(e.clientX, e.clientY, el.getBoundingClientRect()));
|
||||
state.rafId = null;
|
||||
});
|
||||
}
|
||||
|
||||
function onPointerLeave(e) {
|
||||
const el = e.currentTarget;
|
||||
if (el.dataset.holoInit !== '1') return;
|
||||
const state = pointerState.get(el);
|
||||
if (state && state.rafId) { cancelAnimationFrame(state.rafId); state.rafId = null; }
|
||||
delete el.dataset.holoActive;
|
||||
// Remove inline style so CSS default (--card-opacity: 0) takes over instantly
|
||||
el.style.removeProperty('--card-opacity');
|
||||
}
|
||||
|
||||
function attachListeners(el) {
|
||||
if (el.dataset.holoListeners) return;
|
||||
// On touch-only devices the CSS static shimmer handles the effect.
|
||||
// Skip JS pointer tracking — pointermove never fires on touchscreens
|
||||
// and registering listeners wastes memory with no benefit.
|
||||
if (!window.matchMedia('(hover: hover)').matches) return;
|
||||
el.addEventListener('pointerenter', onPointerEnter, { passive: true });
|
||||
el.addEventListener('pointermove', onPointerMove, { passive: true });
|
||||
el.addEventListener('pointerleave', onPointerLeave, { passive: true });
|
||||
el.dataset.holoListeners = '1';
|
||||
}
|
||||
|
||||
function stampAll(root) {
|
||||
(root || document).querySelectorAll(ALL_WRAPPERS_SEL).forEach(function(el) {
|
||||
stamp(el);
|
||||
if (el.dataset.holoInit === '1') attachListeners(el);
|
||||
});
|
||||
}
|
||||
|
||||
function observeGrid() {
|
||||
var grid = document.getElementById('cardGrid');
|
||||
if (!grid) return;
|
||||
new MutationObserver(function(mutations) {
|
||||
mutations.forEach(function(m) {
|
||||
m.addedNodes.forEach(function(node) {
|
||||
if (node.nodeType !== 1) return;
|
||||
if (node.matches && node.matches(ALL_WRAPPERS_SEL)) {
|
||||
stamp(node);
|
||||
if (node.dataset.holoInit === '1') attachListeners(node);
|
||||
}
|
||||
if (node.querySelectorAll) {
|
||||
node.querySelectorAll(ALL_WRAPPERS_SEL).forEach(function(el) {
|
||||
stamp(el);
|
||||
if (el.dataset.holoInit === '1') attachListeners(el);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}).observe(grid, { childList: true, subtree: true });
|
||||
}
|
||||
|
||||
function observeModal() {
|
||||
var modal = document.getElementById('cardModal');
|
||||
if (!modal) return;
|
||||
|
||||
new MutationObserver(function(mutations) {
|
||||
mutations.forEach(function(m) {
|
||||
m.addedNodes.forEach(function(node) {
|
||||
if (node.nodeType !== 1) return;
|
||||
|
||||
var wrappers = [];
|
||||
if (node.matches && node.matches(ALL_WRAPPERS_SEL)) wrappers.push(node);
|
||||
if (node.querySelectorAll) {
|
||||
node.querySelectorAll(ALL_WRAPPERS_SEL).forEach(function(el) {
|
||||
wrappers.push(el);
|
||||
});
|
||||
}
|
||||
|
||||
wrappers.forEach(function(el) {
|
||||
// Reset stamp so each new card is evaluated fresh
|
||||
delete el.dataset.holoInit;
|
||||
stamp(el);
|
||||
if (el.dataset.holoInit === '1') attachListeners(el);
|
||||
});
|
||||
});
|
||||
});
|
||||
}).observe(modal, { childList: true, subtree: true });
|
||||
}
|
||||
|
||||
function init() {
|
||||
stampAll();
|
||||
observeGrid();
|
||||
observeModal();
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
|
||||
})();
|
||||
@@ -39,10 +39,27 @@ function buildChartData(history, rangeKey) {
|
||||
|
||||
const filtered = history.filter(r => new Date(r.calculatedAt) >= cutoff);
|
||||
|
||||
const allDates = [...new Set(filtered.map(r => r.calculatedAt))]
|
||||
.sort((a, b) => new Date(a) - new Date(b));
|
||||
// Always build the full date axis for the selected window, even if sparse.
|
||||
// Generate one label per day in the range so the x-axis reflects the
|
||||
// chosen period rather than collapsing to only the days that have data.
|
||||
const dataDateSet = new Set(filtered.map(r => r.calculatedAt));
|
||||
const allDates = [...dataDateSet].sort((a, b) => new Date(a) - new Date(b));
|
||||
|
||||
const labels = allDates.map(formatDate);
|
||||
// If we have real data, expand the axis to span from cutoff → today so
|
||||
// empty stretches at the start/end of a range are visible.
|
||||
let axisLabels = allDates;
|
||||
if (allDates.length > 0 && RANGE_DAYS[rangeKey] !== Infinity) {
|
||||
const start = new Date(cutoff);
|
||||
const end = new Date();
|
||||
const expanded = [];
|
||||
// Step through every day in the window
|
||||
for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) {
|
||||
expanded.push(d.toISOString().split('T')[0]);
|
||||
}
|
||||
axisLabels = expanded;
|
||||
}
|
||||
|
||||
const labels = axisLabels.map(formatDate);
|
||||
|
||||
const lookup = {};
|
||||
for (const row of filtered) {
|
||||
@@ -50,16 +67,14 @@ function buildChartData(history, rangeKey) {
|
||||
lookup[row.condition][row.calculatedAt] = Number(row.marketPrice);
|
||||
}
|
||||
|
||||
// Check specifically whether the active condition has any data points
|
||||
const activeConditionDates = allDates.filter(
|
||||
const activeConditionHasData = allDates.some(
|
||||
date => lookup[activeCondition]?.[date] != null
|
||||
);
|
||||
const activeConditionHasData = activeConditionDates.length > 0;
|
||||
|
||||
const datasets = CONDITIONS.map(condition => {
|
||||
const isActive = condition === activeCondition;
|
||||
const colors = CONDITION_COLORS[condition];
|
||||
const data = allDates.map(date => lookup[condition]?.[date] ?? null);
|
||||
const data = axisLabels.map(date => lookup[condition]?.[date] ?? null);
|
||||
return {
|
||||
label: condition,
|
||||
data,
|
||||
@@ -75,23 +90,29 @@ function buildChartData(history, rangeKey) {
|
||||
};
|
||||
});
|
||||
|
||||
return { labels, datasets, hasData: allDates.length > 0, activeConditionHasData };
|
||||
return {
|
||||
labels,
|
||||
datasets,
|
||||
hasData: allDates.length > 0,
|
||||
activeConditionHasData,
|
||||
};
|
||||
}
|
||||
|
||||
function updateChart() {
|
||||
if (!chartInstance) return;
|
||||
const { labels, datasets, hasData, activeConditionHasData } = buildChartData(allHistory, activeRange);
|
||||
|
||||
// Show empty state if no data at all, or if the active condition specifically has no data
|
||||
if (!hasData || !activeConditionHasData) {
|
||||
setEmptyState(true);
|
||||
return;
|
||||
}
|
||||
|
||||
setEmptyState(false);
|
||||
chartInstance.data.labels = labels;
|
||||
// Always push the new labels/datasets to the chart so the x-axis
|
||||
// reflects the selected time window — even when there's no data for
|
||||
// the active condition. Then toggle the empty state overlay on top.
|
||||
chartInstance.data.labels = labels;
|
||||
chartInstance.data.datasets = datasets;
|
||||
chartInstance.update('none');
|
||||
|
||||
// Show the empty state overlay if the active condition has no points
|
||||
// in this window, but leave the (empty) chart visible underneath so
|
||||
// the axis communicates the selected period.
|
||||
setEmptyState(!hasData || !activeConditionHasData);
|
||||
}
|
||||
|
||||
function initPriceChart(canvas) {
|
||||
@@ -114,12 +135,8 @@ function initPriceChart(canvas) {
|
||||
|
||||
const { labels, datasets, hasData, activeConditionHasData } = buildChartData(allHistory, activeRange);
|
||||
|
||||
if (!hasData || !activeConditionHasData) {
|
||||
setEmptyState(true);
|
||||
return;
|
||||
}
|
||||
|
||||
setEmptyState(false);
|
||||
// Render the chart regardless — show empty state overlay if needed
|
||||
setEmptyState(!hasData || !activeConditionHasData);
|
||||
|
||||
chartInstance = new Chart(canvas.getContext('2d'), {
|
||||
type: 'line',
|
||||
|
||||
@@ -15,7 +15,7 @@ import BackToTop from "./BackToTop.astro"
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-10 mt-0">
|
||||
<div class="d-flex flex-row align-items-center mb-2">
|
||||
<div class="d-flex flex-column gap-1 flex-md-row align-items-center mb-3 w-100 justify-content-start">
|
||||
<div id="sortBy"></div>
|
||||
<div id="totalResults"></div>
|
||||
<div id="activeFilters"></div>
|
||||
@@ -44,16 +44,15 @@ import BackToTop from "./BackToTop.astro"
|
||||
|
||||
<BackToTop />
|
||||
|
||||
<script src="src/assets/js/holofoil-init.js" is:inline></script>
|
||||
|
||||
<script is:inline>
|
||||
(function () {
|
||||
|
||||
// ── Sort dropdown ─────────────────────────────────────────────────────────
|
||||
// Plain JS toggle — no dependency on Bootstrap's Dropdown JS initialising.
|
||||
// Uses event delegation so it works after OOB swaps repopulate #sortBy.
|
||||
document.addEventListener('click', (e) => {
|
||||
const sortBy = document.getElementById('sortBy');
|
||||
|
||||
// Toggle the menu when the button is clicked
|
||||
const btn = e.target.closest('#sortBy [data-bs-toggle="dropdown"]');
|
||||
if (btn) {
|
||||
e.preventDefault();
|
||||
@@ -64,7 +63,6 @@ import BackToTop from "./BackToTop.astro"
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle sort option selection
|
||||
const opt = e.target.closest('#sortBy .sort-option');
|
||||
if (opt) {
|
||||
e.preventDefault();
|
||||
@@ -87,7 +85,6 @@ import BackToTop from "./BackToTop.astro"
|
||||
return;
|
||||
}
|
||||
|
||||
// Click outside — close any open sort menu
|
||||
const menu = document.querySelector('#sortBy .dropdown-menu.show');
|
||||
if (menu) {
|
||||
menu.classList.remove('show');
|
||||
@@ -96,6 +93,23 @@ import BackToTop from "./BackToTop.astro"
|
||||
}
|
||||
});
|
||||
|
||||
// ── Language toggle ───────────────────────────────────────────────────────
|
||||
document.addEventListener('click', (e) => {
|
||||
const btn = e.target.closest('.language-btn');
|
||||
if (!btn) return;
|
||||
e.preventDefault();
|
||||
|
||||
const input = document.getElementById('languageInput');
|
||||
if (input) input.value = btn.dataset.lang;
|
||||
|
||||
const start = document.getElementById('start');
|
||||
if (start) start.value = '0';
|
||||
|
||||
document.getElementById('searchform').dispatchEvent(
|
||||
new Event('submit', { bubbles: true, cancelable: true })
|
||||
);
|
||||
});
|
||||
|
||||
// ── Global helpers ────────────────────────────────────────────────────────
|
||||
window.copyImage = async function(img) {
|
||||
try {
|
||||
@@ -103,7 +117,15 @@ import BackToTop from "./BackToTop.astro"
|
||||
const ctx = canvas.getContext("2d");
|
||||
canvas.width = img.naturalWidth;
|
||||
canvas.height = img.naturalHeight;
|
||||
ctx.drawImage(img, 0, 0);
|
||||
|
||||
// Load with crossOrigin so toBlob() stays untainted
|
||||
await new Promise((resolve) => {
|
||||
const clean = new Image();
|
||||
clean.crossOrigin = 'anonymous';
|
||||
clean.onload = () => { ctx.drawImage(clean, 0, 0); resolve(); };
|
||||
clean.onerror = () => { ctx.drawImage(img, 0, 0); resolve(); };
|
||||
clean.src = img.src;
|
||||
});
|
||||
|
||||
if (navigator.clipboard && navigator.clipboard.write) {
|
||||
const blob = await new Promise((resolve, reject) => {
|
||||
@@ -201,7 +223,6 @@ import BackToTop from "./BackToTop.astro"
|
||||
nextBtn.classList.toggle('d-none', next === null);
|
||||
}
|
||||
|
||||
// ── Trigger infinite scroll sentinel ─────────────────────────────────────
|
||||
function tryTriggerSentinel() {
|
||||
const sentinel = cardGrid.querySelector('[hx-trigger="revealed"]');
|
||||
if (!sentinel) return;
|
||||
@@ -212,7 +233,6 @@ import BackToTop from "./BackToTop.astro"
|
||||
}
|
||||
}
|
||||
|
||||
// ── Fire card-modal:swapped so the partial's script can init the chart ────
|
||||
function initChartAfterSwap(modal) {
|
||||
const canvas = modal.querySelector('#priceHistoryChart');
|
||||
if (!canvas) return;
|
||||
@@ -269,11 +289,9 @@ import BackToTop from "./BackToTop.astro"
|
||||
if (next) loadCard(next, 'next');
|
||||
}
|
||||
|
||||
// ── Nav button clicks ─────────────────────────────────────────────────────
|
||||
document.getElementById('modalPrevBtn').addEventListener('click', navigatePrev);
|
||||
document.getElementById('modalNextBtn').addEventListener('click', navigateNext);
|
||||
|
||||
// ── Keyboard ──────────────────────────────────────────────────────────────
|
||||
document.addEventListener('keydown', (e) => {
|
||||
const modal = document.getElementById('cardModal');
|
||||
if (!modal.classList.contains('show')) return;
|
||||
@@ -281,7 +299,6 @@ import BackToTop from "./BackToTop.astro"
|
||||
if (e.key === 'ArrowRight') { e.preventDefault(); navigateNext(); }
|
||||
});
|
||||
|
||||
// ── Touch / swipe ─────────────────────────────────────────────────────────
|
||||
let touchStartX = 0;
|
||||
let touchStartY = 0;
|
||||
const SWIPE_THRESHOLD = 50;
|
||||
@@ -299,7 +316,6 @@ import BackToTop from "./BackToTop.astro"
|
||||
else navigatePrev();
|
||||
}, { passive: true });
|
||||
|
||||
// ── HTMX card-modal opens ─────────────────────────────────────────────────
|
||||
document.body.addEventListener('htmx:beforeRequest', async (e) => {
|
||||
if (e.detail.elt.getAttribute('hx-target') !== '#cardModal') return;
|
||||
|
||||
@@ -363,7 +379,6 @@ import BackToTop from "./BackToTop.astro"
|
||||
}
|
||||
});
|
||||
|
||||
// ── Bootstrap modal events ────────────────────────────────────────────────
|
||||
const cardModal = document.getElementById('cardModal');
|
||||
cardModal.addEventListener('shown.bs.modal', () => {
|
||||
updateNavButtons(cardModal);
|
||||
@@ -373,6 +388,5 @@ import BackToTop from "./BackToTop.astro"
|
||||
currentCardId = null;
|
||||
updateNavButtons(null);
|
||||
});
|
||||
|
||||
})();
|
||||
</script>
|
||||
@@ -26,15 +26,21 @@ import { Show } from '@clerk/astro/components'
|
||||
</script>
|
||||
|
||||
<Show when="signed-in">
|
||||
<form class="d-flex ms-2" role="search" id="searchform" hx-post="/partials/cards" hx-target="#cardGrid" hx-trigger="load, submit" hx-vals='{"start":"0"}' hx-on--after-request="afterUpdate()" hx-on--before-request="beforeSearch()">
|
||||
<a class="btn btn-secondary btn-lg me-2" data-bs-toggle="offcanvas" href="#filterBar" role="button" aria-controls="filterBar" aria-label="filter"><span class="d-block d-md-none filter-icon mt-1"><svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><path opacity=".4" d="M96 160L283.3 347.3C286.3 350.3 288 354.4 288 358.6L288 480L352 544L352 358.6C352 354.4 353.7 350.3 356.7 347.3L544 160L96 160z"/><path d="M66.4 147.8C71.4 135.8 83.1 128 96 128L544 128C556.9 128 568.6 135.8 573.6 147.8C578.6 159.8 575.8 173.5 566.7 182.7L384 365.3L384 544C384 556.9 376.2 568.6 364.2 573.6C352.2 578.6 338.5 575.8 329.3 566.7L265.3 502.7C259.3 496.7 255.9 488.6 255.9 480.1L256 365.3L73.4 182.6C64.2 173.5 61.5 159.7 66.4 147.8zM544 160L96 160L283.3 347.3C286.3 350.3 288 354.4 288 358.6L288 480L352 544L352 358.6C352 354.4 353.7 350.3 356.7 347.3L544 160z"/></svg></span><span class="d-none d-md-block">Filters</span></a>
|
||||
<form class="d-flex ms-2 align-items-center gap-2" role="search" id="searchform" hx-post="/partials/cards" hx-target="#cardGrid" hx-trigger="load, submit" hx-vals='{"start":"0"}' hx-on--after-request="afterUpdate()" hx-on--before-request="beforeSearch()">
|
||||
<a class="btn btn-secondary btn-lg" data-bs-toggle="offcanvas" href="#filterBar" role="button" aria-controls="filterBar" aria-label="filter">
|
||||
<span class="d-block d-md-none filter-icon py-2">
|
||||
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><path opacity=".4" d="M96 160L283.3 347.3C286.3 350.3 288 354.4 288 358.6L288 480L352 544L352 358.6C352 354.4 353.7 350.3 356.7 347.3L544 160L96 160z"/><path d="M66.4 147.8C71.4 135.8 83.1 128 96 128L544 128C556.9 128 568.6 135.8 573.6 147.8C578.6 159.8 575.8 173.5 566.7 182.7L384 365.3L384 544C384 556.9 376.2 568.6 364.2 573.6C352.2 578.6 338.5 575.8 329.3 566.7L265.3 502.7C259.3 496.7 255.9 488.6 255.9 480.1L256 365.3L73.4 182.6C64.2 173.5 61.5 159.7 66.4 147.8zM544 160L96 160L283.3 347.3C286.3 350.3 288 354.4 288 358.6L288 480L352 544L352 358.6C352 354.4 353.7 350.3 356.7 347.3L544 160z"/></svg>
|
||||
</span>
|
||||
<span class="d-none d-md-block">Filters</span>
|
||||
</a>
|
||||
|
||||
<div class="input-group">
|
||||
<input type="hidden" name="start" id="start" value="0" />
|
||||
<input type="hidden" name="sort" id="sortInput" value="" />
|
||||
<input type="search" name="q" class="form-control form-control-lg" placeholder="Search cards..." />
|
||||
<button type="submit" class="btn btn-danger btn-lg border border-danger-subtle border-start-0" aria-label="search" value="" onclick="const q = this.closest('form').querySelector('[name=q]').value; dataLayer.push({ event: 'view_search_results', search_term: q });">
|
||||
<svg aria-hidden="true" class="search-button d-block" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><path d="M432 272C432 183.6 360.4 112 272 112C183.6 112 112 183.6 112 272C112 360.4 183.6 432 272 432C360.4 432 432 360.4 432 272zM401.1 435.1C365.7 463.2 320.8 480 272 480C157.1 480 64 386.9 64 272C64 157.1 157.1 64 272 64C386.9 64 480 157.1 480 272C480 320.8 463.2 365.7 435.1 401.1L569 535C578.4 544.4 578.4 558.1 569 567.5C559.6 575.9 544.4 575.9 536 567.5L401.1 435.1z"/></svg>
|
||||
</button>
|
||||
<input type="hidden" name="start" id="start" value="0" />
|
||||
<input type="hidden" name="sort" id="sortInput" value="" />
|
||||
<input type="hidden" name="language" id="languageInput" value="all" />
|
||||
<input type="search" name="q" class="form-control form-control-lg" placeholder="Search cards..." />
|
||||
<button type="submit" class="btn btn-danger btn-lg border border-danger-subtle border-start-0" aria-label="search" value="" onclick="const q = this.closest('form').querySelector('[name=q]').value; dataLayer.push({ event: 'view_search_results', search_term: q });">
|
||||
<svg aria-hidden="true" class="search-button d-block" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><path d="M432 272C432 183.6 360.4 112 272 112C183.6 112 112 183.6 112 272C112 360.4 183.6 432 272 432C360.4 432 432 360.4 432 272zM401.1 435.1C365.7 463.2 320.8 480 272 480C157.1 480 64 386.9 64 272C64 157.1 157.1 64 272 64C386.9 64 480 157.1 480 272C480 320.8 463.2 365.7 435.1 401.1L569 535C578.4 544.4 578.4 558.1 569 567.5C559.6 575.9 544.4 575.9 536 567.5L401.1 435.1z"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</Show>
|
||||
</form>
|
||||
@@ -124,6 +124,7 @@ import phantasmal_flames from "/src/svg/set/phantasmal_flames.svg?raw";
|
||||
import destined_rivals from "/src/svg/set/destined_rivals.svg?raw";
|
||||
import surging_sparks from "/src/svg/set/surging_sparks.svg?raw";
|
||||
import team_rocket from "/src/svg/set/team_rocket.svg?raw";
|
||||
import perfect_order from "/src/svg/set/perfect_order.svg?raw";
|
||||
|
||||
const { set } = Astro.props;
|
||||
|
||||
@@ -252,6 +253,7 @@ const setMap = {
|
||||
"ASC": ascended_heroes,
|
||||
"DRI": destined_rivals,
|
||||
"SSP": surging_sparks,
|
||||
"ME03": perfect_order,
|
||||
};
|
||||
|
||||
const svg = setMap[set as keyof typeof setMap] ?? "";
|
||||
|
||||
@@ -97,7 +97,7 @@ export const skus = pokeSchema.table('skus', {
|
||||
priceCount: integer(),
|
||||
},
|
||||
(table) => [
|
||||
index('idx_product_id_condition').on(table.productId, table.variant),
|
||||
index('idx_product_id_condition').on(table.productId, table.variant, table.condition),
|
||||
]);
|
||||
|
||||
export const priceHistory = pokeSchema.table('price_history', {
|
||||
|
||||
@@ -190,13 +190,28 @@ const altSearchUrl = (card: any) => {
|
||||
<!-- Card image column -->
|
||||
<div class="col-sm-12 col-md-3">
|
||||
<div class="position-relative mt-1">
|
||||
<img
|
||||
src={`/cards/${card?.productId}.jpg`}
|
||||
class="card-image w-100 img-fluid rounded-4"
|
||||
alt={card?.productName}
|
||||
onerror="this.onerror=null;this.src='/cards/default.jpg'"
|
||||
onclick="copyImage(this); dataLayer.push({'event': 'copiedImage'});"
|
||||
/>
|
||||
|
||||
<!-- card-image-wrap gives the modal image shimmer effects
|
||||
without the hover lift/scale that image-grow has in main.scss -->
|
||||
<div
|
||||
class="card-image-wrap rounded-4"
|
||||
data-energy={card?.energyType}
|
||||
data-rarity={card?.rarityName}
|
||||
data-variant={card?.variant}
|
||||
data-name={card?.productName}
|
||||
>
|
||||
<img
|
||||
src={`/cards/${card?.productId}.jpg`}
|
||||
class="card-image w-100 img-fluid rounded-4"
|
||||
alt={card?.productName}
|
||||
crossorigin="anonymous"
|
||||
onerror="this.onerror=null; this.src='/cards/default.jpg'; this.closest('.image-grow, .card-image-wrap')?.setAttribute('data-default','true')"
|
||||
onclick="copyImage(this); dataLayer.push({'event': 'copiedImage'});"
|
||||
/>
|
||||
<div class="holo-shine"></div>
|
||||
<div class="holo-glare"></div>
|
||||
</div>
|
||||
|
||||
<span class="position-absolute top-50 start-0 d-inline"><FirstEditionIcon edition={card?.variant} /></span>
|
||||
<span class="position-absolute bottom-0 start-0 d-inline"><SetIcon set={card?.set?.setCode} /></span>
|
||||
<span class="position-absolute top-0 end-0 d-inline"><EnergyIcon energy={card?.energyType} /></span>
|
||||
@@ -266,9 +281,9 @@ const altSearchUrl = (card: any) => {
|
||||
</div>
|
||||
<div class={`alert rounded p-2 flex-fill d-flex flex-column mb-0 ${attributes?.volatilityClass}`}>
|
||||
<h6 class="mb-auto d-flex justify-content-between align-items-start">
|
||||
<span>Volatility</span>
|
||||
<span class="me-1">Volatility</span>
|
||||
<span
|
||||
class="volatility-info"
|
||||
class="volatility-info float-end mt-0"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-placement="top"
|
||||
data-bs-container="body"
|
||||
|
||||
@@ -9,7 +9,7 @@ import * as util from 'util';
|
||||
|
||||
// all the facet fields we want to use for filtering
|
||||
const facetFields:any = {
|
||||
"productLineName": "Product Line",
|
||||
//"productLineName": "Product Line",
|
||||
"setName": "Set",
|
||||
"variant": "Variant",
|
||||
"rarityName": "Rarity",
|
||||
@@ -18,11 +18,6 @@ const facetFields:any = {
|
||||
}
|
||||
|
||||
// ── Allowed sort values ───────────────────────────────────────────────────
|
||||
// Maps the client-supplied key to the actual Typesense sort_by string.
|
||||
// Never pass raw user input directly to sort_by.
|
||||
// Note: price sorting uses nmMarketPrice — a field you need to denormalize
|
||||
// onto your card document in your Typesense indexing step (NM market price
|
||||
// as an integer in cents, e.g. nmMarketPrice: 499 = $4.99).
|
||||
const sortMap: Record<string, string> = {
|
||||
'releaseDate:desc,number:asc': '_text_match:asc,releaseDate:desc,number:asc',
|
||||
'releaseDate:asc,number:asc': '_text_match:asc,releaseDate:asc,number:asc',
|
||||
@@ -40,8 +35,32 @@ const start = Number(formData.get('start')?.toString() || '0');
|
||||
const sortKey = formData.get('sort')?.toString() || '';
|
||||
const resolvedSort = sortMap[sortKey] ?? DEFAULT_SORT;
|
||||
|
||||
// ── Language filter ───────────────────────────────────────────────────────
|
||||
// Expects a `language` field on your card documents in Typesense.
|
||||
// Valid values: 'en', 'jp' — anything else (or 'all') means no filter.
|
||||
const language = formData.get('language')?.toString() || 'all';
|
||||
const languageFilter = language === 'en' ? " && productLineName:=`Pokemon`"
|
||||
: language === 'jp' ? " && productLineName:=`Pokemon Japan`"
|
||||
: '';
|
||||
|
||||
// ── Query alias expansion ─────────────────────────────────────────────────
|
||||
// Intercepts known shorthand queries that can't be handled by Typesense
|
||||
// synonyms alone (e.g. terms that need to match across multiple set names)
|
||||
// and rewrites them into a direct filter, clearing the query so it doesn't
|
||||
// also try to text-match against card names.
|
||||
const EREADER_SETS = ['Expedition Base Set', 'Aquapolis', 'Skyridge', 'Battle-e'];
|
||||
const EREADER_RE = /^(e-?reader|e reader)$/i;
|
||||
|
||||
let resolvedQuery = query;
|
||||
let queryFilter = '';
|
||||
|
||||
if (EREADER_RE.test(query.trim())) {
|
||||
resolvedQuery = '';
|
||||
queryFilter = `setName:=[${EREADER_SETS.map(s => '`' + s + '`').join(',')}]`;
|
||||
}
|
||||
|
||||
const filters = Array.from(formData.entries())
|
||||
.filter(([key, value]) => key !== 'q' && key !== 'start' && key !== 'sort')
|
||||
.filter(([key]) => key !== 'q' && key !== 'start' && key !== 'sort' && key !== 'language')
|
||||
.reduce((acc, [key, value]) => {
|
||||
if (!acc[key]) {
|
||||
acc[key] = [];
|
||||
@@ -63,14 +82,15 @@ const facetFilter = (facet:string) => {
|
||||
.filter(([field]) => field !== facet)
|
||||
.map(([field, values]) => `${field}:=[${values.map(v => '`'+v+'`').join(',')}]`)
|
||||
.join(' && ');
|
||||
return `sealed:false${otherFilters ? ` && ${otherFilters}` : ''}`;
|
||||
// Language filter is always included so facet counts stay accurate
|
||||
return `sealed:false${languageFilter}${queryFilter ? ` && ${queryFilter}` : ''}${otherFilters ? ` && ${otherFilters}` : ''}`;
|
||||
};
|
||||
|
||||
|
||||
// primary search values (for cards)
|
||||
let searchArray = [{
|
||||
collection: 'cards',
|
||||
filter_by: `sealed:false${filterBy ? ` && ${filterBy}` : ''}`,
|
||||
filter_by: `sealed:false${languageFilter}${queryFilter ? ` && ${queryFilter}` : ''}${filterBy ? ` && ${filterBy}` : ''}`,
|
||||
per_page: 20,
|
||||
facet_by: '',
|
||||
max_facet_values: 0,
|
||||
@@ -97,8 +117,8 @@ if (start === 0) {
|
||||
|
||||
const searchRequests = { searches: searchArray };
|
||||
const commonSearchParams = {
|
||||
q: query,
|
||||
query_by: 'content'
|
||||
q: resolvedQuery,
|
||||
query_by: 'content,setName,productLineName,rarityName,energyType,cardType'
|
||||
};
|
||||
|
||||
// use typesense to search for cards matching the query and return the productIds of the results
|
||||
@@ -135,8 +155,8 @@ const facetNames = (name:string) => {
|
||||
}
|
||||
|
||||
const facets = searchResults.results.slice(1).map((result: any) => {
|
||||
const facet = result.facet_counts[0];
|
||||
if (!facet) return facet;
|
||||
const facet = result.facet_counts?.[0];
|
||||
if (!facet) return null;
|
||||
|
||||
// Sort: checked items first, then alphabetically
|
||||
facet.counts = facet.counts.sort((a: any, b: any) => {
|
||||
@@ -148,7 +168,7 @@ const facets = searchResults.results.slice(1).map((result: any) => {
|
||||
});
|
||||
|
||||
return facet;
|
||||
});
|
||||
}).filter(Boolean);
|
||||
|
||||
---
|
||||
|
||||
@@ -178,37 +198,41 @@ const facets = searchResults.results.slice(1).map((result: any) => {
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div id="sortBy" class="mb-2 d-flex align-items-center justify-content-start small" hx-swap-oob="true">
|
||||
<div id="sortBy" class="d-flex flex-fill align-items-center me-auto gap-2" hx-swap-oob="true">
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-sm btn-dark dropdown-toggle small" data-bs-toggle="dropdown" aria-expanded="false">Sort by</button>
|
||||
<button class="btn btn-sm btn-dark dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Sort by</button>
|
||||
<ul class="dropdown-menu dropdown-menu-dark">
|
||||
<li><a class="dropdown-item sort-option small" href="#" data-sort="releaseDate:desc,number:asc" data-label="Set: Newest to Oldest">Set: Newest to Oldest</a></li>
|
||||
<li><a class="dropdown-item sort-option small" href="#" data-sort="releaseDate:asc,number:asc" data-label="Set: Oldest to Newest">Set: Oldest to Newest</a></li>
|
||||
<li><a class="dropdown-item sort-option small" href="#" data-sort="marketPrice:desc" data-label="Price: High to Low">Price: High to Low</a></li>
|
||||
<li><a class="dropdown-item sort-option small" href="#" data-sort="marketPrice:asc" data-label="Price: Low to High">Price: Low to High</a></li>
|
||||
<li><a class="dropdown-item sort-option small" href="#" data-sort="number:asc" data-label="Card Number: Ascending">Card Number: Ascending</a></li>
|
||||
<li><a class="dropdown-item sort-option small" href="#" data-sort="number:desc" data-label="Card Number: Descending">Card Number: Descending</a></li>
|
||||
<li><a class="dropdown-item sort-option" href="#" data-sort="releaseDate:desc,number:asc" data-label="Set: Newest to Oldest">Set: Newest to Oldest</a></li>
|
||||
<li><a class="dropdown-item sort-option" href="#" data-sort="releaseDate:asc,number:asc" data-label="Set: Oldest to Newest">Set: Oldest to Newest</a></li>
|
||||
<li><a class="dropdown-item sort-option" href="#" data-sort="marketPrice:desc" data-label="Price: High to Low">Price: High to Low</a></li>
|
||||
<li><a class="dropdown-item sort-option" href="#" data-sort="marketPrice:asc" data-label="Price: Low to High">Price: Low to High</a></li>
|
||||
<li><a class="dropdown-item sort-option" href="#" data-sort="number:asc" data-label="Card Number: Ascending">Card Number: Ascending</a></li>
|
||||
<li><a class="dropdown-item sort-option" href="#" data-sort="number:desc" data-label="Card Number: Descending">Card Number: Descending</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<span id="sortLabel" class="ms-2 text-secondary">{sortKey ? ({"releaseDate:desc,number:asc":"Set: Newest to Oldest","releaseDate:asc,number:asc":"Set: Oldest to Newest","marketPrice:desc":"Price: High to Low","marketPrice:asc":"Price: Low to High","number:asc":"Card Number: Ascending","number:desc":"Card Number: Descending"}[sortKey] ?? '') : ''}</span>
|
||||
<span id="sortLabel" class="ms-1 text-secondary small">{sortKey ? ({"releaseDate:desc,number:asc":"Set: Newest to Oldest","releaseDate:asc,number:asc":"Set: Oldest to Newest","marketPrice:desc":"Price: High to Low","marketPrice:asc":"Price: Low to High","number:asc":"Card Number: Ascending","number:desc":"Card Number: Descending"}[sortKey] ?? '') : ''}</span>
|
||||
<div class="btn-group btn-group-sm ms-2 flex-shrink-0" role="group" aria-label="Language filter">
|
||||
<button type="button" class={`btn btn-dark language-btn${language === 'all' ? ' active' : ''}`} data-lang="all">All</button>
|
||||
<button type="button" class={`btn btn-dark language-btn${language === 'en' ? ' active' : ''}`} data-lang="en">EN</button>
|
||||
<button type="button" class={`btn btn-dark language-btn${language === 'jp' ? ' active' : ''}`} data-lang="jp">JP</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="totalResults" class="mb-2 ms-5 text-secondary small" hx-swap-oob="true">
|
||||
<div id="totalResults" class="d-flex text-secondary small d-none align-items-center" hx-swap-oob="true">
|
||||
{totalHits} {totalHits === 1 ? ' result' : ' results'}
|
||||
</div>
|
||||
<div id="activeFilters" class="mb-2 d-flex align-items-center small ms-auto" hx-swap-oob="true">
|
||||
<div id="activeFilters" class="d-flex small ms-auto align-items-center" hx-swap-oob="true">
|
||||
{(Object.entries(filters).length > 0) &&
|
||||
<span class="me-1 small">Filtered by:</span>
|
||||
<ul class="list-group list-group-horizontal">
|
||||
{Object.entries(filters).map(([filter, values]) => (
|
||||
values.map((value) => (
|
||||
<li data-facet={filter} data-value={value} class="list-group-item small remove-filter">{value}</li>
|
||||
<li data-facet={filter} data-value={value} class="list-group-item small p-2 remove-filter">{value}</li>
|
||||
))
|
||||
))}
|
||||
</ul>
|
||||
<span class="ms-2"><button type="button" class="btn-close" aria-label="Clear all filters" id="clear-all-filters"></button></span>
|
||||
}
|
||||
</div>
|
||||
|
||||
<script define:vars={{ totalHits, filters, facets }} is:inline>
|
||||
|
||||
// Filter the facet values to make things like Set easier to find
|
||||
@@ -263,7 +287,10 @@ const facets = searchResults.results.slice(1).map((result: any) => {
|
||||
<div class="inventory-label pt-2">+/-</div>
|
||||
</div>
|
||||
<div class="card-trigger position-relative" data-card-id={card.cardId} hx-get={`/partials/card-modal?cardId=${card.cardId}`} hx-target="#cardModal" hx-trigger="click" data-bs-toggle="modal" data-bs-target="#cardModal" onclick="const cardTitle = this.querySelector('#cardImage').getAttribute('alt'); dataLayer.push({'event': 'virtualPageview', 'pageUrl': this.getAttribute('hx-get'), 'pageTitle': cardTitle, 'previousUrl': '/pokemon'});">
|
||||
<div class="image-grow"><img src={`/cards/${card.productId}.jpg`} alt={card.productName} id="cardImage" loading="lazy" decoding="async" class="img-fluid rounded-4 mb-2 card-image w-100" onerror="this.onerror=null;this.src='/cards/default.jpg'"/><span class="position-absolute top-50 start-0 d-inline medium-icon"><FirstEditionIcon edition={card?.variant} /></span></div>
|
||||
<div class="image-grow rounded-4 card-image" data-energy={card.energyType} data-rarity={card.rarityName} data-variant={card.variant} data-name={card.productName}><img src={`/cards/${card.productId}.jpg`} alt={card.productName} id="cardImage" loading="lazy" decoding="async" class="img-fluid rounded-4 mb-2 w-100" onerror="this.onerror=null; this.src='/cards/default.jpg'; this.closest('.image-grow')?.setAttribute('data-default','true')"/><span class="position-absolute top-50 start-0 d-inline medium-icon"><FirstEditionIcon edition={card?.variant} /></span>
|
||||
<div class="holo-shine"></div>
|
||||
<div class="holo-glare"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row row-cols-5 gx-1 price-row mb-2">
|
||||
{conditionOrder.map((condition) => (
|
||||
|
||||
1
src/svg/set/perfect_order.svg
Normal file
|
After Width: | Height: | Size: 6.7 KiB |