diff --git a/app/characters/page.tsx b/app/characters/page.tsx index c29a2c7..19b8544 100644 --- a/app/characters/page.tsx +++ b/app/characters/page.tsx @@ -10,133 +10,144 @@ import shared from '../../styles/shared.module.css'; import styles from '../../styles/Characters.module.css'; type Character = { - idx: number; - name: string; - iconFilePath: string; + idx: number; + name: string; + iconFilePath: string; }; const getIconUrl = (iconFilePath: string) => { - const fileName = iconFilePath.split('/').pop()?.split('.')[0]; - return `${DB_BASE_URL}/icons/character-icons/${fileName}.png`; + const fileName = iconFilePath.split('/').pop()?.split('.')[0]; + return `${DB_BASE_URL}/icons/character-icons/${fileName}.png`; }; type RoleFilter = 'all' | 'survivors' | 'killers'; export default function CharactersPage() { - const store = useInventoryStore(); + const store = useInventoryStore(); - const [characters, setCharacters] = useState([]); - const [search, setSearch] = useState(''); - const [role, setRole] = useState('all'); + const [characters, setCharacters] = useState([]); + const [search, setSearch] = useState(''); + const [role, setRole] = useState('all'); - useEffect(() => { - fetchCharacters().then(setCharacters); - }, []); + useEffect(() => { + fetchCharacters().then(setCharacters); + }, []); - const filtered = useMemo(() => { - return characters.filter(c => { - if (role === 'survivors' && isKiller(c.idx)) return false; - if (role === 'killers' && !isKiller(c.idx)) return false; - if (search.trim() && !c.name.toLowerCase().includes(search.toLowerCase())) return false; - return true; - }); - }, [characters, search, role]); + const filtered = useMemo(() => { + return characters.filter((c) => { + if (role === 'survivors' && isKiller(c.idx)) return false; + if (role === 'killers' && !isKiller(c.idx)) return false; + if (search.trim() && !c.name.toLowerCase().includes(search.toLowerCase())) + return false; + return true; + }); + }, [characters, search, role]); - const handleToggle = (idx: number) => { - store.toggleItem(idx.toString(), 'characters'); - }; + const handleToggle = (idx: number) => { + store.toggleItem(idx.toString(), 'characters'); + }; - const handleUnlockAll = () => { - const ids = filtered.map(c => c.idx.toString()); - const outside = store.unlockedCharacters.filter( - id => !filtered.some(c => c.idx.toString() === id) - ); - store.unlockAllInCategory('characters', [...outside, ...ids]); - }; + const handleUnlockAll = () => { + const ids = filtered.map((c) => c.idx.toString()); + const outside = store.unlockedCharacters.filter( + (id) => !filtered.some((c) => c.idx.toString() === id) + ); + store.unlockAllInCategory('characters', [...outside, ...ids]); + }; - const handleLockAll = () => { - const ids = filtered.map(c => c.idx.toString()); - const newUnlocked = store.unlockedCharacters.filter(id => !ids.includes(id)); - store.unlockAllInCategory('characters', newUnlocked); - }; + const handleLockAll = () => { + const ids = filtered.map((c) => c.idx.toString()); + const newUnlocked = store.unlockedCharacters.filter( + (id) => !ids.includes(id) + ); + store.unlockAllInCategory('characters', newUnlocked); + }; - const handleClear = () => { - store.clearCategory('characters'); - }; - const unlockedCount = store.unlockedCharacters.length; + const handleClear = () => { + store.clearCategory('characters'); + }; + const unlockedCount = store.unlockedCharacters.length; - return (
-
-
-

Characters

-

{unlockedCount} of {characters.length} unlocked

-
-
+ return ( +
+
+
+

Characters

+

+ {unlockedCount} of {characters.length} unlocked +

+
+
-
- setSearch(e.target.value)} - /> +
+ setSearch(e.target.value)} + /> -
- {(['all', 'survivors', 'killers'] as RoleFilter[]).map(r => ( - - ))} -
+
+ {(['all', 'survivors', 'killers'] as RoleFilter[]).map((r) => ( + + ))} +
- + - {filtered.length} shown + {filtered.length} shown - + - - -
+ + +
- {filtered.length === 0 ? ( -
No characters match
- ) : ( -
- {filtered.map(char => { - const unlocked = store.unlockedCharacters.includes(char.idx.toString()); - const killer = isKiller(char.idx); - return ( -
handleToggle(char.idx)} - > - {char.name} - {char.name} - - {killer ? 'Killer' : 'Survivor'} - -
- ); - })} -
- )} -
) -} \ No newline at end of file + {filtered.length === 0 ? ( +
No characters match
+ ) : ( +
+ {filtered.map((char) => { + const unlocked = store.unlockedCharacters.includes( + char.idx.toString() + ); + const killer = isKiller(char.idx); + return ( +
handleToggle(char.idx)} + > + {char.name} + {char.name} + + {killer ? 'Killer' : 'Survivor'} + +
+ ); + })} +
+ )} +
+ ); +} diff --git a/app/customizations/CharacterCosmetics.tsx b/app/customizations/CharacterCosmetics.tsx index 57ab216..5e41ce6 100644 --- a/app/customizations/CharacterCosmetics.tsx +++ b/app/customizations/CharacterCosmetics.tsx @@ -2,72 +2,87 @@ import shared from '../../styles/shared.module.css'; import styles from '../../styles/Customizations.module.css'; -import { CustomizationItem, CATEGORY_LABELS, CATEGORY_ORDER, getCosmeticIconUrl } from './types'; +import { + CustomizationItem, + CATEGORY_LABELS, + CATEGORY_ORDER, + getCosmeticIconUrl +} from './types'; type Props = { - charName: string; - charCosmetics: Record; - unlockedSet: Set; - characterMap: Map; - onBack: () => void; - onUnlockAll: () => void; - onLockAll: () => void; - onToggle: (id: string) => void; + charName: string; + charCosmetics: Record; + unlockedSet: Set; + characterMap: Map; + onBack: () => void; + onUnlockAll: () => void; + onLockAll: () => void; + onToggle: (id: string) => void; }; export default function CharacterCosmetics({ - charName, - charCosmetics, - unlockedSet, - characterMap, - onBack, - onUnlockAll, - onLockAll, - onToggle, + charName, + charCosmetics, + unlockedSet, + characterMap, + onBack, + onUnlockAll, + onLockAll, + onToggle }: Props) { - const allItems = Object.values(charCosmetics).flat(); - const unlockedCount = allItems.filter(i => unlockedSet.has(i.id)).length; + const allItems = Object.values(charCosmetics).flat(); + const unlockedCount = allItems.filter((i) => unlockedSet.has(i.id)).length; - return ( -
-
- - {charName} - {unlockedCount} / {allItems.length} unlocked - - - -
+ return ( +
+
+ + {charName} + + {unlockedCount} / {allItems.length} unlocked + + + + +
-
- {CATEGORY_ORDER.filter(cat => charCosmetics[cat]?.length > 0).map(cat => ( -
-
- {CATEGORY_LABELS[cat] ?? `Category ${cat}`} -
-
- {charCosmetics[cat].map(item => { - const unlocked = unlockedSet.has(item.id); - return ( -
onToggle(item.id)} - > - {item.name} - {item.name} -
- ); - })} -
-
- ))} -
-
- ); +
+ {CATEGORY_ORDER.filter((cat) => charCosmetics[cat]?.length > 0).map( + (cat) => ( +
+
+ {CATEGORY_LABELS[cat] ?? `Category ${cat}`} +
+
+ {charCosmetics[cat].map((item) => { + const unlocked = unlockedSet.has(item.id); + return ( +
onToggle(item.id)} + > + {item.name} + {item.name} +
+ ); + })} +
+
+ ) + )} +
+
+ ); } diff --git a/app/customizations/CharacterPicker.tsx b/app/customizations/CharacterPicker.tsx index 5ebfadf..93b00a0 100644 --- a/app/customizations/CharacterPicker.tsx +++ b/app/customizations/CharacterPicker.tsx @@ -6,84 +6,87 @@ import { DB_BASE_URL } from '../../lib/db'; import { Character, RoleFilter } from './types'; type Props = { - filteredChars: Character[]; - charSearch: string; - charRole: RoleFilter; - charFullyUnlocked: Map; - onSelect: (idx: number) => void; - onSearchChange: (s: string) => void; - onRoleChange: (r: RoleFilter) => void; - onUnlockShownCosmetics: () => void; - onLockShownCosmetics: () => void; + filteredChars: Character[]; + charSearch: string; + charRole: RoleFilter; + charFullyUnlocked: Map; + onSelect: (idx: number) => void; + onSearchChange: (s: string) => void; + onRoleChange: (r: RoleFilter) => void; + onUnlockShownCosmetics: () => void; + onLockShownCosmetics: () => void; }; const getCharIconUrl = (iconFilePath: string) => { - const file = (iconFilePath.split('/').pop() ?? '').split('.')[0]; - return `${DB_BASE_URL}/icons/character-icons/${file}.png`; + const file = (iconFilePath.split('/').pop() ?? '').split('.')[0]; + return `${DB_BASE_URL}/icons/character-icons/${file}.png`; }; export default function CharacterPicker({ - filteredChars, - charSearch, - charRole, - charFullyUnlocked, - onSelect, - onSearchChange, - onRoleChange, - onUnlockShownCosmetics, - onLockShownCosmetics, + filteredChars, + charSearch, + charRole, + charFullyUnlocked, + onSelect, + onSearchChange, + onRoleChange, + onUnlockShownCosmetics, + onLockShownCosmetics }: Props) { - return ( - <> -
- onSearchChange(e.target.value)} - /> -
- {(['all', 'survivors', 'killers'] as RoleFilter[]).map(r => ( - - ))} -
- - {filteredChars.length} shown - - -
+ return ( + <> +
+ onSearchChange(e.target.value)} + /> +
+ {(['all', 'survivors', 'killers'] as RoleFilter[]).map((r) => ( + + ))} +
+ + {filteredChars.length} shown + + +
-
- {filteredChars.map(char => { - const fullyUnlocked = charFullyUnlocked.get(char.idx) ?? false; - return ( -
onSelect(char.idx)} - > - {char.name} - {char.name} -
- ); - })} -
- - ); +
+ {filteredChars.map((char) => { + const fullyUnlocked = charFullyUnlocked.get(char.idx) ?? false; + return ( +
onSelect(char.idx)} + > + {char.name} + {char.name} +
+ ); + })} +
+ + ); } diff --git a/app/customizations/FlatCategory.tsx b/app/customizations/FlatCategory.tsx index aae3f86..316c7e8 100644 --- a/app/customizations/FlatCategory.tsx +++ b/app/customizations/FlatCategory.tsx @@ -5,132 +5,146 @@ import styles from '../../styles/Customizations.module.css'; import { CustomizationItem, Tab, getCosmeticIconUrl } from './types'; type Props = { - tab: Tab; - allFilteredItems: CustomizationItem[]; - pagedItems: CustomizationItem[]; - page: number; - totalPages: number; - search: string; - unlockedSet: Set; - characterMap: Map; - onSearchChange: (s: string) => void; - onUnlockAll: () => void; - onLockAll: () => void; - onUnlockPage: () => void; - onLockPage: () => void; - onToggle: (id: string) => void; - onPageChange: (p: number) => void; + tab: Tab; + allFilteredItems: CustomizationItem[]; + pagedItems: CustomizationItem[]; + page: number; + totalPages: number; + search: string; + unlockedSet: Set; + characterMap: Map; + onSearchChange: (s: string) => void; + onUnlockAll: () => void; + onLockAll: () => void; + onUnlockPage: () => void; + onLockPage: () => void; + onToggle: (id: string) => void; + onPageChange: (p: number) => void; }; export default function FlatCategory({ - tab, - allFilteredItems, - pagedItems, - page, - totalPages, - search, - unlockedSet, - characterMap, - onSearchChange, - onUnlockAll, - onLockAll, - onUnlockPage, - onLockPage, - onToggle, - onPageChange, + tab, + allFilteredItems, + pagedItems, + page, + totalPages, + search, + unlockedSet, + characterMap, + onSearchChange, + onUnlockAll, + onLockAll, + onUnlockPage, + onLockPage, + onToggle, + onPageChange }: Props) { - const buildPageNumbers = () => { - const pages: number[] = []; - const start = Math.max(1, Math.min(page - 2, totalPages - 4)); - const end = Math.min(totalPages, start + 4); - for (let i = start; i <= end; i++) pages.push(i); - return pages; - }; + const buildPageNumbers = () => { + const pages: number[] = []; + const start = Math.max(1, Math.min(page - 2, totalPages - 4)); + const end = Math.min(totalPages, start + 4); + for (let i = start; i <= end; i++) pages.push(i); + return pages; + }; - return ( - <> -
- onSearchChange(e.target.value)} - /> - - {allFilteredItems.length} items - - - - -
+ return ( + <> +
+ onSearchChange(e.target.value)} + /> + + + {allFilteredItems.length} items + + + + + +
- {allFilteredItems.length === 0 ? ( -
No items found
- ) : ( - <> -
- {pagedItems.map(item => { - const unlocked = unlockedSet.has(item.id); - return ( -
onToggle(item.id)} - > - {item.name} - {item.name} -
- ); - })} -
+ {allFilteredItems.length === 0 ? ( +
No items found
+ ) : ( + <> +
+ {pagedItems.map((item) => { + const unlocked = unlockedSet.has(item.id); + return ( +
onToggle(item.id)} + > + {item.name} + {item.name} +
+ ); + })} +
- {totalPages > 1 && ( -
- - + {totalPages > 1 && ( +
+ + - {buildPageNumbers().map(n => ( - - ))} + {buildPageNumbers().map((n) => ( + + ))} - - + + - {page} / {totalPages} -
- )} - - )} - - ); -} \ No newline at end of file + + {page} / {totalPages} + +
+ )} + + )} + + ); +} diff --git a/app/customizations/page.tsx b/app/customizations/page.tsx index 74fd7f6..4a236ef 100644 --- a/app/customizations/page.tsx +++ b/app/customizations/page.tsx @@ -8,7 +8,14 @@ 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 { + Character, + CustomizationItem, + RoleFilter, + Tab, + TAB_CATEGORIES, + CATEGORY_ORDER +} from './types'; import CharacterPicker from './CharacterPicker'; import CharacterCosmetics from './CharacterCosmetics'; import FlatCategory from './FlatCategory'; @@ -18,210 +25,265 @@ import FlatCategory from './FlatCategory'; */ const PAGE_SIZE = 60; - export default function CustomizationsPage() { - const store = useInventoryStore(); + const store = useInventoryStore(); - const [allItems, setAllItems] = useState([]); - const [characters, setCharacters] = useState([]); - const [tab, setTab] = useState('cosmetics'); - const [selectedChar, setSelectedChar] = useState(null); - const [charSearch, setCharSearch] = useState(''); - const [charRole, setCharRole] = useState('all'); - const [search, setSearch] = useState(''); - const [page, setPage] = useState(1); + const [allItems, setAllItems] = useState([]); + const [characters, setCharacters] = useState([]); + const [tab, setTab] = useState('cosmetics'); + const [selectedChar, setSelectedChar] = useState(null); + const [charSearch, setCharSearch] = useState(''); + const [charRole, setCharRole] = useState('all'); + const [search, setSearch] = useState(''); + const [page, setPage] = useState(1); - useEffect(() => { - Promise.all([fetchCustomizations(), fetchCharacters()]) - .then(([items, chars]) => { - setAllItems(items); - setCharacters(chars); - }); - }, []); + useEffect(() => { + Promise.all([fetchCustomizations(), fetchCharacters()]).then( + ([items, chars]) => { + setAllItems(items); + setCharacters(chars); + } + ); + }, []); - useEffect(() => { setPage(1); }, [tab, search]); + useEffect(() => { + setPage(1); + }, [tab, search]); - /* + /* derived data */ - const characterMap = useMemo(() => { - const m = new Map(); - characters.forEach(c => m.set(c.idx, c.name)); - return m; - }, [characters]); + const characterMap = useMemo(() => { + const m = new Map(); + characters.forEach((c) => m.set(c.idx, c.name)); + return m; + }, [characters]); - const unlockedSet = useMemo( - () => new Set(store.unlockedCustomizations), - [store.unlockedCustomizations] - ); + const unlockedSet = useMemo( + () => new Set(store.unlockedCustomizations), + [store.unlockedCustomizations] + ); - const itemsByCharacter = useMemo(() => { - const map = new Map(); - 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 itemsByCharacter = useMemo(() => { + const map = new Map(); + 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(); - 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 charFullyUnlocked = useMemo(() => { + const result = new Map(); + 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 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 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 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 = {}; - CATEGORY_ORDER.forEach(cat => { - const group = items.filter(i => i.category === cat); - if (group.length > 0) groups[cat] = group; - }); - return groups; - }, [itemsByCharacter, selectedChar]); + const charCosmetics = useMemo(() => { + if (selectedChar === null) return {}; + const items = itemsByCharacter.get(selectedChar) ?? []; + const groups: Record = {}; + 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 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); - }; + 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 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 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 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 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 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)); + const handleUnlockPage = () => mergeUnlock(pagedItems.map((i) => i.id)); + const handleLockPage = () => removeLock(pagedItems.map((i) => i.id)); - return ( -
-
-
-

Customizations

-

- {store.unlockedCustomizations.length} of {allItems.length || '-'} unlocked -

-
- -
-
- {(['cosmetics', 'charms', 'badges', 'banners', 'portraits'] as Tab[]).map(t => ( - - ))} -
- {tab === 'cosmetics' && selectedChar === null && ( - - )} - {tab === 'cosmetics' && selectedChar !== null && ( - setSelectedChar(null)} - onUnlockAll={handleUnlockCharCosmetics} - onLockAll={handleLockCharCosmetics} - onToggle={handleToggle} - /> - )} - {tab !== 'cosmetics' && ( - - )} -
- ) -} \ No newline at end of file + return ( +
+
+
+

Customizations

+

+ {store.unlockedCustomizations.length} of {allItems.length || '-'}{' '} + unlocked +

+
+ +
+
+ {( + ['cosmetics', 'charms', 'badges', 'banners', 'portraits'] as Tab[] + ).map((t) => ( + + ))} +
+ {tab === 'cosmetics' && selectedChar === null && ( + + )} + {tab === 'cosmetics' && selectedChar !== null && ( + setSelectedChar(null)} + onUnlockAll={handleUnlockCharCosmetics} + onLockAll={handleLockCharCosmetics} + onToggle={handleToggle} + /> + )} + {tab !== 'cosmetics' && ( + + )} +
+ ); +} diff --git a/app/customizations/types.ts b/app/customizations/types.ts index 30eb018..1a70c81 100644 --- a/app/customizations/types.ts +++ b/app/customizations/types.ts @@ -1,57 +1,77 @@ export type CustomizationItem = { - id: string; - name: string; - iconFilePath: string; - category: number; - associatedCharacter: number; + id: string; + name: string; + iconFilePath: string; + category: number; + associatedCharacter: number; }; export type Character = { - idx: number; - name: string; - iconFilePath: string; + idx: number; + name: string; + iconFilePath: string; }; export type Tab = 'cosmetics' | 'charms' | 'badges' | 'banners' | 'portraits'; export type RoleFilter = 'all' | 'survivors' | 'killers'; export const CATEGORY_LABELS: Record = { - 1: 'Heads', 2: 'Torsos', 3: 'Legs', - 4: 'Heads', 5: 'Bodies', 6: 'Weapons', 7: 'Outfits', + 1: 'Heads', + 2: 'Torsos', + 3: 'Legs', + 4: 'Heads', + 5: 'Bodies', + 6: 'Weapons', + 7: 'Outfits' }; export const CATEGORY_ORDER = [7, 1, 4, 2, 3, 5, 6]; export const TAB_CATEGORIES: Partial> = { - charms: 8, badges: 9, banners: 10, portraits: 11, + charms: 8, + badges: 9, + banners: 10, + portraits: 11 }; import { DB_BASE_URL } from '../../lib/db'; export const getCosmeticIconUrl = ( - item: CustomizationItem, - characterMap: Map + item: CustomizationItem, + characterMap: Map ): string => { - const file = (item.iconFilePath.split('/').pop() ?? '').split('.')[0]; - const base = DB_BASE_URL; + const file = (item.iconFilePath.split('/').pop() ?? '').split('.')[0]; + const base = DB_BASE_URL; - switch (item.category) { - case 8: return `${base}/icons/customization/charms/${file}.png`; - case 9: return `${base}/icons/customization/badges/${file}.png`; - case 10: return `${base}/icons/customization/banners/${file}.png`; - case 11: return `${base}/icons/customization/portrait-backgrounds/${file}.png`; - } + switch (item.category) { + case 8: + return `${base}/icons/customization/charms/${file}.png`; + case 9: + return `${base}/icons/customization/badges/${file}.png`; + case 10: + return `${base}/icons/customization/banners/${file}.png`; + case 11: + return `${base}/icons/customization/portrait-backgrounds/${file}.png`; + } - const subfolder = - item.category === 1 || item.category === 4 ? 'heads' : - item.category === 2 ? 'torsos' : - item.category === 3 ? 'legs' : - item.category === 5 ? 'bodys' : - item.category === 6 ? 'weapons' : 'outfits'; + const subfolder = + item.category === 1 || item.category === 4 + ? 'heads' + : item.category === 2 + ? 'torsos' + : item.category === 3 + ? 'legs' + : item.category === 5 + ? 'bodys' + : item.category === 6 + ? 'weapons' + : 'outfits'; - const charName = characterMap.get(item.associatedCharacter); - const charFolder = (charName ?? item.associatedCharacter.toString()) - .replace(/[\\/:*?"<>|]/g, '_'); + const charName = characterMap.get(item.associatedCharacter); + const charFolder = (charName ?? item.associatedCharacter.toString()).replace( + /[\\/:*?"<>|]/g, + '_' + ); - return `${base}/icons/customization/characters/${charFolder}/${subfolder}/${file}.png`; -}; \ No newline at end of file + return `${base}/icons/customization/characters/${charFolder}/${subfolder}/${file}.png`; +}; diff --git a/app/dlcs/page.tsx b/app/dlcs/page.tsx index d2f07b1..c01815d 100644 --- a/app/dlcs/page.tsx +++ b/app/dlcs/page.tsx @@ -4,139 +4,177 @@ import { useState, useEffect, useMemo } from 'react'; import { useInventoryStore } from '@/store/useInventoryStore'; import shared from '../../styles/shared.module.css'; import styles from '../../styles/Dlcs.module.css'; -import { PlatformFilter, isOnSteam, isOnEpic, isOnXbox, matchesPlatform, } from './types'; +import { + PlatformFilter, + isOnSteam, + isOnEpic, + isOnXbox, + matchesPlatform +} from './types'; import { DLC, isNamedDLC } from '@/lib/utils'; import { fetchDLCs } from '@/lib/db'; -const PLATFORM_FILTER_LABELS: Record = { all: 'All', steam: 'Steam', epic: 'Epic', xbox: 'Xbox' }; +const PLATFORM_FILTER_LABELS: Record = { + all: 'All', + steam: 'Steam', + epic: 'Epic', + xbox: 'Xbox' +}; export default function DlcsPage() { - const store = useInventoryStore(); + const store = useInventoryStore(); - const [allDlcs, setAllDlcs] = useState([]); - const [search, setSearch] = useState(''); - const [platformFilter, setPlatformFilter] = useState('all'); - const [statusFilter, setStatusFilter] = useState<'all' | 'unlocked' | 'locked'>('all'); + const [allDlcs, setAllDlcs] = useState([]); + const [search, setSearch] = useState(''); + const [platformFilter, setPlatformFilter] = useState('all'); + const [statusFilter, setStatusFilter] = useState< + 'all' | 'unlocked' | 'locked' + >('all'); - useEffect(() => { - fetchDLCs() - .then((data: DLC[]) => setAllDlcs(data.filter(isNamedDLC))); - }, []); + useEffect(() => { + fetchDLCs().then((data: DLC[]) => setAllDlcs(data.filter(isNamedDLC))); + }, []); - const filtered = useMemo(() => { - return allDlcs.filter(dlc => { - if (!matchesPlatform(dlc, platformFilter)) return false; - if (search.trim() && !dlc.name!.toLowerCase().includes(search.toLowerCase())) return false; - if (statusFilter === 'unlocked' && !store.unlockedDLCs.includes(dlc.id)) return false; - if (statusFilter === 'locked' && store.unlockedDLCs.includes(dlc.id)) return false; - return true; - }); - }, [allDlcs, search, platformFilter, statusFilter, store.unlockedDLCs]); + const filtered = useMemo(() => { + return allDlcs.filter((dlc) => { + if (!matchesPlatform(dlc, platformFilter)) return false; + if ( + search.trim() && + !dlc.name!.toLowerCase().includes(search.toLowerCase()) + ) + return false; + if (statusFilter === 'unlocked' && !store.unlockedDLCs.includes(dlc.id)) + return false; + if (statusFilter === 'locked' && store.unlockedDLCs.includes(dlc.id)) + return false; + return true; + }); + }, [allDlcs, search, platformFilter, statusFilter, store.unlockedDLCs]); - const handleToggle = (id: string) => store.toggleItem(id, 'dlcs'); + const handleToggle = (id: string) => store.toggleItem(id, 'dlcs'); - const handleUnlockShown = () => { - const ids = filtered.map(d => d.id); - const merged = Array.from(new Set([...store.unlockedDLCs, ...ids])); - store.unlockAllInCategory('dlcs', merged); - }; + const handleUnlockShown = () => { + const ids = filtered.map((d) => d.id); + const merged = Array.from(new Set([...store.unlockedDLCs, ...ids])); + store.unlockAllInCategory('dlcs', merged); + }; - const handleLockShown = () => { - const ids = new Set(filtered.map(d => d.id)); - const remaining = store.unlockedDLCs.filter(id => !ids.has(id)); - store.unlockAllInCategory('dlcs', remaining); - }; + const handleLockShown = () => { + const ids = new Set(filtered.map((d) => d.id)); + const remaining = store.unlockedDLCs.filter((id) => !ids.has(id)); + store.unlockAllInCategory('dlcs', remaining); + }; - const handleUnlockAll = () => { - store.unlockAllInCategory('dlcs', allDlcs.map(d => d.id)); - }; + const handleUnlockAll = () => { + store.unlockAllInCategory( + 'dlcs', + allDlcs.map((d) => d.id) + ); + }; - return ( -
-
-
-

DLCs

-

- {store.unlockedDLCs.length} of {allDlcs.length || '-'} dlcs unlocked -

-
-
- - -
-
+ return ( +
+
+
+

DLCs

+

+ {store.unlockedDLCs.length} of {allDlcs.length || '-'} dlcs unlocked +

+
+
+ + +
+
-
- setSearch(e.target.value)} - /> +
+ setSearch(e.target.value)} + /> -
- {(Object.keys(PLATFORM_FILTER_LABELS) as PlatformFilter[]).map(p => ( - - ))} -
+
+ {(Object.keys(PLATFORM_FILTER_LABELS) as PlatformFilter[]).map( + (p) => ( + + ) + )} +
-
- {(['all', 'unlocked', 'locked'] as const).map(s => ( - - ))} -
+
+ {(['all', 'unlocked', 'locked'] as const).map((s) => ( + + ))} +
- - {filtered.length} shown + + {filtered.length} shown - - -
+ + +
- {filtered.length === 0 ? ( -
No DLCs match
- ) : ( -
- {filtered.map(dlc => { - const unlocked = store.unlockedDLCs.includes(dlc.id); - return ( -
handleToggle(dlc.id)} - > - {dlc.name} -
- ST - EP - XB -
-
- ); - })} -
- )} -
- ); -} \ No newline at end of file + {filtered.length === 0 ? ( +
No DLCs match
+ ) : ( +
+ {filtered.map((dlc) => { + const unlocked = store.unlockedDLCs.includes(dlc.id); + return ( +
handleToggle(dlc.id)} + > + {dlc.name} +
+ + ST + + + EP + + + XB + +
+
+ ); + })} +
+ )} +
+ ); +} diff --git a/app/dlcs/types.ts b/app/dlcs/types.ts index a4d580f..81ecdda 100644 --- a/app/dlcs/types.ts +++ b/app/dlcs/types.ts @@ -1,20 +1,20 @@ -import { DLC } from "@/lib/utils"; +import { DLC } from '@/lib/utils'; export type PlatformFilter = 'all' | 'steam' | 'epic' | 'xbox'; export const isOnSteam = (dlc: DLC) => - dlc.dlcIds.steam !== '0' && dlc.dlcIds.steam !== '-1'; + dlc.dlcIds.steam !== '0' && dlc.dlcIds.steam !== '-1'; export const isOnEpic = (dlc: DLC) => - dlc.dlcIds.epic !== 'FFFFFFFFFFFFFFFF' && dlc.dlcIds.epic !== '0'; + dlc.dlcIds.epic !== 'FFFFFFFFFFFFFFFF' && dlc.dlcIds.epic !== '0'; export const isOnXbox = (dlc: DLC) => - dlc.dlcIds.grdk !== '9ZZZZZZZZZZZ' && dlc.dlcIds.grdk !== '0'; + dlc.dlcIds.grdk !== '9ZZZZZZZZZZZ' && dlc.dlcIds.grdk !== '0'; export const matchesPlatform = (dlc: DLC, filter: PlatformFilter) => { - if (filter === 'all') return true; - if (filter === 'steam') return isOnSteam(dlc); - if (filter === 'epic') return isOnEpic(dlc); - if (filter === 'xbox') return isOnXbox(dlc); - return true; -}; \ No newline at end of file + if (filter === 'all') return true; + if (filter === 'steam') return isOnSteam(dlc); + if (filter === 'epic') return isOnEpic(dlc); + if (filter === 'xbox') return isOnXbox(dlc); + return true; +}; diff --git a/app/globals.css b/app/globals.css index 9269045..ba66c6c 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,39 +1,44 @@ @import url('https://fonts.googleapis.com/css2?family=Oswald:wght@400;700&family=Roboto+Condensed:wght@400;700&display=swap'); -@import "tailwindcss"; +@import 'tailwindcss'; * { - box-sizing: border-box; + box-sizing: border-box; } body { - background: #050505; - color: #c9c9c9; - font-family: 'Roboto Condensed', sans-serif; - margin: 0; - padding: 0; - overflow: hidden; + background: #050505; + color: #c9c9c9; + font-family: 'Roboto Condensed', sans-serif; + margin: 0; + padding: 0; + overflow: hidden; } -h1, h2, h3, h4, h5, h6 { - font-family: 'Oswald', sans-serif; +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: 'Oswald', sans-serif; } ::-webkit-scrollbar { - width: 6px; - height: 6px; + width: 6px; + height: 6px; } ::-webkit-scrollbar-track { - background: #050505; + background: #050505; } ::-webkit-scrollbar-thumb { - background: #330000; - border-radius: 3px; - border: 1px solid #1a1a1a; + background: #330000; + border-radius: 3px; + border: 1px solid #1a1a1a; } ::-webkit-scrollbar-thumb:hover { - background: #a30000; -} \ No newline at end of file + background: #a30000; +} diff --git a/app/items/AddonGrid.tsx b/app/items/AddonGrid.tsx index b312693..af8c288 100644 --- a/app/items/AddonGrid.tsx +++ b/app/items/AddonGrid.tsx @@ -7,75 +7,113 @@ import QuantityCard from '../../components/QuantityCard'; import { Addon, getAddonIconUrl, randInRange } from './types'; type Props = { - addons: Addon[]; - quantities: Record; - randMin: number; - randMax: number; - onSetQty: (id: string, qty: number) => void; + addons: Addon[]; + quantities: Record; + 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'); +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 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 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; + const activeCount = Object.values(quantities).filter((q) => q > 0).length; - return ( - <> -
- setSearch(e.target.value)} - /> + return ( + <> +
+ setSearch(e.target.value)} + /> -
- - - -
+
+ + + +
- - {filtered.length} shown · {activeCount} active out of {addons.length} total + + + {filtered.length} shown · {activeCount} active out of {addons.length}{' '} + total + - - - -
+ + + +
- {filtered.length === 0 ? ( -
No addons match
- ) : ( -
- {filtered.map(addon => ( - - ))} -
- )} - - ); + {filtered.length === 0 ? ( +
No addons match
+ ) : ( +
+ {filtered.map((addon) => ( + + ))} +
+ )} + + ); } diff --git a/app/items/ItemGrid.tsx b/app/items/ItemGrid.tsx index a0163fd..b11e60a 100644 --- a/app/items/ItemGrid.tsx +++ b/app/items/ItemGrid.tsx @@ -4,84 +4,111 @@ 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 } from './types'; +import { + Item, + ItemType, + ITEM_TYPE_LABELS, + getItemType, + getItemIconUrl +} from './types'; import { randInRange } from '@/lib/utils'; type Props = { - items: Item[]; - quantities: Record; - randMin: number; - randMax: number; - onSetQty: (id: string, qty: number) => void; + items: Item[]; + quantities: Record; + 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('all'); +export default function ItemGrid({ + items, + quantities, + randMin, + randMax, + onSetQty +}: Props) { + const [search, setSearch] = useState(''); + const [typeFilter, setTypeFilter] = useState('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 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 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; + const activeCount = Object.values(quantities).filter((q) => q > 0).length; - return ( - <> -
- setSearch(e.target.value)} - /> + return ( + <> +
+ setSearch(e.target.value)} + /> -
- {(Object.keys(ITEM_TYPE_LABELS) as ItemType[]).map(t => ( - - ))} -
+
+ {(Object.keys(ITEM_TYPE_LABELS) as ItemType[]).map((t) => ( + + ))} +
- - {filtered.length} shown · {activeCount} active + + + {filtered.length} shown · {activeCount} active + - - - -
+ + + +
- {filtered.length === 0 ? ( -
No items match
- ) : ( -
- {filtered.map(item => ( - - ))} -
- )} - - ); -} \ No newline at end of file + {filtered.length === 0 ? ( +
No items match
+ ) : ( +
+ {filtered.map((item) => ( + + ))} +
+ )} + + ); +} diff --git a/app/items/OfferingGrid.tsx b/app/items/OfferingGrid.tsx index 72732eb..c2e0a05 100644 --- a/app/items/OfferingGrid.tsx +++ b/app/items/OfferingGrid.tsx @@ -8,92 +8,112 @@ import { Offering, OfferingRole, getOfferingIconUrl } from './types'; import { randInRange } from '@/lib/utils'; type Props = { - offerings: Offering[]; - quantities: Record; - randMin: number; - randMax: number; - onSetQty: (id: string, qty: number) => void; + offerings: Offering[]; + quantities: Record; + randMin: number; + randMax: number; + onSetQty: (id: string, qty: number) => void; }; const ROLE_LABELS: Record = { - all: 'All', shared: 'Shared', killer: 'Killer', survivor: 'Survivor', + 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; + 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('all'); +export default function OfferingGrid({ + offerings, + quantities, + randMin, + randMax, + onSetQty +}: Props) { + const [search, setSearch] = useState(''); + const [roleFilter, setRoleFilter] = useState('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 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 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; + const activeCount = Object.values(quantities).filter((q) => q > 0).length; - return ( - <> -
- setSearch(e.target.value)} - /> + return ( + <> +
+ setSearch(e.target.value)} + /> -
- {(Object.keys(ROLE_LABELS) as OfferingRole[]).map(r => ( - - ))} -
+
+ {(Object.keys(ROLE_LABELS) as OfferingRole[]).map((r) => ( + + ))} +
- - {filtered.length} shown · {activeCount} active + + + {filtered.length} shown · {activeCount} active + - - - -
+ + + +
- {filtered.length === 0 ? ( -
No offerings match
- ) : ( -
- {filtered.map(offering => ( - - ))} -
- )} - - ); -} \ No newline at end of file + {filtered.length === 0 ? ( +
No offerings match
+ ) : ( +
+ {filtered.map((offering) => ( + + ))} +
+ )} + + ); +} diff --git a/app/items/page.tsx b/app/items/page.tsx index 375f549..5c16645 100644 --- a/app/items/page.tsx +++ b/app/items/page.tsx @@ -15,110 +15,120 @@ import AddonGrid from './AddonGrid'; type Tab = 'items' | 'offerings' | 'addons'; export default function ItemsPage() { - const store = useInventoryStore(); + const store = useInventoryStore(); - const [tab, setTab] = useState('items'); - const [items, setItems] = useState([]); - const [offerings, setOfferings] = useState([]); - const [addons, setAddons] = useState([]); + const [tab, setTab] = useState('items'); + const [items, setItems] = useState([]); + const [offerings, setOfferings] = useState([]); + const [addons, setAddons] = useState([]); - const [randMin, setRandMin] = useState(50); - const [randMax, setRandMax] = useState(200); + const [randMin, setRandMin] = useState(50); + const [randMax, setRandMax] = useState(200); - useEffect(() => { - Promise.all([fetchItems(), fetchOfferings(), fetchAddons()]) - .then(([i, o, a]) => { - setItems(i); - setOfferings(o); - setAddons(a); - }); - }, []); + useEffect(() => { + Promise.all([fetchItems(), fetchOfferings(), fetchAddons()]).then( + ([i, o, a]) => { + setItems(i); + setOfferings(o); + setAddons(a); + } + ); + }, []); - const handleClearAll = () => { - store.clearCategory('items'); - store.clearCategory('offerings'); - store.clearCategory('addons'); - }; + const handleClearAll = () => { + store.clearCategory('items'); + store.clearCategory('offerings'); + store.clearCategory('addons'); + }; - const activeItems = Object.values(store.items).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; + const activeItems = Object.values(store.items).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 ( -
-
-
-

Items & Offerings

-

- {activeItems} items · {activeOfferings} offerings · {activeAddons} addons -

-
+ return ( +
+
+
+

Items & Offerings

+

+ {activeItems} items · {activeOfferings} offerings · {activeAddons}{' '} + addons +

+
-
- Rand range - setRandMin(Math.max(0, parseInt(e.target.value) || 0))} - /> - - setRandMax(Math.min(5000, parseInt(e.target.value) || 0))} - /> -
+
+ Rand range + + setRandMin(Math.max(0, parseInt(e.target.value) || 0)) + } + /> + + + setRandMax(Math.min(5000, parseInt(e.target.value) || 0)) + } + /> +
- -
+ +
-
- {(['items', 'offerings', 'addons'] as Tab[]).map(t => ( - - ))} -
+
+ {(['items', 'offerings', 'addons'] as Tab[]).map((t) => ( + + ))} +
- {tab === 'items' && ( - - )} + {tab === 'items' && ( + + )} - {tab === 'offerings' && ( - - )} + {tab === 'offerings' && ( + + )} - {tab === 'addons' && ( - - )} -
- ); -} \ No newline at end of file + {tab === 'addons' && ( + + )} + + ); +} diff --git a/app/items/types.ts b/app/items/types.ts index 2bca35c..fe22b19 100644 --- a/app/items/types.ts +++ b/app/items/types.ts @@ -1,59 +1,71 @@ import { DB_BASE_URL } from '../../lib/db'; export type Item = { - id: string; - name: string; - iconFilePath: string; + id: string; + name: string; + iconFilePath: string; }; export type Offering = { - id: string; - name: string; - iconFilePath: string; - role: number; + id: string; + name: string; + iconFilePath: string; + role: number; }; export type Addon = { - id: string; - name: string; - iconFilePath: string; - role: number; + 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 const ITEM_TYPE_LABELS: Record = { - all: 'All', toolbox: 'Toolbox', flashlight: 'Flashlight', - medkit: 'Med-Kit', key: 'Key', map: 'Map', other: 'Other', + 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'; + 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 `${DB_BASE_URL}/icons/item-icons/${file}.png`; + const file = (iconFilePath.split('/').pop() ?? '').split('.')[0]; + return `${DB_BASE_URL}/icons/item-icons/${file}.png`; }; export const getOfferingIconUrl = (iconFilePath: string) => { - const file = (iconFilePath.split('/').pop() ?? '').split('.')[0]; - return `${DB_BASE_URL}/icons/offering-icons/${file}.png`; + const file = (iconFilePath.split('/').pop() ?? '').split('.')[0]; + 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`; + 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; + const lo = Math.min(min, max); + const hi = Math.max(min, max); + return Math.floor(Math.random() * (hi - lo + 1)) + lo; }; diff --git a/app/layout.tsx b/app/layout.tsx index a8e8931..a31325a 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,31 +1,29 @@ -import type { Metadata } from "next"; -import Sidebar from "../components/Sidebar"; -import styles from "../styles/Layout.module.css"; -import "./globals.css"; -import AppContainer from "@/components/Container"; +import type { Metadata } from 'next'; +import Sidebar from '../components/Sidebar'; +import styles from '../styles/Layout.module.css'; +import './globals.css'; +import AppContainer from '@/components/Container'; export const metadata: Metadata = { - title: "Hex: Unlocked", - description: "", + title: 'Hex: Unlocked', + description: '' }; export default function RootLayout({ - children, + children }: Readonly<{ - children: React.ReactNode; + children: React.ReactNode; }>) { - return ( - - - -
- -
- {children} -
-
-
- - - ); -} \ No newline at end of file + return ( + + + +
+ +
{children}
+
+
+ + + ); +} diff --git a/app/page.tsx b/app/page.tsx index 75196df..5319916 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -4,202 +4,287 @@ import { useState, useEffect, useMemo } from 'react'; import { useInventoryStore } from '@/store/useInventoryStore'; import styles from '../styles/Home.module.css'; import { isNamedDLC } from '@/lib/utils'; -import { fetchCharacters, fetchItems, fetchOfferings, fetchCustomizations, fetchDLCs, fetchAddons, fetchPerks } from '@/lib/db'; +import { + fetchCharacters, + fetchItems, + fetchOfferings, + fetchCustomizations, + fetchDLCs, + fetchAddons, + fetchPerks +} from '@/lib/db'; export default function Home() { - const store = useInventoryStore(); + const store = useInventoryStore(); - const [charCount, setCharCount] = useState(0); - const [custCount, setCustCount] = useState(0); - const [itemsCount, setItemsCount] = useState(0); - const [offeringsCount, setOfferingsCount] = useState(0); - const [dlcsCount, setDlcsCount] = useState(0); - const [addonsCount, setAddonsCount] = useState(0); - const [perksCount, setPerksCount] = useState(0); + const [charCount, setCharCount] = useState(0); + const [custCount, setCustCount] = useState(0); + const [itemsCount, setItemsCount] = useState(0); + const [offeringsCount, setOfferingsCount] = useState(0); + const [dlcsCount, setDlcsCount] = useState(0); + const [addonsCount, setAddonsCount] = useState(0); + const [perksCount, setPerksCount] = useState(0); - const [importText, setImportText] = useState(''); + const [importText, setImportText] = useState(''); - useEffect(() => { - Promise.all([ - fetchCharacters(), - fetchItems(), - fetchOfferings(), - fetchCustomizations(), - fetchDLCs(), - fetchAddons(), - fetchPerks() - ]).then(([chars, items, offerings, customizations, dlcs, addons, perks]) => { - setCharCount(chars.length); - setItemsCount(items.length); - setOfferingsCount(offerings.length); - setCustCount(customizations.length); - setDlcsCount(dlcs.filter(isNamedDLC).length); - setAddonsCount(addons.length); - setPerksCount(perks.length); - }); - }, []); + useEffect(() => { + Promise.all([ + fetchCharacters(), + fetchItems(), + fetchOfferings(), + fetchCustomizations(), + fetchDLCs(), + fetchAddons(), + fetchPerks() + ]).then( + ([chars, items, offerings, customizations, dlcs, addons, perks]) => { + setCharCount(chars.length); + setItemsCount(items.length); + setOfferingsCount(offerings.length); + setCustCount(customizations.length); + setDlcsCount(dlcs.filter(isNamedDLC).length); + setAddonsCount(addons.length); + setPerksCount(perks.length); + } + ); + }, []); - /* + /* profile handling */ - const handleDownload = () => { - const blob = new Blob([importText], { type: 'application/json' }); - const url = URL.createObjectURL(blob); - const link = document.createElement('a'); - link.href = url; - link.download = 'profile.json'; - link.click(); - URL.revokeObjectURL(url); - }; + const handleDownload = () => { + const blob = new Blob([importText], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = 'profile.json'; + link.click(); + URL.revokeObjectURL(url); + }; - const handleCopy = () => { - navigator.clipboard.writeText(importText); - }; + const handleCopy = () => { + navigator.clipboard.writeText(importText); + }; - const getExportText = () => { - return JSON.stringify({ - unlockedCharacters: store.unlockedCharacters, - unlockedCustomizations: store.unlockedCustomizations, - unlockedDLCs: store.unlockedDLCs, - unlockedPerks: store.unlockedPerks, - items: store.items, - offerings: store.offerings, - addons: store.addons - }, null, 2); - }; + const getExportText = () => { + return JSON.stringify( + { + unlockedCharacters: store.unlockedCharacters, + unlockedCustomizations: store.unlockedCustomizations, + unlockedDLCs: store.unlockedDLCs, + unlockedPerks: store.unlockedPerks, + items: store.items, + offerings: store.offerings, + addons: store.addons + }, + null, + 2 + ); + }; - const handleImport = async () => { - try { - const parsed = JSON.parse(importText); - store.importProfile(parsed); - } catch (e) { - console.error("Invalid JSON", e); - } - }; + const handleImport = async () => { + try { + const parsed = JSON.parse(importText); + store.importProfile(parsed); + } catch (e) { + console.error('Invalid JSON', e); + } + }; - useEffect(() => { - setImportText(getExportText()); - }, [store.unlockedCharacters, store.unlockedCustomizations, store.unlockedDLCs, store.unlockedPerks, store.items, store.offerings, store.addons]); + useEffect(() => { + setImportText(getExportText()); + }, [ + store.unlockedCharacters, + store.unlockedCustomizations, + store.unlockedDLCs, + store.unlockedPerks, + store.items, + store.offerings, + store.addons + ]); - /* + /* stats */ - const unlockedItems = useMemo(() => Object.values(store.items).filter(qty => qty > 0).length, [store.items]); - const unlockedOfferings = useMemo(() => Object.values(store.offerings).filter(qty => qty > 0).length, [store.offerings]); - const unlockedAddons = useMemo(() => Object.values(store.addons).filter(qty => qty > 0).length, [store.addons]); - const unlockedPerks = store.unlockedPerks.length; + const unlockedItems = useMemo( + () => Object.values(store.items).filter((qty) => qty > 0).length, + [store.items] + ); + const unlockedOfferings = useMemo( + () => Object.values(store.offerings).filter((qty) => qty > 0).length, + [store.offerings] + ); + const unlockedAddons = useMemo( + () => Object.values(store.addons).filter((qty) => qty > 0).length, + [store.addons] + ); + const unlockedPerks = store.unlockedPerks.length; - return (
-
-

Dashboard

-

Status overview and profile management

-
+ return ( +
+
+

Dashboard

+

+ Status overview and profile management +

+
- {/* stats cards */} -
-
-
Characters
-
{store.unlockedCharacters.length} / {charCount || '-'}
-
-
-
Customizations
-
{store.unlockedCustomizations.length} / {custCount || '-'}
-
-
-
DLCs
-
{store.unlockedDLCs.length} / {dlcsCount || '-'}
-
-
-
Items
-
{unlockedItems} / {itemsCount || '-'}
-
-
-
Offerings
-
{unlockedOfferings} / {offeringsCount || '-'}
-
-
-
Addons
-
{unlockedAddons} / {addonsCount || '-'}
-
-
-
Perks
-
{unlockedPerks} / {perksCount || '-'}
-
-
+ {/* stats cards */} +
+
+
Characters
+
+ {store.unlockedCharacters.length}{' '} + / {charCount || '-'} +
+
+
+
Customizations
+
+ {store.unlockedCustomizations.length}{' '} + / {custCount || '-'} +
+
+
+
DLCs
+
+ {store.unlockedDLCs.length}{' '} + / {dlcsCount || '-'} +
+
+
+
Items
+
+ {unlockedItems}{' '} + / {itemsCount || '-'} +
+
+
+
Offerings
+
+ {unlockedOfferings}{' '} + / {offeringsCount || '-'} +
+
+
+
Addons
+
+ {unlockedAddons}{' '} + / {addonsCount || '-'} +
+
+
+
Perks
+
+ {unlockedPerks}{' '} + / {perksCount || '-'} +
+
+
- {/* toggles */} -
-
-

Spoofer Toggles

- -
-
- Item Spoofing - Spoof inventory items, addons, and offerings -
- -
+ {/* toggles */} +
+
+

Spoofer Toggles

-
-
- Perk Spoofing - Unlock all perk slots and custom builds -
- -
+
+
+ Item Spoofing + + Spoof inventory items, addons, and offerings + +
+ +
-
-
- Catalog Spoofing - Unlock cosmetics, characters, and outfits -
- -
+
+
+ Perk Spoofing + + Unlock all perk slots and custom builds + +
+ +
-
-
- DLC Spoofing - Bypass Steam, Epic, and Xbox DLC ownership -
- -
-
+
+
+ Catalog Spoofing + + Unlock cosmetics, characters, and outfits + +
+ +
- {/* profile */} -
-

Profile Import / Export

+
+
+ DLC Spoofing + + Bypass Steam, Epic, and Xbox DLC ownership + +
+ +
+
-