feat: add Addons
This commit is contained in:
@@ -0,0 +1,81 @@
|
|||||||
|
'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 { Addon, getAddonIconUrl, randInRange } from './types';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
addons: Addon[];
|
||||||
|
quantities: Record<string, number>;
|
||||||
|
randMin: number;
|
||||||
|
randMax: number;
|
||||||
|
onSetQty: (id: string, qty: number) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function AddonGrid({ addons, quantities, randMin, randMax, onSetQty }: Props) {
|
||||||
|
const [search, setSearch] = useState('');
|
||||||
|
const [roleFilter, setRoleFilter] = useState<'all' | 'killer' | 'survivor'>('all');
|
||||||
|
|
||||||
|
const filtered = useMemo(() => {
|
||||||
|
return addons.filter(addon => {
|
||||||
|
if (roleFilter === 'killer' && addon.role !== 1) return false;
|
||||||
|
if (roleFilter === 'survivor' && addon.role !== 2) return false;
|
||||||
|
if (search.trim() && !addon.name.toLowerCase().includes(search.toLowerCase())) return false;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}, [addons, search, roleFilter]);
|
||||||
|
|
||||||
|
const handleGive100Visible = () => filtered.forEach(a => onSetQty(a.id, 100));
|
||||||
|
const handleRandVisible = () => filtered.forEach(a => onSetQty(a.id, randInRange(randMin, randMax)));
|
||||||
|
const handleLockVisible = () => filtered.forEach(a => onSetQty(a.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 addons..."
|
||||||
|
value={search}
|
||||||
|
onChange={e => setSearch(e.target.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className={shared.roleFilter}>
|
||||||
|
<button className={`${shared.roleBtn} ${roleFilter === 'all' ? shared.roleBtnActive : ''}`} onClick={() => setRoleFilter('all')}>All</button>
|
||||||
|
<button className={`${shared.roleBtn} ${roleFilter === 'survivor' ? shared.roleBtnActive : ''}`} onClick={() => setRoleFilter('survivor')}>Survivor</button>
|
||||||
|
<button className={`${shared.roleBtn} ${roleFilter === 'killer' ? shared.roleBtnActive : ''}`} onClick={() => setRoleFilter('killer')}>Killer</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span className={shared.spacer} />
|
||||||
|
<span className={shared.resultCount}>{filtered.length} shown · {activeCount} active out of {addons.length} total</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 addons match</div>
|
||||||
|
) : (
|
||||||
|
<div className={styles.grid}>
|
||||||
|
{filtered.map(addon => (
|
||||||
|
<QuantityCard
|
||||||
|
key={addon.id}
|
||||||
|
id={addon.id}
|
||||||
|
name={addon.name}
|
||||||
|
iconUrl={getAddonIconUrl(addon.iconFilePath)}
|
||||||
|
qty={quantities[addon.id] ?? 0}
|
||||||
|
randMin={randMin}
|
||||||
|
randMax={randMax}
|
||||||
|
onSetQty={onSetQty}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
+22
-7
@@ -6,12 +6,13 @@ import { useInventoryStore } from '@/store/useInventoryStore';
|
|||||||
import shared from '../../styles/shared.module.css';
|
import shared from '../../styles/shared.module.css';
|
||||||
import styles from '../../styles/Items.module.css';
|
import styles from '../../styles/Items.module.css';
|
||||||
|
|
||||||
import { Item, Offering } from './types';
|
import { Item, Offering, Addon } from './types';
|
||||||
import { fetchItems, fetchOfferings } from '../../lib/db';
|
import { fetchItems, fetchOfferings, fetchAddons } from '../../lib/db';
|
||||||
import ItemGrid from './ItemGrid';
|
import ItemGrid from './ItemGrid';
|
||||||
import OfferingGrid from './OfferingGrid';
|
import OfferingGrid from './OfferingGrid';
|
||||||
|
import AddonGrid from './AddonGrid';
|
||||||
|
|
||||||
type Tab = 'items' | 'offerings';
|
type Tab = 'items' | 'offerings' | 'addons';
|
||||||
|
|
||||||
export default function ItemsPage() {
|
export default function ItemsPage() {
|
||||||
const store = useInventoryStore();
|
const store = useInventoryStore();
|
||||||
@@ -19,25 +20,29 @@ export default function ItemsPage() {
|
|||||||
const [tab, setTab] = useState<Tab>('items');
|
const [tab, setTab] = useState<Tab>('items');
|
||||||
const [items, setItems] = useState<Item[]>([]);
|
const [items, setItems] = useState<Item[]>([]);
|
||||||
const [offerings, setOfferings] = useState<Offering[]>([]);
|
const [offerings, setOfferings] = useState<Offering[]>([]);
|
||||||
|
const [addons, setAddons] = useState<Addon[]>([]);
|
||||||
|
|
||||||
const [randMin, setRandMin] = useState(50);
|
const [randMin, setRandMin] = useState(50);
|
||||||
const [randMax, setRandMax] = useState(200);
|
const [randMax, setRandMax] = useState(200);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Promise.all([fetchItems(), fetchOfferings()])
|
Promise.all([fetchItems(), fetchOfferings(), fetchAddons()])
|
||||||
.then(([i, o]) => {
|
.then(([i, o, a]) => {
|
||||||
setItems(i);
|
setItems(i);
|
||||||
setOfferings(o);
|
setOfferings(o);
|
||||||
|
setAddons(a);
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleClearAll = () => {
|
const handleClearAll = () => {
|
||||||
store.clearCategory('items');
|
store.clearCategory('items');
|
||||||
store.clearCategory('offerings');
|
store.clearCategory('offerings');
|
||||||
|
store.clearCategory('addons');
|
||||||
};
|
};
|
||||||
|
|
||||||
const activeItems = Object.values(store.items).filter(q => q > 0).length;
|
const activeItems = Object.values(store.items).filter(q => q > 0).length;
|
||||||
const activeOfferings = Object.values(store.offerings).filter(q => q > 0).length;
|
const activeOfferings = Object.values(store.offerings).filter(q => q > 0).length;
|
||||||
|
const activeAddons = Object.values(store.addons).filter(q => q > 0).length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={shared.container}>
|
<div className={shared.container}>
|
||||||
@@ -45,7 +50,7 @@ export default function ItemsPage() {
|
|||||||
<div>
|
<div>
|
||||||
<h1 className={shared.title}>Items & Offerings</h1>
|
<h1 className={shared.title}>Items & Offerings</h1>
|
||||||
<p className={shared.subtitle}>
|
<p className={shared.subtitle}>
|
||||||
{activeItems} active items · {activeOfferings} active offerings
|
{activeItems} items · {activeOfferings} offerings · {activeAddons} addons
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -74,7 +79,7 @@ export default function ItemsPage() {
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className={styles.tabs}>
|
<div className={styles.tabs}>
|
||||||
{(['items', 'offerings'] as Tab[]).map(t => (
|
{(['items', 'offerings', 'addons'] as Tab[]).map(t => (
|
||||||
<button
|
<button
|
||||||
key={t}
|
key={t}
|
||||||
className={`${styles.tab} ${tab === t ? styles.tabActive : ''}`}
|
className={`${styles.tab} ${tab === t ? styles.tabActive : ''}`}
|
||||||
@@ -104,6 +109,16 @@ export default function ItemsPage() {
|
|||||||
onSetQty={store.setOfferingQuantity}
|
onSetQty={store.setOfferingQuantity}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{tab === 'addons' && (
|
||||||
|
<AddonGrid
|
||||||
|
addons={addons}
|
||||||
|
quantities={store.addons}
|
||||||
|
randMin={randMin}
|
||||||
|
randMax={randMax}
|
||||||
|
onSetQty={store.setAddonQuantity}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -13,6 +13,13 @@ export type Offering = {
|
|||||||
role: number;
|
role: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type Addon = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
iconFilePath: string;
|
||||||
|
role: number;
|
||||||
|
};
|
||||||
|
|
||||||
export type ItemType = 'all' | 'toolbox' | 'flashlight' | 'medkit' | 'key' | 'map' | 'other';
|
export type ItemType = 'all' | 'toolbox' | 'flashlight' | 'medkit' | 'key' | 'map' | 'other';
|
||||||
export type OfferingRole = 'all' | 'shared' | 'killer' | 'survivor';
|
export type OfferingRole = 'all' | 'shared' | 'killer' | 'survivor';
|
||||||
|
|
||||||
@@ -39,3 +46,14 @@ export const getOfferingIconUrl = (iconFilePath: string) => {
|
|||||||
const file = (iconFilePath.split('/').pop() ?? '').split('.')[0];
|
const file = (iconFilePath.split('/').pop() ?? '').split('.')[0];
|
||||||
return `${DB_BASE_URL}/icons/offering-icons/${file}.png`;
|
return `${DB_BASE_URL}/icons/offering-icons/${file}.png`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getAddonIconUrl = (iconFilePath: string) => {
|
||||||
|
const file = (iconFilePath.split('/').pop() ?? '').split('.')[0];
|
||||||
|
return `${DB_BASE_URL}/icons/addon-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;
|
||||||
|
};
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export default function Sidebar() {
|
|||||||
<Link href="/" className={styles.navLink}>Dashboard</Link>
|
<Link href="/" className={styles.navLink}>Dashboard</Link>
|
||||||
<Link href="/characters" className={styles.navLink}>Characters</Link>
|
<Link href="/characters" className={styles.navLink}>Characters</Link>
|
||||||
<Link href="/customizations" className={styles.navLink}>Customizations</Link>
|
<Link href="/customizations" className={styles.navLink}>Customizations</Link>
|
||||||
<Link href="/items" className={styles.navLink}>Items</Link>
|
<Link href="/items" className={styles.navLink}>Items, offerings & addons</Link>
|
||||||
<Link href="/dlcs" className={styles.navLink}>DLCs</Link>
|
<Link href="/dlcs" className={styles.navLink}>DLCs</Link>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user