frontend refresh and update backend for frontend's new behavior
This commit is contained in:
parent
4139a07cd2
commit
68976a7683
@ -32,7 +32,8 @@ exports.addItem = async (req, res) => {
|
||||
|
||||
exports.markBought = async (req, res) => {
|
||||
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" });
|
||||
};
|
||||
|
||||
|
||||
@ -34,7 +34,30 @@ exports.getUnboughtItems = async () => {
|
||||
|
||||
exports.getItemByName = async (itemName) => {
|
||||
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]
|
||||
);
|
||||
|
||||
@ -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(
|
||||
"UPDATE grocery_list SET bought = TRUE, modified_on = NOW() WHERE id = $1",
|
||||
[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) => {
|
||||
const result = await pool.query(
|
||||
`SELECT DISTINCT item_name
|
||||
`SELECT DISTINCT LOWER(item_name) as item_name
|
||||
FROM grocery_list
|
||||
WHERE item_name ILIKE $1
|
||||
LIMIT 10`,
|
||||
|
||||
@ -27,7 +27,7 @@ export const updateItemWithClassification = (id, itemName, quantity, classificat
|
||||
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 getRecentlyBought = () => api.get("/list/recently-bought");
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@ import { memo, useRef, useState } from "react";
|
||||
import AddImageModal from "../modals/AddImageModal";
|
||||
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 [showConfirmBuyModal, setShowConfirmBuyModal] = useState(false);
|
||||
const [currentItem, setCurrentItem] = useState(item);
|
||||
@ -120,7 +120,7 @@ function GroceryListItem({ item, onClick, onImageAdded, onLongPress, allItems =
|
||||
return (
|
||||
<>
|
||||
<li
|
||||
className="glist-li"
|
||||
className={`glist-li ${compact ? 'compact' : ''}`}
|
||||
onClick={handleItemClick}
|
||||
onTouchStart={handleTouchStart}
|
||||
onTouchMove={handleTouchMove}
|
||||
|
||||
47
frontend/src/components/modals/ConfirmAddExistingModal.jsx
Normal file
47
frontend/src/components/modals/ConfirmAddExistingModal.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import "../../styles/ConfirmBuyModal.css";
|
||||
|
||||
export default function ConfirmBuyModal({
|
||||
@ -11,6 +11,11 @@ export default function ConfirmBuyModal({
|
||||
const [quantity, setQuantity] = useState(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
|
||||
const currentIndex = allItems.findIndex(i => i.id === item.id);
|
||||
const hasPrev = currentIndex > 0;
|
||||
|
||||
@ -15,6 +15,7 @@ import SortDropdown from "../components/common/SortDropdown";
|
||||
import AddItemForm from "../components/forms/AddItemForm";
|
||||
import GroceryListItem from "../components/items/GroceryListItem";
|
||||
import AddItemWithDetailsModal from "../components/modals/AddItemWithDetailsModal";
|
||||
import ConfirmAddExistingModal from "../components/modals/ConfirmAddExistingModal";
|
||||
import EditItemModal from "../components/modals/EditItemModal";
|
||||
import SimilarItemModal from "../components/modals/SimilarItemModal";
|
||||
import { ZONE_FLOW } from "../constants/classifications";
|
||||
@ -45,6 +46,9 @@ export default function GroceryList() {
|
||||
const [showEditModal, setShowEditModal] = useState(false);
|
||||
const [editingItem, setEditingItem] = useState(null);
|
||||
const [recentlyBoughtCollapsed, setRecentlyBoughtCollapsed] = useState(settings.recentlyBoughtCollapsed);
|
||||
const [collapsedZones, setCollapsedZones] = useState({});
|
||||
const [showConfirmAddExisting, setShowConfirmAddExisting] = useState(false);
|
||||
const [confirmAddExistingData, setConfirmAddExistingData] = useState(null);
|
||||
|
||||
|
||||
// === 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 ===
|
||||
const sortedItems = useMemo(() => {
|
||||
const sorted = [...items];
|
||||
@ -125,17 +137,19 @@ export default function GroceryList() {
|
||||
return;
|
||||
}
|
||||
|
||||
const allItems = [...items, ...recentlyBoughtItems];
|
||||
const lowerText = text.toLowerCase().trim();
|
||||
const exactMatch = allItems.find(item => item.item_name.toLowerCase() === lowerText);
|
||||
|
||||
setButtonText(exactMatch ? "Add" : "Create + Add");
|
||||
|
||||
try {
|
||||
const suggestions = await getSuggestions(text);
|
||||
setSuggestions(suggestions.data.map(s => s.item_name));
|
||||
const response = await getSuggestions(text);
|
||||
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 {
|
||||
setSuggestions([]);
|
||||
setButtonText("Create + Add");
|
||||
}
|
||||
};
|
||||
|
||||
@ -184,31 +198,24 @@ export default function GroceryList() {
|
||||
if (existingItem?.bought === false) {
|
||||
const currentQuantity = existingItem.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);
|
||||
setSuggestions([]);
|
||||
setButtonText("Add Item");
|
||||
|
||||
if (response.data) {
|
||||
setItems(prevItems =>
|
||||
prevItems.map(item =>
|
||||
item.id === existingItem.id ? { ...item, ...response.data } : item
|
||||
)
|
||||
);
|
||||
}
|
||||
// Show modal instead of window.confirm
|
||||
setConfirmAddExistingData({
|
||||
itemName,
|
||||
currentQuantity,
|
||||
addingQuantity: quantity,
|
||||
newQuantity,
|
||||
existingItem
|
||||
});
|
||||
setShowConfirmAddExisting(true);
|
||||
} else if (existingItem) {
|
||||
const response = await addItem(itemName, quantity, null);
|
||||
await addItem(itemName, quantity, null);
|
||||
setSuggestions([]);
|
||||
setButtonText("Add Item");
|
||||
|
||||
if (response.data) {
|
||||
setItems(prevItems => [...prevItems, response.data]);
|
||||
setRecentlyBoughtItems(prevItems => prevItems.filter(item => item.id !== existingItem.id));
|
||||
}
|
||||
// Reload lists to reflect the changes
|
||||
await loadItems();
|
||||
await loadRecentlyBought();
|
||||
} else {
|
||||
setPendingItem({ itemName, quantity });
|
||||
setShowAddDetailsModal(true);
|
||||
@ -239,6 +246,45 @@ export default function GroceryList() {
|
||||
}, [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 ===
|
||||
const handleAddDetailsConfirm = useCallback(async (imageFile, classification) => {
|
||||
if (!pendingItem) return;
|
||||
@ -300,10 +346,26 @@ export default function GroceryList() {
|
||||
|
||||
// === Item Action Handlers ===
|
||||
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));
|
||||
} 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();
|
||||
}, []);
|
||||
}, [items]);
|
||||
|
||||
|
||||
const handleImageAdded = useCallback(async (id, itemName, quantity, imageFile) => {
|
||||
@ -415,17 +477,31 @@ export default function GroceryList() {
|
||||
{sortMode === "zone" ? (
|
||||
(() => {
|
||||
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">
|
||||
<h3 className="glist-classification-header">
|
||||
<h3
|
||||
className="glist-classification-header clickable"
|
||||
onClick={() => toggleZoneCollapse(zone)}
|
||||
>
|
||||
<span>
|
||||
{zone === 'unclassified' ? 'Unclassified' : zone}
|
||||
<span className="glist-zone-count"> ({itemCount})</span>
|
||||
</span>
|
||||
<span className="glist-zone-indicator">
|
||||
{isCollapsed ? "▼" : "▲"}
|
||||
</span>
|
||||
</h3>
|
||||
<ul className="glist-ul">
|
||||
{!isCollapsed && (
|
||||
<ul className={`glist-ul ${settings.compactView ? 'compact' : ''}`}>
|
||||
{grouped[zone].map((item) => (
|
||||
<GroceryListItem
|
||||
key={item.id}
|
||||
item={item}
|
||||
allItems={sortedItems}
|
||||
compact={settings.compactView}
|
||||
onClick={(id, quantity) =>
|
||||
[ROLES.ADMIN, ROLES.EDITOR].includes(role) && handleBought(id, quantity)
|
||||
}
|
||||
@ -438,16 +514,19 @@ export default function GroceryList() {
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
));
|
||||
);
|
||||
});
|
||||
})()
|
||||
) : (
|
||||
<ul className="glist-ul">
|
||||
<ul className={`glist-ul ${settings.compactView ? 'compact' : ''}`}>
|
||||
{sortedItems.map((item) => (
|
||||
<GroceryListItem
|
||||
key={item.id}
|
||||
item={item}
|
||||
allItems={sortedItems}
|
||||
compact={settings.compactView}
|
||||
onClick={(id, quantity) =>
|
||||
[ROLES.ADMIN, ROLES.EDITOR].includes(role) && handleBought(id, quantity)
|
||||
}
|
||||
@ -464,24 +543,25 @@ export default function GroceryList() {
|
||||
|
||||
{recentlyBoughtItems.length > 0 && settings.showRecentlyBought && (
|
||||
<>
|
||||
<div className="glist-section-header">
|
||||
<h2 className="glist-section-title">Recently Bought (24HR)</h2>
|
||||
<button
|
||||
className="glist-collapse-btn"
|
||||
<h2
|
||||
className="glist-section-title clickable"
|
||||
onClick={() => setRecentlyBoughtCollapsed(!recentlyBoughtCollapsed)}
|
||||
>
|
||||
{recentlyBoughtCollapsed ? "▼ Show" : "▲ Hide"}
|
||||
</button>
|
||||
</div>
|
||||
<span>Recently Bought (24HR)</span>
|
||||
<span className="glist-section-indicator">
|
||||
{recentlyBoughtCollapsed ? "▼" : "▲"}
|
||||
</span>
|
||||
</h2>
|
||||
|
||||
{!recentlyBoughtCollapsed && (
|
||||
<>
|
||||
<ul className="glist-ul">
|
||||
<ul className={`glist-ul ${settings.compactView ? 'compact' : ''}`}>
|
||||
{recentlyBoughtItems.slice(0, recentlyBoughtDisplayCount).map((item) => (
|
||||
<GroceryListItem
|
||||
key={item.id}
|
||||
item={item}
|
||||
allItems={recentlyBoughtItems}
|
||||
compact={settings.compactView}
|
||||
onClick={null}
|
||||
onImageAdded={
|
||||
[ROLES.ADMIN, ROLES.EDITOR].includes(role) ? handleImageAdded : null
|
||||
@ -542,6 +622,16 @@ export default function GroceryList() {
|
||||
onImageUpdate={handleImageAdded}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showConfirmAddExisting && confirmAddExistingData && (
|
||||
<ConfirmAddExistingModal
|
||||
itemName={confirmAddExistingData.itemName}
|
||||
currentQuantity={confirmAddExistingData.currentQuantity}
|
||||
addingQuantity={confirmAddExistingData.addingQuantity}
|
||||
onConfirm={handleConfirmAddExisting}
|
||||
onCancel={handleCancelAddExisting}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
144
frontend/src/styles/components/ConfirmAddExistingModal.css
Normal file
144
frontend/src/styles/components/ConfirmAddExistingModal.css
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -29,6 +29,30 @@
|
||||
color: var(--color-gray-700);
|
||||
border-top: var(--border-width-medium) solid var(--color-border-light);
|
||||
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 {
|
||||
@ -79,6 +103,34 @@
|
||||
background: var(--color-primary-light);
|
||||
border-left: var(--border-width-thick) solid var(--color-primary);
|
||||
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 */
|
||||
@ -211,11 +263,6 @@
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.glist-item-image.has-image:hover {
|
||||
opacity: 0.8;
|
||||
box-shadow: 0 0 8px var(--color-primary-light);
|
||||
}
|
||||
|
||||
.glist-item-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -258,6 +305,35 @@
|
||||
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 */
|
||||
.glist-sort {
|
||||
width: 100%;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user