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.

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