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