feat: Initialize project with Vite, React, and TypeScript

Sets up the foundational structure for the Obsidian | RP Plattform. This includes configuring Vite as the build tool, integrating React for the UI, and establishing TypeScript for type safety. Also includes initial styling and placeholder data to define the application's core interfaces.
This commit is contained in:
Lars Behrends
2025-12-28 02:15:09 +01:00
parent 4ab4a1d64a
commit d1b797a320
23 changed files with 2514 additions and 8 deletions

91
components/IconSet.tsx Normal file
View File

@@ -0,0 +1,91 @@
import React from 'react';
// UI Icons (Lucide-style wrappers)
export const Icons = {
Home: ({ className }: { className?: string }) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}><path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
),
Users: ({ className }: { className?: string }) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
),
Map: ({ className }: { className?: string }) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}><polygon points="3 6 9 3 15 6 21 3 21 18 15 21 9 18 3 21"/><line x1="9" x2="9" y1="3" y2="18"/><line x1="15" x2="15" y1="6" y2="21"/></svg>
),
Layers: ({ className }: { className?: string }) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}><polygon points="12 2 2 7 12 12 22 7 12 2"/><polyline points="2 17 12 22 22 17"/><polyline points="2 12 12 17 22 12"/></svg>
),
Terminal: ({ className }: { className?: string }) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}><polyline points="4 17 10 11 4 5"/><line x1="12" x2="20" y1="19" y2="19"/></svg>
),
Box: ({ className }: { className?: string }) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" x2="12" y1="22.08" y2="12"/></svg>
),
Search: ({ className }: { className?: string }) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}><circle cx="11" cy="11" r="8"/><line x1="21" x2="16.65" y1="21" y2="16.65"/></svg>
),
Crown: ({ className }: { className?: string }) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}><path d="m2 4 3 12h14l3-12-6 7-4-7-4 7-6-7zm3 16h14"/></svg>
),
Shield: ({ className }: { className?: string }) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
),
Coins: ({ className }: { className?: string }) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}><circle cx="8" cy="8" r="6"/><path d="M18.09 10.37A6 6 0 1 1 10.34 18"/><path d="M7 6h1v4"/><path d="M17.121 6.364l1.414 1.414"/><path d="M15.536 11H13.5"/></svg>
),
Scroll: ({ className }: { className?: string }) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}><path d="M19 4H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2zM4 10h16M4 14h16M4 18h16"/></svg>
),
ShoppingBag: ({ className }: { className?: string }) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}><path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4Z"/><path d="M3 6h18"/><path d="M16 10a4 4 0 0 1-8 0"/></svg>
),
Tag: ({ className }: { className?: string }) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}><path d="M12 2H2v10l9.29 9.29c.94.94 2.48.94 3.42 0l6.58-6.58c.94-.94.94-2.48 0-3.42L12 2Z"/><path d="M7 7h.01"/></svg>
),
Hammer: ({ className }: { className?: string }) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}><path d="m15 12-8.5 8.5c-.83.83-2.17.83-3 0 0 0 0 0 0 0a2.12 2.12 0 0 1 0-3L12 9"/><path d="M17.64 15 22 10.64"/><path d="m20.91 11.7-1.25-1.25c-.6-.6-.93-1.4-.93-2.25V7.86c0-.55-.45-1-1-1H16.4c-.84 0-1.65-.33-2.25-.93L12.9 4.68c-.6-.6-1.4-.93-2.25-.93H4.86c-.55 0-1 .45-1 1v1.36c0 .84.33 1.65.93 2.25L12 15.64"/></svg>
)
};
// Algorithmic Minecraft Item Icons (Vector)
export const ItemIcon = ({ type, className }: { type: string; className?: string }) => {
const commonClasses = `w-full h-full ${className}`;
if (type === 'tool') {
return (
<svg viewBox="0 0 32 32" className={commonClasses} fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="square">
<path d="M24 8 L28 4 L24 8 Z" fill="currentColor" className="text-accentInfo/50" />
<path d="M22 10 L26 6" className="text-accentInfo" />
<path d="M10 22 L22 10" className="text-textMain" />
<path d="M4 28 L8 24" className="text-textMuted" />
</svg>
);
}
if (type === 'block') {
return (
<svg viewBox="0 0 32 32" className={commonClasses} fill="none" stroke="currentColor" strokeWidth="2">
<path d="M16 2 L2 9 L16 16 L30 9 L16 2 Z" fill="currentColor" className="text-textMuted/20" />
<path d="M2 9 V 23 L16 30 V 16" fill="currentColor" className="text-textMuted/40" />
<path d="M30 9 V 23 L16 30" fill="currentColor" className="text-textMuted/10" />
</svg>
);
}
if (type === 'consumable') {
return (
<svg viewBox="0 0 32 32" className={commonClasses} fill="none" stroke="currentColor" strokeWidth="2">
<path d="M10 12 C10 6 22 6 22 12 C22 18 16 26 16 26 C16 26 10 18 10 12" fill="currentColor" className="text-accentWarn/20" />
<path d="M16 6 V 4" strokeLinecap="round" />
<path d="M18 6 V 3" strokeLinecap="round" />
</svg>
);
}
// Misc / Default
return (
<svg viewBox="0 0 32 32" className={commonClasses} fill="none" stroke="currentColor" strokeWidth="2">
<circle cx="16" cy="16" r="10" className="text-textMuted/20" />
<path d="M16 6 V 26 M 6 16 H 26" className="text-accentInfo/50" />
</svg>
);
};

View File

@@ -0,0 +1,63 @@
import React, { useState } from 'react';
import { Item } from '../types';
import { ItemIcon } from './IconSet';
interface InventoryGridProps {
items: (Item | null)[];
}
const InventoryGrid: React.FC<InventoryGridProps> = ({ items }) => {
return (
<div className="bg-surface rounded-xl border border-border p-4 shadow-card">
<div className="flex justify-between items-center mb-4">
<h3 className="text-sm font-bold uppercase tracking-wider text-textMuted">Inventar</h3>
<div className="text-xs text-textMuted bg-surfaceHighlight px-2 py-1 rounded">
{items.filter(i => i !== null).length} / {items.length} Plätze
</div>
</div>
<div className="grid grid-cols-5 sm:grid-cols-9 gap-2">
{items.map((item, index) => (
<div
key={index}
className="group relative aspect-square bg-surfaceHighlight/50 border border-white/5 rounded-md transition-all duration-200 hover:scale-105 hover:bg-surfaceHighlight hover:border-accentInfo/30 hover:shadow-glow hover:z-10 cursor-pointer flex items-center justify-center"
>
{item ? (
<>
<div className="w-8 h-8 text-textMuted group-hover:text-accentInfo transition-colors">
<ItemIcon type={item.type} />
</div>
{/* Count Badge */}
{item.count > 1 && (
<span className="absolute bottom-1 right-1 font-mono text-[10px] font-bold text-textMain leading-none drop-shadow-md">
{item.count}
</span>
)}
{/* Tooltip */}
<div className="absolute opacity-0 group-hover:opacity-100 transition-opacity duration-200 bottom-full left-1/2 -translate-x-1/2 mb-2 w-max max-w-[150px] z-50 pointer-events-none">
<div className="bg-surface border border-border rounded p-2 shadow-xl text-xs">
<div className={`font-semibold ${item.rarity === 'epic' ? 'text-accentInfo' : 'text-textMain'}`}>
{item.name}
</div>
{item.nbtSummary && (
<div className="text-[10px] text-textMuted mt-1 border-t border-white/10 pt-1">
{item.nbtSummary}
</div>
)}
</div>
</div>
</>
) : (
// Empty slot styling
<div className="absolute inset-2 border border-dashed border-white/5 rounded-sm" />
)}
</div>
))}
</div>
</div>
);
};
export default InventoryGrid;

126
components/Layout.tsx Normal file
View File

@@ -0,0 +1,126 @@
import React, { useState } from 'react';
import { Icons } from './IconSet';
interface LayoutProps {
children: React.ReactNode;
activeTab: string;
onNavigate: (tab: string) => void;
}
const NavItem = ({
active,
label,
onClick
}: {
active: boolean;
label: string;
onClick: () => void;
}) => (
<button
onClick={onClick}
className={`text-sm font-medium transition-colors duration-200 px-1 py-4 border-b-2 ${
active
? 'text-textMain border-accentInfo'
: 'text-textMuted border-transparent hover:text-textMain hover:border-border'
}`}
>
{label}
</button>
);
const Layout: React.FC<LayoutProps> = ({ children, activeTab, onNavigate }) => {
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
return (
<div className="min-h-screen flex flex-col font-sans">
{/* Top Header - Website Style */}
<header className="sticky top-0 z-40 bg-background/80 backdrop-blur-md border-b border-border">
<div className="max-w-7xl mx-auto px-6 h-16 flex items-center justify-between">
<div className="flex items-center gap-10">
{/* Logo */}
<div
className="flex items-center gap-3 cursor-pointer group"
onClick={() => onNavigate('dashboard')}
>
<div className="w-8 h-8 bg-gradient-to-br from-accentInfo to-blue-900 rounded flex items-center justify-center shadow-glow group-hover:shadow-lg transition-shadow">
<span className="font-bold text-white text-sm">P.V.</span>
</div>
<span className="font-bold text-lg tracking-tight text-textMain">Projekt: Vollidion</span>
</div>
{/* Desktop Nav */}
<nav className="hidden md:flex items-center gap-6 h-full">
<NavItem active={activeTab === 'dashboard'} label="Übersicht" onClick={() => onNavigate('dashboard')} />
<NavItem active={activeTab === 'cities'} label="Städte" onClick={() => onNavigate('cities')} />
<NavItem active={activeTab === 'players'} label="Bürger" onClick={() => onNavigate('players')} />
{/* <NavItem active={activeTab === 'organizations'} label="Organisationen" onClick={() => onNavigate('organizations')} />*/}
<NavItem active={activeTab === 'projects'} label="Unternehmen" onClick={() => onNavigate('projects')} />
</nav>
</div>
<div className="flex items-center gap-4">
<button
onClick={() => onNavigate('datapack')}
className="hidden md:flex items-center gap-2 text-xs font-medium text-textMain hover:text-accentInfo transition-colors"
>
<Icons.Box className="w-3 h-3" />
<span>Datapack holen</span>
</button>
<button
onClick={() => onNavigate('setup')}
className="hidden md:flex items-center gap-2 text-xs font-medium text-textMuted hover:text-accentInfo transition-colors border border-border rounded-full px-4 py-1.5 hover:bg-surfaceHighlight"
>
<Icons.Terminal className="w-3 h-3" />
<span>Admin Setup</span>
</button>
{/* Mobile Menu Toggle */}
<button
className="md:hidden text-textMuted hover:text-textMain"
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
>
<Icons.Layers className="w-6 h-6" />
</button>
</div>
</div>
{/* Mobile Nav Dropdown */}
{mobileMenuOpen && (
<div className="md:hidden border-t border-border bg-surface px-6 py-4 space-y-4 shadow-xl">
<div onClick={() => { onNavigate('dashboard'); setMobileMenuOpen(false); }} className="block py-2 text-textMuted hover:text-textMain">Übersicht</div>
<div onClick={() => { onNavigate('cities'); setMobileMenuOpen(false); }} className="block py-2 text-textMuted hover:text-textMain">Städte</div>
<div onClick={() => { onNavigate('players'); setMobileMenuOpen(false); }} className="block py-2 text-textMuted hover:text-textMain">Bürger</div>
<div onClick={() => { onNavigate('organizations'); setMobileMenuOpen(false); }} className="block py-2 text-textMuted hover:text-textMain">Organisationen</div>
<div onClick={() => { onNavigate('projects'); setMobileMenuOpen(false); }} className="block py-2 text-textMuted hover:text-textMain">Unternehmen</div>
<div onClick={() => { onNavigate('datapack'); setMobileMenuOpen(false); }} className="block py-2 text-textMain">Datapack holen</div>
<div onClick={() => { onNavigate('setup'); setMobileMenuOpen(false); }} className="block py-2 text-accentInfo font-mono text-sm border-t border-white/5 pt-4">Admin Setup >_</div>
</div>
)}
</header>
{/* Main Content - Page Flow */}
<main className="flex-1 w-full max-w-7xl mx-auto px-6 py-12 md:py-16">
<div className="animate-in fade-in duration-700 slide-in-from-bottom-4">
{children}
</div>
</main>
{/* Footer - Adds to the "Website" feel */}
<footer className="border-t border-border mt-auto bg-surface/30">
<div className="max-w-7xl mx-auto px-6 py-10 flex flex-col md:flex-row justify-between items-center text-sm text-textMuted">
<div className="flex items-center gap-2 mb-4 md:mb-0 opacity-50">
<div className="w-4 h-4 bg-textMuted rounded-full"></div>
<p>© 2024 Obsidian Platform</p>
</div>
<div className="flex gap-8">
<span className="cursor-pointer hover:text-textMain transition-colors">Dokumentation</span>
<span className="cursor-pointer hover:text-textMain transition-colors">Server Status</span>
<span className="cursor-pointer hover:text-textMain transition-colors">Datenschutz</span>
</div>
</div>
</footer>
</div>
);
};
export default Layout;