adding firearm filter and category summary

This commit is contained in:
Corey Blais
2026-03-26 17:12:47 -04:00
parent 8d42df6c27
commit de2b63cd87
2 changed files with 85 additions and 2 deletions
+34
View File
@@ -412,6 +412,10 @@ label span {
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));
} }
.firearms-stats {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.mini-stat { .mini-stat {
border-radius: 22px; border-radius: 22px;
padding: 1.2rem 1.35rem; padding: 1.2rem 1.35rem;
@@ -429,6 +433,28 @@ label span {
font-size: 1.65rem; font-size: 1.65rem;
} }
.category-count-list {
display: grid;
gap: 0.55rem;
}
.category-count-item {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 1rem;
color: #dfe5d3;
}
.category-count-item span {
margin-bottom: 0;
color: #b7bead;
}
.category-count-item strong {
font-size: 1rem;
}
.view-grid { .view-grid {
grid-template-columns: minmax(0, 1.35fr) minmax(360px, 0.8fr); grid-template-columns: minmax(0, 1.35fr) minmax(360px, 0.8fr);
} }
@@ -524,6 +550,14 @@ label span {
padding: 1rem 0; padding: 1rem 0;
} }
.filter-control {
min-width: 210px;
}
.filter-control span {
margin-bottom: 0.45rem;
}
.settings-row { .settings-row {
padding: 0.95rem 0; padding: 0.95rem 0;
border-top: 1px solid rgba(171, 180, 140, 0.08); border-top: 1px solid rgba(171, 180, 140, 0.08);
+51 -2
View File
@@ -116,6 +116,7 @@ const profileStorageKey = 'arsenal-iq-profile';
const ammoPageSelectionKey = 'arsenal-iq-ammo-page-calibers'; const ammoPageSelectionKey = 'arsenal-iq-ammo-page-calibers';
const ammoPageSelectionMigrationKey = 'arsenal-iq-ammo-page-calibers-v2'; const ammoPageSelectionMigrationKey = 'arsenal-iq-ammo-page-calibers-v2';
const firearmCategories = ['Handgun', 'Rifle', 'Shotgun', 'PCC', 'Other']; const firearmCategories = ['Handgun', 'Rifle', 'Shotgun', 'PCC', 'Other'];
const allFirearmCategoriesLabel = 'All categories';
const defaultCaliberNames = ['9mm', '.22 LR', '5.56 NATO', '.308 Win', '12 Gauge', '.45 ACP']; const defaultCaliberNames = ['9mm', '.22 LR', '5.56 NATO', '.308 Win', '12 Gauge', '.45 ACP'];
const currency = new Intl.NumberFormat('en-US', { const currency = new Intl.NumberFormat('en-US', {
@@ -204,6 +205,7 @@ export default function Home() {
const [firearmDrafts, setFirearmDrafts] = useState<Record<string, FirearmForm>>({}); const [firearmDrafts, setFirearmDrafts] = useState<Record<string, FirearmForm>>({});
const [newFirearm, setNewFirearm] = useState<FirearmForm>(emptyFirearmForm); const [newFirearm, setNewFirearm] = useState<FirearmForm>(emptyFirearmForm);
const [showNewFirearmForm, setShowNewFirearmForm] = useState(false); const [showNewFirearmForm, setShowNewFirearmForm] = useState(false);
const [selectedFirearmCategory, setSelectedFirearmCategory] = useState(allFirearmCategoriesLabel);
const [ammoAdjustments, setAmmoAdjustments] = useState<AmmoAdjustments>({}); const [ammoAdjustments, setAmmoAdjustments] = useState<AmmoAdjustments>({});
const [ammoPageCaliberIds, setAmmoPageCaliberIds] = useState<string[]>(() => loadStoredAmmoPageSelection()); const [ammoPageCaliberIds, setAmmoPageCaliberIds] = useState<string[]>(() => loadStoredAmmoPageSelection());
const [selectedAmmoCaliberId, setSelectedAmmoCaliberId] = useState(''); const [selectedAmmoCaliberId, setSelectedAmmoCaliberId] = useState('');
@@ -268,6 +270,25 @@ export default function Home() {
() => enabledCalibers.filter((caliber) => !ammoPageCaliberIds.includes(caliber.id)), () => enabledCalibers.filter((caliber) => !ammoPageCaliberIds.includes(caliber.id)),
[ammoPageCaliberIds, enabledCalibers], [ammoPageCaliberIds, enabledCalibers],
); );
const firearmCategoryCounts = useMemo(
() =>
firearmCategories.map((category) => ({
category,
count: data?.firearms.filter((firearm) => firearm.category === category).length ?? 0,
})),
[data],
);
const filteredFirearms = useMemo(() => {
if (!data) {
return [];
}
if (selectedFirearmCategory === allFirearmCategoriesLabel) {
return data.firearms;
}
return data.firearms.filter((firearm) => firearm.category === selectedFirearmCategory);
}, [data, selectedFirearmCategory]);
const apiFetch = async <T,>(path: string, init: RequestInit = {}, useProfile = true): Promise<T> => { const apiFetch = async <T,>(path: string, init: RequestInit = {}, useProfile = true): Promise<T> => {
const headers = new Headers(init.headers || {}); const headers = new Headers(init.headers || {});
@@ -962,7 +983,7 @@ export default function Home() {
</header> </header>
{activeView !== 'settings' ? ( {activeView !== 'settings' ? (
<section className="stage-stats"> <section className={activeView === 'firearms' ? 'stage-stats firearms-stats' : 'stage-stats'}>
<div className="mini-stat"> <div className="mini-stat">
<span>{activeView === 'ammo' ? 'Ammo types' : 'Firearms'}</span> <span>{activeView === 'ammo' ? 'Ammo types' : 'Firearms'}</span>
<strong>{activeView === 'ammo' ? ammoTypesWithRounds : data.summary.totalFirearms}</strong> <strong>{activeView === 'ammo' ? ammoTypesWithRounds : data.summary.totalFirearms}</strong>
@@ -975,6 +996,19 @@ export default function Home() {
: currency.format(data.summary.firearmsInvestment)} : currency.format(data.summary.firearmsInvestment)}
</strong> </strong>
</div> </div>
{activeView === 'firearms' ? (
<div className="mini-stat">
<span>Categories</span>
<div className="category-count-list" aria-label="Firearm counts by category">
{firearmCategoryCounts.map((item) => (
<div className="category-count-item" key={item.category}>
<span>{item.category}</span>
<strong>{item.count}</strong>
</div>
))}
</div>
</div>
) : null}
</section> </section>
) : null} ) : null}
@@ -989,13 +1023,28 @@ export default function Home() {
<span className="panel-kicker">Registry</span> <span className="panel-kicker">Registry</span>
<h3>Existing firearms</h3> <h3>Existing firearms</h3>
</div> </div>
<label className="filter-control">
<span>Category filter</span>
<select value={selectedFirearmCategory} onChange={(event) => setSelectedFirearmCategory(event.target.value)}>
<option value={allFirearmCategoriesLabel}>
{allFirearmCategoriesLabel} ({data.summary.totalFirearms})
</option>
{firearmCategoryCounts.map((item) => (
<option key={item.category} value={item.category}>
{item.category} ({item.count})
</option>
))}
</select>
</label>
</div> </div>
<div className="firearm-grid"> <div className="firearm-grid">
{data.firearms.length === 0 ? ( {data.firearms.length === 0 ? (
<p className="placeholder-copy">No firearms yet. Add your first record from the panel on the right.</p> <p className="placeholder-copy">No firearms yet. Add your first record from the panel on the right.</p>
) : filteredFirearms.length === 0 ? (
<p className="placeholder-copy">No firearms match the selected category.</p>
) : ( ) : (
data.firearms.map((firearm) => { filteredFirearms.map((firearm) => {
const draft = firearmDrafts[firearm.id] ?? buildFirearmForm(firearm); const draft = firearmDrafts[firearm.id] ?? buildFirearmForm(firearm);
return ( return (