Files
2026-06-20 10:11:49 -03:00

288 lines
7.6 KiB
TypeScript

'use client';
import { useState, useEffect, useMemo } from 'react';
import { useInventoryStore } from '@/store/useInventoryStore';
import shared from '../../styles/shared.module.css';
import styles from '../../styles/Customizations.module.css';
import { getFileName, cleanFolderName, isKiller } from '../../lib/utils';
import { fetchCharacters, fetchCustomizations } from '../../lib/db';
import {
Character,
CustomizationItem,
RoleFilter,
Tab,
TAB_CATEGORIES,
CATEGORY_ORDER
} from './types';
import CharacterPicker from './CharacterPicker';
import CharacterCosmetics from './CharacterCosmetics';
import FlatCategory from './FlatCategory';
/*
constants
*/
const PAGE_SIZE = 60;
export default function CustomizationsPage() {
const store = useInventoryStore();
const [allItems, setAllItems] = useState<CustomizationItem[]>([]);
const [characters, setCharacters] = useState<Character[]>([]);
const [tab, setTab] = useState<Tab>('cosmetics');
const [selectedChar, setSelectedChar] = useState<number | null>(null);
const [charSearch, setCharSearch] = useState('');
const [charRole, setCharRole] = useState<RoleFilter>('all');
const [search, setSearch] = useState('');
const [page, setPage] = useState(1);
useEffect(() => {
Promise.all([fetchCustomizations(), fetchCharacters()]).then(
([items, chars]) => {
setAllItems(items);
setCharacters(chars);
}
);
}, []);
useEffect(() => {
setPage(1);
}, [tab, search]);
/*
derived data
*/
const characterMap = useMemo(() => {
const m = new Map<number, string>();
characters.forEach((c) => m.set(c.idx, c.name));
return m;
}, [characters]);
const unlockedSet = useMemo(
() => new Set(store.unlockedCustomizations),
[store.unlockedCustomizations]
);
const itemsByCharacter = useMemo(() => {
const map = new Map<number, CustomizationItem[]>();
allItems.forEach((item) => {
if (
item.category >= 1 &&
item.category <= 7 &&
item.associatedCharacter > -1
) {
if (!map.has(item.associatedCharacter))
map.set(item.associatedCharacter, []);
map.get(item.associatedCharacter)!.push(item);
}
});
return map;
}, [allItems]);
const charFullyUnlocked = useMemo(() => {
const result = new Map<number, boolean>();
characters.forEach((char) => {
const items = itemsByCharacter.get(char.idx) ?? [];
result.set(
char.idx,
items.length > 0 && items.every((i) => unlockedSet.has(i.id))
);
});
return result;
}, [characters, itemsByCharacter, unlockedSet]);
const filteredChars = useMemo(() => {
return characters.filter((c) => {
if (charRole === 'survivors' && isKiller(c.idx)) return false;
if (charRole === 'killers' && !isKiller(c.idx)) return false;
if (
charSearch.trim() &&
!c.name.toLowerCase().includes(charSearch.toLowerCase())
)
return false;
return true;
});
}, [characters, charRole, charSearch]);
const flatItems = useMemo(() => {
if (tab === 'cosmetics') return [];
const cat = TAB_CATEGORIES[tab];
if (cat === undefined) return [];
return allItems.filter((item) => {
if (item.category !== cat) return false;
if (
search.trim() &&
!item.name.toLowerCase().includes(search.toLowerCase())
)
return false;
return true;
});
}, [allItems, tab, search]);
const totalPages = Math.max(1, Math.ceil(flatItems.length / PAGE_SIZE));
const pagedItems = useMemo(
() => flatItems.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE),
[flatItems, page]
);
const charCosmetics = useMemo(() => {
if (selectedChar === null) return {};
const items = itemsByCharacter.get(selectedChar) ?? [];
const groups: Record<number, CustomizationItem[]> = {};
CATEGORY_ORDER.forEach((cat) => {
const group = items.filter((i) => i.category === cat);
if (group.length > 0) groups[cat] = group;
});
return groups;
}, [itemsByCharacter, selectedChar]);
/*
lock/unlock helpers
*/
const mergeUnlock = (ids: string[]) => {
const merged = Array.from(
new Set([...store.unlockedCustomizations, ...ids])
);
store.unlockAllInCategory('customizations', merged);
};
const removeLock = (ids: string[]) => {
const toRemove = new Set(ids);
const remaining = store.unlockedCustomizations.filter(
(id) => !toRemove.has(id)
);
store.unlockAllInCategory('customizations', remaining);
};
/*
handlers
*/
const handleToggle = (id: string) => store.toggleItem(id, 'customizations');
const handleUnlockShownCosmetics = () => {
const charSet = new Set(filteredChars.map((c) => c.idx));
mergeUnlock(
allItems
.filter(
(i) =>
charSet.has(i.associatedCharacter) &&
i.category >= 1 &&
i.category <= 7
)
.map((i) => i.id)
);
};
const handleLockShownCosmetics = () => {
const charSet = new Set(filteredChars.map((c) => c.idx));
removeLock(
allItems
.filter(
(i) =>
charSet.has(i.associatedCharacter) &&
i.category >= 1 &&
i.category <= 7
)
.map((i) => i.id)
);
};
const handleUnlockCharCosmetics = () =>
mergeUnlock(
Object.values(charCosmetics)
.flat()
.map((i) => i.id)
);
const handleLockCharCosmetics = () =>
removeLock(
Object.values(charCosmetics)
.flat()
.map((i) => i.id)
);
const handleUnlockAll = () => mergeUnlock(flatItems.map((i) => i.id));
const handleLockAll = () => removeLock(flatItems.map((i) => i.id));
const handleUnlockPage = () => mergeUnlock(pagedItems.map((i) => i.id));
const handleLockPage = () => removeLock(pagedItems.map((i) => i.id));
return (
<div className={shared.container}>
<header className={shared.header}>
<div>
<h1 className={shared.title}>Customizations</h1>
<p className={shared.subtitle}>
{store.unlockedCustomizations.length} of {allItems.length || '-'}{' '}
unlocked
</p>
</div>
<button
className={shared.clearBtn}
onClick={() => store.clearCategory('customizations')}
>
Clear All
</button>
</header>
<div className={styles.tabs}>
{(['cosmetics', 'charms', 'badges', 'banners'] as Tab[]).map((t) => (
<button
key={t}
className={`${styles.tab} ${tab === t ? styles.tabActive : ''}`}
onClick={() => {
setTab(t);
setSelectedChar(null);
}}
>
{t === 'cosmetics' ? 'Character Cosmetics' : t}
</button>
))}
</div>
{tab === 'cosmetics' && selectedChar === null && (
<CharacterPicker
filteredChars={filteredChars}
charSearch={charSearch}
charRole={charRole}
charFullyUnlocked={charFullyUnlocked}
onSelect={setSelectedChar}
onSearchChange={setCharSearch}
onRoleChange={setCharRole}
onUnlockShownCosmetics={handleUnlockShownCosmetics}
onLockShownCosmetics={handleLockShownCosmetics}
/>
)}
{tab === 'cosmetics' && selectedChar !== null && (
<CharacterCosmetics
charName={characterMap.get(selectedChar) ?? selectedChar.toString()}
charCosmetics={charCosmetics}
unlockedSet={unlockedSet}
characterMap={characterMap}
onBack={() => setSelectedChar(null)}
onUnlockAll={handleUnlockCharCosmetics}
onLockAll={handleLockCharCosmetics}
onToggle={handleToggle}
/>
)}
{tab !== 'cosmetics' && (
<FlatCategory
tab={tab}
allFilteredItems={flatItems}
pagedItems={pagedItems}
page={page}
totalPages={totalPages}
search={search}
unlockedSet={unlockedSet}
characterMap={characterMap}
onSearchChange={setSearch}
onUnlockAll={handleUnlockAll}
onLockAll={handleLockAll}
onUnlockPage={handleUnlockPage}
onLockPage={handleLockPage}
onToggle={handleToggle}
onPageChange={setPage}
/>
)}
</div>
);
}