Initial Commit

This commit is contained in:
2026-03-08 16:25:59 +00:00
commit 6e63caeb93
15 changed files with 1471 additions and 0 deletions

37
.vscode/launch.json vendored Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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()];
}