diff --git a/README.md b/README.md
index bdf50f1..913cd92 100644
--- a/README.md
+++ b/README.md
@@ -2,9 +2,24 @@
PriceWolves Customer Facing Frontend Web App | Price Tracker & History App | Auto Purchase
-## REFERENCES
+## Developer Notes
-- [AWS Amplify Setup Guide](https://aws.amazon.com/getting-started/hands-on/build-react-app-amplify-graphql/)
+- [Price Wolves Confluence Docs](https://kobetran.atlassian.net/wiki/spaces/~71202094dd75410d634f5d8b40e06cc4c97a42/pages/2949140/Developer+Notes?atlOrigin=eyJpIjoiYzEzMDdlZTJhMjZmNDY3MGEyOTI3MWJjODM2OThjZGMiLCJwIjoiYyJ9)
+
+## Running Locally
+
+- Install dependencies: `npm install`
+- Render UI locally: `npm run dev`
+- Run test cases: `npm run test`
+- When updating AWS amplify code, run `npx ampx sandbox` to reflect in the AWS cloud environment.
+ - Successful Confirmation message will be:
+
+ ```
+ [Sandbox] watching for file changes...
+ File written: amplify_outputs.json
+ ```
+
+ - Exit out of terminal and test.
## React + TypeScript + Vite
diff --git a/amplify/data/resource.ts b/amplify/data/resource.ts
index 7e1c6ae..a0d887d 100644
--- a/amplify/data/resource.ts
+++ b/amplify/data/resource.ts
@@ -7,10 +7,36 @@ specifies that any unauthenticated user can "create", "read", "update",
and "delete" any "Todo" records.
=========================================================================*/
const schema = a.schema({
- Todo: a
+ Item: a
.model({
- content: a.string(),
+ itemId: a.id(),
+ barcode: a.string(),
+ itemName: a.string().required(),
+ itemImageUrl: a.url(),
+ itemPrice: a.float().required(),
+ units: a.string().required(),
+ category: a.string().required(),
+ description: a.string(),
+ storeName: a.string().required(),
+ storeId: a.id().required(),
+ isDiscount: a.boolean(),
+ discountedPrice: a.float(),
})
+ .identifier(["itemId"])
+ .secondaryIndexes((index) => [
+ index("barcode"),
+ index("itemName"),
+ ])
+ .authorization((allow) => [allow.guest()]),
+ Store: a
+ .model({
+ storeId: a.id(),
+ storeName: a.string().required(),
+ storeLocations: a.json().array().required(),
+ isBigChain: a.boolean().required(),
+ storeLogoUrl: a.url(),
+ })
+ .identifier(["storeId"])
.authorization((allow) => [allow.guest()]),
});
diff --git a/package-lock.json b/package-lock.json
index a4d983c..34cb650 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,9 +12,12 @@
"@fortawesome/free-solid-svg-icons": "^6.7.2",
"@fortawesome/react-fontawesome": "^0.2.2",
"aws-amplify": "^6.15.3",
+ "dompurify": "^3.2.6",
"react": "^19.1.0",
"react-dom": "^19.1.0",
- "react-router-dom": "^7.6.3"
+ "react-router-dom": "^7.6.3",
+ "sweetalert2": "^11.22.2",
+ "validator": "^13.15.15"
},
"devDependencies": {
"@aws-amplify/backend": "^1.16.1",
@@ -23,6 +26,7 @@
"@types/node": "^24.0.10",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
+ "@types/validator": "^13.15.2",
"@vitejs/plugin-react": "^4.5.2",
"aws-cdk-lib": "^2.189.1",
"constructs": "^10.4.2",
@@ -34,7 +38,8 @@
"tsx": "^4.20.3",
"typescript": "^5.8.3",
"typescript-eslint": "^8.34.1",
- "vite": "^7.0.0"
+ "vite": "^7.0.0",
+ "vitest": "^3.2.4"
}
},
"node_modules/@ampproject/remapping": {
@@ -47555,6 +47560,23 @@
"@babel/types": "^7.20.7"
}
},
+ "node_modules/@types/chai": {
+ "version": "5.2.2",
+ "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz",
+ "integrity": "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/deep-eql": "*"
+ }
+ },
+ "node_modules/@types/deep-eql": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
+ "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
@@ -47604,12 +47626,26 @@
"@types/react": "^19.0.0"
}
},
+ "node_modules/@types/trusted-types": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
+ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
+ "license": "MIT",
+ "optional": true
+ },
"node_modules/@types/uuid": {
"version": "9.0.8",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz",
"integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==",
"license": "MIT"
},
+ "node_modules/@types/validator": {
+ "version": "13.15.2",
+ "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.2.tgz",
+ "integrity": "sha512-y7pa/oEJJ4iGYBxOpfAKn5b9+xuihvzDVnC/OSvlVnGxVg0pOqmjiMafiJ1KVNQEaPZf9HsEp5icEwGg8uIe5Q==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.35.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.35.1.tgz",
@@ -47910,6 +47946,121 @@
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0"
}
},
+ "node_modules/@vitest/expect": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz",
+ "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/chai": "^5.2.2",
+ "@vitest/spy": "3.2.4",
+ "@vitest/utils": "3.2.4",
+ "chai": "^5.2.0",
+ "tinyrainbow": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/mocker": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz",
+ "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/spy": "3.2.4",
+ "estree-walker": "^3.0.3",
+ "magic-string": "^0.30.17"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "msw": "^2.4.9",
+ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
+ },
+ "peerDependenciesMeta": {
+ "msw": {
+ "optional": true
+ },
+ "vite": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vitest/pretty-format": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz",
+ "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tinyrainbow": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/runner": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz",
+ "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/utils": "3.2.4",
+ "pathe": "^2.0.3",
+ "strip-literal": "^3.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/snapshot": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz",
+ "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "3.2.4",
+ "magic-string": "^0.30.17",
+ "pathe": "^2.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/spy": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz",
+ "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tinyspy": "^4.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/utils": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz",
+ "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "3.2.4",
+ "loupe": "^3.1.4",
+ "tinyrainbow": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
"node_modules/@whatwg-node/disposablestack": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/@whatwg-node/disposablestack/-/disposablestack-0.0.6.tgz",
@@ -48274,6 +48425,16 @@
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
"dev": true
},
+ "node_modules/assertion-error": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
+ "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/ast-types": {
"version": "0.13.4",
"resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz",
@@ -49064,6 +49225,16 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "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==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/call-bind": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
@@ -49332,6 +49503,23 @@
"integrity": "sha512-khVnUNqEfRrkkCkEKzSmibqgVHrt7jl/5by8JOyR8reaXbXOIWubLuKdu+QZpRvuRXIci1BpbvxczQKEsl+ldw==",
"dev": true
},
+ "node_modules/chai": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.1.tgz",
+ "integrity": "sha512-5nFxhUrX0PqtyogoYOA8IPswy5sZFTOsBFl/9bNsmDLgsxYTzSZQJDPppDnZPTQbzSEm0hqGjWPzRemQCYbD6A==",
+ "dev": true,
+ "license": "MIT",
+ "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": ">=18"
+ }
+ },
"node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -49412,6 +49600,16 @@
"node": "*"
}
},
+ "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==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 16"
+ }
+ },
"node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
@@ -49882,6 +50080,16 @@
"node": ">=0.10.0"
}
},
+ "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==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/deep-is": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
@@ -50052,6 +50260,15 @@
"node": ">=8"
}
},
+ "node_modules/dompurify": {
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.6.tgz",
+ "integrity": "sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==",
+ "license": "(MPL-2.0 OR Apache-2.0)",
+ "optionalDependencies": {
+ "@types/trusted-types": "^2.0.7"
+ }
+ },
"node_modules/dot-case": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz",
@@ -50214,6 +50431,13 @@
"node": ">= 0.4"
}
},
+ "node_modules/es-module-lexer": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
+ "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
@@ -50555,6 +50779,16 @@
"node": ">=4.0"
}
},
+ "node_modules/estree-walker": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
+ "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0"
+ }
+ },
"node_modules/esutils": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
@@ -50621,6 +50855,16 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/expect-type": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz",
+ "integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
"node_modules/external-editor": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz",
@@ -52687,6 +52931,13 @@
"loose-envify": "cli.js"
}
},
+ "node_modules/loupe": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.0.tgz",
+ "integrity": "sha512-2NCfZcT5VGVNX9mSZIxLRkEAegDGBpuQZBy13desuHeVORmBDyAET4TkJr4SjqQy3A8JDofMN6LpkK8Xcm/dlw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/lower-case": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz",
@@ -52715,6 +52966,16 @@
"yallist": "^3.0.2"
}
},
+ "node_modules/magic-string": {
+ "version": "0.30.17",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
+ "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0"
+ }
+ },
"node_modules/map-cache": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz",
@@ -53525,6 +53786,23 @@
"node": ">=8"
}
},
+ "node_modules/pathe": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
+ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/pathval": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz",
+ "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14.16"
+ }
+ },
"node_modules/pg": {
"version": "8.11.6",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.11.6.tgz",
@@ -54634,6 +54912,13 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/siginfo": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
+ "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
+ "dev": true,
+ "license": "ISC"
+ },
"node_modules/signal-exit": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
@@ -54797,6 +55082,20 @@
"node": ">= 0.6"
}
},
+ "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==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/std-env": {
+ "version": "3.9.0",
+ "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz",
+ "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/stdin-discarder": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz",
@@ -55069,6 +55368,26 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/strip-literal": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.0.0.tgz",
+ "integrity": "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^9.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/strip-literal/node_modules/js-tokens": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz",
+ "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/strnum": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz",
@@ -55115,6 +55434,16 @@
"tslib": "^2.0.3"
}
},
+ "node_modules/sweetalert2": {
+ "version": "11.22.2",
+ "resolved": "https://registry.npmjs.org/sweetalert2/-/sweetalert2-11.22.2.tgz",
+ "integrity": "sha512-GFQGzw8ZXF23PO79WMAYXLl4zYmLiaKqYJwcp5eBF07wiI5BYPbZtKi2pcvVmfUQK+FqL1risJAMxugcPbGIyg==",
+ "license": "MIT",
+ "funding": {
+ "type": "individual",
+ "url": "https://github.com/sponsors/limonte"
+ }
+ },
"node_modules/sync-fetch": {
"version": "0.6.0-2",
"resolved": "https://registry.npmjs.org/sync-fetch/-/sync-fetch-0.6.0-2.tgz",
@@ -55235,6 +55564,20 @@
"node": ">=16"
}
},
+ "node_modules/tinybench": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
+ "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinyexec": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",
+ "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/tinyglobby": {
"version": "0.2.14",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",
@@ -55280,6 +55623,36 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
+ "node_modules/tinypool": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz",
+ "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ }
+ },
+ "node_modules/tinyrainbow": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz",
+ "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tinyspy": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.3.tgz",
+ "integrity": "sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
"node_modules/title-case": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/title-case/-/title-case-3.0.3.tgz",
@@ -55718,6 +56091,15 @@
"uuid": "dist/bin/uuid"
}
},
+ "node_modules/validator": {
+ "version": "13.15.15",
+ "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.15.tgz",
+ "integrity": "sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
"node_modules/value-or-promise": {
"version": "1.0.12",
"resolved": "https://registry.npmjs.org/value-or-promise/-/value-or-promise-1.0.12.tgz",
@@ -55802,6 +56184,29 @@
}
}
},
+ "node_modules/vite-node": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz",
+ "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cac": "^6.7.14",
+ "debug": "^4.4.1",
+ "es-module-lexer": "^1.7.0",
+ "pathe": "^2.0.3",
+ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
+ },
+ "bin": {
+ "vite-node": "vite-node.mjs"
+ },
+ "engines": {
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
"node_modules/vite/node_modules/fdir": {
"version": "6.4.6",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz",
@@ -55830,6 +56235,92 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
+ "node_modules/vitest": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz",
+ "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/chai": "^5.2.2",
+ "@vitest/expect": "3.2.4",
+ "@vitest/mocker": "3.2.4",
+ "@vitest/pretty-format": "^3.2.4",
+ "@vitest/runner": "3.2.4",
+ "@vitest/snapshot": "3.2.4",
+ "@vitest/spy": "3.2.4",
+ "@vitest/utils": "3.2.4",
+ "chai": "^5.2.0",
+ "debug": "^4.4.1",
+ "expect-type": "^1.2.1",
+ "magic-string": "^0.30.17",
+ "pathe": "^2.0.3",
+ "picomatch": "^4.0.2",
+ "std-env": "^3.9.0",
+ "tinybench": "^2.9.0",
+ "tinyexec": "^0.3.2",
+ "tinyglobby": "^0.2.14",
+ "tinypool": "^1.1.1",
+ "tinyrainbow": "^2.0.0",
+ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0",
+ "vite-node": "3.2.4",
+ "why-is-node-running": "^2.3.0"
+ },
+ "bin": {
+ "vitest": "vitest.mjs"
+ },
+ "engines": {
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@edge-runtime/vm": "*",
+ "@types/debug": "^4.1.12",
+ "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
+ "@vitest/browser": "3.2.4",
+ "@vitest/ui": "3.2.4",
+ "happy-dom": "*",
+ "jsdom": "*"
+ },
+ "peerDependenciesMeta": {
+ "@edge-runtime/vm": {
+ "optional": true
+ },
+ "@types/debug": {
+ "optional": true
+ },
+ "@types/node": {
+ "optional": true
+ },
+ "@vitest/browser": {
+ "optional": true
+ },
+ "@vitest/ui": {
+ "optional": true
+ },
+ "happy-dom": {
+ "optional": true
+ },
+ "jsdom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vitest/node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
"node_modules/wcwidth": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",
@@ -55986,6 +56477,23 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "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==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "siginfo": "^2.0.0",
+ "stackback": "0.0.2"
+ },
+ "bin": {
+ "why-is-node-running": "cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/word-wrap": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
diff --git a/package.json b/package.json
index 8c11179..a078ea2 100644
--- a/package.json
+++ b/package.json
@@ -5,6 +5,7 @@
"type": "module",
"scripts": {
"dev": "vite",
+ "test": "vitest",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
@@ -14,9 +15,12 @@
"@fortawesome/free-solid-svg-icons": "^6.7.2",
"@fortawesome/react-fontawesome": "^0.2.2",
"aws-amplify": "^6.15.3",
+ "dompurify": "^3.2.6",
"react": "^19.1.0",
"react-dom": "^19.1.0",
- "react-router-dom": "^7.6.3"
+ "react-router-dom": "^7.6.3",
+ "sweetalert2": "^11.22.2",
+ "validator": "^13.15.15"
},
"devDependencies": {
"@aws-amplify/backend": "^1.16.1",
@@ -25,6 +29,7 @@
"@types/node": "^24.0.10",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
+ "@types/validator": "^13.15.2",
"@vitejs/plugin-react": "^4.5.2",
"aws-cdk-lib": "^2.189.1",
"constructs": "^10.4.2",
@@ -36,6 +41,7 @@
"tsx": "^4.20.3",
"typescript": "^5.8.3",
"typescript-eslint": "^8.34.1",
- "vite": "^7.0.0"
+ "vite": "^7.0.0",
+ "vitest": "^3.2.4"
}
}
diff --git a/src/index.css b/src/index.css
index ecb025f..d74a2c8 100644
--- a/src/index.css
+++ b/src/index.css
@@ -39,6 +39,11 @@ h1 {
line-height: 1.1;
}
+.required-form-item{
+ color: red;
+ margin-left: 0.25rem;
+}
+
.button {
border-radius: 3rem ;
border: 1px solid transparent;
@@ -255,6 +260,10 @@ h1 {
width: 100%;
}
+.add-form textarea {
+ height: 10rem;
+}
+
.add-form input[type="checkbox"] {
width: auto;
}
diff --git a/src/main.tsx b/src/main.tsx
index 2fc4bf7..1376250 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -2,10 +2,10 @@ import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import './index.css';
import App from './App';
-// import { Amplify } from 'aws-amplify';
-// import outputs from '../amplify_outputs.json';
+import { Amplify } from 'aws-amplify';
+import outputs from '../amplify_outputs.json';
-// Amplify.configure(outputs);
+Amplify.configure(outputs);
const container = document.getElementById('root');
if (!container) throw new Error("Unable to find root element");
diff --git a/src/pages/AddItem.tsx b/src/pages/AddItem.tsx
index 8cbaeb1..a691e1c 100644
--- a/src/pages/AddItem.tsx
+++ b/src/pages/AddItem.tsx
@@ -6,40 +6,6 @@ const AddItem: React.FC = () => {
);
diff --git a/src/pages/CreateNewItem.tsx b/src/pages/CreateNewItem.tsx
index 3199f09..866e9c4 100644
--- a/src/pages/CreateNewItem.tsx
+++ b/src/pages/CreateNewItem.tsx
@@ -1,16 +1,324 @@
-import React from "react"
+import React, { useState } from "react";
+import DOMPurify from "dompurify";
+import Swal from 'sweetalert2'
+import { generateClient } from "aws-amplify/data";
+import type { Schema } from "../../amplify/data/resource";
+import { isSafeUrl } from "../utils/isSafeUrl";
+
+const client = generateClient();
+
+interface ItemInputs {
+ barcode: string;
+ itemName: string;
+ itemImageUrl: string;
+ itemPrice: number;
+ units: string;
+ category: string;
+ description: string;
+ storeName: string;
+ storeId: string;
+ isDiscount: boolean;
+ discountedPrice: number;
+}
+
+const initialItemInputs: ItemInputs = {
+ barcode: "",
+ itemName: "",
+ itemImageUrl: "",
+ itemPrice: 0,
+ units: "",
+ category: "",
+ description: "",
+ storeName: "",
+ storeId: "",
+ isDiscount: false,
+ discountedPrice: 0,
+};
const CreateNewItem: React.FC = () => {
+ const [inputs, setInputs] = useState(initialItemInputs);
+
+ // Generic change handler for text, number, and checkbox fields
+ const handleChange = (
+ e: React.ChangeEvent
+ ) => {
+ const { name, type, value } = e.target;
+ const fieldValue =
+ type === 'checkbox' ? (e.target as HTMLInputElement).checked : value;
+
+ setInputs((prev) => ({
+ ...prev,
+ [name]: fieldValue,
+ }));
+ };
+
+ // Submit to AWS Amplify Data API
+ const handleSubmit = async (
+ e: React.FormEvent
+ ): Promise => {
+ e.preventDefault();
+
+ const itemPrice = Number(inputs.itemPrice);
+ if(isNaN(itemPrice) || itemPrice < 0) {
+ Swal.fire({
+ title: 'Something went wrong!',
+ text: 'Item price must be a positive number.',
+ icon: 'error',
+ })
+ throw new Error(`Invalid price upon submission`)
+ }
+
+ const discountedPrice = Number(inputs.discountedPrice);
+ if(isNaN(discountedPrice) || discountedPrice < 0) {
+ Swal.fire({
+ title: 'Something went wrong!',
+ text: 'Discounted price must be a positive number.',
+ icon: 'error',
+ })
+ throw new Error("Invalid discounted price upon submission")
+ } else if(discountedPrice >= itemPrice) {
+ Swal.fire({
+ title: 'Something went wrong!',
+ text: 'Discounted price must be less than the original price.',
+ icon: 'error',
+ })
+ throw new Error("Discounted price cannot be greater than or the same as the original price upon submission")
+ }
+
+ const itemImageUrl = inputs.itemImageUrl;
+ let safeItemImageUrl: string;
+ if(itemImageUrl && isSafeUrl(itemImageUrl)) {
+ const url = new URL(itemImageUrl);
+ safeItemImageUrl = url.href;
+ } else {
+ Swal.fire({
+ title: 'Something went wrong!',
+ text: 'Item image URL is not safe or valid.',
+ icon: 'error',
+ })
+ throw new Error("Invalid image URL upon submission")
+ }
+
+ const sanitizedItemInputs = {
+ barcode: DOMPurify.sanitize(inputs.barcode) || undefined,
+ itemName: DOMPurify.sanitize(inputs.itemName),
+ itemImageUrl: DOMPurify.sanitize(safeItemImageUrl) || undefined,
+ itemPrice: itemPrice,
+ units: DOMPurify.sanitize(inputs.units),
+ category: DOMPurify.sanitize(inputs.category),
+ description: DOMPurify.sanitize(inputs.description) || undefined,
+ storeName: DOMPurify.sanitize(inputs.storeName),
+ storeId: DOMPurify.sanitize(inputs.storeId),
+ isDiscount: inputs.isDiscount,
+ discountedPrice: inputs.isDiscount
+ ? discountedPrice
+ : undefined,
+ }
+
+ try {
+ await client.models.Item.create(sanitizedItemInputs);
+ const successMessage = `Item: ${sanitizedItemInputs.itemName} created successfully for $${sanitizedItemInputs.itemPrice} per ${sanitizedItemInputs.units}`;
+ Swal.fire({
+ title: 'New Item Successfully Created',
+ text: successMessage,
+ icon: 'success',
+ })
+
+ setInputs(initialItemInputs);
+ return;
+ } catch (error) {
+ console.error("Error creating item:", error);
+ Swal.fire({
+ title: 'Something went wrong!',
+ text: 'Failed to create item. Please try again. Contact admin for support if issue persists.',
+ icon: 'error',
+ })
+ return;
+ }
+ };
+
return (
-
-
- Create New Item
-
- Create New Item
-
-
-
+ <>
+ Create New Item - Price Wolves
+
+
+
+
+
+
+
+
+
+ >
);
};
-export default CreateNewItem
+export default CreateNewItem;
diff --git a/src/utils/isSafeUrl.spec.ts b/src/utils/isSafeUrl.spec.ts
new file mode 100644
index 0000000..b9c2331
--- /dev/null
+++ b/src/utils/isSafeUrl.spec.ts
@@ -0,0 +1,58 @@
+import { describe, it, expect } from 'vitest'
+import { isSafeUrl } from './isSafeUrl'
+
+// Some private-IP examples
+const privateIPv4 = 'https://10.0.5.2';
+const privateIPv4b = 'https://172.20.10.4';
+const privateIPv6 = 'https://[::1]';
+
+describe('isSafeUrl()', () => {
+ it('accepts a basic https URL', () => {
+ expect(isSafeUrl('https://example.com')).toBe(true)
+ })
+
+ it('rejects non-https protocols', () => {
+ expect(isSafeUrl('http://example.com')).toBe(false)
+ expect(isSafeUrl('ftp://example.com')).toBe(false)
+ })
+
+ it('rejects URLs with credentials', () => {
+ expect(isSafeUrl('https://user:pass@example.com')).toBe(false)
+ })
+
+ it('rejects non-443 ports', () => {
+ expect(isSafeUrl('https://example.com:8443')).toBe(false)
+ expect(isSafeUrl('https://example.com:443')).toBe(true)
+ })
+
+ it('rejects excessively long URLs', () => {
+ const long = 'https://' + 'a'.repeat(2050) + '.com'
+ expect(isSafeUrl(long)).toBe(false)
+ })
+
+ it('rejects private IPv4 addresses', () => {
+ expect(isSafeUrl(privateIPv4)).toBe(false)
+ expect(isSafeUrl(privateIPv4b)).toBe(false)
+ })
+
+ it('rejects private IPv6 addresses', () => {
+ expect(isSafeUrl(privateIPv6)).toBe(false)
+ })
+
+ it('rejects localhost and 0.0.0.0', () => {
+ expect(isSafeUrl('https://localhost')).toBe(false)
+ expect(isSafeUrl('https://0.0.0.0')).toBe(false)
+ })
+
+ it('rejects mixed-case or trailing-dot hosts', () => {
+ expect(isSafeUrl('https://ExAmPlE.com')).toBe(false)
+ expect(isSafeUrl('https://example.com.')).toBe(false)
+ })
+
+ it('handles punycode (IDN) correctly', () => {
+ // user-provided domain in Unicode
+ const unicode = 'https://☕️.example'
+ // URL() converts to punycode under the hood
+ expect(isSafeUrl(unicode)).toBe(true)
+ })
+})
diff --git a/src/utils/isSafeUrl.ts b/src/utils/isSafeUrl.ts
new file mode 100644
index 0000000..e2529be
--- /dev/null
+++ b/src/utils/isSafeUrl.ts
@@ -0,0 +1,69 @@
+import isIP from "validator/es/lib/isIP";
+
+export const isSafeUrl = (str: string): boolean => {
+ // 1) Must start with "https://" exactly
+ if (!str.startsWith("https://")) return false;
+
+ // 2) Extract the raw host (before any slash or port)
+ let rawHost = str.slice(8).split("/")[0];
+ // Handle IPv6 brackets
+ if (rawHost.startsWith("[")) {
+ rawHost = rawHost.split("]")[0] + "]";
+ } else {
+ rawHost = rawHost.split(":")[0];
+ }
+
+ // 3) Reject mixed‐case or trailing dots in the *original* host
+ if (/[A-Z]/.test(rawHost) || rawHost.endsWith(".")) {
+ return false;
+ }
+
+ // 4) Parse, then standard checks
+ let url: URL;
+ try {
+ url = new URL(str);
+ } catch {
+ return false;
+ }
+
+ if (url.protocol !== "https:") return false;
+ if (url.username || url.password) return false;
+ if (url.port && url.port !== "443") return false;
+ if (str.length > 2048) return false;
+
+ const host = url.hostname; // now lowercased, no brackets
+
+ // 5) Block localhost or IPv4 0.0.0.0
+ if (host === "localhost" || host === "0.0.0.0") return false;
+
+ // 6) Block any private IP (IPv4 & IPv6)
+ const cleanHost = host.startsWith('[') && host.endsWith(']') ? host.slice(1, -1) : host;
+ if (isPrivateIp(cleanHost)) return false;
+
+ return true;
+};
+
+function isPrivateIp(ipStr: string): boolean {
+ // IPv6 private ranges
+ if (isIP(ipStr, 6)) {
+ return (
+ ipStr === "::1" ||
+ /^f[cd]/i.test(ipStr) ||
+ /^fe80:/i.test(ipStr)
+ );
+ }
+
+ // IPv4 private ranges
+ if (isIP(ipStr, 4)) {
+ const [o1, o2] = ipStr.split(".").map(Number);
+ return (
+ o1 === 10 ||
+ (o1 === 172 && o2 >= 16 && o2 <= 31) ||
+ (o1 === 192 && o2 === 168) ||
+ o1 === 127 ||
+ (o1 === 169 && o2 === 254)
+ );
+ }
+
+ return false;
+}