Collection Management
Learn how to manage your beer collection with advanced features like inventory tracking, consumption history, and storage management.
Collection Management
BrewHoard provides comprehensive tools for managing your beer collection, from adding new acquisitions to tracking consumption and storage details.
Adding Beers to Your Collection
You can add beers manually or through the scanner feature. Each beer entry includes detailed information about the beer, acquisition details, and storage tracking.
Manual Beer Addition
Svelte
<!-- src/components/collection/AddBeerForm.svelte -->
<script>
let { onBeerAdded } = $props();
let beerData = $state({
name: '',
brewery: '',
style: '',
abv: 0,
ibu: 0,
quantity: 1,
acquisitionDate: new Date().toISOString().split('T')[0],
purchasePrice: 0,
storageLocation: '',
bestBeforeDate: '',
batchNumber: ''
});
let isSubmitting = $state(false);
async function handleSubmit() {
isSubmitting = true;
try {
const response = await fetch('/api/beers', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(beerData)
});
if (response.ok) {
onBeerAdded?.();
// Reset form
beerData = {
name: '',
brewery: '',
style: '',
abv: 0,
ibu: 0,
quantity: 1,
acquisitionDate: new Date().toISOString().split('T')[0],
purchasePrice: 0,
storageLocation: '',
bestBeforeDate: '',
batchNumber: ''
};
}
} finally {
isSubmitting = false;
}
}
</script>
<form onsubmit|preventDefault={handleSubmit}>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label for="name">Beer Name</label>
<input id="name" bind:value={beerData.name} required />
</div>
<div>
<label for="brewery">Brewery</label>
<input id="brewery" bind:value={beerData.brewery} required />
</div>
<div>
<label for="style">Style</label>
<input id="style" bind:value={beerData.style} />
</div>
<div>
<label for="abv">ABV (%)</label>
<input id="abv" type="number" step="0.1" bind:value={beerData.abv} />
</div>
<div>
<label for="quantity">Quantity</label>
<input id="quantity" type="number" bind:value={beerData.quantity} min="1" />
</div>
<div>
<label for="acquisitionDate">Acquisition Date</label>
<input id="acquisitionDate" type="date" bind:value={beerData.acquisitionDate} />
</div>
<div>
<label for="purchasePrice">Purchase Price</label>
<input id="purchasePrice" type="number" step="0.01" bind:value={beerData.purchasePrice} />
</div>
<div>
<label for="storageLocation">Storage Location</label>
<input id="storageLocation" bind:value={beerData.storageLocation} placeholder="Fridge, cellar, etc." />
</div>
<div>
<label for="bestBeforeDate">Best Before Date</label>
<input id="bestBeforeDate" type="date" bind:value={beerData.bestBeforeDate} />
</div>
<div>
<label for="batchNumber">Batch Number</label>
<input id="batchNumber" bind:value={beerData.batchNumber} />
</div>
</div>
<button type="submit" disabled={isSubmitting}>
{#if isSubmitting}
Adding Beer...
{:else}
Add to Collection
{/if}
</button>
</form>Inventory Tracking
Track your beer quantities and monitor stock levels across different storage locations.
Inventory Management Component
Svelte
<!-- src/components/collection/InventoryTracker.svelte -->
<script>
let { beers } = $props();
// Group beers by storage location
let inventoryByLocation = $derived(() => {
const grouped = {};
for (const beer of beers) {
const location = beer.storageLocation || 'Unspecified';
if (!grouped[location]) {
grouped[location] = [];
}
grouped[location].push(beer);
}
return grouped;
});
// Calculate total value and quantity
let totalStats = $derived(() => {
let totalQuantity = 0;
let totalValue = 0;
for (const beer of beers) {
totalQuantity += beer.quantity || 1;
totalValue += (beer.purchasePrice || 0) * (beer.quantity || 1);
}
return { totalQuantity, totalValue };
});
</script>
<div class="inventory-dashboard">
<div class="stats-grid">
<div class="stat-card">
<h3>Total Beers</h3>
<p class="stat-value">{totalStats.totalQuantity}</p>
</div>
<div class="stat-card">
<h3>Total Value</h3>
<p class="stat-value">${totalStats.totalValue.toFixed(2)}</p>
</div>
</div>
{#each Object.entries(inventoryByLocation) as [location, locationBeers]}
<div class="location-section">
<h4>{location}</h4>
<div class="beer-grid">
{#each locationBeers as beer}
<div class="beer-card">
<h5>{beer.name}</h5>
<p>{beer.brewery}</p>
<p>Quantity: {beer.quantity}</p>
{#if beer.bestBeforeDate}
<p class="expiry {new Date(beer.bestBeforeDate) < new Date() ? 'expired' : ''}">
Best before: {new Date(beer.bestBeforeDate).toLocaleDateString()}
</p>
{/if}
</div>
{/each}
</div>
</div>
{/each}
</div>
<style>
.inventory-dashboard {
padding: 1rem;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin-bottom: 2rem;
}
.stat-card {
background: var(--card-bg);
padding: 1rem;
border-radius: 8px;
text-align: center;
}
.stat-value {
font-size: 2rem;
font-weight: bold;
color: var(--primary);
}
.location-section {
margin-bottom: 2rem;
}
.beer-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 1rem;
}
.beer-card {
background: var(--card-bg);
padding: 1rem;
border-radius: 8px;
}
.expiry.expired {
color: var(--error);
}
</style>Consumption History
Track when and how you consume beers from your collection, including serving types and occasions.
Consumption Logging
Svelte
<!-- src/components/collection/ConsumptionLogger.svelte -->
<script>
let { beer, onConsumed } = $props();
let consumptionData = $state({
date: new Date().toISOString().split('T')[0],
servingType: 'bottle',
occasion: '',
notes: ''
});
let isLogging = $state(false);
async function logConsumption() {
isLogging = true;
try {
const response = await fetch(`/api/beers/${beer.id}/consume`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(consumptionData)
});
if (response.ok) {
onConsumed?.();
}
} finally {
isLogging = false;
}
}
</script>
<div class="consumption-logger">
<h4>Log Consumption: {beer.name}</h4>
<form onsubmit|preventDefault={logConsumption}>
<div class="form-grid">
<div>
<label for="date">Consumption Date</label>
<input id="date" type="date" bind:value={consumptionData.date} required />
</div>
<div>
<label for="servingType">Serving Type</label>
<select id="servingType" bind:value={consumptionData.servingType}>
<option value="bottle">Bottle</option>
<option value="can">Can</option>
<option value="draft">Draft</option>
<option value="cask">Cask</option>
</select>
</div>
<div>
<label for="occasion">Occasion</label>
<input id="occasion" bind:value={consumptionData.occasion} placeholder="Dinner, party, etc." />
</div>
<div class="full-width">
<label for="notes">Notes</label>
<textarea id="notes" bind:value={consumptionData.notes} rows="3" placeholder="Additional notes..."></textarea>
</div>
</div>
<button type="submit" disabled={isLogging}>
{#if isLogging}
Logging...
{:else}
Log Consumption
{/if}
</button>
</form>
</div>
<style>
.consumption-logger {
background: var(--card-bg);
padding: 1rem;
border-radius: 8px;
}
.form-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
margin-bottom: 1rem;
}
.full-width {
grid-column: 1 / -1;
}
</style>Batch Tracking and Best-Before Dates
Monitor beer freshness with batch tracking and expiration alerts.
Expiry Monitoring
Svelte
<!-- src/components/collection/ExpiryMonitor.svelte -->
<script>
let { beers } = $props();
// Filter beers with best-before dates
let beersWithExpiry = $derived(() => {
return beers
.filter(beer => beer.bestBeforeDate)
.sort((a, b) => new Date(a.bestBeforeDate) - new Date(b.bestBeforeDate));
});
// Categorize by expiry status
let expiryCategories = $derived(() => {
const now = new Date();
const categories = {
expired: [],
expiringSoon: [], // within 30 days
good: []
};
for (const beer of beersWithExpiry) {
const expiryDate = new Date(beer.bestBeforeDate);
const daysUntilExpiry = Math.ceil((expiryDate - now) / (1000 * 60 * 60 * 24));
if (daysUntilExpiry < 0) {
categories.expired.push({ ...beer, daysUntilExpiry });
} else if (daysUntilExpiry <= 30) {
categories.expiringSoon.push({ ...beer, daysUntilExpiry });
} else {
categories.good.push({ ...beer, daysUntilExpiry });
}
}
return categories;
});
</script>
<div class="expiry-monitor">
{#each Object.entries(expiryCategories) as [category, beers]}
{#if beers.length > 0}
<div class="expiry-section {category}">
<h4>
{#if category === 'expired'}
Expired Beers ({beers.length})
{:else if category === 'expiringSoon'}
Expiring Soon ({beers.length})
{:else}
Good to Drink ({beers.length})
{/if}
</h4>
<div class="beer-list">
{#each beers as beer}
<div class="beer-item">
<div class="beer-info">
<strong>{beer.name}</strong> by {beer.brewery}
{#if beer.batchNumber}
<br><small>Batch: {beer.batchNumber}</small>
{/if}
</div>
<div class="expiry-info">
{#if beer.daysUntilExpiry < 0}
Expired {Math.abs(beer.daysUntilExpiry)} days ago
{:else}
{beer.daysUntilExpiry} days left
{/if}
</div>
</div>
{/each}
</div>
</div>
{/if}
{/each}
</div>
<style>
.expiry-monitor {
space-y: 1rem;
}
.expiry-section {
background: var(--card-bg);
padding: 1rem;
border-radius: 8px;
margin-bottom: 1rem;
}
.expired {
border-left: 4px solid var(--error);
}
.expiringSoon {
border-left: 4px solid var(--warning);
}
.good {
border-left: 4px solid var(--success);
}
.beer-list {
space-y: 0.5rem;
}
.beer-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem;
background: var(--bg-secondary);
border-radius: 4px;
}
</style>Next Steps
- Scanner Guide - Add beers using camera scanning
- Ratings Guide - Rate and review your beers
- Analytics Dashboard - View collection insights