adding firearm filter and category summary
This commit is contained in:
@@ -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);
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
Reference in New Issue
Block a user