feat: add addons and perks to dashboard

This commit is contained in:
2026-06-19 04:24:26 -03:00
parent ebc3dab8e6
commit f9b5fe5501
2 changed files with 138 additions and 38 deletions
+35 -13
View File
@@ -4,7 +4,7 @@ import { useState, useEffect, useMemo } from 'react';
import { useInventoryStore } from '@/store/useInventoryStore'; import { useInventoryStore } from '@/store/useInventoryStore';
import styles from '../styles/Home.module.css'; import styles from '../styles/Home.module.css';
import { isNamedDLC } from '@/lib/utils'; import { isNamedDLC } from '@/lib/utils';
import { fetchCharacters, fetchItems, fetchOfferings, fetchCustomizations, fetchDLCs } from '@/lib/db'; import { fetchCharacters, fetchItems, fetchOfferings, fetchCustomizations, fetchDLCs, fetchAddons, fetchPerks } from '@/lib/db';
export default function Home() { export default function Home() {
const store = useInventoryStore(); const store = useInventoryStore();
@@ -14,6 +14,8 @@ export default function Home() {
const [itemsCount, setItemsCount] = useState(0); const [itemsCount, setItemsCount] = useState(0);
const [offeringsCount, setOfferingsCount] = useState(0); const [offeringsCount, setOfferingsCount] = useState(0);
const [dlcsCount, setDlcsCount] = 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('');
@@ -23,13 +25,17 @@ export default function Home() {
fetchItems(), fetchItems(),
fetchOfferings(), fetchOfferings(),
fetchCustomizations(), fetchCustomizations(),
fetchDLCs() fetchDLCs(),
]).then(([chars, items, offerings, customizations, dlcs]) => { fetchAddons(),
fetchPerks()
]).then(([chars, items, offerings, customizations, dlcs, addons, perks]) => {
setCharCount(chars.length); setCharCount(chars.length);
setItemsCount(items.length); setItemsCount(items.length);
setOfferingsCount(offerings.length); setOfferingsCount(offerings.length);
setCustCount(customizations.length); setCustCount(customizations.length);
setDlcsCount(dlcs.filter(isNamedDLC).length); setDlcsCount(dlcs.filter(isNamedDLC).length);
setAddonsCount(addons.length);
setPerksCount(perks.length);
}); });
}, []); }, []);
@@ -55,25 +61,33 @@ export default function Home() {
unlockedCharacters: store.unlockedCharacters, unlockedCharacters: store.unlockedCharacters,
unlockedCustomizations: store.unlockedCustomizations, unlockedCustomizations: store.unlockedCustomizations,
unlockedDLCs: store.unlockedDLCs, unlockedDLCs: store.unlockedDLCs,
unlockedPerks: store.unlockedPerks,
items: store.items, items: store.items,
offerings: store.offerings offerings: store.offerings,
addons: store.addons
}, null, 2); }, null, 2);
}; };
const handleImport = async () => { const handleImport = async () => {
const parsed = JSON.parse(importText); try {
store.importProfile(parsed); const parsed = JSON.parse(importText);
store.importProfile(parsed);
} catch (e) {
console.error("Invalid JSON", e);
}
}; };
useEffect(() => { useEffect(() => {
setImportText(getExportText()); setImportText(getExportText());
}, [store.unlockedCharacters, store.unlockedCustomizations, store.unlockedDLCs, store.items, store.offerings]); }, [store.unlockedCharacters, store.unlockedCustomizations, store.unlockedDLCs, store.unlockedPerks, store.items, store.offerings, store.addons]);
/* /*
stats stats
*/ */
const unlockedItems = useMemo(() => Object.values(store.items).filter(qty => qty > 0).length, [store.items]); 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 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 (<div className={styles.container}> return (<div className={styles.container}>
<header className={styles.header}> <header className={styles.header}>
@@ -85,27 +99,35 @@ export default function Home() {
<section className={styles.statsGrid}> <section className={styles.statsGrid}>
<div className={styles.statCard}> <div className={styles.statCard}>
<div className={styles.statLabel}>Characters</div> <div className={styles.statLabel}>Characters</div>
<div className={styles.statValue}>{store.unlockedCharacters.length} / {charCount || '-'}</div> <div className={styles.statValue}>{store.unlockedCharacters.length} <span className={styles.statTotal}>/ {charCount || '-'}</span></div>
</div> </div>
<div className={styles.statCard}> <div className={styles.statCard}>
<div className={styles.statLabel}>Customizations</div> <div className={styles.statLabel}>Customizations</div>
<div className={styles.statValue}>{store.unlockedCustomizations.length} / {custCount || '-'}</div> <div className={styles.statValue}>{store.unlockedCustomizations.length} <span className={styles.statTotal}>/ {custCount || '-'}</span></div>
</div> </div>
<div className={styles.statCard}> <div className={styles.statCard}>
<div className={styles.statLabel}>DLCs</div> <div className={styles.statLabel}>DLCs</div>
<div className={styles.statValue}>{store.unlockedDLCs.length} / {dlcsCount || '-'}</div> <div className={styles.statValue}>{store.unlockedDLCs.length} <span className={styles.statTotal}>/ {dlcsCount || '-'}</span></div>
</div> </div>
<div className={styles.statCard}> <div className={styles.statCard}>
<div className={styles.statLabel}>Items</div> <div className={styles.statLabel}>Items</div>
<div className={styles.statValue}>{unlockedItems} / {itemsCount || '-'}</div> <div className={styles.statValue}>{unlockedItems} <span className={styles.statTotal}>/ {itemsCount || '-'}</span></div>
</div> </div>
<div className={styles.statCard}> <div className={styles.statCard}>
<div className={styles.statLabel}>Offerings</div> <div className={styles.statLabel}>Offerings</div>
<div className={styles.statValue}>{unlockedOfferings} / {offeringsCount || '-'}</div> <div className={styles.statValue}>{unlockedOfferings} <span className={styles.statTotal}>/ {offeringsCount || '-'}</span></div>
</div>
<div className={styles.statCard}>
<div className={styles.statLabel}>Addons</div>
<div className={styles.statValue}>{unlockedAddons} <span className={styles.statTotal}>/ {addonsCount || '-'}</span></div>
</div>
<div className={styles.statCard}>
<div className={styles.statLabel}>Perks</div>
<div className={styles.statValue}>{unlockedPerks} <span className={styles.statTotal}>/ {perksCount || '-'}</span></div>
</div> </div>
</section> </section>
{/* profile import export stuff */} {/* Control panels */}
<section className={styles.panelGrid}> <section className={styles.panelGrid}>
<div className={styles.card}> <div className={styles.card}>
<h3 className={styles.cardTitle}>Profile Import / Export</h3> <h3 className={styles.cardTitle}>Profile Import / Export</h3>
+103 -25
View File
@@ -29,11 +29,23 @@
.statsGrid { .statsGrid {
display: grid; display: grid;
grid-template-columns: repeat(5, 1fr); grid-template-columns: repeat(4, 1fr);
gap: 1.5rem; gap: 1.5rem;
margin-bottom: 3rem; margin-bottom: 3rem;
} }
@media (max-width: 1000px) {
.statsGrid {
grid-template-columns: repeat(3, 1fr);
}
}
@media (max-width: 600px) {
.statsGrid {
grid-template-columns: repeat(2, 1fr);
}
}
.statCard { .statCard {
background: #0d0d0d; background: #0d0d0d;
border: 1px solid #1a1a1a; border: 1px solid #1a1a1a;
@@ -64,17 +76,30 @@
line-height: 1; line-height: 1;
} }
.statTotal {
font-size: 0.9rem;
color: #555555;
}
.panelGrid { .panelGrid {
display: grid; display: grid;
grid-template-columns: 1fr; grid-template-columns: 1fr 1.2fr;
gap: 2rem; gap: 2rem;
} }
@media (max-width: 900px) {
.panelGrid {
grid-template-columns: 1fr;
}
}
.card { .card {
background: #0d0d0d; background: #0d0d0d;
border: 1px solid #1a1a1a; border: 1px solid #1a1a1a;
border-top: 3px solid #630000; border-top: 3px solid #630000;
padding: 2rem; padding: 2rem;
display: flex;
flex-direction: column;
} }
.cardTitle { .cardTitle {
@@ -83,12 +108,7 @@
color: #ffffff; color: #ffffff;
margin: 0 0 1.5rem 0; margin: 0 0 1.5rem 0;
letter-spacing: 0.05em; letter-spacing: 0.05em;
} font-family: 'Oswald', sans-serif;
.formatBtnActive {
background: #a30000;
border-color: #ff0000;
color: #ffffff;
} }
.textarea { .textarea {
@@ -149,26 +169,84 @@
color: #ffffff; color: #ffffff;
} }
.toast { .toggleRow {
position: fixed; display: flex;
bottom: 2rem; align-items: center;
right: 2rem; justify-content: space-between;
background: #0d0d0d; padding: 1rem 0;
border: 1px solid #00ff66; border-bottom: 1px solid #1a1a1a;
color: #00ff66; }
padding: 1rem 1.5rem;
font-size: 0.8rem; .toggleRow:last-child {
text-transform: uppercase; border-bottom: none;
}
.toggleInfo {
display: flex;
flex-direction: column;
gap: 0.2rem;
padding-right: 1.5rem;
}
.toggleLabel {
font-size: 0.85rem;
font-weight: bold; font-weight: bold;
animation: slideIn 0.3s ease-out forwards; text-transform: uppercase;
color: #ffffff;
letter-spacing: 0.05em;
font-family: 'Oswald', sans-serif;
} }
.toastError { .toggleDesc {
border-color: #ff0000; font-size: 0.68rem;
color: #ff0000; color: #555555;
line-height: 1.3;
} }
@keyframes slideIn { .switch {
from { transform: translateY(20px); opacity: 0; } position: relative;
to { transform: translateY(0); opacity: 1; } display: inline-block;
width: 48px;
height: 24px;
flex-shrink: 0;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #121212;
border: 1px solid #333333;
transition: 0.2s;
}
.slider:before {
position: absolute;
content: "";
height: 16px;
width: 16px;
left: 3px;
bottom: 3px;
background-color: #555555;
transition: 0.2s;
}
input:checked + .slider {
background-color: #2b0000;
border-color: #a30000;
}
input:checked + .slider:before {
transform: translateX(24px);
background-color: #a30000;
box-shadow: 0 0 8px rgba(163, 0, 0, 0.7);
} }