feat: add items
This commit is contained in:
@@ -0,0 +1,86 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useMemo } from 'react';
|
||||||
|
import shared from '../../styles/shared.module.css';
|
||||||
|
import styles from '../../styles/Items.module.css';
|
||||||
|
import QuantityCard from '../../components/QuantityCard';
|
||||||
|
import { Item, ItemType, ITEM_TYPE_LABELS, getItemType, getItemIconUrl, randInRange } from './types';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
items: Item[];
|
||||||
|
quantities: Record<string, number>;
|
||||||
|
randMin: number;
|
||||||
|
randMax: number;
|
||||||
|
onSetQty: (id: string, qty: number) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ItemGrid({ items, quantities, randMin, randMax, onSetQty }: Props) {
|
||||||
|
const [search, setSearch] = useState('');
|
||||||
|
const [typeFilter, setTypeFilter] = useState<ItemType>('all');
|
||||||
|
|
||||||
|
const filtered = useMemo(() => {
|
||||||
|
return items.filter(item => {
|
||||||
|
if (typeFilter !== 'all' && getItemType(item.id) !== typeFilter) return false;
|
||||||
|
if (search.trim() && !item.name.toLowerCase().includes(search.toLowerCase())) return false;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}, [items, typeFilter, search]);
|
||||||
|
|
||||||
|
const handleGive100Visible = () => filtered.forEach(i => onSetQty(i.id, 100));
|
||||||
|
const handleRandVisible = () => filtered.forEach(i => onSetQty(i.id, randInRange(randMin, randMax)));
|
||||||
|
const handleLockVisible = () => filtered.forEach(i => onSetQty(i.id, 0));
|
||||||
|
|
||||||
|
const activeCount = Object.values(quantities).filter(q => q > 0).length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={shared.toolbar}>
|
||||||
|
<input
|
||||||
|
className={shared.searchInput}
|
||||||
|
type="text"
|
||||||
|
placeholder="Search items..."
|
||||||
|
value={search}
|
||||||
|
onChange={e => setSearch(e.target.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className={shared.roleFilter}>
|
||||||
|
{(Object.keys(ITEM_TYPE_LABELS) as ItemType[]).map(t => (
|
||||||
|
<button
|
||||||
|
key={t}
|
||||||
|
className={`${shared.roleBtn} ${typeFilter === t ? shared.roleBtnActive : ''}`}
|
||||||
|
onClick={() => setTypeFilter(t)}
|
||||||
|
>
|
||||||
|
{ITEM_TYPE_LABELS[t]}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span className={shared.spacer} />
|
||||||
|
<span className={shared.resultCount}>{filtered.length} shown · {activeCount} active</span>
|
||||||
|
|
||||||
|
<button className={shared.unlockAllBtn} onClick={handleGive100Visible}>Set all to 100</button>
|
||||||
|
<button className={styles.randBtn} onClick={handleRandVisible}>Randomize</button>
|
||||||
|
<button className={shared.lockAllBtn} onClick={handleLockVisible}>Remove all visible</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{filtered.length === 0 ? (
|
||||||
|
<div className={shared.empty}>No items match</div>
|
||||||
|
) : (
|
||||||
|
<div className={styles.grid}>
|
||||||
|
{filtered.map(item => (
|
||||||
|
<QuantityCard
|
||||||
|
key={item.id}
|
||||||
|
id={item.id}
|
||||||
|
name={item.name}
|
||||||
|
iconUrl={getItemIconUrl(item.iconFilePath)}
|
||||||
|
qty={quantities[item.id] ?? 0}
|
||||||
|
randMin={randMin}
|
||||||
|
randMax={randMax}
|
||||||
|
onSetQty={onSetQty}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useMemo } from 'react';
|
||||||
|
import shared from '../../styles/shared.module.css';
|
||||||
|
import styles from '../../styles/Items.module.css';
|
||||||
|
import QuantityCard from '../../components/QuantityCard';
|
||||||
|
import { Offering, OfferingRole, getOfferingIconUrl, randInRange } from './types';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
offerings: Offering[];
|
||||||
|
quantities: Record<string, number>;
|
||||||
|
randMin: number;
|
||||||
|
randMax: number;
|
||||||
|
onSetQty: (id: string, qty: number) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ROLE_LABELS: Record<OfferingRole, string> = {
|
||||||
|
all: 'All', shared: 'Shared', killer: 'Killer', survivor: 'Survivor',
|
||||||
|
};
|
||||||
|
|
||||||
|
const roleMatches = (offeringRole: number, filter: OfferingRole) => {
|
||||||
|
if (filter === 'all') return true;
|
||||||
|
if (filter === 'shared') return offeringRole === 0;
|
||||||
|
if (filter === 'killer') return offeringRole === 1;
|
||||||
|
if (filter === 'survivor') return offeringRole === 2;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function OfferingGrid({ offerings, quantities, randMin, randMax, onSetQty }: Props) {
|
||||||
|
const [search, setSearch] = useState('');
|
||||||
|
const [roleFilter, setRoleFilter] = useState<OfferingRole>('all');
|
||||||
|
|
||||||
|
const filtered = useMemo(() => {
|
||||||
|
return offerings.filter(o => {
|
||||||
|
if (!roleMatches(o.role, roleFilter)) return false;
|
||||||
|
if (search.trim() && !o.name.toLowerCase().includes(search.toLowerCase())) return false;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}, [offerings, roleFilter, search]);
|
||||||
|
|
||||||
|
const handleGive100Visible = () => filtered.forEach(o => onSetQty(o.id, 100));
|
||||||
|
const handleRandVisible = () => filtered.forEach(o => onSetQty(o.id, randInRange(randMin, randMax)));
|
||||||
|
const handleLockVisible = () => filtered.forEach(o => onSetQty(o.id, 0));
|
||||||
|
|
||||||
|
const activeCount = Object.values(quantities).filter(q => q > 0).length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={shared.toolbar}>
|
||||||
|
<input
|
||||||
|
className={shared.searchInput}
|
||||||
|
type="text"
|
||||||
|
placeholder="Search offerings..."
|
||||||
|
value={search}
|
||||||
|
onChange={e => setSearch(e.target.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className={shared.roleFilter}>
|
||||||
|
{(Object.keys(ROLE_LABELS) as OfferingRole[]).map(r => (
|
||||||
|
<button
|
||||||
|
key={r}
|
||||||
|
className={`${shared.roleBtn} ${roleFilter === r ? shared.roleBtnActive : ''}`}
|
||||||
|
onClick={() => setRoleFilter(r)}
|
||||||
|
>
|
||||||
|
{ROLE_LABELS[r]}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span className={shared.spacer} />
|
||||||
|
<span className={shared.resultCount}>{filtered.length} shown · {activeCount} active</span>
|
||||||
|
|
||||||
|
<button className={shared.unlockAllBtn} onClick={handleGive100Visible}>Set all to 100</button>
|
||||||
|
<button className={styles.randBtn} onClick={handleRandVisible}>Randomize</button>
|
||||||
|
<button className={shared.lockAllBtn} onClick={handleLockVisible}>Remove all visible</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{filtered.length === 0 ? (
|
||||||
|
<div className={shared.empty}>No offerings match</div>
|
||||||
|
) : (
|
||||||
|
<div className={styles.grid}>
|
||||||
|
{filtered.map(offering => (
|
||||||
|
<QuantityCard
|
||||||
|
key={offering.id}
|
||||||
|
id={offering.id}
|
||||||
|
name={offering.name}
|
||||||
|
iconUrl={getOfferingIconUrl(offering.iconFilePath)}
|
||||||
|
qty={quantities[offering.id] ?? 0}
|
||||||
|
randMin={randMin}
|
||||||
|
randMax={randMax}
|
||||||
|
onSetQty={onSetQty}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { useInventoryStore } from '@/store/useInventoryStore';
|
||||||
|
|
||||||
|
import shared from '../../styles/shared.module.css';
|
||||||
|
import styles from '../../styles/Items.module.css';
|
||||||
|
|
||||||
|
import { Item, Offering } from './types';
|
||||||
|
import ItemGrid from './ItemGrid';
|
||||||
|
import OfferingGrid from './OfferingGrid';
|
||||||
|
|
||||||
|
type Tab = 'items' | 'offerings';
|
||||||
|
|
||||||
|
export default function ItemsPage() {
|
||||||
|
const store = useInventoryStore();
|
||||||
|
|
||||||
|
const [tab, setTab] = useState<Tab>('items');
|
||||||
|
const [items, setItems] = useState<Item[]>([]);
|
||||||
|
const [offerings, setOfferings] = useState<Offering[]>([]);
|
||||||
|
|
||||||
|
const [randMin, setRandMin] = useState(50);
|
||||||
|
const [randMax, setRandMax] = useState(200);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
Promise.all([
|
||||||
|
fetch('/data/items.json').then(r => r.json()).catch(() => []),
|
||||||
|
fetch('/data/offerings.json').then(r => r.json()).catch(() => []),
|
||||||
|
]).then(([i, o]) => {
|
||||||
|
setItems(i);
|
||||||
|
setOfferings(o);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleClearAll = () => {
|
||||||
|
store.clearCategory('items');
|
||||||
|
store.clearCategory('offerings');
|
||||||
|
};
|
||||||
|
|
||||||
|
const activeItems = Object.values(store.items).filter(q => q > 0).length;
|
||||||
|
const activeOfferings = Object.values(store.offerings).filter(q => q > 0).length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={shared.container}>
|
||||||
|
<header className={shared.header}>
|
||||||
|
<div>
|
||||||
|
<h1 className={shared.title}>Items & Offerings</h1>
|
||||||
|
<p className={shared.subtitle}>
|
||||||
|
{activeItems} active items · {activeOfferings} active offerings
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.rangeGroup}>
|
||||||
|
<span className={styles.rangeLabel}>Rand range</span>
|
||||||
|
<input
|
||||||
|
className={styles.rangeInput}
|
||||||
|
type="number"
|
||||||
|
value={randMin}
|
||||||
|
min={0}
|
||||||
|
max={5000}
|
||||||
|
onChange={e => setRandMin(Math.max(0, parseInt(e.target.value) || 0))}
|
||||||
|
/>
|
||||||
|
<span className={styles.rangeSep}>–</span>
|
||||||
|
<input
|
||||||
|
className={styles.rangeInput}
|
||||||
|
type="number"
|
||||||
|
value={randMax}
|
||||||
|
min={0}
|
||||||
|
max={5000}
|
||||||
|
onChange={e => setRandMax(Math.min(5000, parseInt(e.target.value) || 0))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button className={shared.clearBtn} onClick={handleClearAll}>Clear All</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className={styles.tabs}>
|
||||||
|
{(['items', 'offerings'] as Tab[]).map(t => (
|
||||||
|
<button
|
||||||
|
key={t}
|
||||||
|
className={`${styles.tab} ${tab === t ? styles.tabActive : ''}`}
|
||||||
|
onClick={() => setTab(t)}
|
||||||
|
>
|
||||||
|
{t}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{tab === 'items' && (
|
||||||
|
<ItemGrid
|
||||||
|
items={items}
|
||||||
|
quantities={store.items}
|
||||||
|
randMin={randMin}
|
||||||
|
randMax={randMax}
|
||||||
|
onSetQty={store.setItemQuantity}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{tab === 'offerings' && (
|
||||||
|
<OfferingGrid
|
||||||
|
offerings={offerings}
|
||||||
|
quantities={store.offerings}
|
||||||
|
randMin={randMin}
|
||||||
|
randMax={randMax}
|
||||||
|
onSetQty={store.setOfferingQuantity}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
export type Item = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
iconFilePath: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Offering = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
iconFilePath: string;
|
||||||
|
role: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ItemType = 'all' | 'toolbox' | 'flashlight' | 'medkit' | 'key' | 'map' | 'other';
|
||||||
|
export type OfferingRole = 'all' | 'shared' | 'killer' | 'survivor';
|
||||||
|
|
||||||
|
export const ITEM_TYPE_LABELS: Record<ItemType, string> = {
|
||||||
|
all: 'All', toolbox: 'Toolbox', flashlight: 'Flashlight',
|
||||||
|
medkit: 'Med-Kit', key: 'Key', map: 'Map', other: 'Other',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getItemType = (id: string): ItemType => {
|
||||||
|
if (/toolbox/i.test(id)) return 'toolbox';
|
||||||
|
if (/flashlight/i.test(id)) return 'flashlight';
|
||||||
|
if (/medkit|med_?kit/i.test(id)) return 'medkit';
|
||||||
|
if (/key/i.test(id)) return 'key';
|
||||||
|
if (/map/i.test(id)) return 'map';
|
||||||
|
return 'other';
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export const getItemIconUrl = (iconFilePath: string) => {
|
||||||
|
const file = (iconFilePath.split('/').pop() ?? '').split('.')[0];
|
||||||
|
return `/icons/item-icons/${file}.png`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getOfferingIconUrl = (iconFilePath: string) => {
|
||||||
|
const file = (iconFilePath.split('/').pop() ?? '').split('.')[0];
|
||||||
|
return `/icons/offering-icons/${file}.png`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const randInRange = (min: number, max: number) => {
|
||||||
|
const lo = Math.min(min, max);
|
||||||
|
const hi = Math.max(min, max);
|
||||||
|
return Math.floor(Math.random() * (hi - lo + 1)) + lo;
|
||||||
|
};
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import styles from '../styles/QuantityCard.module.css';
|
||||||
|
import { randInRange } from '../app/items/types';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
iconUrl: string;
|
||||||
|
qty: number;
|
||||||
|
randMin: number;
|
||||||
|
randMax: number;
|
||||||
|
onSetQty: (id: string, qty: number) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const clamp = (v: number) => Math.min(32767, Math.max(0, v));
|
||||||
|
|
||||||
|
export default function QuantityCard({ id, name, iconUrl, qty, randMin, randMax, onSetQty }: Props) {
|
||||||
|
const active = qty > 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`${styles.card} ${active ? styles.cardActive : ''}`}>
|
||||||
|
<img className={styles.icon} src={iconUrl} alt={name} loading="lazy" />
|
||||||
|
<span className={styles.name}>{name}</span>
|
||||||
|
|
||||||
|
{active ? (
|
||||||
|
<>
|
||||||
|
<div className={styles.qtyRow}>
|
||||||
|
<button className={styles.qtyBtn} onClick={() => onSetQty(id, clamp(qty - 1))}>−</button>
|
||||||
|
<input
|
||||||
|
className={styles.qtyInput}
|
||||||
|
type="number"
|
||||||
|
value={qty}
|
||||||
|
min={0}
|
||||||
|
max={5000}
|
||||||
|
onChange={e => {
|
||||||
|
const v = parseInt(e.target.value);
|
||||||
|
if (!isNaN(v)) onSetQty(id, clamp(v));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button className={styles.qtyBtn} onClick={() => onSetQty(id, clamp(qty + 1))}>+</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.quickRow}>
|
||||||
|
<button className={styles.quickBtn} onClick={() => onSetQty(id, 100)}>100</button>
|
||||||
|
<button
|
||||||
|
className={`${styles.quickBtn} ${styles.quickBtnRand}`}
|
||||||
|
onClick={() => onSetQty(id, randInRange(randMin, randMax))}
|
||||||
|
>
|
||||||
|
rand
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={`${styles.quickBtn} ${styles.quickBtnRemove}`}
|
||||||
|
onClick={() => onSetQty(id, 0)}
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<button className={styles.addBtn} onClick={() => onSetQty(id, 100)}>+ Add</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
/*
|
||||||
|
tabs
|
||||||
|
*/
|
||||||
|
.tabs {
|
||||||
|
display: flex;
|
||||||
|
border-bottom: 1px solid #1a1a1a;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
margin-bottom: -1px;
|
||||||
|
color: #444444;
|
||||||
|
padding: 0.7rem 1.4rem;
|
||||||
|
font-family: 'Oswald', sans-serif;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab:hover { color: #888888; }
|
||||||
|
|
||||||
|
.tabActive {
|
||||||
|
color: #ffffff;
|
||||||
|
border-bottom-color: #a30000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
range ctl
|
||||||
|
*/
|
||||||
|
|
||||||
|
.rangeGroup {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.5rem 0.85rem;
|
||||||
|
background: #0d0d0d;
|
||||||
|
border: 1px solid #1a1a1a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rangeLabel {
|
||||||
|
font-family: 'Oswald', sans-serif;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
color: #444444;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rangeInput {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 1px solid #2a2a2a;
|
||||||
|
color: #5599ff;
|
||||||
|
font-family: 'Oswald', sans-serif;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
width: 48px;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0.1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rangeInput::-webkit-outer-spin-button,
|
||||||
|
.rangeInput::-webkit-inner-spin-button {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rangeInput:focus {
|
||||||
|
outline: none;
|
||||||
|
border-bottom-color: #5599ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rangeSep {
|
||||||
|
color: #333333;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
randomization btn
|
||||||
|
*/
|
||||||
|
.randBtn {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid #1a3a88;
|
||||||
|
color: #3366cc;
|
||||||
|
padding: 0.6rem 1.25rem;
|
||||||
|
font-family: 'Oswald', sans-serif;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.randBtn:hover {
|
||||||
|
background: #0a1a44;
|
||||||
|
color: #88aaff;
|
||||||
|
border-color: #4466cc;
|
||||||
|
box-shadow: 0 0 12px rgba(50, 80, 200, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
grid
|
||||||
|
*/
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
|
||||||
|
gap: 0.75rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
flex: 1;
|
||||||
|
align-content: start;
|
||||||
|
padding-right: 0.5rem;
|
||||||
|
}
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
/*
|
||||||
|
cards
|
||||||
|
*/
|
||||||
|
.card {
|
||||||
|
background: #0d0d0d;
|
||||||
|
border: 1px solid #1a1a1a;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1rem 0.6rem 0.75rem;
|
||||||
|
gap: 0.5rem;
|
||||||
|
transition: border-color 0.15s ease, background 0.15s ease;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardActive {
|
||||||
|
border-color: #a30000;
|
||||||
|
background: #0d0000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
width: 52px;
|
||||||
|
height: 52px;
|
||||||
|
object-fit: contain;
|
||||||
|
filter: grayscale(1) brightness(0.3);
|
||||||
|
transition: filter 0.15s ease;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardActive .icon {
|
||||||
|
filter: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
font-size: 0.62rem;
|
||||||
|
text-align: center;
|
||||||
|
color: #3a3a3a;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
line-height: 1.3;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardActive .name {
|
||||||
|
color: #c9c9c9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
quantity rows
|
||||||
|
*/
|
||||||
|
.qtyRow {
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid #2a2a2a;
|
||||||
|
background: #060606;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qtyBtn {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: #555555;
|
||||||
|
width: 22px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1;
|
||||||
|
transition: all 0.1s ease;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qtyBtn:hover {
|
||||||
|
color: #ffffff;
|
||||||
|
background: #1a0000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qtyInput {
|
||||||
|
flex: 1;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: #a30000;
|
||||||
|
font-family: 'Oswald', sans-serif;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
text-align: center;
|
||||||
|
width: 0;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 0.3rem 0;
|
||||||
|
}
|
||||||
|
.qtyInput::-webkit-outer-spin-button,
|
||||||
|
.qtyInput::-webkit-inner-spin-button {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qtyInput:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
quick actions
|
||||||
|
*/
|
||||||
|
.quickRow {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.3rem;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quickBtn {
|
||||||
|
flex: 1;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid #1e1e1e;
|
||||||
|
color: #3a3a3a;
|
||||||
|
padding: 0.25rem 0;
|
||||||
|
font-family: 'Oswald', sans-serif;
|
||||||
|
font-size: 0.65rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.1s ease;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quickBtn:hover {
|
||||||
|
color: #c9c9c9;
|
||||||
|
border-color: #333333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quickBtnRand:hover {
|
||||||
|
color: #5599ff;
|
||||||
|
border-color: #1a3a88;
|
||||||
|
background: #05060d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quickBtnRemove:hover {
|
||||||
|
color: #ff4444;
|
||||||
|
border-color: #661111;
|
||||||
|
background: #0d0505;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
buttons
|
||||||
|
*/
|
||||||
|
.addBtn {
|
||||||
|
width: 100%;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid #1a1a1a;
|
||||||
|
color: #2e2e2e;
|
||||||
|
padding: 0.45rem;
|
||||||
|
font-family: 'Oswald', sans-serif;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.addBtn:hover {
|
||||||
|
color: #c9c9c9;
|
||||||
|
border-color: #4a0000;
|
||||||
|
background: #0d0000;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user