Files
pantry/api.php
Eric Wagoner 726325e501 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>
2025-12-23 22:20:27 -05:00

278 lines
9.1 KiB
PHP

<?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']);
}