feat: add dashboard and layout
This commit is contained in:
+13
-4
@@ -1,9 +1,11 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
|
import Sidebar from "../components/Sidebar";
|
||||||
|
import styles from "../styles/Layout.module.css";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Create Next App",
|
title: "Hex: Unlocked",
|
||||||
description: "Generated by create next app",
|
description: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
@@ -13,7 +15,14 @@ export default function RootLayout({
|
|||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<body>{children}</body>
|
<body>
|
||||||
|
<div className={styles.layoutContainer}>
|
||||||
|
<Sidebar />
|
||||||
|
<main className={styles.mainContent}>
|
||||||
|
{children}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
+129
-6
@@ -1,9 +1,132 @@
|
|||||||
import Sidebar from "./components/Sidebar"
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useEffect, useMemo } from 'react';
|
||||||
|
import { useInventoryStore } from '@/store/useInventoryStore';
|
||||||
|
import styles from '../styles/Home.module.css';
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
const store = useInventoryStore();
|
||||||
<main>
|
|
||||||
<Sidebar/>
|
const [charCount, setCharCount] = useState(0);
|
||||||
</main>
|
const [custCount, setCustCount] = useState(0);
|
||||||
);
|
const [itemsCount, setItemsCount] = useState(0);
|
||||||
|
const [offeringsCount, setOfferingsCount] = useState(0);
|
||||||
|
const [dlcsCount, setDlcsCount] = useState(0);
|
||||||
|
|
||||||
|
const [importText, setImportText] = useState('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
Promise.all([
|
||||||
|
fetch('/data/characters.json').then(r => r.json()).catch(() => []),
|
||||||
|
fetch('/data/items.json').then(r => r.json()).catch(() => []),
|
||||||
|
fetch('/data/offerings.json').then(r => r.json()).catch(() => []),
|
||||||
|
fetch('/data/customization_items.json').then(r => r.json()).catch(() => []),
|
||||||
|
fetch('/data/dlcs.json').then(r => r.json()).catch(() => [])
|
||||||
|
]).then(([chars, items, offerings, customizations, dlcs]) => {
|
||||||
|
setCharCount(chars.length);
|
||||||
|
setItemsCount(items.length);
|
||||||
|
setOfferingsCount(offerings.length);
|
||||||
|
setCustCount(customizations.length);
|
||||||
|
setDlcsCount(dlcs.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 handleCopy = () => {
|
||||||
|
navigator.clipboard.writeText(importText);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getExportText = () => {
|
||||||
|
return JSON.stringify({
|
||||||
|
unlockedCharacters: store.unlockedCharacters,
|
||||||
|
unlockedCustomizations: store.unlockedCustomizations,
|
||||||
|
unlockedDLCs: store.unlockedDLCs,
|
||||||
|
items: store.items,
|
||||||
|
offerings: store.offerings
|
||||||
|
}, null, 2);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleImport = async () => {
|
||||||
|
const parsed = JSON.parse(importText);
|
||||||
|
store.importProfile(parsed);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setImportText(getExportText());
|
||||||
|
}, [store.unlockedCharacters, store.unlockedCustomizations, store.unlockedDLCs, store.items, store.offerings]);
|
||||||
|
|
||||||
|
/*
|
||||||
|
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]);
|
||||||
|
|
||||||
|
return (<div className={styles.container}>
|
||||||
|
<header className={styles.header}>
|
||||||
|
<h1 className={styles.title}>Dashboard</h1>
|
||||||
|
<p className={styles.subtitle}>Status overview and profile management</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* stats cards */}
|
||||||
|
<section className={styles.statsGrid}>
|
||||||
|
<div className={styles.statCard}>
|
||||||
|
<div className={styles.statLabel}>Characters</div>
|
||||||
|
<div className={styles.statValue}>{store.unlockedCharacters.length} / {charCount || '-'}</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.statCard}>
|
||||||
|
<div className={styles.statLabel}>Customizations</div>
|
||||||
|
<div className={styles.statValue}>{store.unlockedCustomizations.length} / {custCount || '-'}</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.statCard}>
|
||||||
|
<div className={styles.statLabel}>DLCs</div>
|
||||||
|
<div className={styles.statValue}>{store.unlockedDLCs.length} / {dlcsCount || '-'}</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.statCard}>
|
||||||
|
<div className={styles.statLabel}>Items</div>
|
||||||
|
<div className={styles.statValue}>{unlockedItems} / {itemsCount || '-'}</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.statCard}>
|
||||||
|
<div className={styles.statLabel}>Offerings</div>
|
||||||
|
<div className={styles.statValue}>{unlockedOfferings} / {offeringsCount || '-'}</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* profile import export stuff */}
|
||||||
|
<section className={styles.panelGrid}>
|
||||||
|
<div className={styles.card}>
|
||||||
|
<h3 className={styles.cardTitle}>Profile Import / Export</h3>
|
||||||
|
|
||||||
|
<textarea
|
||||||
|
className={styles.textarea}
|
||||||
|
value={importText}
|
||||||
|
onChange={(e) => setImportText(e.target.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className={styles.btnRow}>
|
||||||
|
<button className={styles.primaryBtn} onClick={handleImport}>
|
||||||
|
Validate & Import
|
||||||
|
</button>
|
||||||
|
<button className={styles.secondaryBtn} onClick={handleCopy}>
|
||||||
|
Copy to Clipboard
|
||||||
|
</button>
|
||||||
|
<button className={styles.secondaryBtn} onClick={handleDownload}>
|
||||||
|
Download file
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user