From 28ab28f9424ee120a0f1a6bb811f4e8c4edf3676 Mon Sep 17 00:00:00 2001 From: neru Date: Fri, 19 Jun 2026 04:25:17 -0300 Subject: [PATCH] feat: add perks tab --- app/perks/page.tsx | 86 ++++++++++++++++++++++++++++++++++++++++++ app/perks/types.ts | 13 +++++++ components/Sidebar.tsx | 1 + 3 files changed, 100 insertions(+) create mode 100644 app/perks/page.tsx create mode 100644 app/perks/types.ts diff --git a/app/perks/page.tsx b/app/perks/page.tsx new file mode 100644 index 0000000..78a30cc --- /dev/null +++ b/app/perks/page.tsx @@ -0,0 +1,86 @@ +'use client'; + +import { useState, useEffect, useMemo } from 'react'; +import { useInventoryStore } from '@/store/useInventoryStore'; +import shared from '../../styles/shared.module.css'; +import styles from '../../styles/Characters.module.css'; +import { fetchPerks } from '../../lib/db'; +import { getPerkIconUrl, Perk } from './types'; + +export default function PerksPage() { + const store = useInventoryStore(); + const [perks, setPerks] = useState([]); + const [search, setSearch] = useState(''); + const [roleFilter, setRoleFilter] = useState<'all' | 'killer' | 'survivor'>('all'); + + useEffect(() => { + fetchPerks().then(setPerks); + }, []); + + const filtered = useMemo(() => { + return perks.filter(p => { + if (roleFilter === 'killer' && p.role !== 1) return false; + if (roleFilter === 'survivor' && p.role !== 2) return false; + if (search.trim() && !p.name.toLowerCase().includes(search.toLowerCase())) return false; + return true; + }); + }, [perks, search, roleFilter]); + + const handleClearAll = () => store.clearCategory('perks'); + const handleUnlockVisible = () => store.unlockAllInCategory('perks', Array.from(new Set([...store.unlockedPerks, ...filtered.map(p => p.id)]))); + const handleLockVisible = () => store.unlockAllInCategory('perks', store.unlockedPerks.filter(id => !filtered.find(p => p.id === id))); + + const activeCount = store.unlockedPerks.length; + + return ( +
+
+
+

Perks

+

{activeCount} active out of {perks.length} total

+
+ +
+ +
+ setSearch(e.target.value)} /> + +
+ + + +
+ + + {filtered.length} shown + + +
+ +
+ {filtered.map(perk => { + const unlocked = store.unlockedPerks.includes(perk.id); + const killer = perk.role === 1; + return ( +
store.toggleItem(perk.id, 'perks')} + > + {perk.name} + {perk.name} + + {killer ? 'Killer' : 'Survivor'} + +
+ ); + })} +
+
+ ); +} \ No newline at end of file diff --git a/app/perks/types.ts b/app/perks/types.ts new file mode 100644 index 0000000..e2cca3b --- /dev/null +++ b/app/perks/types.ts @@ -0,0 +1,13 @@ +import { DB_BASE_URL } from "@/lib/db"; + +export type Perk = { + id: string; + name: string; + iconFilePath: string; + role: number; +}; + +export const getPerkIconUrl = (iconFilePath: string) => { + const file = (iconFilePath.split('/').pop() ?? '').split('.')[0]; + return `${DB_BASE_URL}/icons/perk-icons/${file}.png`; +}; diff --git a/components/Sidebar.tsx b/components/Sidebar.tsx index 2dc638b..3aafceb 100644 --- a/components/Sidebar.tsx +++ b/components/Sidebar.tsx @@ -12,6 +12,7 @@ export default function Sidebar() { Characters Customizations Items, offerings & addons + Perks DLCs