From 935340d90e07a12ca087e907d7cfcb1fc4455e67 Mon Sep 17 00:00:00 2001
From: David Ball
Date: Tue, 16 Jul 2024 14:02:47 -0400
Subject: [PATCH] Added SwiperJS swipers on ProductCards and added preliminary
support for nested categories using a tree of TreeNode structures. Added unit
tests for TreeNode.
---
package-lock.json | 297 +++++++++++++++++-
package.json | 6 +-
src/data/categories.ts | 163 +++++-----
src/pages/[productLookup].astro | 4 +-
...ookup].astro => [...categoryLookup].astro} | 16 +-
src/pages/index.astro | 4 +-
src/types/tree.ts | 174 ++++++++++
test/tree.test.ts | 83 +++++
vitest.config.ts | 9 +
9 files changed, 667 insertions(+), 89 deletions(-)
rename src/pages/category/{[categoryLookup].astro => [...categoryLookup].astro} (89%)
create mode 100644 src/types/tree.ts
create mode 100644 test/tree.test.ts
create mode 100644 vitest.config.ts
diff --git a/package-lock.json b/package-lock.json
index 87f2863..e6fea4e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -24,7 +24,8 @@
"playwright": "*",
"react": "^18.3.1",
"react-dom": "^18.3.1",
- "swiper": "^11.1.4"
+ "swiper": "^11.1.4",
+ "vitest": "^2.0.3"
},
"devDependencies": {
"@apify/tsconfig": "^0.1.0",
@@ -1857,6 +1858,81 @@
"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": {
"version": "2.4.0",
"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"
}
},
+ "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": {
"version": "4.11.5",
"resolved": "https://registry.npmjs.org/astro/-/astro-4.11.5.tgz",
@@ -2429,6 +2513,14 @@
"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": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz",
@@ -2529,6 +2621,21 @@
"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": {
"version": "5.3.0",
"license": "MIT",
@@ -2571,6 +2678,14 @@
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
"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": {
"version": "1.0.0-rc.12",
"resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz",
@@ -3053,6 +3168,14 @@
"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": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz",
@@ -3680,6 +3803,14 @@
"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": {
"version": "8.0.1",
"license": "MIT",
@@ -4966,6 +5097,14 @@
"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": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz",
@@ -6309,6 +6448,19 @@
"version": "6.2.2",
"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": {
"version": "0.0.11",
"resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz",
@@ -7237,6 +7389,11 @@
"@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": {
"version": "4.1.0",
"license": "ISC",
@@ -7355,6 +7512,16 @@
"version": "1.0.3",
"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": {
"version": "0.2.2",
"license": "MIT",
@@ -7542,6 +7709,35 @@
"integrity": "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==",
"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": {
"version": "6.1.30",
"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": {
"version": "2.3.3",
"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": {
"version": "0.0.59",
"resolved": "https://registry.npmjs.org/volar-service-css/-/volar-service-css-0.0.59.tgz",
@@ -8386,6 +8666,21 @@
"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": {
"version": "4.0.1",
"license": "MIT",
diff --git a/package.json b/package.json
index 19b5470..4213107 100644
--- a/package.json
+++ b/package.json
@@ -7,7 +7,8 @@
"start": "astro dev",
"build": "astro check && astro build",
"preview": "astro preview",
- "astro": "astro"
+ "astro": "astro",
+ "test": "vitest"
},
"dependencies": {
"@astrojs/check": "^0.8.1",
@@ -26,7 +27,8 @@
"playwright": "*",
"react": "^18.3.1",
"react-dom": "^18.3.1",
- "swiper": "^11.1.4"
+ "swiper": "^11.1.4",
+ "vitest": "^2.0.3"
},
"devDependencies": {
"@apify/tsconfig": "^0.1.0",
diff --git a/src/data/categories.ts b/src/data/categories.ts
index e6eb421..12e24a7 100644
--- a/src/data/categories.ts
+++ b/src/data/categories.ts
@@ -1,3 +1,5 @@
+import { TreeNode } from "../types/tree";
+
/**
* Category of Products.
*/
@@ -22,10 +24,18 @@ export interface Category {
* Description of Product Category.
*/
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 {
/**
@@ -42,80 +52,81 @@ export class StaticCategory {
}
}
-/**
- * A list of all the categories.
- */
-export const ALL_CATEGORIES: Category[] = [
- {
- id: StaticCategory.nextId(),
- category: "Vehicle Essentials",
- slug: "vehicle-essentials",
- imageUrl: "/assets/vehicle-essentials.png",
- description: "Essential items for your vehicle to ensure smooth deliveries.",
- },
- {
- id: StaticCategory.nextId(),
- category: "Delivery Gear",
- slug: "delivery-gear",
- imageUrl: "/assets/delivery-gear-3.jpg",
- description: "Gear to help you deliver food efficiently and keep it in top condition.",
- },
- {
- id: StaticCategory.nextId(),
- category: "Personal Items",
- slug: "personal-items",
- imageUrl: "/assets/personal-items.jpg",
- description: "Personal essentials to keep you comfortable and prepared on the go.",
- },
- {
- id: StaticCategory.nextId(),
- category: "Safety Equipment",
- slug: "safety-equipment",
- imageUrl: "/assets/safety-equipment.jpg",
- description: "Safety gear to protect you during deliveries.",
- },
- {
- id: StaticCategory.nextId(),
- category: "Tech Gadgets",
- slug: "tech-gadgets",
- imageUrl: "/assets/tech-gadgets.jpg",
- description: "Technology tools to enhance your efficiency and connectivity.",
- },
- {
- id: StaticCategory.nextId(),
- category: "Biking Gear",
- slug: "biking-gear",
- imageUrl: "/assets/biking-gear.jpg",
- description: "Equipment for Dashers who deliver by bike.",
- },
- {
- id: StaticCategory.nextId(),
- category: "Comfort and Convenience",
- slug: "comfort-convenience",
- imageUrl: "/assets/comfort-convenience.png",
- description: "Items to increase comfort and convenience during deliveries.",
- },
- {
- id: StaticCategory.nextId(),
- category: "Health and Wellness",
- slug: "health-wellness",
- imageUrl: "/assets/wearable-tech.jpg",
- description: "Products to help you stay healthy and well during your shifts.",
- },
- {
- id: StaticCategory.nextId(),
- category: "Miscellaneous",
- slug: "misc",
- imageUrl: "/assets/misc.jpg",
- description: "Various other items that can be useful for Dashers.",
- }
-].sort((a, b) => a.slug.localeCompare(b.slug));
+export const ALL_CATEGORIES = TreeNode.createRoot({
+ id: 0,
+ category: "All Categories",
+ slug: "all-categories",
+ description: "All categories in the whole store."
+}).addChild({
+ id: StaticCategory.nextId(),
+ category: "Vehicle Essentials",
+ slug: "vehicle-essentials",
+ imageUrl: "/assets/vehicle-essentials.png",
+ 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(),
+ category: "Delivery Gear",
+ slug: "delivery-gear",
+ imageUrl: "/assets/delivery-gear-3.jpg",
+ description: "Gear to help you deliver food efficiently and keep it in top condition.",
+}).root().addChild({
+ id: StaticCategory.nextId(),
+ category: "Personal Items",
+ slug: "personal-items",
+ imageUrl: "/assets/personal-items.jpg",
+ description: "Personal essentials to keep you comfortable and prepared on the go.",
+}).root().addChild({
+ id: StaticCategory.nextId(),
+ category: "Safety Equipment",
+ slug: "safety-equipment",
+ imageUrl: "/assets/safety-equipment.jpg",
+ description: "Safety gear to protect you during deliveries.",
+}).root().addChild({
+ id: StaticCategory.nextId(),
+ category: "Tech Gadgets",
+ slug: "tech-gadgets",
+ imageUrl: "/assets/tech-gadgets.jpg",
+ description: "Technology tools to enhance your efficiency and connectivity.",
+}).root().addChild({
+ id: StaticCategory.nextId(),
+ category: "Biking Gear",
+ slug: "biking-gear",
+ imageUrl: "/assets/biking-gear.jpg",
+ description: "Equipment for Dashers who deliver by bike.",
+}).root().addChild({
+ id: StaticCategory.nextId(),
+ category: "Comfort and Convenience",
+ slug: "comfort-convenience",
+ imageUrl: "/assets/comfort-convenience.png",
+ description: "Items to increase comfort and convenience during deliveries.",
+}).root().addChild({
+ id: StaticCategory.nextId(),
+ category: "Health and Wellness",
+ slug: "health-wellness",
+ imageUrl: "/assets/wearable-tech.jpg",
+ description: "Products to help you stay healthy and well during your shifts.",
+}).root().addChild({
+ id: StaticCategory.nextId(),
+ category: "Miscellaneous",
+ slug: "misc",
+ imageUrl: "/assets/misc.jpg",
+ description: "Various other items that can be useful for Dashers.",
+}).root();
-export function getCategoryIdForSlug(slug: string): number|null {
- for (const category of ALL_CATEGORIES) {
- if (category.slug == slug) {
- return category.id;
- }
- };
- return null;
+export function getCategoryNodeForSlug(slug: string): TreeNode|undefined {
+ return ALL_CATEGORIES.findOneRecursive(category => category.slug === slug) || undefined;
+}
+
+export function getCategoryForSlug(slug: string): Category|undefined {
+ return getCategoryNodeForSlug(slug)?.value || undefined;
+}
+
+export function getCategoryIdForSlug(slug: string): number|undefined {
+ return getCategoryForSlug(slug)?.id || undefined;
}
diff --git a/src/pages/[productLookup].astro b/src/pages/[productLookup].astro
index a78a5ea..5c62762 100644
--- a/src/pages/[productLookup].astro
+++ b/src/pages/[productLookup].astro
@@ -7,6 +7,7 @@ import StarRating from '../components/StarRating.astro';
import ImageCarousel from '../components/ImageCarousel.astro';
import markdownIt from 'markdown-it';
import markdownItAttrs from 'markdown-it-attrs';
+import type { TreeNode } from '../types/tree';
const md = markdownIt({
html: true,
linkify: true,
@@ -34,7 +35,8 @@ const formatAsCurrency = (amount: number) => amount.toLocaleString('en-US', { st
const { productLookup } = Astro.params;
const product: Product = ALL_PRODUCTS.find(p => p.slug === productLookup)!;
-const category: Category = ALL_CATEGORIES.find(c => c.id === product.categoryId)!;
+const categoryNode: TreeNode = ALL_CATEGORIES.findOneRecursive(c => c.id === product.categoryId)!;
+const category: Category = categoryNode.value;
const brand: Brand = ALL_BRANDS.find(b => b.slug === product.brandStoreSlug)!;
---
diff --git a/src/pages/category/[categoryLookup].astro b/src/pages/category/[...categoryLookup].astro
similarity index 89%
rename from src/pages/category/[categoryLookup].astro
rename to src/pages/category/[...categoryLookup].astro
index fde1a48..4116c4a 100644
--- a/src/pages/category/[categoryLookup].astro
+++ b/src/pages/category/[...categoryLookup].astro
@@ -1,22 +1,24 @@
---
import Layout from '../../layouts/Layout.astro';
// 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 ProductCard from '../../components/ProductCard.astro';
+import { DeepArray } from '../../types/deep-array';
type CategoryStaticPath = { params: { categoryLookup: string }};
export function getStaticPaths() {
- return ALL_CATEGORIES.map((category) => { return {
- params: {
- categoryLookup: category.slug
- }
- }});
+ return ALL_CATEGORIES.getAllNodes().map(categoryNode => { return {
+ params: {
+ categoryLookup: categoryNode.value.slug
+ }
+ }});
}
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 isAnyAmazon = categoryProducts.find(p => p.amazonLink) || false;
---
diff --git a/src/pages/index.astro b/src/pages/index.astro
index c254607..91f5f2f 100644
--- a/src/pages/index.astro
+++ b/src/pages/index.astro
@@ -14,9 +14,9 @@ import { getProductsForCategoryId } from '../data/products';
Your one-stop shop for all your after-market Dasher supplies.
- {ALL_CATEGORIES.filter(category => getProductsForCategoryId(category.id)?.length > 0).map(category => (
+ {ALL_CATEGORIES.children.filter(categoryNode => getProductsForCategoryId(categoryNode.value.id).length > 0).map(categoryNode => (
))}
diff --git a/src/types/tree.ts b/src/types/tree.ts
new file mode 100644
index 0000000..547aaff
--- /dev/null
+++ b/src/types/tree.ts
@@ -0,0 +1,174 @@
+/**
+ * A simple tree data structure.
+ */
+export class TreeNode {
+ private _value: NodeT;
+ private _children: TreeNode[];
+ private _parent?: TreeNode;
+ 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(value: NodeT): TreeNode {
+ 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 {
+ 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 {
+ return this._parent||this;
+ }
+ /**
+ * Gets root TreeNode node of tree.
+ * @returns Root TreeNode node of tree.
+ */
+ public root(): TreeNode {
+ 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) {
+ this._parent = parent;
+ return this;
+ }
+ /**
+ * Gets children of this TreeNode node as an Array of node values.
+ */
+ public get children(): TreeNode[] {
+ 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 | 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[] {
+ let results: TreeNode[] = [];
+ 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 | 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[] {
+ let results: TreeNode[] = [];
+ 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[] {
+ let nodes: TreeNode[] = [];
+ nodes.push(this);
+ for (let child of this.children) {
+ nodes.push(...child.getAllNodes());
+ }
+ return nodes;
+ }
+}
diff --git a/test/tree.test.ts b/test/tree.test.ts
new file mode 100644
index 0000000..a7c2d30
--- /dev/null
+++ b/test/tree.test.ts
@@ -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);
+})
\ No newline at end of file
diff --git a/vitest.config.ts b/vitest.config.ts
new file mode 100644
index 0000000..9ff3633
--- /dev/null
+++ b/vitest.config.ts
@@ -0,0 +1,9 @@
+///
+import { getViteConfig } from 'astro/config';
+
+export default getViteConfig({
+ test: {
+ /* for example, use global to avoid globals imports (describe, test, expect): */
+ // globals: true,
+ },
+});
\ No newline at end of file