Initial Commit
This commit is contained in:
37
.vscode/launch.json
vendored
Normal file
37
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Launch index.html",
|
||||
"type": "firefox",
|
||||
"request": "launch",
|
||||
"reAttach": true,
|
||||
"file": "${workspaceFolder}/index.html"
|
||||
},
|
||||
{
|
||||
"name": "Launch localhost",
|
||||
"type": "firefox",
|
||||
"request": "launch",
|
||||
"reAttach": true,
|
||||
"url": "http://localhost/index.html",
|
||||
"webRoot": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
"name": "Attach",
|
||||
"type": "firefox",
|
||||
"request": "attach",
|
||||
"url": "http://localhost/index.html",
|
||||
"webRoot": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
"name": "Launch WebExtension",
|
||||
"type": "firefox",
|
||||
"request": "launch",
|
||||
"reAttach": true,
|
||||
"addonPath": "${workspaceFolder}"
|
||||
}
|
||||
]
|
||||
}
|
||||
83
ITEM_COMPONENT.md
Normal file
83
ITEM_COMPONENT.md
Normal file
@@ -0,0 +1,83 @@
|
||||
# Item Component
|
||||
|
||||
A small reusable UI component that shows an image on the left and three text areas on the right.
|
||||
|
||||
## Files
|
||||
|
||||
- `item-component.js` — ES module that exports `createItemComponent()` (default export).
|
||||
- `index.html` — example usage (the project includes a demo where multiple items are laid out in a responsive grid).
|
||||
|
||||
## Import
|
||||
|
||||
Use as an ES module in a browser environment:
|
||||
|
||||
```html
|
||||
<script type="module">
|
||||
import createItemComponent from './item-component.js';
|
||||
|
||||
const el = createItemComponent({
|
||||
imgSrc: 'images/example.jpg',
|
||||
imgAlt: 'Item image',
|
||||
text1: 'Title',
|
||||
text2: 'Subtitle',
|
||||
text3: 'Description or note',
|
||||
editable: false
|
||||
});
|
||||
|
||||
document.body.appendChild(el);
|
||||
</script>
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
- `imgSrc` (string) — image URL. If omitted, an empty `<img>` is created.
|
||||
- `imgAlt` (string) — image alt text.
|
||||
- `text1` (string) — first text area (displayed as title by default).
|
||||
- `text2` (string) — second text area (subtitle).
|
||||
- `text3` (string) — third text area (description).
|
||||
- `editable` (boolean) — when `true` the text areas are rendered as editable `<textarea>` elements instead of static text.
|
||||
- `imgWidth`, `imgHeight` (number) — optional image dimensions in pixels (default 80 each).
|
||||
|
||||
## Layout & Styling
|
||||
|
||||
`item-component.js` injects a small stylesheet into the document head when first used. Key classes:
|
||||
|
||||
- `.item-component` — single item container.
|
||||
- `.item-component__img img` — the image element.
|
||||
- `.item-component__content` — wrapper for the three text lines.
|
||||
- `.item-component__line--title`, `--subtitle`, `--desc` — line-specific classes.
|
||||
- `.item-component-grid` — helper grid wrapper (3 columns by default). Wrap multiple item components in an element with this class to arrange them side-by-side.
|
||||
|
||||
Example: place components side-by-side by giving the parent `class="item-component-grid"` (see `index.html`). The stylesheet includes responsive breakpoints (3 → 2 → 1 column).
|
||||
|
||||
## Examples
|
||||
|
||||
Render multiple items in a grid (as used in `index.html`):
|
||||
|
||||
```html
|
||||
<div id="items" class="item-component-grid"></div>
|
||||
<script type="module">
|
||||
import createItemComponent from './item-component.js';
|
||||
const root = document.getElementById('items');
|
||||
[
|
||||
{ imgSrc: 'https://picsum.photos/seed/1/200/200', text1: 'A', text2: 'B', text3: 'C' },
|
||||
{ imgSrc: 'https://picsum.photos/seed/2/200/200', text1: 'D', text2: 'E', text3: 'F' }
|
||||
].forEach(data => root.appendChild(createItemComponent(data)));
|
||||
</script>
|
||||
```
|
||||
|
||||
## Customization
|
||||
|
||||
- To change image size, pass `imgWidth` and `imgHeight` when creating the component.
|
||||
- To alter the default grid columns or spacing globally, add your own CSS targeting `.item-component-grid` (the injected styles are minimal and can be overridden by another stylesheet loaded later).
|
||||
- To change the visual chrome (border, background, radius), override `.item-component` styles in `main.css` or another stylesheet loaded after the component.
|
||||
|
||||
## Notes
|
||||
|
||||
- The component is vanilla JS with no external dependencies.
|
||||
- Works in modern browsers that support ES modules.
|
||||
|
||||
If you want, I can:
|
||||
- Add a small README with screenshots.
|
||||
- Add local example images under an `images/` folder and update `index.html` to use them.
|
||||
- Export the component as a custom element (`<item-component>`) instead of a factory function.
|
||||
242
README.md
Normal file
242
README.md
Normal file
@@ -0,0 +1,242 @@
|
||||
# Webpage Playground
|
||||
|
||||
A modern inventory management demo app featuring multiple pages for browsing, searching, and scanning barcodes. Built with vanilla HTML, CSS, and JavaScript.
|
||||
|
||||
## Overview
|
||||
|
||||
Webpage Playground is a lightweight, responsive web application for managing and tracking inventory across multiple storage locations (Pantry, Fridge, Freezer). The app includes:
|
||||
|
||||
- **Dynamic Navigation** — Easy page switching with a responsive navbar
|
||||
- **Inventory Demo** — Display items using reusable item components
|
||||
- **Advanced Search** — Filter inventory by name, location, and quantity range
|
||||
- **Barcode Scanner** — Scan barcodes for inventory management (keyboard and hardware scanner support)
|
||||
- **Data Visualization** — Pie chart showing inventory distribution
|
||||
- **Responsive Design** — Works on desktop, tablet, and mobile devices
|
||||
|
||||
## Project Structure
|
||||
|
||||
### Pages
|
||||
- `index.html` — Homepage with item component demo and inventory pie chart
|
||||
- `search.html` — Search and filter inventory by name, location, and quantity
|
||||
- `barcode.html` — Barcode scanner with console logging for testing
|
||||
- `pantry.html` — Pantry inventory container
|
||||
- `fridge.html` — Fridge inventory container
|
||||
- `freezer.html` — Freezer inventory container
|
||||
|
||||
### Core Modules
|
||||
- `navbar.js` — Dynamic navigation bar system loaded by all pages
|
||||
- `main.css` — Global styles and responsive design
|
||||
- `item-component.js` — Reusable ES6 module for displaying inventory items in a grid
|
||||
- `search.js` — Search and filtering logic with 20 sample inventory items
|
||||
- `barcode-scanner.js` — Barcode capture and console logging module
|
||||
- `piechart.js` — Inventory distribution pie chart visualization
|
||||
|
||||
### Documentation
|
||||
- `ITEM_COMPONENT.md` — Detailed documentation for the item component factory
|
||||
- `README.md` — This file
|
||||
|
||||
## Features Overview
|
||||
|
||||
### 🏠 Homepage (`index.html`)
|
||||
The landing page showcasing the project with:
|
||||
- Interactive item component grid (3 sample items)
|
||||
- Inventory breakdown pie chart showing distribution across storage locations
|
||||
- Responsive grid layout that adapts to screen size
|
||||
|
||||
**Sample Data:**
|
||||
- Pantry: 104 items
|
||||
- Fridge: 30 items
|
||||
- Freezer: 87 items
|
||||
|
||||
### 🔍 Search Page (`search.html`)
|
||||
Advanced inventory search and filtering interface.
|
||||
|
||||
**Features:**
|
||||
- Search by item name (case-insensitive)
|
||||
- Filter by storage location (All, Pantry, Fridge, Freezer)
|
||||
- Filter by quantity range (min/max values)
|
||||
- Real-time result display with item cards
|
||||
- Reset button to clear all filters
|
||||
- Keyboard support (press Enter to search)
|
||||
|
||||
**Sample Usage:**
|
||||
```
|
||||
Search for "milk" in the Fridge with quantity >= 1
|
||||
Results displayed using item-component for consistency
|
||||
```
|
||||
|
||||
**Available Items (20 total across all locations):**
|
||||
- Pantry: Pasta, Rice, Cereal, Flour, Sugar, Salt, Olive Oil, Canned Beans
|
||||
- Fridge: Milk, Cheese, Greek Yogurt, Eggs, Butter, Chicken Salad
|
||||
- Freezer: Ice Cream, Frozen Vegetables, Chicken Breast, Ground Beef, Pizza, Ice
|
||||
|
||||
### 📱 Barcode Scanner (`barcode.html`)
|
||||
Barcode capture interface with console logging for testing.
|
||||
|
||||
**Features:**
|
||||
- Keyboard input simulation (type barcode + press Enter)
|
||||
- Hardware barcode scanner device support (ready for integration)
|
||||
- Paste support (Ctrl/Cmd+V)
|
||||
- Console logging with formatted output including:
|
||||
- Timestamp (HH:MM:SS.mmm format)
|
||||
- Barcode value
|
||||
- Input type detection (keyboard, hardware-scanner, keyboard-paste)
|
||||
- Metadata for debugging
|
||||
- Auto-focus input field after each scan
|
||||
- Visual status indicator
|
||||
- Helper instructions and "Open Console" button
|
||||
|
||||
**Console Output Format:**
|
||||
```
|
||||
[Barcode Scanned] 14:07:32.456 | Barcode: 5901234123457 | Input: keyboard
|
||||
```
|
||||
|
||||
**Testing Instructions:**
|
||||
1. Navigate to the Barcode Scanner page via navbar
|
||||
2. Type a barcode value or use a barcode scanner device
|
||||
3. Press Enter to complete the scan
|
||||
4. Press F12 to open Developer Tools
|
||||
5. Switch to the Console tab to view scanned barcodes with metadata
|
||||
|
||||
### 📦 Storage Location Pages
|
||||
- `pantry.html` — Pantry inventory (expandable for displaying specific items)
|
||||
- `fridge.html` — Fridge inventory
|
||||
- `freezer.html` — Freezer inventory
|
||||
|
||||
These pages are currently placeholder containers ready for future development (e.g., displaying items specific to each location).
|
||||
|
||||
## Quick Start
|
||||
|
||||
The project is a static site. For best results, serve it over HTTP (ES module imports can be blocked when opened via the `file://` protocol in some browsers).
|
||||
|
||||
### Python 3 (Recommended)
|
||||
|
||||
```bash
|
||||
# From the project root
|
||||
python -m http.server 8000
|
||||
|
||||
# Then open http://localhost:8000 in your browser
|
||||
```
|
||||
|
||||
### Alternative Options
|
||||
|
||||
- **VS Code Live Server Extension** — Right-click `index.html` → "Open with Live Server"
|
||||
- **Node.js http-server** — `npx http-server`
|
||||
- **Any static file server**
|
||||
|
||||
## Usage
|
||||
|
||||
### Navigation
|
||||
The navbar appears at the top of every page and provides links to:
|
||||
- Homepage — Main demo page with pie chart
|
||||
- Search — Advanced inventory search and filtering
|
||||
- Barcode Scanner — Barcode capture interface (testing via console)
|
||||
- Pantry, Fridge, Freezer — Individual storage location pages
|
||||
|
||||
### Using the Search Page
|
||||
1. Open the Search page from the navbar
|
||||
2. Enter search criteria:
|
||||
- Item name (optional)
|
||||
- Storage location (optional, defaults to "All")
|
||||
- Quantity range (optional, defaults to 0-999)
|
||||
3. Click "Search" or press Enter
|
||||
4. Results display as item cards using the item component
|
||||
5. Click "Reset" to clear all filters
|
||||
|
||||
### Using the Barcode Scanner
|
||||
1. Open the Barcode Scanner page from the navbar
|
||||
2. Click in the input field (auto-focused)
|
||||
3. Enter a barcode:
|
||||
- Type manually and press Enter
|
||||
- Scan with a barcode scanner device
|
||||
- Paste a value (Ctrl/Cmd+V)
|
||||
4. Press F12 to open Developer Tools Console
|
||||
5. View scanned barcodes in the Console tab with timestamp and metadata
|
||||
|
||||
### The Item Component
|
||||
The `item-component` is a reusable UI building block used throughout the app. See `ITEM_COMPONENT.md` for detailed documentation on using it in your own pages.
|
||||
|
||||
**Quick Example:**
|
||||
```html
|
||||
<script type="module">
|
||||
import createItemComponent from './item-component.js';
|
||||
|
||||
const item = createItemComponent({
|
||||
imgSrc: 'https://picsum.photos/seed/1/200/200',
|
||||
imgAlt: 'Milk',
|
||||
text1: 'Milk',
|
||||
text2: 'Fridge — 1 carton',
|
||||
text3: 'Expires in 5 days'
|
||||
});
|
||||
|
||||
document.getElementById('container').appendChild(item);
|
||||
</script>
|
||||
```
|
||||
|
||||
## Customizing and Extending
|
||||
|
||||
### Adding New Items to Search
|
||||
Edit `search.js` and add items to the `inventoryData` array:
|
||||
```javascript
|
||||
export const inventoryData = [
|
||||
{ id: 21, name: 'Coffee', location: 'Pantry', quantity: 2, unit: 'bags', img: 'https://picsum.photos/seed/coffee/200/200' },
|
||||
// ... more items
|
||||
];
|
||||
```
|
||||
|
||||
### Styling
|
||||
- Override styles in `main.css` for global changes
|
||||
- Page-specific styles are included in `<style>` tags within each HTML file
|
||||
- The item component injects responsive grid CSS automatically
|
||||
|
||||
### Creating New Pages
|
||||
1. Create a new `.html` file following the template:
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="main.css">
|
||||
<title>Page Title</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="navbar-placeholder"></div>
|
||||
<div id="page-content">
|
||||
<h2>Page Title</h2>
|
||||
<!-- Your content here -->
|
||||
</div>
|
||||
<script src="navbar.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
2. Update `navbar.js` to add a link to your new page:
|
||||
```javascript
|
||||
<li><a href="yourpage.html" data-route="yourpage.html">Your Page</a></li>
|
||||
```
|
||||
|
||||
### Converting to Production
|
||||
- Replace placeholder images with real inventory photos
|
||||
- Implement backend storage (currently using static data)
|
||||
- Add user authentication
|
||||
- Consider a frontend framework (React, Vue, etc.) for scale
|
||||
- Add unit/integration tests
|
||||
- Set up CI/CD pipeline
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Suggested improvements:
|
||||
|
||||
- [ ] Add unit and visual tests
|
||||
- [ ] Replace placeholder images with real inventory photos
|
||||
- [ ] Implement barcode scanner backend integration (save to database)
|
||||
- [ ] Add user accounts and authentication
|
||||
- [ ] Implement item editing/deletion on storage pages
|
||||
- [ ] Add barcode/UPC lookup for real products
|
||||
- [ ] Convert item-component to custom element (`<item-component>`)
|
||||
- [ ] Add more detailed inventory tracking (expiration dates, locations within rooms, etc.)
|
||||
|
||||
## License
|
||||
|
||||
Use as you like; no license file is included by default.
|
||||
147
barcode-scanner.js
Normal file
147
barcode-scanner.js
Normal file
@@ -0,0 +1,147 @@
|
||||
// barcode-scanner.js - Barcode scanning module with console logging for testing
|
||||
|
||||
/**
|
||||
* Initialize barcode scanner with dual input support:
|
||||
* - Keyboard input (simulation)
|
||||
* - Hardware barcode scanner devices
|
||||
*
|
||||
* Logs scanned barcodes to console for testing purposes
|
||||
*/
|
||||
|
||||
let barcodeBuffer = '';
|
||||
let lastBarcodeTime = 0;
|
||||
const SCANNER_TIMEOUT = 100; // ms - detect hardware scanner vs human typing
|
||||
|
||||
/**
|
||||
* Log barcode to console with metadata
|
||||
* @param {string} barcode - The scanned barcode value
|
||||
* @param {string} inputType - Either 'keyboard' or 'hardware-scanner'
|
||||
*/
|
||||
export function logBarcodeToConsole(barcode, inputType = 'keyboard') {
|
||||
const timestamp = new Date().toLocaleTimeString('en-US', {
|
||||
hour12: false,
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
fractionalSecondDigits: 3
|
||||
});
|
||||
|
||||
console.log(
|
||||
`%c[Barcode Scanned] %c${timestamp} | %cBarcode: %c${barcode} %c| Input: %c${inputType}`,
|
||||
'color: #4CAF50; font-weight: bold;',
|
||||
'color: #666; font-family: monospace;',
|
||||
'color: #333; font-weight: bold;',
|
||||
'color: #2196F3; font-weight: bold; font-family: monospace;',
|
||||
'color: #666;',
|
||||
'color: #FF9800; font-weight: bold;'
|
||||
);
|
||||
|
||||
// Additional metadata for debugging
|
||||
console.log({
|
||||
barcode,
|
||||
timestamp: new Date().toISOString(),
|
||||
inputType,
|
||||
length: barcode.length
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect if input is from a hardware scanner or keyboard
|
||||
* Hardware scanners typically input a full barcode very quickly (within SCANNER_TIMEOUT ms)
|
||||
* @returns {string} - 'hardware-scanner' or 'keyboard'
|
||||
*/
|
||||
function detectInputType(currentTime) {
|
||||
const timeSinceLastInput = currentTime - lastBarcodeTime;
|
||||
lastBarcodeTime = currentTime;
|
||||
|
||||
// If more than SCANNER_TIMEOUT ms since last character, it's likely keyboard input
|
||||
return timeSinceLastInput > SCANNER_TIMEOUT ? 'keyboard' : 'hardware-scanner';
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle barcode input from both keyboard and hardware scanner
|
||||
* @param {HTMLElement} inputElement - The input field element
|
||||
* @param {Function} onBarcodeScanned - Callback function when barcode is complete
|
||||
*/
|
||||
export function initializeBarcodeScanner(inputElement, onBarcodeScanned) {
|
||||
if (!inputElement) {
|
||||
console.error('Barcode input element not found');
|
||||
return;
|
||||
}
|
||||
|
||||
// Auto-focus for seamless scanning
|
||||
inputElement.focus();
|
||||
|
||||
inputElement.addEventListener('keypress', (event) => {
|
||||
// Most barcode scanners send Enter key at the end
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
|
||||
const barcode = inputElement.value.trim();
|
||||
|
||||
if (barcode) {
|
||||
// Detect input type
|
||||
const inputType = barcodeBuffer.length === 0 ? 'keyboard' : 'hardware-scanner';
|
||||
barcodeBuffer = '';
|
||||
|
||||
// Log to console
|
||||
logBarcodeToConsole(barcode, inputType);
|
||||
|
||||
// Call the callback function
|
||||
if (onBarcodeScanned) {
|
||||
onBarcodeScanned(barcode, inputType);
|
||||
}
|
||||
}
|
||||
|
||||
// Clear input for next scan
|
||||
inputElement.value = '';
|
||||
inputElement.focus();
|
||||
} else {
|
||||
// Accumulate characters in buffer for input type detection
|
||||
const now = Date.now();
|
||||
if (barcodeBuffer === '') {
|
||||
lastBarcodeTime = now;
|
||||
}
|
||||
barcodeBuffer += event.key;
|
||||
}
|
||||
});
|
||||
|
||||
// Handle pasted input (in case user pastes barcode)
|
||||
inputElement.addEventListener('paste', (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const pastedText = (event.clipboardData || window.clipboardData).getData('text').trim();
|
||||
|
||||
if (pastedText) {
|
||||
logBarcodeToConsole(pastedText, 'keyboard-paste');
|
||||
|
||||
if (onBarcodeScanned) {
|
||||
onBarcodeScanned(pastedText, 'keyboard-paste');
|
||||
}
|
||||
}
|
||||
|
||||
inputElement.value = '';
|
||||
inputElement.focus();
|
||||
});
|
||||
|
||||
// Maintain focus
|
||||
inputElement.addEventListener('blur', () => {
|
||||
setTimeout(() => inputElement.focus(), 0);
|
||||
});
|
||||
|
||||
console.log('%cBarcode Scanner Initialized', 'color: #4CAF50; font-weight: bold;');
|
||||
console.log('Ready to scan. Focus the input field and scan a barcode or press Enter to complete.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get scanning statistics from console (for testing)
|
||||
* This is just a helper for development/testing
|
||||
* @returns {Object} - Statistics object
|
||||
*/
|
||||
export function getScanningStats() {
|
||||
return {
|
||||
initialized: true,
|
||||
timestamp: new Date().toISOString(),
|
||||
message: 'Barcode scanner is active. Check console for scanned barcodes.'
|
||||
};
|
||||
}
|
||||
288
barcode.html
Normal file
288
barcode.html
Normal file
@@ -0,0 +1,288 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="main.css">
|
||||
<title>Barcode Scanner</title>
|
||||
<style>
|
||||
.scanner-container {
|
||||
background: #f9f9f9;
|
||||
padding: 30px;
|
||||
border-radius: 8px;
|
||||
max-width: 600px;
|
||||
margin: 30px auto;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.scanner-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.scanner-header h3 {
|
||||
color: #333;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.scanner-header p {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
margin: 10px 0 0 0;
|
||||
}
|
||||
|
||||
.barcode-input-wrapper {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.barcode-input-wrapper label {
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.barcode-input {
|
||||
width: 100%;
|
||||
padding: 15px;
|
||||
border: 2px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 18px;
|
||||
font-family: 'Courier New', monospace;
|
||||
box-sizing: border-box;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.barcode-input:focus {
|
||||
outline: none;
|
||||
border-color: #4CAF50;
|
||||
box-shadow: 0 0 8px rgba(76, 175, 80, 0.3);
|
||||
background-color: #fffef0;
|
||||
}
|
||||
|
||||
.scanner-status {
|
||||
text-align: center;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
background-color: #e8f5e9;
|
||||
border: 1px solid #c8e6c9;
|
||||
color: #2e7d32;
|
||||
font-size: 14px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.scanner-status.active {
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
background-color: #e8f5e9;
|
||||
}
|
||||
50% {
|
||||
background-color: #c8e6c9;
|
||||
}
|
||||
}
|
||||
|
||||
.scanner-info {
|
||||
background-color: #e3f2fd;
|
||||
border: 1px solid #bbdefb;
|
||||
border-radius: 4px;
|
||||
padding: 15px;
|
||||
margin-top: 20px;
|
||||
font-size: 13px;
|
||||
color: #1565c0;
|
||||
}
|
||||
|
||||
.scanner-info h4 {
|
||||
margin: 0 0 10px 0;
|
||||
color: #0d47a1;
|
||||
}
|
||||
|
||||
.scanner-info ul {
|
||||
margin: 10px 0 0 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.scanner-info li {
|
||||
margin: 5px 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.console-hint {
|
||||
background-color: #fff3e0;
|
||||
border: 1px solid #ffe0b2;
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
margin-top: 15px;
|
||||
font-size: 13px;
|
||||
color: #e65100;
|
||||
}
|
||||
|
||||
.console-hint strong {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
color: #d84315;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
flex: 1;
|
||||
padding: 12px 20px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: #ddd;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background-color: #bbb;
|
||||
}
|
||||
|
||||
.open-console {
|
||||
background-color: #2196F3;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.open-console:hover {
|
||||
background-color: #0b7dda;
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 600px) {
|
||||
.scanner-container {
|
||||
padding: 20px;
|
||||
margin: 20px 10px;
|
||||
}
|
||||
|
||||
.barcode-input {
|
||||
font-size: 16px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="navbar-placeholder"></div>
|
||||
<div id="page-content">
|
||||
<h2>Barcode Scanner</h2>
|
||||
<hr>
|
||||
|
||||
<div class="scanner-container">
|
||||
<div class="scanner-header">
|
||||
<h3>📱 Barcode Scanner</h3>
|
||||
<p>Scan a barcode or manually enter one below</p>
|
||||
</div>
|
||||
|
||||
<div class="scanner-status active" id="scanner-status">
|
||||
✓ Scanner Ready - Click below and scan a barcode
|
||||
</div>
|
||||
|
||||
<div class="barcode-input-wrapper">
|
||||
<label for="barcode-input">Barcode Input:</label>
|
||||
<input
|
||||
type="text"
|
||||
id="barcode-input"
|
||||
class="barcode-input"
|
||||
placeholder="Scan or type barcode here..."
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="scanner-info">
|
||||
<h4>How to Use:</h4>
|
||||
<ul>
|
||||
<li><strong>Keyboard Entry:</strong> Type or paste a barcode and press Enter</li>
|
||||
<li><strong>Hardware Scanner:</strong> Plug in a barcode scanner and scan directly</li>
|
||||
<li><strong>Testing:</strong> Scanned barcodes are logged to the browser console (Press F12)</li>
|
||||
<li><strong>Input Focus:</strong> The input field automatically refocuses after each scan</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="console-hint">
|
||||
<strong>🔍 View Console Output:</strong>
|
||||
Press F12 (or Cmd+Option+I on Mac) to open Developer Tools. Check the Console tab to see scanned barcodes logged with timestamps and input type detection.
|
||||
</div>
|
||||
|
||||
<div class="button-group">
|
||||
<button type="button" class="btn open-console" id="open-console-btn">
|
||||
📟 Open Console (F12)
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary" id="clear-input-btn">
|
||||
🗑️ Clear Input
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="navbar.js"></script>
|
||||
<script type="module">
|
||||
import { initializeBarcodeScanner, logBarcodeToConsole } from './barcode-scanner.js';
|
||||
|
||||
const barcodeInput = document.getElementById('barcode-input');
|
||||
const scannerStatus = document.getElementById('scanner-status');
|
||||
const openConsoleBtn = document.getElementById('open-console-btn');
|
||||
const clearInputBtn = document.getElementById('clear-input-btn');
|
||||
|
||||
/**
|
||||
* Callback when a barcode is scanned
|
||||
* @param {string} barcode - The scanned barcode value
|
||||
* @param {string} inputType - The type of input device
|
||||
*/
|
||||
function onBarcodeScanned(barcode, inputType) {
|
||||
// Update status temporarily
|
||||
const originalStatus = scannerStatus.textContent;
|
||||
scannerStatus.textContent = `✓ Barcode scanned: ${barcode}`;
|
||||
scannerStatus.style.backgroundColor = '#a5d6a7';
|
||||
|
||||
// Reset after 2 seconds
|
||||
setTimeout(() => {
|
||||
scannerStatus.textContent = originalStatus;
|
||||
scannerStatus.style.backgroundColor = '';
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
// Initialize the barcode scanner
|
||||
initializeBarcodeScanner(barcodeInput, onBarcodeScanned);
|
||||
|
||||
// Clear input button
|
||||
clearInputBtn.addEventListener('click', () => {
|
||||
barcodeInput.value = '';
|
||||
barcodeInput.focus();
|
||||
});
|
||||
|
||||
// Open console button - opens DevTools
|
||||
openConsoleBtn.addEventListener('click', () => {
|
||||
console.log('%c📊 Barcode Scanner Console\n\nUse this console to view scanned barcodes and their details.\nStart scanning to see logs appear here.', 'color: #4CAF50; font-size: 14px; font-weight: bold; line-height: 1.8;');
|
||||
// In a real app, we can't programmatically open DevTools for security reasons
|
||||
// But we can log a helpful message and suggest the user press F12
|
||||
alert('Press F12 (or Cmd+Option+I on Mac) to open the Developer Tools Console.\n\nScanned barcodes will appear in the Console tab.');
|
||||
});
|
||||
|
||||
// Log initial message
|
||||
console.log('%c🎯 Barcode Scanner Ready', 'color: #4CAF50; font-weight: bold; font-size: 16px;');
|
||||
console.log('Scan a barcode to see it logged here with timestamp and input type detection.');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
16
freezer.html
Normal file
16
freezer.html
Normal file
@@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="main.css">
|
||||
<title>Freezer</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="navbar-placeholder"></div>
|
||||
<div id="page-content">
|
||||
<h2>Freezer Inventory</h2>
|
||||
</div>
|
||||
<script src="navbar.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
16
fridge.html
Normal file
16
fridge.html
Normal file
@@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="main.css">
|
||||
<title>Fridge</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="navbar-placeholder"></div>
|
||||
<div id="page-content">
|
||||
<h2>Fridge Inventory</h2>
|
||||
</div>
|
||||
<script src="navbar.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
47
index.html
Normal file
47
index.html
Normal file
@@ -0,0 +1,47 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="main.css">
|
||||
<title>Homepage</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="navbar-placeholder"></div>
|
||||
<div id="page-content">
|
||||
<h2>Homepage</h2>
|
||||
<hr>
|
||||
<h3>To use</h3>
|
||||
<div id="item-component-demo"></div>
|
||||
<script type="module">
|
||||
console.log('Demo script running');
|
||||
import createItemComponent from './item-component.js';
|
||||
const demoRoot = document.getElementById('item-component-demo');
|
||||
demoRoot.className = 'item-component-grid';
|
||||
|
||||
const items = [
|
||||
{ imgSrc: 'https://picsum.photos/seed/1/200/200', imgAlt: 'Chicken Salad', text1: 'Chicken Salad', text2: 'Fresh — 2 left', text3: 'Use within 3 days' },
|
||||
{ imgSrc: 'https://picsum.photos/seed/2/200/200', imgAlt: 'Yogurt', text1: 'Greek Yogurt', text2: 'Chilled — 6 left', text3: 'Best before 5 days' },
|
||||
{ imgSrc: 'https://picsum.photos/seed/3/200/200', imgAlt: 'Apples', text1: 'Red Apples', text2: 'Room temp — 12 left', text3: 'Keep away from moisture' }, ];
|
||||
|
||||
items.forEach(data => {
|
||||
const comp = createItemComponent({ ...data, editable: false });
|
||||
demoRoot.appendChild(comp);
|
||||
});
|
||||
</script>
|
||||
<hr>
|
||||
<h3>Inventory Breakdown</h3>
|
||||
<div class="chart-container">
|
||||
<svg width="350" height="350" viewBox="0 0 42 42">
|
||||
<circle cx="21" cy="21" r="10" fill="transparent" stroke="#f2f2f2" stroke-width="20"></circle>
|
||||
<circle id="seg-1" cx="21" cy="21" r="10" fill="transparent" stroke-width="20"></circle>
|
||||
<circle id="seg-2" cx="21" cy="21" r="10" fill="transparent" stroke-width="20"></circle>
|
||||
<circle id="seg-3" cx="21" cy="21" r="10" fill="transparent" stroke-width="20"></circle>
|
||||
</svg>
|
||||
<div id="legend"></div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="navbar.js"></script>
|
||||
<script src="piechart.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
92
item-component.js
Normal file
92
item-component.js
Normal file
@@ -0,0 +1,92 @@
|
||||
// item-component.js
|
||||
// Reusable item component: image on left, three text areas on right.
|
||||
|
||||
const _ensureStyles = (() => {
|
||||
if (document.getElementById('item-component-styles')) return true;
|
||||
const style = document.createElement('style');
|
||||
style.id = 'item-component-styles';
|
||||
style.textContent = `
|
||||
/* Grid wrapper for multiple item components */
|
||||
.item-component-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;align-items:start}
|
||||
.item-component{display:flex;align-items:flex-start;gap:12px;font-family:inherit;padding:10px;border:1px solid #e6e6e6;border-radius:8px;background:#fff}
|
||||
.item-component__img{flex:0 0 auto}
|
||||
.item-component__img img{display:block;width:72px;height:72px;object-fit:cover;border-radius:6px}
|
||||
.item-component__content{flex:1;display:flex;flex-direction:column;gap:6px}
|
||||
.item-component__line{margin:0;padding:0;color:#222}
|
||||
.item-component__line--title{font-weight:700}
|
||||
.item-component__line--subtitle{color:#555;font-size:0.95em}
|
||||
.item-component__line--desc{color:#666;font-size:0.9em}
|
||||
.item-component__textarea{width:100%;box-sizing:border-box;padding:6px;font:inherit;border:1px solid #ccc;border-radius:4px}
|
||||
|
||||
/* Responsive: on small screens show 1 or 2 columns */
|
||||
@media (max-width: 900px){
|
||||
.item-component-grid{grid-template-columns:repeat(2,1fr)}
|
||||
}
|
||||
@media (max-width: 560px){
|
||||
.item-component-grid{grid-template-columns:repeat(1,1fr)}
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
return true;
|
||||
})();
|
||||
|
||||
/**
|
||||
* Create an item component element.
|
||||
* @param {Object} opts
|
||||
* @param {string} opts.imgSrc - image URL
|
||||
* @param {string} opts.imgAlt - image alt text
|
||||
* @param {string} opts.text1 - first text area (title)
|
||||
* @param {string} opts.text2 - second text area (subtitle)
|
||||
* @param {string} opts.text3 - third text area (description)
|
||||
* @param {boolean} opts.editable - if true, text areas are `<textarea>` elements for editing
|
||||
* @param {number} opts.imgWidth - image width in px
|
||||
* @param {number} opts.imgHeight - image height in px
|
||||
* @returns {HTMLElement}
|
||||
*/
|
||||
export function createItemComponent({imgSrc = '', imgAlt = '', text1 = '', text2 = '', text3 = '', editable = false, imgWidth = 80, imgHeight = 80} = {}) {
|
||||
_ensureStyles;
|
||||
|
||||
const root = document.createElement('div');
|
||||
root.className = 'item-component';
|
||||
|
||||
// Image container
|
||||
const imgWrap = document.createElement('div');
|
||||
imgWrap.className = 'item-component__img';
|
||||
const img = document.createElement('img');
|
||||
if (imgSrc) img.src = imgSrc;
|
||||
img.alt = imgAlt || '';
|
||||
img.width = imgWidth;
|
||||
img.height = imgHeight;
|
||||
imgWrap.appendChild(img);
|
||||
root.appendChild(imgWrap);
|
||||
|
||||
// Content container with three text areas
|
||||
const content = document.createElement('div');
|
||||
content.className = 'item-component__content';
|
||||
|
||||
function makeTextNode(text, cls) {
|
||||
if (editable) {
|
||||
const ta = document.createElement('textarea');
|
||||
ta.className = 'item-component__textarea ' + cls;
|
||||
ta.value = text;
|
||||
return ta;
|
||||
}
|
||||
const el = document.createElement('div');
|
||||
el.className = 'item-component__line ' + cls;
|
||||
el.textContent = text;
|
||||
return el;
|
||||
}
|
||||
|
||||
const line1 = makeTextNode(text1, 'item-component__line--title');
|
||||
const line2 = makeTextNode(text2, 'item-component__line--subtitle');
|
||||
const line3 = makeTextNode(text3, 'item-component__line--desc');
|
||||
|
||||
content.appendChild(line1);
|
||||
content.appendChild(line2);
|
||||
content.appendChild(line3);
|
||||
root.appendChild(content);
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
export default createItemComponent;
|
||||
105
main.css
Normal file
105
main.css
Normal file
@@ -0,0 +1,105 @@
|
||||
/* Dynamically Changing the content padding depending on the width of the screen */
|
||||
@media screen and (min-width: 1921px) and (max-width: 2560px) {
|
||||
body {
|
||||
padding-left: 20%;
|
||||
padding-right: 20%;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1920px) {
|
||||
body {
|
||||
padding-left: 10%;
|
||||
padding-right: 10%;
|
||||
}
|
||||
}
|
||||
|
||||
/* General Styling */
|
||||
body{
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
h2 {
|
||||
text-align: center;
|
||||
font-size: xx-large;
|
||||
}
|
||||
h3 {
|
||||
text-align: center;
|
||||
font-size: x-large;
|
||||
}
|
||||
hr {
|
||||
border: 0;
|
||||
height: 2px;
|
||||
background: #5b5b5b;
|
||||
margin: 20px 0;
|
||||
width: 100%;
|
||||
}
|
||||
/* Navbar Styling */
|
||||
nav ul {
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: #333333;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Give navbar the same rounded corners as the item components and clip children */
|
||||
nav ul {
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
ul li a {
|
||||
display: block;
|
||||
color: white;
|
||||
padding: 14px 16px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
ul li a:hover {
|
||||
background-color: #111111;
|
||||
}
|
||||
|
||||
/* Pie chart styling */
|
||||
.chart-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: sans-serif;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
#pie-segment {
|
||||
transform: rotate(-90deg);
|
||||
transform-origin: center;
|
||||
transition: stroke-dasharray 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
.label h2 {
|
||||
margin: 0;
|
||||
color: #3498db;
|
||||
}
|
||||
|
||||
circle {
|
||||
transition: stroke-dasharray 0.6s ease, stroke-dashoffset 0.6s ease;
|
||||
transform: rotate(-90deg);
|
||||
transform-origin: center;
|
||||
}
|
||||
|
||||
#legend {
|
||||
font-family: sans-serif;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
19
navbar.js
Normal file
19
navbar.js
Normal file
@@ -0,0 +1,19 @@
|
||||
function createNavbar() {
|
||||
const navbarHTML = `
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="index.html" data-route="index.html">Homepage</a></li>
|
||||
<li><a href="search.html" data-route="search.html">Search</a></li>
|
||||
<li><a href="barcode.html" data-route="barcode.html">Barcode Scanner</a></li>
|
||||
<li><a href="pantry.html" data-route="pantry.html">Pantry</a></li>
|
||||
<li><a href="fridge.html" data-route="fridge.html">Fridge</a></li>
|
||||
<li><a href="freezer.html" data-route="freezer.html">Freezer</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
`;
|
||||
const navbarContainer = document.getElementById('navbar-placeholder');
|
||||
if (navbarContainer) {
|
||||
navbarContainer.innerHTML = navbarHTML;
|
||||
}
|
||||
}
|
||||
createNavbar();
|
||||
16
pantry.html
Normal file
16
pantry.html
Normal file
@@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="main.css">
|
||||
<title>Pantry</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="navbar-placeholder"></div>
|
||||
<div id="page-content">
|
||||
<h2>Pantry Inventory</h2>
|
||||
</div>
|
||||
<script src="navbar.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
48
piechart.js
Normal file
48
piechart.js
Normal file
@@ -0,0 +1,48 @@
|
||||
const segments = [
|
||||
{ label: "Pantry", value: 104, color: getRandomColor() },
|
||||
{ label: "Fridge", value: 30, color: getRandomColor() },
|
||||
{ label: "Freezer", value: 87, color: getRandomColor() }
|
||||
];
|
||||
|
||||
const totalValue = segments.reduce((accumulator, current) => {
|
||||
return accumulator + current.value;
|
||||
}, 0);
|
||||
|
||||
const circumference = 2 * Math.PI * 10;
|
||||
|
||||
function renderPieChart(data) {
|
||||
let currentOffset = 0;
|
||||
|
||||
data.forEach((item, index) => {
|
||||
const fraction = item.value / totalValue;
|
||||
const segmentLength = fraction * circumference;
|
||||
const circle = document.getElementById(`seg-${index + 1}`);
|
||||
circle.style.strokeDasharray = `${segmentLength} ${circumference}`;
|
||||
circle.style.strokeDashoffset = -currentOffset;
|
||||
circle.style.stroke = item.color;
|
||||
currentOffset += segmentLength;
|
||||
addLegendItem(item);
|
||||
});
|
||||
}
|
||||
|
||||
function addLegendItem(item) {
|
||||
const legend = document.getElementById('legend');
|
||||
const div = document.createElement('div');
|
||||
div.className = 'legend-item';
|
||||
div.innerHTML = `
|
||||
<span class="dot" style="background:${item.color}"></span>
|
||||
<span>${item.label}: <strong>${item.value}</strong></span>
|
||||
`;
|
||||
legend.appendChild(div);
|
||||
}
|
||||
|
||||
function getRandomColor() {
|
||||
const letters = '0123456789ABCDEF';
|
||||
let color = '#';
|
||||
for (let i = 0; i < 6; i++) {
|
||||
color += letters[Math.floor(Math.random() * 10)];
|
||||
}
|
||||
return color;
|
||||
}
|
||||
|
||||
renderPieChart(segments);
|
||||
254
search.html
Normal file
254
search.html
Normal file
@@ -0,0 +1,254 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="main.css">
|
||||
<title>Search Inventory</title>
|
||||
<style>
|
||||
.search-container {
|
||||
background: #f9f9f9;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group select {
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.form-group input:focus,
|
||||
.form-group select:focus {
|
||||
outline: none;
|
||||
border-color: #4CAF50;
|
||||
box-shadow: 0 0 5px rgba(76, 175, 80, 0.3);
|
||||
}
|
||||
|
||||
.quantity-range {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.quantity-range .form-group {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.search-buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 12px 20px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: #4CAF50;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: #45a049;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: #ddd;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background-color: #bbb;
|
||||
}
|
||||
|
||||
.results-info {
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.no-results {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: #999;
|
||||
font-size: 18px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="navbar-placeholder"></div>
|
||||
<div id="page-content">
|
||||
<h2>Search Inventory</h2>
|
||||
<hr>
|
||||
|
||||
<div class="search-container">
|
||||
<form class="search-form" id="search-form">
|
||||
<div class="form-group">
|
||||
<label for="search-name">Item Name:</label>
|
||||
<input
|
||||
type="text"
|
||||
id="search-name"
|
||||
placeholder="e.g., Milk, Pasta, Chicken..."
|
||||
autocomplete="off"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="search-location">Storage Location:</label>
|
||||
<select id="search-location">
|
||||
<!-- Options populated by script -->
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Quantity Range:</label>
|
||||
<div class="quantity-range">
|
||||
<div class="form-group">
|
||||
<label for="min-quantity" style="font-size: 12px;">Min:</label>
|
||||
<input
|
||||
type="number"
|
||||
id="min-quantity"
|
||||
min="0"
|
||||
value="0"
|
||||
placeholder="0"
|
||||
>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="max-quantity" style="font-size: 12px;">Max:</label>
|
||||
<input
|
||||
type="number"
|
||||
id="max-quantity"
|
||||
min="0"
|
||||
value="999"
|
||||
placeholder="999"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="search-buttons">
|
||||
<button type="button" class="btn btn-primary" id="search-btn">Search</button>
|
||||
<button type="button" class="btn btn-secondary" id="reset-btn">Reset</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="results-container">
|
||||
<div class="results-info" id="results-info"></div>
|
||||
<div id="results-grid"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="navbar.js"></script>
|
||||
<script type="module">
|
||||
import { searchInventory, getLocations } from './search.js';
|
||||
import createItemComponent from './item-component.js';
|
||||
|
||||
const searchForm = document.getElementById('search-form');
|
||||
const searchNameInput = document.getElementById('search-name');
|
||||
const locationSelect = document.getElementById('search-location');
|
||||
const minQuantityInput = document.getElementById('min-quantity');
|
||||
const maxQuantityInput = document.getElementById('max-quantity');
|
||||
const searchBtn = document.getElementById('search-btn');
|
||||
const resetBtn = document.getElementById('reset-btn');
|
||||
const resultsGrid = document.getElementById('results-grid');
|
||||
const resultsInfo = document.getElementById('results-info');
|
||||
|
||||
// Populate location dropdown
|
||||
function populateLocations() {
|
||||
const locations = getLocations();
|
||||
locationSelect.innerHTML = locations
|
||||
.map(loc => `<option value="${loc}">${loc}</option>`)
|
||||
.join('');
|
||||
locationSelect.value = 'All';
|
||||
}
|
||||
|
||||
// Display search results
|
||||
function displayResults(results) {
|
||||
resultsGrid.innerHTML = '';
|
||||
|
||||
if (results.length === 0) {
|
||||
resultsGrid.innerHTML = '<div class="no-results">No items found matching your search criteria.</div>';
|
||||
resultsInfo.textContent = 'No results';
|
||||
} else {
|
||||
resultsGrid.className = 'item-component-grid';
|
||||
results.forEach(item => {
|
||||
const comp = createItemComponent({
|
||||
imgSrc: item.img,
|
||||
imgAlt: item.name,
|
||||
text1: item.name,
|
||||
text2: `${item.location} — ${item.quantity} ${item.unit}`,
|
||||
text3: `Total: ${item.quantity} ${item.unit}`,
|
||||
editable: false
|
||||
});
|
||||
resultsGrid.appendChild(comp);
|
||||
});
|
||||
resultsInfo.textContent = `Found ${results.length} item${results.length !== 1 ? 's' : ''}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Perform search
|
||||
function performSearch() {
|
||||
const searchName = searchNameInput.value;
|
||||
const location = locationSelect.value;
|
||||
const minQty = parseInt(minQuantityInput.value) || 0;
|
||||
const maxQty = parseInt(maxQuantityInput.value) || Infinity;
|
||||
|
||||
const results = searchInventory(searchName, location, minQty, maxQty);
|
||||
displayResults(results);
|
||||
}
|
||||
|
||||
// Reset form
|
||||
function resetForm() {
|
||||
searchForm.reset();
|
||||
minQuantityInput.value = '0';
|
||||
maxQuantityInput.value = '999';
|
||||
locationSelect.value = 'All';
|
||||
resultsGrid.innerHTML = '';
|
||||
resultsInfo.textContent = '';
|
||||
}
|
||||
|
||||
// Event listeners
|
||||
searchBtn.addEventListener('click', performSearch);
|
||||
resetBtn.addEventListener('click', resetForm);
|
||||
|
||||
// Allow Enter key to search
|
||||
searchNameInput.addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter') performSearch();
|
||||
});
|
||||
|
||||
// Initialize
|
||||
populateLocations();
|
||||
displayResults(searchInventory());
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
61
search.js
Normal file
61
search.js
Normal file
@@ -0,0 +1,61 @@
|
||||
// search.js - Search module with inventory data and filtering logic
|
||||
|
||||
export const inventoryData = [
|
||||
// Pantry items
|
||||
{ id: 1, name: 'Pasta', location: 'Pantry', quantity: 5, unit: 'boxes', img: 'https://picsum.photos/seed/pasta/200/200' },
|
||||
{ id: 2, name: 'Rice', location: 'Pantry', quantity: 3, unit: 'bags', img: 'https://picsum.photos/seed/rice/200/200' },
|
||||
{ id: 3, name: 'Cereal', location: 'Pantry', quantity: 2, unit: 'boxes', img: 'https://picsum.photos/seed/cereal/200/200' },
|
||||
{ id: 4, name: 'Flour', location: 'Pantry', quantity: 1, unit: 'bag', img: 'https://picsum.photos/seed/flour/200/200' },
|
||||
{ id: 5, name: 'Sugar', location: 'Pantry', quantity: 2, unit: 'bags', img: 'https://picsum.photos/seed/sugar/200/200' },
|
||||
{ id: 6, name: 'Salt', location: 'Pantry', quantity: 1, unit: 'box', img: 'https://picsum.photos/seed/salt/200/200' },
|
||||
{ id: 7, name: 'Olive Oil', location: 'Pantry', quantity: 2, unit: 'bottles', img: 'https://picsum.photos/seed/oil/200/200' },
|
||||
{ id: 8, name: 'Canned Beans', location: 'Pantry', quantity: 12, unit: 'cans', img: 'https://picsum.photos/seed/beans/200/200' },
|
||||
|
||||
// Fridge items
|
||||
{ id: 9, name: 'Milk', location: 'Fridge', quantity: 1, unit: 'carton', img: 'https://picsum.photos/seed/milk/200/200' },
|
||||
{ id: 10, name: 'Cheese', location: 'Fridge', quantity: 2, unit: 'blocks', img: 'https://picsum.photos/seed/cheese/200/200' },
|
||||
{ id: 11, name: 'Greek Yogurt', location: 'Fridge', quantity: 3, unit: 'containers', img: 'https://picsum.photos/seed/yogurt/200/200' },
|
||||
{ id: 12, name: 'Eggs', location: 'Fridge', quantity: 24, unit: 'eggs', img: 'https://picsum.photos/seed/eggs/200/200' },
|
||||
{ id: 13, name: 'Butter', location: 'Fridge', quantity: 1, unit: 'pack', img: 'https://picsum.photos/seed/butter/200/200' },
|
||||
{ id: 14, name: 'Chicken Salad', location: 'Fridge', quantity: 2, unit: 'containers', img: 'https://picsum.photos/seed/salad/200/200' },
|
||||
|
||||
// Freezer items
|
||||
{ id: 15, name: 'Ice Cream', location: 'Freezer', quantity: 1, unit: 'tub', img: 'https://picsum.photos/seed/icecream/200/200' },
|
||||
{ id: 16, name: 'Frozen Vegetables', location: 'Freezer', quantity: 5, unit: 'bags', img: 'https://picsum.photos/seed/veggies/200/200' },
|
||||
{ id: 17, name: 'Chicken Breast', location: 'Freezer', quantity: 4, unit: 'packages', img: 'https://picsum.photos/seed/chicken/200/200' },
|
||||
{ id: 18, name: 'Ground Beef', location: 'Freezer', quantity: 3, unit: 'packages', img: 'https://picsum.photos/seed/beef/200/200' },
|
||||
{ id: 19, name: 'Pizza', location: 'Freezer', quantity: 2, unit: 'boxes', img: 'https://picsum.photos/seed/pizza/200/200' },
|
||||
{ id: 20, name: 'Ice', location: 'Freezer', quantity: 1, unit: 'bag', img: 'https://picsum.photos/seed/ice/200/200' },
|
||||
];
|
||||
|
||||
/**
|
||||
* Search and filter inventory items
|
||||
* @param {string} searchName - Search term for item name (case insensitive)
|
||||
* @param {string} selectedLocation - Filter by location ('All', 'Pantry', 'Fridge', 'Freezer')
|
||||
* @param {number} minQuantity - Minimum quantity filter
|
||||
* @param {number} maxQuantity - Maximum quantity filter
|
||||
* @returns {Array} Filtered inventory items
|
||||
*/
|
||||
export function searchInventory(searchName = '', selectedLocation = 'All', minQuantity = 0, maxQuantity = Infinity) {
|
||||
return inventoryData.filter(item => {
|
||||
// Filter by name
|
||||
const nameMatch = item.name.toLowerCase().includes(searchName.toLowerCase());
|
||||
|
||||
// Filter by location
|
||||
const locationMatch = selectedLocation === 'All' || item.location === selectedLocation;
|
||||
|
||||
// Filter by quantity range
|
||||
const quantityMatch = item.quantity >= minQuantity && item.quantity <= maxQuantity;
|
||||
|
||||
return nameMatch && locationMatch && quantityMatch;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all unique locations
|
||||
* @returns {Array} List of unique locations
|
||||
*/
|
||||
export function getLocations() {
|
||||
const locations = [...new Set(inventoryData.map(item => item.location))];
|
||||
return ['All', ...locations.sort()];
|
||||
}
|
||||
Reference in New Issue
Block a user