frontend refresh and update backend for frontend's new behavior

This commit is contained in:
Nico 2026-01-23 00:25:11 -08:00
parent 4139a07cd2
commit 68976a7683
9 changed files with 486 additions and 80 deletions

View File

@ -32,7 +32,8 @@ exports.addItem = async (req, res) => {
exports.markBought = async (req, res) => { exports.markBought = async (req, res) => {
const userId = req.user.id; const userId = req.user.id;
await List.setBought(req.body.id, userId); const { id, quantity } = req.body;
await List.setBought(id, userId, quantity);
res.json({ message: "Item marked bought" }); res.json({ message: "Item marked bought" });
}; };

View File

@ -34,7 +34,30 @@ exports.getUnboughtItems = async () => {
exports.getItemByName = async (itemName) => { exports.getItemByName = async (itemName) => {
const result = await pool.query( const result = await pool.query(
"SELECT * FROM grocery_list WHERE item_name ILIKE $1", `SELECT
gl.id,
LOWER(gl.item_name) AS item_name,
gl.quantity,
gl.bought,
ENCODE(gl.item_image, 'base64') as item_image,
gl.image_mime_type,
(
SELECT ARRAY_AGG(DISTINCT u.name)
FROM (
SELECT DISTINCT gh.added_by
FROM grocery_history gh
WHERE gh.list_item_id = gl.id
ORDER BY gh.added_by
) gh
JOIN users u ON gh.added_by = u.id
) as added_by_users,
gl.modified_on as last_added_on,
ic.item_type,
ic.item_group,
ic.zone
FROM grocery_list gl
LEFT JOIN item_classification ic ON gl.id = ic.id
WHERE gl.item_name ILIKE $1`,
[itemName] [itemName]
); );
@ -87,11 +110,31 @@ exports.addOrUpdateItem = async (itemName, quantity, userId, imageBuffer = null,
}; };
exports.setBought = async (id, userId) => { exports.setBought = async (id, userId, quantityBought) => {
// Get current item
const item = await pool.query(
"SELECT quantity FROM grocery_list WHERE id = $1",
[id]
);
if (!item.rows[0]) return;
const currentQuantity = item.rows[0].quantity;
const remainingQuantity = currentQuantity - quantityBought;
if (remainingQuantity <= 0) {
// Mark as bought if all quantity is purchased
await pool.query( await pool.query(
"UPDATE grocery_list SET bought = TRUE, modified_on = NOW() WHERE id = $1", "UPDATE grocery_list SET bought = TRUE, modified_on = NOW() WHERE id = $1",
[id] [id]
); );
} else {
// Reduce quantity if partial purchase
await pool.query(
"UPDATE grocery_list SET quantity = $1, modified_on = NOW() WHERE id = $2",
[remainingQuantity, id]
);
}
}; };
@ -106,7 +149,7 @@ exports.addHistoryRecord = async (itemId, quantity, userId) => {
exports.getSuggestions = async (query) => { exports.getSuggestions = async (query) => {
const result = await pool.query( const result = await pool.query(
`SELECT DISTINCT item_name `SELECT DISTINCT LOWER(item_name) as item_name
FROM grocery_list FROM grocery_list
WHERE item_name ILIKE $1 WHERE item_name ILIKE $1
LIMIT 10`, LIMIT 10`,

View File

@ -27,7 +27,7 @@ export const updateItemWithClassification = (id, itemName, quantity, classificat
classification classification
}); });
}; };
export const markBought = (id) => api.post("/list/mark-bought", { id }); export const markBought = (id, quantity) => api.post("/list/mark-bought", { id, quantity });
export const getSuggestions = (query) => api.get("/list/suggest", { params: { query: query } }); export const getSuggestions = (query) => api.get("/list/suggest", { params: { query: query } });
export const getRecentlyBought = () => api.get("/list/recently-bought"); export const getRecentlyBought = () => api.get("/list/recently-bought");

View File

@ -2,7 +2,7 @@ import { memo, useRef, useState } from "react";
import AddImageModal from "../modals/AddImageModal"; import AddImageModal from "../modals/AddImageModal";
import ConfirmBuyModal from "../modals/ConfirmBuyModal"; import ConfirmBuyModal from "../modals/ConfirmBuyModal";
function GroceryListItem({ item, onClick, onImageAdded, onLongPress, allItems = [] }) { function GroceryListItem({ item, onClick, onImageAdded, onLongPress, allItems = [], compact = false }) {
const [showAddImageModal, setShowAddImageModal] = useState(false); const [showAddImageModal, setShowAddImageModal] = useState(false);
const [showConfirmBuyModal, setShowConfirmBuyModal] = useState(false); const [showConfirmBuyModal, setShowConfirmBuyModal] = useState(false);
const [currentItem, setCurrentItem] = useState(item); const [currentItem, setCurrentItem] = useState(item);
@ -120,7 +120,7 @@ function GroceryListItem({ item, onClick, onImageAdded, onLongPress, allItems =
return ( return (
<> <>
<li <li
className="glist-li" className={`glist-li ${compact ? 'compact' : ''}`}
onClick={handleItemClick} onClick={handleItemClick}
onTouchStart={handleTouchStart} onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove} onTouchMove={handleTouchMove}

View File

@ -0,0 +1,47 @@
import "../../styles/components/ConfirmAddExistingModal.css";
export default function ConfirmAddExistingModal({
itemName,
currentQuantity,
addingQuantity,
onConfirm,
onCancel
}) {
const newQuantity = currentQuantity + addingQuantity;
return (
<div className="confirm-add-existing-overlay" onClick={onCancel}>
<div className="confirm-add-existing-modal" onClick={(e) => e.stopPropagation()}>
<h2 className="confirm-add-existing-title">
<strong>{itemName}</strong> is already in your list
</h2>
<div className="confirm-add-existing-content">
<div className="confirm-add-existing-qty-info">
<div className="qty-row">
<span className="qty-label">Current quantity:</span>
<span className="qty-value">{currentQuantity}</span>
</div>
<div className="qty-row">
<span className="qty-label">Adding:</span>
<span className="qty-value">+{addingQuantity}</span>
</div>
<div className="qty-row qty-total">
<span className="qty-label">New total:</span>
<span className="qty-value">{newQuantity}</span>
</div>
</div>
</div>
<div className="confirm-add-existing-actions">
<button className="confirm-add-existing-btn cancel" onClick={onCancel}>
Cancel
</button>
<button className="confirm-add-existing-btn confirm" onClick={onConfirm}>
Update Quantity
</button>
</div>
</div>
</div>
);
}

View File

@ -1,4 +1,4 @@
import { useState } from "react"; import { useEffect, useState } from "react";
import "../../styles/ConfirmBuyModal.css"; import "../../styles/ConfirmBuyModal.css";
export default function ConfirmBuyModal({ export default function ConfirmBuyModal({
@ -11,6 +11,11 @@ export default function ConfirmBuyModal({
const [quantity, setQuantity] = useState(item.quantity); const [quantity, setQuantity] = useState(item.quantity);
const maxQuantity = item.quantity; const maxQuantity = item.quantity;
// Update quantity when item changes (navigation)
useEffect(() => {
setQuantity(item.quantity);
}, [item.id, item.quantity]);
// Find current index and check for prev/next // Find current index and check for prev/next
const currentIndex = allItems.findIndex(i => i.id === item.id); const currentIndex = allItems.findIndex(i => i.id === item.id);
const hasPrev = currentIndex > 0; const hasPrev = currentIndex > 0;

View File

@ -15,6 +15,7 @@ import SortDropdown from "../components/common/SortDropdown";
import AddItemForm from "../components/forms/AddItemForm"; import AddItemForm from "../components/forms/AddItemForm";
import GroceryListItem from "../components/items/GroceryListItem"; import GroceryListItem from "../components/items/GroceryListItem";
import AddItemWithDetailsModal from "../components/modals/AddItemWithDetailsModal"; import AddItemWithDetailsModal from "../components/modals/AddItemWithDetailsModal";
import ConfirmAddExistingModal from "../components/modals/ConfirmAddExistingModal";
import EditItemModal from "../components/modals/EditItemModal"; import EditItemModal from "../components/modals/EditItemModal";
import SimilarItemModal from "../components/modals/SimilarItemModal"; import SimilarItemModal from "../components/modals/SimilarItemModal";
import { ZONE_FLOW } from "../constants/classifications"; import { ZONE_FLOW } from "../constants/classifications";
@ -45,6 +46,9 @@ export default function GroceryList() {
const [showEditModal, setShowEditModal] = useState(false); const [showEditModal, setShowEditModal] = useState(false);
const [editingItem, setEditingItem] = useState(null); const [editingItem, setEditingItem] = useState(null);
const [recentlyBoughtCollapsed, setRecentlyBoughtCollapsed] = useState(settings.recentlyBoughtCollapsed); const [recentlyBoughtCollapsed, setRecentlyBoughtCollapsed] = useState(settings.recentlyBoughtCollapsed);
const [collapsedZones, setCollapsedZones] = useState({});
const [showConfirmAddExisting, setShowConfirmAddExisting] = useState(false);
const [confirmAddExistingData, setConfirmAddExistingData] = useState(null);
// === Data Loading === // === Data Loading ===
@ -74,6 +78,14 @@ export default function GroceryList() {
}, []); }, []);
// === Zone Collapse Handler ===
const toggleZoneCollapse = (zone) => {
setCollapsedZones(prev => ({
...prev,
[zone]: !prev[zone]
}));
};
// === Sorted Items Computation === // === Sorted Items Computation ===
const sortedItems = useMemo(() => { const sortedItems = useMemo(() => {
const sorted = [...items]; const sorted = [...items];
@ -125,17 +137,19 @@ export default function GroceryList() {
return; return;
} }
const allItems = [...items, ...recentlyBoughtItems];
const lowerText = text.toLowerCase().trim(); const lowerText = text.toLowerCase().trim();
const exactMatch = allItems.find(item => item.item_name.toLowerCase() === lowerText);
setButtonText(exactMatch ? "Add" : "Create + Add");
try { try {
const suggestions = await getSuggestions(text); const response = await getSuggestions(text);
setSuggestions(suggestions.data.map(s => s.item_name)); const suggestionList = response.data.map(s => s.item_name);
setSuggestions(suggestionList);
// All suggestions are now lowercase from DB, direct comparison
const exactMatch = suggestionList.includes(lowerText);
setButtonText(exactMatch ? "Add" : "Create + Add");
} catch { } catch {
setSuggestions([]); setSuggestions([]);
setButtonText("Create + Add");
} }
}; };
@ -184,31 +198,24 @@ export default function GroceryList() {
if (existingItem?.bought === false) { if (existingItem?.bought === false) {
const currentQuantity = existingItem.quantity; const currentQuantity = existingItem.quantity;
const newQuantity = currentQuantity + quantity; const newQuantity = currentQuantity + quantity;
const yes = window.confirm(
`Item "${itemName}" already exists in the list. Do you want to update its quantity from ${currentQuantity} to ${newQuantity}?`
);
if (!yes) return;
const response = await addItem(itemName, newQuantity, null); // Show modal instead of window.confirm
setSuggestions([]); setConfirmAddExistingData({
setButtonText("Add Item"); itemName,
currentQuantity,
if (response.data) { addingQuantity: quantity,
setItems(prevItems => newQuantity,
prevItems.map(item => existingItem
item.id === existingItem.id ? { ...item, ...response.data } : item });
) setShowConfirmAddExisting(true);
);
}
} else if (existingItem) { } else if (existingItem) {
const response = await addItem(itemName, quantity, null); await addItem(itemName, quantity, null);
setSuggestions([]); setSuggestions([]);
setButtonText("Add Item"); setButtonText("Add Item");
if (response.data) { // Reload lists to reflect the changes
setItems(prevItems => [...prevItems, response.data]); await loadItems();
setRecentlyBoughtItems(prevItems => prevItems.filter(item => item.id !== existingItem.id)); await loadRecentlyBought();
}
} else { } else {
setPendingItem({ itemName, quantity }); setPendingItem({ itemName, quantity });
setShowAddDetailsModal(true); setShowAddDetailsModal(true);
@ -239,6 +246,45 @@ export default function GroceryList() {
}, [similarItemSuggestion, processItemAddition]); }, [similarItemSuggestion, processItemAddition]);
// === Confirm Add Existing Modal Handlers ===
const handleConfirmAddExisting = useCallback(async () => {
if (!confirmAddExistingData) return;
const { itemName, newQuantity, existingItem } = confirmAddExistingData;
setShowConfirmAddExisting(false);
setConfirmAddExistingData(null);
try {
// Update the item
await addItem(itemName, newQuantity, null);
// Fetch the updated item with properly formatted data
const response = await getItemByName(itemName);
const updatedItem = response.data;
// Update state with the full item data
setItems(prevItems =>
prevItems.map(item =>
item.id === existingItem.id ? updatedItem : item
)
);
setSuggestions([]);
setButtonText("Add Item");
} catch (error) {
console.error("Failed to update item:", error);
// Fallback to full reload on error
await loadItems();
}
}, [confirmAddExistingData, loadItems]);
const handleCancelAddExisting = useCallback(() => {
setShowConfirmAddExisting(false);
setConfirmAddExistingData(null);
}, []);
// === Add Details Modal Handlers === // === Add Details Modal Handlers ===
const handleAddDetailsConfirm = useCallback(async (imageFile, classification) => { const handleAddDetailsConfirm = useCallback(async (imageFile, classification) => {
if (!pendingItem) return; if (!pendingItem) return;
@ -300,10 +346,26 @@ export default function GroceryList() {
// === Item Action Handlers === // === Item Action Handlers ===
const handleBought = useCallback(async (id, quantity) => { const handleBought = useCallback(async (id, quantity) => {
await markBought(id); const item = items.find(i => i.id === id);
if (!item) return;
await markBought(id, quantity);
// If buying full quantity, remove from list
if (quantity >= item.quantity) {
setItems(prevItems => prevItems.filter(item => item.id !== id)); setItems(prevItems => prevItems.filter(item => item.id !== id));
} else {
// If partial, update quantity
const response = await getItemByName(item.item_name);
if (response.data) {
setItems(prevItems =>
prevItems.map(item => item.id === id ? response.data : item)
);
}
}
loadRecentlyBought(); loadRecentlyBought();
}, []); }, [items]);
const handleImageAdded = useCallback(async (id, itemName, quantity, imageFile) => { const handleImageAdded = useCallback(async (id, itemName, quantity, imageFile) => {
@ -415,17 +477,31 @@ export default function GroceryList() {
{sortMode === "zone" ? ( {sortMode === "zone" ? (
(() => { (() => {
const grouped = groupItemsByZone(sortedItems); const grouped = groupItemsByZone(sortedItems);
return Object.keys(grouped).map(zone => ( return Object.keys(grouped).map(zone => {
const isCollapsed = collapsedZones[zone];
const itemCount = grouped[zone].length;
return (
<div key={zone} className="glist-classification-group"> <div key={zone} className="glist-classification-group">
<h3 className="glist-classification-header"> <h3
className="glist-classification-header clickable"
onClick={() => toggleZoneCollapse(zone)}
>
<span>
{zone === 'unclassified' ? 'Unclassified' : zone} {zone === 'unclassified' ? 'Unclassified' : zone}
<span className="glist-zone-count"> ({itemCount})</span>
</span>
<span className="glist-zone-indicator">
{isCollapsed ? "▼" : "▲"}
</span>
</h3> </h3>
<ul className="glist-ul"> {!isCollapsed && (
<ul className={`glist-ul ${settings.compactView ? 'compact' : ''}`}>
{grouped[zone].map((item) => ( {grouped[zone].map((item) => (
<GroceryListItem <GroceryListItem
key={item.id} key={item.id}
item={item} item={item}
allItems={sortedItems} allItems={sortedItems}
compact={settings.compactView}
onClick={(id, quantity) => onClick={(id, quantity) =>
[ROLES.ADMIN, ROLES.EDITOR].includes(role) && handleBought(id, quantity) [ROLES.ADMIN, ROLES.EDITOR].includes(role) && handleBought(id, quantity)
} }
@ -438,16 +514,19 @@ export default function GroceryList() {
/> />
))} ))}
</ul> </ul>
)}
</div> </div>
)); );
});
})() })()
) : ( ) : (
<ul className="glist-ul"> <ul className={`glist-ul ${settings.compactView ? 'compact' : ''}`}>
{sortedItems.map((item) => ( {sortedItems.map((item) => (
<GroceryListItem <GroceryListItem
key={item.id} key={item.id}
item={item} item={item}
allItems={sortedItems} allItems={sortedItems}
compact={settings.compactView}
onClick={(id, quantity) => onClick={(id, quantity) =>
[ROLES.ADMIN, ROLES.EDITOR].includes(role) && handleBought(id, quantity) [ROLES.ADMIN, ROLES.EDITOR].includes(role) && handleBought(id, quantity)
} }
@ -464,24 +543,25 @@ export default function GroceryList() {
{recentlyBoughtItems.length > 0 && settings.showRecentlyBought && ( {recentlyBoughtItems.length > 0 && settings.showRecentlyBought && (
<> <>
<div className="glist-section-header"> <h2
<h2 className="glist-section-title">Recently Bought (24HR)</h2> className="glist-section-title clickable"
<button
className="glist-collapse-btn"
onClick={() => setRecentlyBoughtCollapsed(!recentlyBoughtCollapsed)} onClick={() => setRecentlyBoughtCollapsed(!recentlyBoughtCollapsed)}
> >
{recentlyBoughtCollapsed ? "▼ Show" : "▲ Hide"} <span>Recently Bought (24HR)</span>
</button> <span className="glist-section-indicator">
</div> {recentlyBoughtCollapsed ? "▼" : "▲"}
</span>
</h2>
{!recentlyBoughtCollapsed && ( {!recentlyBoughtCollapsed && (
<> <>
<ul className="glist-ul"> <ul className={`glist-ul ${settings.compactView ? 'compact' : ''}`}>
{recentlyBoughtItems.slice(0, recentlyBoughtDisplayCount).map((item) => ( {recentlyBoughtItems.slice(0, recentlyBoughtDisplayCount).map((item) => (
<GroceryListItem <GroceryListItem
key={item.id} key={item.id}
item={item} item={item}
allItems={recentlyBoughtItems} allItems={recentlyBoughtItems}
compact={settings.compactView}
onClick={null} onClick={null}
onImageAdded={ onImageAdded={
[ROLES.ADMIN, ROLES.EDITOR].includes(role) ? handleImageAdded : null [ROLES.ADMIN, ROLES.EDITOR].includes(role) ? handleImageAdded : null
@ -542,6 +622,16 @@ export default function GroceryList() {
onImageUpdate={handleImageAdded} onImageUpdate={handleImageAdded}
/> />
)} )}
{showConfirmAddExisting && confirmAddExistingData && (
<ConfirmAddExistingModal
itemName={confirmAddExistingData.itemName}
currentQuantity={confirmAddExistingData.currentQuantity}
addingQuantity={confirmAddExistingData.addingQuantity}
onConfirm={handleConfirmAddExisting}
onCancel={handleCancelAddExisting}
/>
)}
</div> </div>
); );
} }

View File

@ -0,0 +1,144 @@
/* Confirm Add Existing Modal */
.confirm-add-existing-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: var(--modal-backdrop-bg);
display: flex;
align-items: center;
justify-content: center;
z-index: var(--z-modal);
padding: var(--spacing-md);
}
.confirm-add-existing-modal {
background: var(--modal-bg);
border-radius: var(--modal-border-radius);
padding: var(--modal-padding);
max-width: 400px;
width: 100%;
box-shadow: var(--shadow-xl);
animation: slideIn 0.2s ease-out;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.confirm-add-existing-title {
margin: 0 0 var(--spacing-lg) 0;
font-size: var(--font-size-xl);
color: var(--color-text-primary);
text-align: center;
font-weight: var(--font-weight-normal);
}
.confirm-add-existing-title strong {
color: var(--color-primary);
font-weight: var(--font-weight-semibold);
}
.confirm-add-existing-content {
margin-bottom: var(--spacing-lg);
}
.confirm-add-existing-qty-info {
background: var(--color-bg-surface);
border: var(--border-width-thin) solid var(--color-border-light);
border-radius: var(--border-radius-md);
padding: var(--spacing-md);
margin: var(--spacing-md) 0;
}
.qty-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--spacing-xs) 0;
font-size: var(--font-size-base);
}
.qty-row.qty-total {
margin-top: var(--spacing-sm);
padding-top: var(--spacing-sm);
border-top: var(--border-width-medium) solid var(--color-border-medium);
font-weight: var(--font-weight-semibold);
}
.qty-label {
color: var(--color-text-secondary);
}
.qty-value {
color: var(--color-text-primary);
font-weight: var(--font-weight-medium);
font-size: var(--font-size-lg);
}
.qty-total .qty-label,
.qty-total .qty-value {
color: var(--color-primary);
font-size: var(--font-size-lg);
}
.confirm-add-existing-actions {
display: flex;
gap: var(--spacing-md);
margin-top: var(--spacing-lg);
}
.confirm-add-existing-btn {
flex: 1;
padding: var(--button-padding-y) var(--button-padding-x);
border: none;
border-radius: var(--button-border-radius);
font-size: var(--font-size-base);
font-weight: var(--button-font-weight);
cursor: pointer;
transition: var(--transition-base);
}
.confirm-add-existing-btn.cancel {
background: var(--color-bg-surface);
color: var(--color-text-primary);
border: var(--border-width-thin) solid var(--color-border-medium);
}
.confirm-add-existing-btn.cancel:hover {
background: var(--color-bg-hover);
border-color: var(--color-border-dark);
}
.confirm-add-existing-btn.confirm {
background: var(--color-primary);
color: var(--color-text-inverse);
}
.confirm-add-existing-btn.confirm:hover {
background: var(--color-primary-hover);
transform: translateY(-1px);
box-shadow: var(--shadow-md);
}
.confirm-add-existing-btn.confirm:active {
transform: translateY(0);
}
@media (max-width: 480px) {
.confirm-add-existing-modal {
max-width: 90%;
}
.confirm-add-existing-actions {
flex-direction: column;
}
}

View File

@ -29,6 +29,30 @@
color: var(--color-gray-700); color: var(--color-gray-700);
border-top: var(--border-width-medium) solid var(--color-border-light); border-top: var(--border-width-medium) solid var(--color-border-light);
padding-top: var(--spacing-md); padding-top: var(--spacing-md);
display: flex;
justify-content: space-between;
align-items: center;
}
.glist-section-title.clickable {
cursor: pointer;
transition: var(--transition-base);
user-select: none;
padding: var(--spacing-md);
border-radius: var(--border-radius-sm);
background: var(--color-bg-surface);
}
.glist-section-title.clickable:hover {
background: var(--color-bg-hover);
color: var(--color-primary);
border-top-color: var(--color-primary);
}
.glist-section-indicator {
font-size: var(--font-size-base);
opacity: 0.7;
margin-left: var(--spacing-sm);
} }
.glist-section-header { .glist-section-header {
@ -79,6 +103,34 @@
background: var(--color-primary-light); background: var(--color-primary-light);
border-left: var(--border-width-thick) solid var(--color-primary); border-left: var(--border-width-thick) solid var(--color-primary);
border-radius: var(--border-radius-sm); border-radius: var(--border-radius-sm);
display: flex;
justify-content: space-between;
align-items: center;
}
.glist-classification-header.clickable {
cursor: pointer;
transition: var(--transition-base);
user-select: none;
}
.glist-classification-header.clickable:hover {
background: var(--color-primary);
color: var(--color-text-inverse);
transform: translateY(-1px);
}
.glist-zone-count {
font-size: var(--font-size-sm);
font-weight: var(--font-weight-normal);
opacity: 0.8;
margin-left: var(--spacing-xs);
}
.glist-zone-indicator {
font-size: var(--font-size-base);
opacity: 0.7;
margin-left: var(--spacing-sm);
} }
/* Inputs */ /* Inputs */
@ -211,11 +263,6 @@
object-fit: cover; object-fit: cover;
} }
.glist-item-image.has-image:hover {
opacity: 0.8;
box-shadow: 0 0 8px var(--color-primary-light);
}
.glist-item-content { .glist-item-content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -258,6 +305,35 @@
font-style: italic; font-style: italic;
} }
/* Compact View */
.glist-ul.compact .glist-li {
padding: var(--spacing-xs);
margin-bottom: var(--spacing-xs);
}
.glist-ul.compact .glist-item-layout {
gap: var(--spacing-xs);
}
.glist-ul.compact .glist-item-image {
width: 40px;
height: 40px;
font-size: var(--font-size-lg);
}
.glist-ul.compact .glist-item-quantity {
font-size: var(--font-size-xs);
padding: 1px 4px;
}
.glist-ul.compact .glist-item-name {
font-size: var(--font-size-sm);
}
.glist-ul.compact .glist-item-users {
font-size: 0.65em;
}
/* Sorting dropdown */ /* Sorting dropdown */
.glist-sort { .glist-sort {
width: 100%; width: 100%;