Initial commit: Pantry inventory tracker
A simple web app for tracking kitchen pantry items with: - Search and filter by stock status, spices, or container type - PIN-protected editing - Shopping list export (tap Out of Stock to copy) - QR code for quick mobile access - OpenGraph card for social sharing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
16
.gitignore
vendored
Normal file
16
.gitignore
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# User data
|
||||||
|
data/
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Editor files
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# Claude Code local settings
|
||||||
|
.claude/
|
||||||
|
|
||||||
|
# Archives
|
||||||
|
*.zip
|
||||||
50
CLAUDE.md
Normal file
50
CLAUDE.md
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
Pantry is a simple web app for tracking kitchen pantry items. It's a single-page application with no build step, consisting of just two files:
|
||||||
|
- `index.html` - Frontend (HTML/CSS/JS all in one file)
|
||||||
|
- `api.php` - Backend API (JSON file-based storage)
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
**Frontend (`index.html`)**:
|
||||||
|
- Vanilla JavaScript, no frameworks
|
||||||
|
- Uses QRCode.js from CDN for QR code generation
|
||||||
|
- Custom CSS with CSS variables for theming (earthy color palette: cream, terracotta, forest green)
|
||||||
|
- Container types defined in `containerTypes` object (around line 754) - must be kept in sync with backend
|
||||||
|
- All API calls go through the `apiCall()` function which POSTs to `api.php`
|
||||||
|
|
||||||
|
**Backend (`api.php`)**:
|
||||||
|
- JSON file-based storage in `data/` directory
|
||||||
|
- Endpoints: `get`, `add`, `update`, `delete`, `setPin`, `verifyPin`
|
||||||
|
- Default inventory defined in `$defaultInventory` array (lines 31-64)
|
||||||
|
- Container types should match frontend when modified
|
||||||
|
|
||||||
|
**Data Storage**:
|
||||||
|
- `data/inventory.json` - Inventory items
|
||||||
|
- `data/pin.txt` - 4-digit PIN for edit mode (plain text, view-only protection)
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
This is a no-build project. To test locally:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php -S localhost:8000
|
||||||
|
```
|
||||||
|
|
||||||
|
Then open `http://localhost:8000` in a browser.
|
||||||
|
|
||||||
|
## Key Implementation Details
|
||||||
|
|
||||||
|
- PIN system is view-only protection (anyone can view, PIN required to edit)
|
||||||
|
- PIN stored in plain text - adequate for household use only
|
||||||
|
- Items have: `id`, `name`, `container` (type), `outOfStock` (boolean)
|
||||||
|
- Frontend sorts items: in-stock first, then alphabetically
|
||||||
|
- 8 container types with color-coded dots in UI
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
Designed for YunoHost "My Webapp" deployment. Upload `index.html` and `api.php` to the `www/` folder. The `data/` directory is created automatically and must be writable by the web server.
|
||||||
125
README.md
Normal file
125
README.md
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
# Pantry Inventory App
|
||||||
|
|
||||||
|
A simple web app for tracking kitchen pantry items across your household.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Search** - Instantly filter ingredients by name
|
||||||
|
- **Container tracking** - 8 container types with color-coded indicators
|
||||||
|
- **Stock status** - Mark items as in-stock or out-of-stock
|
||||||
|
- **PIN protection** - Anyone can view, but editing requires a 4-digit PIN
|
||||||
|
- **QR Code** - Generate a scannable code to post on your pantry shelf
|
||||||
|
- **Shared data** - All household members see the same inventory
|
||||||
|
|
||||||
|
## Deployment on YunoHost (My Webapp)
|
||||||
|
|
||||||
|
### 1. Install My Webapp
|
||||||
|
|
||||||
|
In YunoHost admin panel:
|
||||||
|
1. Go to **Applications** → **Install**
|
||||||
|
2. Search for "My Webapp"
|
||||||
|
3. Configure:
|
||||||
|
- **Label**: Pantry (or whatever you prefer)
|
||||||
|
- **Domain/Path**: e.g., `yourdomain.com/pantry`
|
||||||
|
- **PHP version**: 8.2 or higher
|
||||||
|
- **Database**: None needed
|
||||||
|
4. Install
|
||||||
|
|
||||||
|
### 2. Upload Files
|
||||||
|
|
||||||
|
Connect via SFTP using the credentials shown after installation, then upload to the `www` folder:
|
||||||
|
|
||||||
|
```
|
||||||
|
www/
|
||||||
|
├── index.html (the main app)
|
||||||
|
├── api.php (backend API)
|
||||||
|
└── data/ (created automatically)
|
||||||
|
├── inventory.json
|
||||||
|
└── pin.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Set Permissions
|
||||||
|
|
||||||
|
The `data` folder needs to be writable by the web server. This should happen automatically, but if you have issues:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# SSH into your YunoHost server
|
||||||
|
cd /var/www/pantry/www # adjust path as needed
|
||||||
|
mkdir -p data
|
||||||
|
chmod 755 data
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. First Use
|
||||||
|
|
||||||
|
1. Visit your app URL (e.g., `https://yourdomain.com/pantry`)
|
||||||
|
2. The app loads with a pre-populated inventory based on your photos
|
||||||
|
3. Tap the lock icon → Set a 4-digit PIN for your household
|
||||||
|
4. Share the PIN with family members who need edit access
|
||||||
|
5. Go to the QR Code tab and print it for your pantry
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
index.html - Frontend (HTML/CSS/JS, no build step)
|
||||||
|
api.php - Backend API (reads/writes JSON files)
|
||||||
|
data/
|
||||||
|
inventory.json - Your inventory data
|
||||||
|
pin.txt - Stored PIN (hashed would be better for production)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Container Types
|
||||||
|
|
||||||
|
The app supports these container types (edit in both index.html and api.php if you want to change them):
|
||||||
|
|
||||||
|
| ID | Name |
|
||||||
|
|----|------|
|
||||||
|
| glass-bail-large | Glass Bail Jar (Large) |
|
||||||
|
| glass-bail-medium | Glass Bail Jar (Medium) |
|
||||||
|
| glass-bail-small | Glass Bail Jar (Small) |
|
||||||
|
| oxo-large | OXO Container (Large) |
|
||||||
|
| oxo-medium | OXO Container (Medium) |
|
||||||
|
| oxo-small | OXO Container (Small) |
|
||||||
|
| original-package | Original Package |
|
||||||
|
| mason-jar | Mason Jar |
|
||||||
|
|
||||||
|
## Customization
|
||||||
|
|
||||||
|
### Adding Container Types
|
||||||
|
|
||||||
|
1. In `index.html`, find the `containerTypes` object and add your new type
|
||||||
|
2. In the `<select>` dropdown for containers, add a new `<option>`
|
||||||
|
|
||||||
|
### Changing the Default Inventory
|
||||||
|
|
||||||
|
Edit the `$defaultInventory` array in `api.php`. This only applies on first run (before `inventory.json` exists).
|
||||||
|
|
||||||
|
### Resetting Everything
|
||||||
|
|
||||||
|
Delete the `data` folder contents to start fresh:
|
||||||
|
```bash
|
||||||
|
rm data/inventory.json data/pin.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Notes
|
||||||
|
|
||||||
|
- The PIN is stored in plain text. This is fine for a household app where you just want to prevent accidental edits, but don't use this for anything sensitive.
|
||||||
|
- There's no authentication beyond the PIN - anyone with the URL can view your inventory.
|
||||||
|
- For a more secure setup, you could put the app behind YunoHost's SSO.
|
||||||
|
|
||||||
|
## Backup
|
||||||
|
|
||||||
|
YunoHost's My Webapp will backup the entire `www` folder including `data/`. Your inventory and PIN are automatically included in YunoHost backups.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
**"Connection error" message**
|
||||||
|
- Check that PHP is enabled for your My Webapp instance
|
||||||
|
- Verify the `data` folder is writable
|
||||||
|
|
||||||
|
**PIN not working**
|
||||||
|
- Check that `data/pin.txt` exists and contains exactly 4 digits
|
||||||
|
- Try deleting `data/pin.txt` to reset
|
||||||
|
|
||||||
|
**Inventory not saving**
|
||||||
|
- Check `data/inventory.json` permissions
|
||||||
|
- Look for PHP errors in YunoHost logs
|
||||||
277
api.php
Normal file
277
api.php
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Pantry Inventory API
|
||||||
|
*
|
||||||
|
* Simple JSON file-based backend for the pantry inventory app.
|
||||||
|
* No database required - stores everything in inventory.json
|
||||||
|
*/
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
header('Access-Control-Allow-Origin: *');
|
||||||
|
header('Access-Control-Allow-Methods: POST, GET, OPTIONS');
|
||||||
|
header('Access-Control-Allow-Headers: Content-Type');
|
||||||
|
|
||||||
|
// Handle preflight requests
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
||||||
|
http_response_code(200);
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
// File paths
|
||||||
|
define('DATA_FILE', __DIR__ . '/data/inventory.json');
|
||||||
|
define('PIN_FILE', __DIR__ . '/data/pin.txt');
|
||||||
|
define('DATA_DIR', __DIR__ . '/data');
|
||||||
|
|
||||||
|
// Ensure data directory exists
|
||||||
|
if (!is_dir(DATA_DIR)) {
|
||||||
|
mkdir(DATA_DIR, 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default inventory (sample data using new container types)
|
||||||
|
$defaultInventory = [
|
||||||
|
['id' => 1, 'name' => 'Bulgur', 'container' => 'glass-flip-med-tall', 'outOfStock' => false],
|
||||||
|
['id' => 2, 'name' => 'Wild Rice Flour', 'container' => 'glass-flip-med-squat', 'outOfStock' => false],
|
||||||
|
['id' => 3, 'name' => 'Self Rising Flour', 'container' => 'plastic-oxo-large', 'outOfStock' => false],
|
||||||
|
['id' => 4, 'name' => 'Whole Wheat Flour', 'container' => 'plastic-oxo-large', 'outOfStock' => false],
|
||||||
|
['id' => 5, 'name' => 'Cake Flour', 'container' => 'plastic-clamp-large', 'outOfStock' => false],
|
||||||
|
['id' => 6, 'name' => 'GF Pancake Mix', 'container' => 'original-package', 'outOfStock' => false],
|
||||||
|
['id' => 7, 'name' => 'Coconut Flour', 'container' => 'glass-flip-small', 'outOfStock' => false],
|
||||||
|
['id' => 8, 'name' => 'All Purpose Flour', 'container' => 'plastic-oxo-large', 'outOfStock' => false],
|
||||||
|
['id' => 9, 'name' => 'Bread Flour', 'container' => 'plastic-clamp-tall', 'outOfStock' => false],
|
||||||
|
['id' => 10, 'name' => 'Powdered Sugar', 'container' => 'plastic-flip', 'outOfStock' => false],
|
||||||
|
['id' => 11, 'name' => 'Linguine', 'container' => 'original-package', 'outOfStock' => false],
|
||||||
|
['id' => 12, 'name' => 'Fettuccine', 'container' => 'original-package', 'outOfStock' => false],
|
||||||
|
['id' => 13, 'name' => 'Penne Rigate', 'container' => 'original-package', 'outOfStock' => false],
|
||||||
|
['id' => 14, 'name' => 'Ancho Chile', 'container' => 'glass-flip-large', 'outOfStock' => false],
|
||||||
|
['id' => 15, 'name' => 'Chocolate Chips', 'container' => 'glass-canning-quart', 'outOfStock' => false],
|
||||||
|
['id' => 16, 'name' => 'Dried Mushrooms', 'container' => 'glass-canning-pint', 'outOfStock' => false],
|
||||||
|
['id' => 17, 'name' => 'Bay Leaves', 'container' => 'spice-hex-large', 'outOfStock' => false],
|
||||||
|
['id' => 18, 'name' => 'Dates', 'container' => 'glass-canning-quart', 'outOfStock' => false],
|
||||||
|
['id' => 19, 'name' => 'Xanthan Gum', 'container' => 'spice-flip-tiny', 'outOfStock' => false],
|
||||||
|
['id' => 20, 'name' => 'M.S.G.', 'container' => 'spice-hex-small', 'outOfStock' => false],
|
||||||
|
['id' => 21, 'name' => 'Potato Starch', 'container' => 'glass-flip-small', 'outOfStock' => false],
|
||||||
|
['id' => 22, 'name' => 'Potato Flour', 'container' => 'glass-flip-small', 'outOfStock' => false],
|
||||||
|
['id' => 23, 'name' => 'Garbanzo Flour', 'container' => 'glass-flip-med-squat', 'outOfStock' => false],
|
||||||
|
['id' => 24, 'name' => 'Mochi Rice Flour', 'container' => 'glass-flip-small', 'outOfStock' => false],
|
||||||
|
['id' => 25, 'name' => 'Couscous', 'container' => 'original-package', 'outOfStock' => false],
|
||||||
|
['id' => 26, 'name' => 'Granola', 'container' => 'original-package', 'outOfStock' => false],
|
||||||
|
['id' => 27, 'name' => 'Cheerios', 'container' => 'original-package', 'outOfStock' => false],
|
||||||
|
['id' => 28, 'name' => 'Baking Powder', 'container' => 'spice-hex-large', 'outOfStock' => false],
|
||||||
|
['id' => 29, 'name' => 'Corn Starch', 'container' => 'glass-canning-small', 'outOfStock' => false],
|
||||||
|
['id' => 30, 'name' => 'Cocoa Powder', 'container' => 'spice-hex-large', 'outOfStock' => false],
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load inventory from file
|
||||||
|
*/
|
||||||
|
function loadInventory() {
|
||||||
|
global $defaultInventory;
|
||||||
|
|
||||||
|
if (file_exists(DATA_FILE)) {
|
||||||
|
$content = file_get_contents(DATA_FILE);
|
||||||
|
$data = json_decode($content, true);
|
||||||
|
if ($data !== null) {
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize with default inventory
|
||||||
|
saveInventory($defaultInventory);
|
||||||
|
return $defaultInventory;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save inventory to file
|
||||||
|
*/
|
||||||
|
function saveInventory($inventory) {
|
||||||
|
file_put_contents(DATA_FILE, json_encode($inventory, JSON_PRETTY_PRINT));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if PIN is set
|
||||||
|
*/
|
||||||
|
function hasPinSet() {
|
||||||
|
return file_exists(PIN_FILE) && strlen(trim(file_get_contents(PIN_FILE))) === 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get stored PIN
|
||||||
|
*/
|
||||||
|
function getPin() {
|
||||||
|
if (file_exists(PIN_FILE)) {
|
||||||
|
return trim(file_get_contents(PIN_FILE));
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set PIN
|
||||||
|
*/
|
||||||
|
function setPin($pin) {
|
||||||
|
file_put_contents(PIN_FILE, $pin);
|
||||||
|
// Secure the file
|
||||||
|
chmod(PIN_FILE, 0600);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify PIN
|
||||||
|
*/
|
||||||
|
function verifyPin($pin) {
|
||||||
|
$storedPin = getPin();
|
||||||
|
return $storedPin !== null && $storedPin === $pin;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get next available ID
|
||||||
|
*/
|
||||||
|
function getNextId($inventory) {
|
||||||
|
if (empty($inventory)) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
$maxId = max(array_column($inventory, 'id'));
|
||||||
|
return $maxId + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitize string input
|
||||||
|
*/
|
||||||
|
function sanitize($str) {
|
||||||
|
return trim($str);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main request handling
|
||||||
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
|
|
||||||
|
if (!$input || !isset($input['action'])) {
|
||||||
|
// Allow GET requests to view inventory
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
|
||||||
|
$inventory = loadInventory();
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'inventory' => $inventory,
|
||||||
|
'hasPin' => hasPinSet()
|
||||||
|
]);
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode(['error' => 'Invalid request']);
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
$action = $input['action'];
|
||||||
|
|
||||||
|
switch ($action) {
|
||||||
|
case 'get':
|
||||||
|
$inventory = loadInventory();
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'inventory' => $inventory,
|
||||||
|
'hasPin' => hasPinSet()
|
||||||
|
]);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'setPin':
|
||||||
|
$pin = $input['pin'] ?? '';
|
||||||
|
|
||||||
|
if (strlen($pin) !== 4 || !ctype_digit($pin)) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'PIN must be 4 digits']);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only allow setting PIN if not already set
|
||||||
|
if (hasPinSet()) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'PIN already set']);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
setPin($pin);
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'verifyPin':
|
||||||
|
$pin = $input['pin'] ?? '';
|
||||||
|
$valid = verifyPin($pin);
|
||||||
|
echo json_encode(['success' => $valid]);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'add':
|
||||||
|
$name = sanitize($input['name'] ?? '');
|
||||||
|
$container = sanitize($input['container'] ?? 'glass-bail-medium');
|
||||||
|
$outOfStock = (bool)($input['outOfStock'] ?? false);
|
||||||
|
|
||||||
|
if (empty($name)) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Name required']);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$inventory = loadInventory();
|
||||||
|
$newItem = [
|
||||||
|
'id' => getNextId($inventory),
|
||||||
|
'name' => $name,
|
||||||
|
'container' => $container,
|
||||||
|
'outOfStock' => $outOfStock
|
||||||
|
];
|
||||||
|
|
||||||
|
$inventory[] = $newItem;
|
||||||
|
saveInventory($inventory);
|
||||||
|
|
||||||
|
echo json_encode(['success' => true, 'item' => $newItem]);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'update':
|
||||||
|
$id = (int)($input['id'] ?? 0);
|
||||||
|
$name = sanitize($input['name'] ?? '');
|
||||||
|
$container = sanitize($input['container'] ?? '');
|
||||||
|
$outOfStock = (bool)($input['outOfStock'] ?? false);
|
||||||
|
|
||||||
|
if ($id <= 0) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Invalid ID']);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$inventory = loadInventory();
|
||||||
|
$found = false;
|
||||||
|
|
||||||
|
foreach ($inventory as &$item) {
|
||||||
|
if ($item['id'] === $id) {
|
||||||
|
if (!empty($name)) $item['name'] = $name;
|
||||||
|
if (!empty($container)) $item['container'] = $container;
|
||||||
|
$item['outOfStock'] = $outOfStock;
|
||||||
|
$found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$found) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Item not found']);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
saveInventory($inventory);
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'delete':
|
||||||
|
$id = (int)($input['id'] ?? 0);
|
||||||
|
|
||||||
|
if ($id <= 0) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Invalid ID']);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$inventory = loadInventory();
|
||||||
|
$originalCount = count($inventory);
|
||||||
|
$inventory = array_values(array_filter($inventory, fn($item) => $item['id'] !== $id));
|
||||||
|
|
||||||
|
if (count($inventory) === $originalCount) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Item not found']);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
saveInventory($inventory);
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
echo json_encode(['error' => 'Unknown action']);
|
||||||
|
}
|
||||||
39
containers.json
Normal file
39
containers.json
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"categories": [
|
||||||
|
{
|
||||||
|
"name": "Glass Jars",
|
||||||
|
"containers": [
|
||||||
|
{ "id": "glass-flip-large", "name": "Flip Top (Large)", "color": "#C4684A" },
|
||||||
|
{ "id": "glass-flip-med-tall", "name": "Flip Top (Medium Tall)", "color": "#D4856B" },
|
||||||
|
{ "id": "glass-flip-med-squat", "name": "Flip Top (Medium Squat)", "color": "#E07B5B" },
|
||||||
|
{ "id": "glass-flip-small", "name": "Flip Top (Small)", "color": "#E8A88B" },
|
||||||
|
{ "id": "glass-canning-quart", "name": "Canning Jar (Quart)", "color": "#6B8E9B" },
|
||||||
|
{ "id": "glass-canning-pint", "name": "Canning Jar (Pint)", "color": "#7FA3B0" },
|
||||||
|
{ "id": "glass-canning-small", "name": "Canning Jar (Small)", "color": "#93B8C5" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Plastic Containers",
|
||||||
|
"containers": [
|
||||||
|
{ "id": "plastic-oxo-large", "name": "OXO Large", "color": "#2D4739" },
|
||||||
|
{ "id": "plastic-flip", "name": "Flip Top", "color": "#3D5A4B" },
|
||||||
|
{ "id": "plastic-clamp-large", "name": "Clamp Top (Large)", "color": "#4D6A5B" },
|
||||||
|
{ "id": "plastic-clamp-tall", "name": "Clamp Top (Tall)", "color": "#5D7A6B" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Glass Spice Jars",
|
||||||
|
"containers": [
|
||||||
|
{ "id": "spice-hex-large", "name": "Hex (Large)", "color": "#8B7355" },
|
||||||
|
{ "id": "spice-hex-small", "name": "Hex (Small)", "color": "#A08668" },
|
||||||
|
{ "id": "spice-flip-tiny", "name": "Flip Top (Tiny)", "color": "#B5997B" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Other",
|
||||||
|
"containers": [
|
||||||
|
{ "id": "original-package", "name": "Original Packaging", "color": "#9A9590" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
24
deploy
Executable file
24
deploy
Executable file
@@ -0,0 +1,24 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
USER=admin
|
||||||
|
HOST=social
|
||||||
|
DIR=/var/www/my_webapp__2/www
|
||||||
|
|
||||||
|
# Deploy code files
|
||||||
|
rsync -avz --no-t --no-p --delete \
|
||||||
|
--exclude 'data/' \
|
||||||
|
index.html api.php containers.json og-image.png ${HOST}:${DIR}
|
||||||
|
|
||||||
|
# Handle data files
|
||||||
|
if [ "$1" = "--reset-data" ]; then
|
||||||
|
echo "Pushing local data to server..."
|
||||||
|
rsync -avz --no-t --no-p \
|
||||||
|
data/ ${HOST}:${DIR}/data/
|
||||||
|
else
|
||||||
|
echo "Pulling data from server..."
|
||||||
|
mkdir -p data
|
||||||
|
rsync -avz --no-t --no-p \
|
||||||
|
--exclude 'pin.txt' \
|
||||||
|
${HOST}:${DIR}/data/ data/
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
1291
index.html
Normal file
1291
index.html
Normal file
File diff suppressed because it is too large
Load Diff
BIN
og-image.png
Normal file
BIN
og-image.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 188 KiB |
20
og-image.svg
Normal file
20
og-image.svg
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<svg width="1200" height="630" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="bg" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||||
|
<stop offset="0%" style="stop-color:#FAF8F5"/>
|
||||||
|
<stop offset="100%" style="stop-color:#F5F0E8"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- Background -->
|
||||||
|
<rect width="1200" height="630" fill="url(#bg)"/>
|
||||||
|
|
||||||
|
<!-- Jar emoji as text (large) -->
|
||||||
|
<text x="600" y="280" font-size="180" text-anchor="middle" dominant-baseline="middle">🫙</text>
|
||||||
|
|
||||||
|
<!-- Title -->
|
||||||
|
<text x="600" y="420" font-family="Georgia, serif" font-size="72" font-weight="600" fill="#2D5A3D" text-anchor="middle">Pantry</text>
|
||||||
|
|
||||||
|
<!-- Subtitle -->
|
||||||
|
<text x="600" y="490" font-family="Arial, sans-serif" font-size="32" fill="#8B7355" text-anchor="middle">Kitchen Inventory Tracker</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 814 B |
Reference in New Issue
Block a user