Added SwiperJS swipers on ProductCards and added preliminary support for nested categories using a tree of TreeNode structures. Added unit tests for TreeNode.

This commit is contained in:
David Ball 2024-07-16 14:02:47 -04:00
parent 9225b3c727
commit 935340d90e
9 changed files with 667 additions and 89 deletions

297
package-lock.json generated
View File

@ -24,7 +24,8 @@
"playwright": "*", "playwright": "*",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"swiper": "^11.1.4" "swiper": "^11.1.4",
"vitest": "^2.0.3"
}, },
"devDependencies": { "devDependencies": {
"@apify/tsconfig": "^0.1.0", "@apify/tsconfig": "^0.1.0",
@ -1857,6 +1858,81 @@
"vite": "^4.2.0 || ^5.0.0" "vite": "^4.2.0 || ^5.0.0"
} }
}, },
"node_modules/@vitest/expect": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.0.3.tgz",
"integrity": "sha512-X6AepoOYePM0lDNUPsGXTxgXZAl3EXd0GYe/MZyVE4HzkUqyUVC6S3PrY5mClDJ6/7/7vALLMV3+xD/Ko60Hqg==",
"dependencies": {
"@vitest/spy": "2.0.3",
"@vitest/utils": "2.0.3",
"chai": "^5.1.1",
"tinyrainbow": "^1.2.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/pretty-format": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.0.3.tgz",
"integrity": "sha512-URM4GLsB2xD37nnTyvf6kfObFafxmycCL8un3OC9gaCs5cti2u+5rJdIflZ2fUJUen4NbvF6jCufwViAFLvz1g==",
"dependencies": {
"tinyrainbow": "^1.2.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/runner": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.0.3.tgz",
"integrity": "sha512-EmSP4mcjYhAcuBWwqgpjR3FYVeiA4ROzRunqKltWjBfLNs1tnMLtF+qtgd5ClTwkDP6/DGlKJTNa6WxNK0bNYQ==",
"dependencies": {
"@vitest/utils": "2.0.3",
"pathe": "^1.1.2"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/snapshot": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.0.3.tgz",
"integrity": "sha512-6OyA6v65Oe3tTzoSuRPcU6kh9m+mPL1vQ2jDlPdn9IQoUxl8rXhBnfICNOC+vwxWY684Vt5UPgtcA2aPFBb6wg==",
"dependencies": {
"@vitest/pretty-format": "2.0.3",
"magic-string": "^0.30.10",
"pathe": "^1.1.2"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/spy": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.0.3.tgz",
"integrity": "sha512-sfqyAw/ypOXlaj4S+w8689qKM1OyPOqnonqOc9T91DsoHbfN5mU7FdifWWv3MtQFf0lEUstEwR9L/q/M390C+A==",
"dependencies": {
"tinyspy": "^3.0.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/utils": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.0.3.tgz",
"integrity": "sha512-c/UdELMuHitQbbc/EVctlBaxoYAwQPQdSNwv7z/vHyBKy2edYZaFgptE27BRueZB7eW8po+cllotMNTDpL3HWg==",
"dependencies": {
"@vitest/pretty-format": "2.0.3",
"estree-walker": "^3.0.3",
"loupe": "^3.1.1",
"tinyrainbow": "^1.2.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vladfrangu/async_event_emitter": { "node_modules/@vladfrangu/async_event_emitter": {
"version": "2.4.0", "version": "2.4.0",
"resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.4.0.tgz", "resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.4.0.tgz",
@ -2139,6 +2215,14 @@
"url": "https://github.com/sponsors/wooorm" "url": "https://github.com/sponsors/wooorm"
} }
}, },
"node_modules/assertion-error": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
"integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
"engines": {
"node": ">=12"
}
},
"node_modules/astro": { "node_modules/astro": {
"version": "4.11.5", "version": "4.11.5",
"resolved": "https://registry.npmjs.org/astro/-/astro-4.11.5.tgz", "resolved": "https://registry.npmjs.org/astro/-/astro-4.11.5.tgz",
@ -2429,6 +2513,14 @@
"ieee754": "^1.1.13" "ieee754": "^1.1.13"
} }
}, },
"node_modules/cac": {
"version": "6.7.14",
"resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
"integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
"engines": {
"node": ">=8"
}
},
"node_modules/cacheable-lookup": { "node_modules/cacheable-lookup": {
"version": "7.0.0", "version": "7.0.0",
"resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz",
@ -2529,6 +2621,21 @@
"url": "https://github.com/sponsors/wooorm" "url": "https://github.com/sponsors/wooorm"
} }
}, },
"node_modules/chai": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/chai/-/chai-5.1.1.tgz",
"integrity": "sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==",
"dependencies": {
"assertion-error": "^2.0.1",
"check-error": "^2.1.1",
"deep-eql": "^5.0.1",
"loupe": "^3.1.0",
"pathval": "^2.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/chalk": { "node_modules/chalk": {
"version": "5.3.0", "version": "5.3.0",
"license": "MIT", "license": "MIT",
@ -2571,6 +2678,14 @@
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/check-error": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz",
"integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==",
"engines": {
"node": ">= 16"
}
},
"node_modules/cheerio": { "node_modules/cheerio": {
"version": "1.0.0-rc.12", "version": "1.0.0-rc.12",
"resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz",
@ -3053,6 +3168,14 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/deep-eql": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
"integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==",
"engines": {
"node": ">=6"
}
},
"node_modules/defaults": { "node_modules/defaults": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz",
@ -3680,6 +3803,14 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/get-func-name": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz",
"integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==",
"engines": {
"node": "*"
}
},
"node_modules/get-stream": { "node_modules/get-stream": {
"version": "8.0.1", "version": "8.0.1",
"license": "MIT", "license": "MIT",
@ -4966,6 +5097,14 @@
"loose-envify": "cli.js" "loose-envify": "cli.js"
} }
}, },
"node_modules/loupe": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.1.tgz",
"integrity": "sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==",
"dependencies": {
"get-func-name": "^2.0.1"
}
},
"node_modules/lowercase-keys": { "node_modules/lowercase-keys": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz",
@ -6309,6 +6448,19 @@
"version": "6.2.2", "version": "6.2.2",
"license": "MIT" "license": "MIT"
}, },
"node_modules/pathe": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz",
"integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="
},
"node_modules/pathval": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz",
"integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==",
"engines": {
"node": ">= 14.16"
}
},
"node_modules/pause-stream": { "node_modules/pause-stream": {
"version": "0.0.11", "version": "0.0.11",
"resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz",
@ -7237,6 +7389,11 @@
"@types/hast": "^3.0.4" "@types/hast": "^3.0.4"
} }
}, },
"node_modules/siginfo": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
"integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="
},
"node_modules/signal-exit": { "node_modules/signal-exit": {
"version": "4.1.0", "version": "4.1.0",
"license": "ISC", "license": "ISC",
@ -7355,6 +7512,16 @@
"version": "1.0.3", "version": "1.0.3",
"license": "BSD-3-Clause" "license": "BSD-3-Clause"
}, },
"node_modules/stackback": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
"integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="
},
"node_modules/std-env": {
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/std-env/-/std-env-3.7.0.tgz",
"integrity": "sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg=="
},
"node_modules/stdin-discarder": { "node_modules/stdin-discarder": {
"version": "0.2.2", "version": "0.2.2",
"license": "MIT", "license": "MIT",
@ -7542,6 +7709,35 @@
"integrity": "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==", "integrity": "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/tinybench": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.8.0.tgz",
"integrity": "sha512-1/eK7zUnIklz4JUUlL+658n58XO2hHLQfSk1Zf2LKieUjxidN16eKFEoDEfjHc3ohofSSqK3X5yO6VGb6iW8Lw=="
},
"node_modules/tinypool": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.0.tgz",
"integrity": "sha512-KIKExllK7jp3uvrNtvRBYBWBOAXSX8ZvoaD8T+7KB/QHIuoJW3Pmr60zucywjAlMb5TeXUkcs/MWeWLu0qvuAQ==",
"engines": {
"node": "^18.0.0 || >=20.0.0"
}
},
"node_modules/tinyrainbow": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz",
"integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==",
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/tinyspy": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.0.tgz",
"integrity": "sha512-q5nmENpTHgiPVd1cJDDc9cVoYN5x4vCvwT3FMilvKPKneCBZAxn2YWQjDF0UMcE9k0Cay1gBiDfTMU0g+mPMQA==",
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/tldts": { "node_modules/tldts": {
"version": "6.1.30", "version": "6.1.30",
"resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.30.tgz", "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.30.tgz",
@ -8060,6 +8256,27 @@
} }
} }
}, },
"node_modules/vite-node": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.0.3.tgz",
"integrity": "sha512-14jzwMx7XTcMB+9BhGQyoEAmSl0eOr3nrnn+Z12WNERtOvLN+d2scbRUvyni05rT3997Bg+rZb47NyP4IQPKXg==",
"dependencies": {
"cac": "^6.7.14",
"debug": "^4.3.5",
"pathe": "^1.1.2",
"tinyrainbow": "^1.2.0",
"vite": "^5.0.0"
},
"bin": {
"vite-node": "vite-node.mjs"
},
"engines": {
"node": "^18.0.0 || >=20.0.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/vite/node_modules/fsevents": { "node_modules/vite/node_modules/fsevents": {
"version": "2.3.3", "version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@ -8086,6 +8303,69 @@
} }
} }
}, },
"node_modules/vitest": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-2.0.3.tgz",
"integrity": "sha512-o3HRvU93q6qZK4rI2JrhKyZMMuxg/JRt30E6qeQs6ueaiz5hr1cPj+Sk2kATgQzMMqsa2DiNI0TIK++1ULx8Jw==",
"dependencies": {
"@ampproject/remapping": "^2.3.0",
"@vitest/expect": "2.0.3",
"@vitest/pretty-format": "^2.0.3",
"@vitest/runner": "2.0.3",
"@vitest/snapshot": "2.0.3",
"@vitest/spy": "2.0.3",
"@vitest/utils": "2.0.3",
"chai": "^5.1.1",
"debug": "^4.3.5",
"execa": "^8.0.1",
"magic-string": "^0.30.10",
"pathe": "^1.1.2",
"std-env": "^3.7.0",
"tinybench": "^2.8.0",
"tinypool": "^1.0.0",
"tinyrainbow": "^1.2.0",
"vite": "^5.0.0",
"vite-node": "2.0.3",
"why-is-node-running": "^2.2.2"
},
"bin": {
"vitest": "vitest.mjs"
},
"engines": {
"node": "^18.0.0 || >=20.0.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
"@edge-runtime/vm": "*",
"@types/node": "^18.0.0 || >=20.0.0",
"@vitest/browser": "2.0.3",
"@vitest/ui": "2.0.3",
"happy-dom": "*",
"jsdom": "*"
},
"peerDependenciesMeta": {
"@edge-runtime/vm": {
"optional": true
},
"@types/node": {
"optional": true
},
"@vitest/browser": {
"optional": true
},
"@vitest/ui": {
"optional": true
},
"happy-dom": {
"optional": true
},
"jsdom": {
"optional": true
}
}
},
"node_modules/volar-service-css": { "node_modules/volar-service-css": {
"version": "0.0.59", "version": "0.0.59",
"resolved": "https://registry.npmjs.org/volar-service-css/-/volar-service-css-0.0.59.tgz", "resolved": "https://registry.npmjs.org/volar-service-css/-/volar-service-css-0.0.59.tgz",
@ -8386,6 +8666,21 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/why-is-node-running": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
"integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
"dependencies": {
"siginfo": "^2.0.0",
"stackback": "0.0.2"
},
"bin": {
"why-is-node-running": "cli.js"
},
"engines": {
"node": ">=8"
}
},
"node_modules/widest-line": { "node_modules/widest-line": {
"version": "4.0.1", "version": "4.0.1",
"license": "MIT", "license": "MIT",

View File

@ -7,7 +7,8 @@
"start": "astro dev", "start": "astro dev",
"build": "astro check && astro build", "build": "astro check && astro build",
"preview": "astro preview", "preview": "astro preview",
"astro": "astro" "astro": "astro",
"test": "vitest"
}, },
"dependencies": { "dependencies": {
"@astrojs/check": "^0.8.1", "@astrojs/check": "^0.8.1",
@ -26,7 +27,8 @@
"playwright": "*", "playwright": "*",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"swiper": "^11.1.4" "swiper": "^11.1.4",
"vitest": "^2.0.3"
}, },
"devDependencies": { "devDependencies": {
"@apify/tsconfig": "^0.1.0", "@apify/tsconfig": "^0.1.0",

View File

@ -1,3 +1,5 @@
import { TreeNode } from "../types/tree";
/** /**
* Category of Products. * Category of Products.
*/ */
@ -22,10 +24,18 @@ export interface Category {
* Description of Product Category. * Description of Product Category.
*/ */
description: string; description: string;
// /**
// * Sub-categories.
// */
// categories?: Category[];
// /**
// * Parent category (do not assign this, it will be automatically created.)
// */
// parentCategory?: Category;
} }
/** /**
* * Static properties of categories.
*/ */
export class StaticCategory { export class StaticCategory {
/** /**
@ -42,80 +52,81 @@ export class StaticCategory {
} }
} }
/** export const ALL_CATEGORIES = TreeNode.createRoot<Category>({
* A list of all the categories. id: 0,
*/ category: "All Categories",
export const ALL_CATEGORIES: Category[] = [ slug: "all-categories",
{ description: "All categories in the whole store."
}).addChild({
id: StaticCategory.nextId(), id: StaticCategory.nextId(),
category: "Vehicle Essentials", category: "Vehicle Essentials",
slug: "vehicle-essentials", slug: "vehicle-essentials",
imageUrl: "/assets/vehicle-essentials.png", imageUrl: "/assets/vehicle-essentials.png",
description: "Essential items for your vehicle to ensure smooth deliveries.", description: "Essential items for your vehicle to ensure smooth deliveries.",
}, }).addChild({
{ id: StaticCategory.nextId(),
category: "Deer Alert Warnings",
slug: "vehicle-essentials/deer-alert-warnings",
// imageUrl: "",
description: "Deer alert warning systems give you peace of mind.",
}).root().addChild({
id: StaticCategory.nextId(), id: StaticCategory.nextId(),
category: "Delivery Gear", category: "Delivery Gear",
slug: "delivery-gear", slug: "delivery-gear",
imageUrl: "/assets/delivery-gear-3.jpg", imageUrl: "/assets/delivery-gear-3.jpg",
description: "Gear to help you deliver food efficiently and keep it in top condition.", description: "Gear to help you deliver food efficiently and keep it in top condition.",
}, }).root().addChild({
{
id: StaticCategory.nextId(), id: StaticCategory.nextId(),
category: "Personal Items", category: "Personal Items",
slug: "personal-items", slug: "personal-items",
imageUrl: "/assets/personal-items.jpg", imageUrl: "/assets/personal-items.jpg",
description: "Personal essentials to keep you comfortable and prepared on the go.", description: "Personal essentials to keep you comfortable and prepared on the go.",
}, }).root().addChild({
{
id: StaticCategory.nextId(), id: StaticCategory.nextId(),
category: "Safety Equipment", category: "Safety Equipment",
slug: "safety-equipment", slug: "safety-equipment",
imageUrl: "/assets/safety-equipment.jpg", imageUrl: "/assets/safety-equipment.jpg",
description: "Safety gear to protect you during deliveries.", description: "Safety gear to protect you during deliveries.",
}, }).root().addChild({
{
id: StaticCategory.nextId(), id: StaticCategory.nextId(),
category: "Tech Gadgets", category: "Tech Gadgets",
slug: "tech-gadgets", slug: "tech-gadgets",
imageUrl: "/assets/tech-gadgets.jpg", imageUrl: "/assets/tech-gadgets.jpg",
description: "Technology tools to enhance your efficiency and connectivity.", description: "Technology tools to enhance your efficiency and connectivity.",
}, }).root().addChild({
{
id: StaticCategory.nextId(), id: StaticCategory.nextId(),
category: "Biking Gear", category: "Biking Gear",
slug: "biking-gear", slug: "biking-gear",
imageUrl: "/assets/biking-gear.jpg", imageUrl: "/assets/biking-gear.jpg",
description: "Equipment for Dashers who deliver by bike.", description: "Equipment for Dashers who deliver by bike.",
}, }).root().addChild({
{
id: StaticCategory.nextId(), id: StaticCategory.nextId(),
category: "Comfort and Convenience", category: "Comfort and Convenience",
slug: "comfort-convenience", slug: "comfort-convenience",
imageUrl: "/assets/comfort-convenience.png", imageUrl: "/assets/comfort-convenience.png",
description: "Items to increase comfort and convenience during deliveries.", description: "Items to increase comfort and convenience during deliveries.",
}, }).root().addChild({
{
id: StaticCategory.nextId(), id: StaticCategory.nextId(),
category: "Health and Wellness", category: "Health and Wellness",
slug: "health-wellness", slug: "health-wellness",
imageUrl: "/assets/wearable-tech.jpg", imageUrl: "/assets/wearable-tech.jpg",
description: "Products to help you stay healthy and well during your shifts.", description: "Products to help you stay healthy and well during your shifts.",
}, }).root().addChild({
{
id: StaticCategory.nextId(), id: StaticCategory.nextId(),
category: "Miscellaneous", category: "Miscellaneous",
slug: "misc", slug: "misc",
imageUrl: "/assets/misc.jpg", imageUrl: "/assets/misc.jpg",
description: "Various other items that can be useful for Dashers.", description: "Various other items that can be useful for Dashers.",
} }).root();
].sort((a, b) => a.slug.localeCompare(b.slug));
export function getCategoryIdForSlug(slug: string): number|null { export function getCategoryNodeForSlug(slug: string): TreeNode<Category>|undefined {
for (const category of ALL_CATEGORIES) { return ALL_CATEGORIES.findOneRecursive(category => category.slug === slug) || undefined;
if (category.slug == slug) {
return category.id;
} }
};
return null; export function getCategoryForSlug(slug: string): Category|undefined {
return getCategoryNodeForSlug(slug)?.value || undefined;
}
export function getCategoryIdForSlug(slug: string): number|undefined {
return getCategoryForSlug(slug)?.id || undefined;
} }

View File

@ -7,6 +7,7 @@ import StarRating from '../components/StarRating.astro';
import ImageCarousel from '../components/ImageCarousel.astro'; import ImageCarousel from '../components/ImageCarousel.astro';
import markdownIt from 'markdown-it'; import markdownIt from 'markdown-it';
import markdownItAttrs from 'markdown-it-attrs'; import markdownItAttrs from 'markdown-it-attrs';
import type { TreeNode } from '../types/tree';
const md = markdownIt({ const md = markdownIt({
html: true, html: true,
linkify: true, linkify: true,
@ -34,7 +35,8 @@ const formatAsCurrency = (amount: number) => amount.toLocaleString('en-US', { st
const { productLookup } = Astro.params; const { productLookup } = Astro.params;
const product: Product = ALL_PRODUCTS.find(p => p.slug === productLookup)!; const product: Product = ALL_PRODUCTS.find(p => p.slug === productLookup)!;
const category: Category = ALL_CATEGORIES.find(c => c.id === product.categoryId)!; const categoryNode: TreeNode<Category> = ALL_CATEGORIES.findOneRecursive(c => c.id === product.categoryId)!;
const category: Category = categoryNode.value;
const brand: Brand = ALL_BRANDS.find(b => b.slug === product.brandStoreSlug)!; const brand: Brand = ALL_BRANDS.find(b => b.slug === product.brandStoreSlug)!;
--- ---

View File

@ -1,22 +1,24 @@
--- ---
import Layout from '../../layouts/Layout.astro'; import Layout from '../../layouts/Layout.astro';
// import { useState, useEffect } from 'react'; // import { useState, useEffect } from 'react';
import { ALL_CATEGORIES } from '../../data/categories'; import { ALL_CATEGORIES, type Category, getCategoryNodeForSlug } from '../../data/categories';
import { ALL_PRODUCTS } from '../../data/products'; import { ALL_PRODUCTS } from '../../data/products';
import ProductCard from '../../components/ProductCard.astro'; import ProductCard from '../../components/ProductCard.astro';
import { DeepArray } from '../../types/deep-array';
type CategoryStaticPath = { params: { categoryLookup: string }}; type CategoryStaticPath = { params: { categoryLookup: string }};
export function getStaticPaths() { export function getStaticPaths() {
return ALL_CATEGORIES.map<CategoryStaticPath>((category) => { return { return ALL_CATEGORIES.getAllNodes().map(categoryNode => { return {
params: { params: {
categoryLookup: category.slug categoryLookup: categoryNode.value.slug
} }
}}); }});
} }
const { categoryLookup } = Astro.params; const { categoryLookup } = Astro.params;
const category = ALL_CATEGORIES.find(c => c.slug === categoryLookup)!; const categoryNode = getCategoryNodeForSlug(categoryLookup);
const category = categoryNode!.value;
const categoryProducts = ALL_PRODUCTS.filter(p => p.categoryId === category.id)!; const categoryProducts = ALL_PRODUCTS.filter(p => p.categoryId === category.id)!;
const isAnyAmazon = categoryProducts.find(p => p.amazonLink) || false; const isAnyAmazon = categoryProducts.find(p => p.amazonLink) || false;
--- ---

View File

@ -14,9 +14,9 @@ import { getProductsForCategoryId } from '../data/products';
Your one-stop shop for all your after-market Dasher supplies. Your one-stop shop for all your after-market Dasher supplies.
</p> </p>
<ul role="list" class="link-card-grid"> <ul role="list" class="link-card-grid">
{ALL_CATEGORIES.filter(category => getProductsForCategoryId(category.id)?.length > 0).map(category => ( {ALL_CATEGORIES.children.filter(categoryNode => getProductsForCategoryId(categoryNode.value.id).length > 0).map(categoryNode => (
<CategoryCard <CategoryCard
category={category} category={categoryNode.value}
/> />
))} ))}
</ul> </ul>

174
src/types/tree.ts Normal file
View File

@ -0,0 +1,174 @@
/**
* A simple tree data structure.
*/
export class TreeNode<NodeT> {
private _value: NodeT;
private _children: TreeNode<NodeT>[];
private _parent?: TreeNode<NodeT>;
constructor(value: NodeT) {
this._value = value;
this._children = [];
this._parent = undefined;
}
/**
* Creates a TreeNode root node.
* @param value Root node value.
* @returns TreeNode root node.
*/
public static createRoot<NodeT>(value: NodeT): TreeNode<NodeT> {
return new TreeNode(value);
}
/**
* Adds value node to parent TreeNode and returns child TreeNode.
* @param value Child node value.
* @returns Child tree node.
*/
public addChild(value: NodeT): TreeNode<NodeT> {
let childTreeNode = new TreeNode(value);
childTreeNode._parent = this;
let length = this._children.push(childTreeNode);
return this._children[length-1];
}
/**
* Gets value of node.
*/
public get value(): NodeT {
return this._value;
}
/**
* Sets value of node.
*/
public set value(value: NodeT) {
this._value = value;
}
/**
* Gets TreeNode parent of current TreeNode.
* @returns Parent TreeNode of current TreeNode.
*/
public parent(): TreeNode<NodeT> {
return this._parent||this;
}
/**
* Gets root TreeNode node of tree.
* @returns Root TreeNode node of tree.
*/
public root(): TreeNode<NodeT> {
if (this.parent() === this) {
return this;
}
else {
return this.parent().root();
}
}
/**
* Sets new parent of node and returns this TreeNode node.
* @param parent Parent of node.
* @returns This TreeNode node.
*/
public setParent(parent: TreeNode<NodeT>) {
this._parent = parent;
return this;
}
/**
* Gets children of this TreeNode node as an Array of node values.
*/
public get children(): TreeNode<NodeT>[] {
return this._children;
}
/**
* Checks to see if the current node value matches the predicate.
* @param predicate Predicate determines truthiness of the match.
* @returns Boolean indicating if this tree node value matches the predicate.
*/
public is(predicate: (value: NodeT) => boolean): boolean {
const result = predicate(this.value);
return result;
}
/**
* Performs shallow search for the predicate among itself and its TreeNode children, checking the values against the predicate.
* @param predicate Predicate determines truthiness of the match.
* @returns The TreeNode matching the predicate, if it exists. Otherwise, undefined.
*/
public findOne(predicate: (value: NodeT) => boolean): TreeNode<NodeT> | undefined {
const initialIs = this.is(predicate);
if (initialIs) {
return this;
}
else {
for (const child of this.children) {
const result = child.is(predicate);
if (result) {
return child;
}
}
}
}
/**
* Performs shallow search for the predicates among itself and its TreeNode children, checking the values against the predicate.
* @param predicate Predicate determines truthiness of the matches.
* @returns An array of TreeNodes matching the predicate, or an empty array if no predicates exist.
*/
public findAll(predicate: (value: NodeT) => boolean): TreeNode<NodeT>[] {
let results: TreeNode<NodeT>[] = [];
if (this.is(predicate)) {
results.push(this);
}
for (const child of this.children) {
if (child.is(predicate)) {
results.push(child);
}
}
return results;
}
/**
* Performs deep search for value among itself and its TreeNode children, checking the values against the predicate.
* @param predicate Predicate determines truthiness of the match.
* @returns The TreeNode matching the predicate, if it exists. Otherwise, undefined.
*/
public findOneRecursive(predicate: (value: NodeT) => boolean): TreeNode<NodeT> | undefined {
const initialFindOne = this.findOne(predicate);
if (initialFindOne) {
return initialFindOne;
}
if (this.children.length) {
for (let child of this.children) {
let result = child.findOneRecursive(predicate);
if (result) {
return result;
}
}
}
return undefined;
}
/**
* Performs deep search for value among TreeNode children, checking the values against the predicate.
* @param predicate Predicate determines truthiness of the matches.
* @returns The TreeNode matching the predicate, if it exists. Otherwise, undefined.
*/
public findAllRecursive(predicate: (value: NodeT) => boolean): TreeNode<NodeT>[] {
let results: TreeNode<NodeT>[] = [];
if (this.is(predicate)) {
results.push(this);
}
if (this.children.length) {
for (let child of this.children) {
let childResults = child.findAllRecursive(predicate);
if (childResults.length) {
results.push(...childResults);
}
}
}
return results;
}
/**
* Flattens the tree into an array of TreeNodes.
*/
public getAllNodes(): TreeNode<NodeT>[] {
let nodes: TreeNode<NodeT>[] = [];
nodes.push(this);
for (let child of this.children) {
nodes.push(...child.getAllNodes());
}
return nodes;
}
}

83
test/tree.test.ts Normal file
View File

@ -0,0 +1,83 @@
import { assert, expect, test } from 'vitest';
import { TreeNode } from '../src/types/tree';
test('Tree of numbers', () => {
const VALUE_NOT_UNDER_TEST = -1000;
const TEST_VALUE_1 = 1;
const TEST_VALUE_10 = 10;
const TEST_VALUE_100 = 100;
const ZERO_CHILDREN = 0;
const ONE_CHILD = 1;
const TWO_CHILDREN = 2;
const THREE_CHILDREN = 3;
let fluentNode = TreeNode.createRoot(TEST_VALUE_1);
expect(fluentNode.value).toBe(TEST_VALUE_1);
expect(fluentNode.children.length).toBe(ZERO_CHILDREN);
fluentNode = fluentNode.addChild(TEST_VALUE_10);
expect(fluentNode.value).toBe(TEST_VALUE_10);
expect(fluentNode.parent().value).toBe(TEST_VALUE_1);
expect(fluentNode.children.length).toBe(ZERO_CHILDREN);
expect(fluentNode.parent().children.length).toBe(ONE_CHILD);
fluentNode = fluentNode.addChild(TEST_VALUE_100);
expect(fluentNode.value).toBe(TEST_VALUE_100);
expect(fluentNode.parent().value).toBe(TEST_VALUE_10);
expect(fluentNode.parent().parent().value).toBe(TEST_VALUE_1);
expect(fluentNode.parent().parent()).toBe(fluentNode.root());
expect(fluentNode.parent().parent().parent().parent().parent().parent().value).toBe(TEST_VALUE_1);
expect(fluentNode.root().value).toBe(TEST_VALUE_1);
expect(fluentNode.root().root().root().root().root().root().root().value).toBe(TEST_VALUE_1);
fluentNode = fluentNode.root();
expect(fluentNode.value).toBe(TEST_VALUE_1);
expect(fluentNode.children.length).toBe(ONE_CHILD);
expect(fluentNode.children[0].children.length).toBe(ONE_CHILD);
expect(fluentNode.children[0].children[0].children.length).toBe(ZERO_CHILDREN);
expect(fluentNode.is(value => value === TEST_VALUE_1)).toBe(true);
expect(fluentNode.is(value => value === TEST_VALUE_10)).toBe(false);
expect(fluentNode.is(value => value === TEST_VALUE_100)).toBe(false);
expect(fluentNode.is(value => value === VALUE_NOT_UNDER_TEST)).toBe(false);
expect(fluentNode.findOne(value => value === TEST_VALUE_1)?.value).toBe(TEST_VALUE_1);
expect(fluentNode.findOne(value => value === TEST_VALUE_10)?.value).toBe(TEST_VALUE_10);
expect(fluentNode.findOne(value => value === TEST_VALUE_100)?.value).toBe(undefined);
expect(fluentNode.findOne(value => value === TEST_VALUE_1)?.value).toBe(TEST_VALUE_1);
expect(fluentNode.findOne(value => value === VALUE_NOT_UNDER_TEST)).toBe(undefined);
expect(fluentNode.findOneRecursive(value => value === TEST_VALUE_1)?.value).toBe(TEST_VALUE_1);
expect(fluentNode.findOneRecursive(value => value === TEST_VALUE_10)?.value).toBe(TEST_VALUE_10);
expect(fluentNode.findOneRecursive(value => value === TEST_VALUE_100)?.value).toBe(TEST_VALUE_100);
expect(fluentNode.findOneRecursive(value => value === VALUE_NOT_UNDER_TEST)).toBe(undefined);
expect(fluentNode.findAll(value => value === TEST_VALUE_1).length).toBe(ONE_CHILD);
expect(fluentNode.findAll(value => value === TEST_VALUE_1)[0].value).toBe(TEST_VALUE_1);
expect(fluentNode.findAll(value => value === TEST_VALUE_10).length).toBe(ONE_CHILD);
expect(fluentNode.findAll(value => value === TEST_VALUE_10)[0].value).toBe(TEST_VALUE_10);
expect(fluentNode.findAll(value => value === TEST_VALUE_100).length).toBe(ZERO_CHILDREN);
expect(fluentNode.findAll(value => value === VALUE_NOT_UNDER_TEST).length).toBe(ZERO_CHILDREN);
expect(fluentNode.findAll(value => value >= TEST_VALUE_1).length).toBe(TWO_CHILDREN);
expect(fluentNode.findAll(value => value >= TEST_VALUE_1)[0].value).toBe(TEST_VALUE_1);
expect(fluentNode.findAll(value => value >= TEST_VALUE_1)[1].value).toBe(TEST_VALUE_10);
expect(fluentNode.findAll(value => value >= TEST_VALUE_10).length).toBe(ONE_CHILD);
expect(fluentNode.findAll(value => value >= TEST_VALUE_10)[0].value).toBe(TEST_VALUE_10);
expect(fluentNode.findAllRecursive(value => value === TEST_VALUE_1).length).toBe(ONE_CHILD);
expect(fluentNode.findAllRecursive(value => value === TEST_VALUE_1)[0].value).toBe(TEST_VALUE_1);
expect(fluentNode.findAllRecursive(value => value === TEST_VALUE_10).length).toBe(ONE_CHILD);
expect(fluentNode.findAllRecursive(value => value === TEST_VALUE_10)[0].value).toBe(TEST_VALUE_10);
expect(fluentNode.findAllRecursive(value => value === TEST_VALUE_100).length).toBe(ONE_CHILD);
expect(fluentNode.findAllRecursive(value => value === TEST_VALUE_100)[0].value).toBe(TEST_VALUE_100);
expect(fluentNode.findAllRecursive(value => value === VALUE_NOT_UNDER_TEST).length).toBe(ZERO_CHILDREN);
expect(fluentNode.findAllRecursive(value => value >= TEST_VALUE_1).length).toBe(THREE_CHILDREN);
expect(fluentNode.findAllRecursive(value => value >= TEST_VALUE_1)[0].value).toBe(TEST_VALUE_1);
expect(fluentNode.findAllRecursive(value => value >= TEST_VALUE_1)[1].value).toBe(TEST_VALUE_10);
expect(fluentNode.findAllRecursive(value => value >= TEST_VALUE_1)[2].value).toBe(TEST_VALUE_100);
expect(fluentNode.getAllNodes().length).toBe(THREE_CHILDREN);
expect(fluentNode.getAllNodes()[0].value).toBe(TEST_VALUE_1);
expect(fluentNode.getAllNodes()[1].value).toBe(TEST_VALUE_10);
expect(fluentNode.getAllNodes()[2].value).toBe(TEST_VALUE_100);
})

9
vitest.config.ts Normal file
View File

@ -0,0 +1,9 @@
/// <reference types="vitest" />
import { getViteConfig } from 'astro/config';
export default getViteConfig({
test: {
/* for example, use global to avoid globals imports (describe, test, expect): */
// globals: true,
},
});