Flatten store location cards #14

Merged
nalalangan merged 1 commits from feature/flatten-store-location-cards into feature-custom-store-locations 2026-05-31 18:58:20 -09:00
3 changed files with 96 additions and 52 deletions
Showing only changes of commit 4b98740a7b - Show all commits

View File

@ -87,8 +87,8 @@ export default function ManageStores() {
<section className="manage-section"> <section className="manage-section">
<h2>Store Locations ({householdStores.length})</h2> <h2>Store Locations ({householdStores.length})</h2>
<p className="manage-stores-help"> <p className="manage-stores-help">
Stores and locations are private to this household. Each location has its own zones, Stores are private to this household. Locations define map-specific zones, item
item defaults, and shopping order. placement, and shopping order.
</p> </p>
{householdStores.length === 0 ? ( {householdStores.length === 0 ? (
<p className="empty-message">No store locations added yet.</p> <p className="empty-message">No store locations added yet.</p>
@ -96,45 +96,55 @@ export default function ManageStores() {
<div className="stores-list"> <div className="stores-list">
{groupedStores.map((storeGroup) => ( {groupedStores.map((storeGroup) => (
<div key={storeGroup.household_store_id} className="store-card"> <div key={storeGroup.household_store_id} className="store-card">
<div className="store-info"> <div className="store-card-header">
<h3>{storeGroup.name}</h3> <div className="store-info">
<h3>{storeGroup.name}</h3>
</div>
<StoreLocationManager
householdId={activeHousehold.id}
storeGroup={storeGroup}
allLocationCount={householdStores.length}
canManage={isAdmin}
refreshAfterStoreChange={refreshAfterStoreChange}
/>
</div> </div>
<StoreLocationManager
householdId={activeHousehold.id}
storeGroup={storeGroup}
allLocationCount={householdStores.length}
canManage={isAdmin}
refreshAfterStoreChange={refreshAfterStoreChange}
/>
<div className="store-location-list"> <div className="store-location-list">
{storeGroup.locations.map((location) => ( {storeGroup.locations.map((location) => {
<div key={location.id} className="store-location-row"> const label = locationLabel(location);
<div className="store-info"> const showLocationName =
<strong>{locationLabel(location)}</strong> storeGroup.locations.length > 1 || label !== storeGroup.name;
{location.address ? (
<p className="store-location">{location.address}</p> return (
) : null} <div key={location.id} className="store-location-row">
{location.is_default ? ( <div className="store-info store-location-copy">
<p className="store-location">Default shopping location</p> {showLocationName ? <strong>{label}</strong> : null}
) : null} {location.address ? (
<p className="store-location">{location.address}</p>
) : null}
{location.is_default ? (
<p className="store-location">Default location</p>
) : null}
</div>
<div className="store-location-controls">
<StoreZoneManager
householdId={activeHousehold.id}
location={location}
canManage={isAdmin}
refreshActiveZones={refreshZones}
/>
<StoreAvailableItemsManager
householdId={activeHousehold.id}
store={location}
isAdmin={isAdmin}
/>
</div>
</div> </div>
);
<StoreZoneManager })}
householdId={activeHousehold.id}
location={location}
canManage={isAdmin}
refreshActiveZones={refreshZones}
/>
<StoreAvailableItemsManager
householdId={activeHousehold.id}
store={location}
isAdmin={isAdmin}
/>
</div>
))}
</div> </div>
</div> </div>
))} ))}

View File

@ -50,11 +50,11 @@
background: var(--background); background: var(--background);
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: 8px; border-radius: 8px;
padding: 1.25rem; padding: 1rem;
transition: all 0.2s; transition: all 0.2s;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1rem; gap: 0.75rem;
} }
.store-card:hover { .store-card:hover {
@ -62,16 +62,23 @@
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
} }
.store-card-header {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 0.75rem;
align-items: center;
}
.store-info h3 { .store-info h3 {
font-size: 1.1rem; font-size: 1.1rem;
font-weight: 600; font-weight: 600;
color: var(--text-primary); color: var(--text-primary);
margin: 0 0 0.5rem 0; margin: 0;
} }
.store-location { .store-location {
color: var(--text-secondary); color: var(--text-secondary);
font-size: 0.9rem; font-size: 0.85rem;
margin: 0; margin: 0;
} }
@ -90,17 +97,37 @@
.store-location-list { .store-location-list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1rem; gap: 0.65rem;
} }
.store-location-row { .store-location-row {
display: flex;
flex-direction: column;
gap: 0.55rem;
padding-top: 0.65rem;
border-top: 1px solid var(--border);
}
.store-location-row:first-child {
padding-top: 0;
border-top: 0;
}
.store-location-copy {
display: flex;
flex-direction: column;
gap: 0.12rem;
}
.store-location-copy strong {
color: var(--text-primary);
font-size: 0.94rem;
}
.store-location-controls {
display: grid; display: grid;
grid-template-columns: minmax(0, 1fr) auto; grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.75rem; gap: 0.5rem;
padding: 1rem;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--card-bg);
} }
.store-location-manager-trigger, .store-location-manager-trigger,
@ -108,9 +135,8 @@
width: 100%; width: 100%;
} }
.store-location-row > .store-zone-manager-trigger, .store-card-header > .store-location-manager-trigger {
.store-location-row > .store-available-items-trigger { width: auto;
grid-column: 1 / -1;
} }
.store-items-modal-toolbar.store-management-create-row { .store-items-modal-toolbar.store-management-create-row {
@ -283,13 +309,18 @@
width: 100%; width: 100%;
} }
.store-location-row, .store-card-header,
.store-location-controls,
.store-items-modal-toolbar.store-management-create-row, .store-items-modal-toolbar.store-management-create-row,
.store-items-modal-toolbar.store-location-create-row, .store-items-modal-toolbar.store-location-create-row,
.store-management-row { .store-management-row {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.store-card-header > .store-location-manager-trigger {
width: 100%;
}
.store-management-order { .store-management-order {
text-align: left; text-align: left;
} }

View File

@ -107,6 +107,9 @@ test("manage stores opens a modal to edit and delete household store items", asy
const storeCard = page.locator(".store-card").filter({ hasText: "Costco" }); const storeCard = page.locator(".store-card").filter({ hasText: "Costco" });
await expect(storeCard).toBeVisible(); await expect(storeCard).toBeVisible();
await expect(storeCard.getByText("Costco", { exact: true })).toHaveCount(1);
await expect(storeCard.getByText("Default location")).toBeVisible();
await expect(storeCard.getByText("Default shopping location")).toHaveCount(0);
await expect(storeCard.getByRole("button", { name: "Manage Items" })).toBeVisible(); await expect(storeCard.getByRole("button", { name: "Manage Items" })).toBeVisible();
await storeCard.getByRole("button", { name: "Manage Items" }).click(); await storeCard.getByRole("button", { name: "Manage Items" }).click();