mirror of
https://github.com/ceratic/project_vollidioten_website.git
synced 2026-05-14 00:16:47 +02:00
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:
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
168
App.tsx
Normal file
168
App.tsx
Normal file
@@ -0,0 +1,168 @@
|
||||
import React, { useState } from 'react';
|
||||
import Layout from './components/Layout';
|
||||
import Dashboard from './pages/Dashboard';
|
||||
import PlayerProfile from './pages/PlayerProfile';
|
||||
import SetupGuide from './pages/SetupGuide';
|
||||
import Projects from './pages/Projects';
|
||||
import Organizations from './pages/Organizations';
|
||||
import Cities from './pages/Cities';
|
||||
import CityProfile from './pages/CityProfile';
|
||||
import ProjectProfile from './pages/ProjectProfile';
|
||||
import DatapackGenerator from './pages/DatapackGenerator';
|
||||
import { MOCK_PLAYERS, MOCK_ORGS, MOCK_PROJECTS } from './constants';
|
||||
import { Icons } from './components/IconSet';
|
||||
|
||||
function App() {
|
||||
const [activeTab, setActiveTab] = useState('dashboard');
|
||||
const [selectedPlayerId, setSelectedPlayerId] = useState<string | null>(null);
|
||||
const [selectedCityId, setSelectedCityId] = useState<string | null>(null);
|
||||
const [selectedProjectId, setSelectedProjectId] = useState<string | null>(null);
|
||||
const [selectedOrgId, setSelectedOrgId] = useState<string | null>(null);
|
||||
|
||||
const handleNavigate = (tab: string) => {
|
||||
setActiveTab(tab);
|
||||
if (tab !== 'players') setSelectedPlayerId(null);
|
||||
if (tab !== 'cities') setSelectedCityId(null);
|
||||
if (tab !== 'projects') setSelectedProjectId(null);
|
||||
if (tab !== 'organizations') setSelectedOrgId(null);
|
||||
};
|
||||
|
||||
// Helper to jump to a player from another view
|
||||
const navigateToPlayer = (id: string) => {
|
||||
setSelectedPlayerId(id);
|
||||
setActiveTab('players');
|
||||
};
|
||||
|
||||
// Helper to jump to a project from another view
|
||||
const navigateToProject = (id: string) => {
|
||||
setSelectedProjectId(id);
|
||||
setActiveTab('projects');
|
||||
};
|
||||
|
||||
// Helper to jump to an org/city
|
||||
const navigateToOrg = (id: string) => {
|
||||
const org = MOCK_ORGS.find(o => o.id === id);
|
||||
if (org?.type === 'City') {
|
||||
setSelectedCityId(id);
|
||||
setActiveTab('cities');
|
||||
} else {
|
||||
setSelectedOrgId(id);
|
||||
setActiveTab('organizations');
|
||||
}
|
||||
};
|
||||
|
||||
const renderContent = () => {
|
||||
if (activeTab === 'dashboard') return <Dashboard />;
|
||||
|
||||
if (activeTab === 'projects') {
|
||||
if (selectedProjectId) {
|
||||
const project = MOCK_PROJECTS.find(p => p.id === selectedProjectId);
|
||||
if (project) return (
|
||||
<ProjectProfile
|
||||
project={project}
|
||||
onBack={() => setSelectedProjectId(null)}
|
||||
onSelectPlayer={navigateToPlayer}
|
||||
onSelectOrg={navigateToOrg}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <Projects onSelectProject={setSelectedProjectId} />;
|
||||
}
|
||||
|
||||
if (activeTab === 'organizations') {
|
||||
if (selectedOrgId) {
|
||||
const org = MOCK_ORGS.find(o => o.id === selectedOrgId);
|
||||
if (org) return (
|
||||
<CityProfile
|
||||
city={org}
|
||||
onBack={() => setSelectedOrgId(null)}
|
||||
backLabel="Zurück zum Verzeichnis"
|
||||
onSelectPlayer={navigateToPlayer}
|
||||
onSelectProject={navigateToProject}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <Organizations onSelectOrg={setSelectedOrgId} />;
|
||||
}
|
||||
|
||||
if (activeTab === 'setup') return <SetupGuide />;
|
||||
|
||||
if (activeTab === 'datapack') return <DatapackGenerator />;
|
||||
|
||||
if (activeTab === 'cities') {
|
||||
if (selectedCityId) {
|
||||
const city = MOCK_ORGS.find(o => o.id === selectedCityId);
|
||||
if (city) return (
|
||||
<CityProfile
|
||||
city={city}
|
||||
onBack={() => setSelectedCityId(null)}
|
||||
backLabel="Zurück zu Städte"
|
||||
onSelectPlayer={navigateToPlayer}
|
||||
onSelectProject={navigateToProject}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <Cities onSelectCity={setSelectedCityId} />;
|
||||
}
|
||||
|
||||
if (activeTab === 'players') {
|
||||
if (selectedPlayerId) {
|
||||
const player = MOCK_PLAYERS.find(p => p.uuid === selectedPlayerId);
|
||||
if (player) return <PlayerProfile player={player} onBack={() => setSelectedPlayerId(null)} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="animate-in fade-in">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h2 className="text-2xl font-bold">Bürgerverzeichnis</h2>
|
||||
<div className="relative">
|
||||
<Icons.Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-textMuted" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Filtern nach Tag oder Name..."
|
||||
className="bg-surfaceHighlight border border-border rounded-lg pl-10 pr-4 py-2 text-sm text-textMain focus:border-accentInfo focus:outline-none w-64 transition-colors"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
|
||||
{MOCK_PLAYERS.map(player => (
|
||||
<div
|
||||
key={player.uuid}
|
||||
onClick={() => setSelectedPlayerId(player.uuid)}
|
||||
className="group bg-surface border border-border p-4 rounded-xl cursor-pointer hover:border-accentInfo/50 transition-all duration-200 hover:shadow-card"
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 rounded-md flex items-center justify-center font-bold text-lg text-textMuted group-hover:text-textMain transition-colors">
|
||||
<img src={"https://minotar.net/armor/bust/"+player.username+"/500.png"}></img>
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-semibold text-textMain group-hover:text-accentInfo transition-colors">{player.username}</div>
|
||||
<div className="text-xs text-textMuted mt-1 flex gap-2">
|
||||
{player.tags.slice(0, 2).map(t => <span key={t}>{t}</span>)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-[50vh] text-textMuted">
|
||||
<Icons.Box className="w-12 h-12 mb-4 opacity-20" />
|
||||
<p>Modul <strong>{activeTab}</strong> wird derzeit gewartet.</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout activeTab={activeTab} onNavigate={handleNavigate}>
|
||||
{renderContent()}
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
25
README.md
25
README.md
@@ -1,11 +1,20 @@
|
||||
<div align="center">
|
||||
|
||||
<img width="1200" height="475" alt="GHBanner" src="https://github.com/user-attachments/assets/0aa67016-6eaf-458a-adb2-6e31a0763ed6" />
|
||||
|
||||
<h1>Built with AI Studio</h2>
|
||||
|
||||
<p>The fastest path from prompt to production with Gemini.</p>
|
||||
|
||||
<a href="https://aistudio.google.com/apps">Start building</a>
|
||||
|
||||
</div>
|
||||
|
||||
# Run and deploy your AI Studio app
|
||||
|
||||
This contains everything you need to run your app locally.
|
||||
|
||||
View your app in AI Studio: https://ai.studio/apps/drive/1o1Z6erDhereYS10vVy3VaRuXeUPxu-Nh
|
||||
|
||||
## Run Locally
|
||||
|
||||
**Prerequisites:** Node.js
|
||||
|
||||
|
||||
1. Install dependencies:
|
||||
`npm install`
|
||||
2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key
|
||||
3. Run the app:
|
||||
`npm run dev`
|
||||
|
||||
91
components/IconSet.tsx
Normal file
91
components/IconSet.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
63
components/InventoryGrid.tsx
Normal file
63
components/InventoryGrid.tsx
Normal 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
126
components/Layout.tsx
Normal 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;
|
||||
191
constants.ts
Normal file
191
constants.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
import { Player, Organization, Project } from './types';
|
||||
|
||||
export const MOCK_PLAYERS: Player[] = [
|
||||
{
|
||||
uuid: '80301bff-74df-4579-bcfc-082ac8d26b5b',
|
||||
username: 'kaiwastoshort',
|
||||
isOnline: true,
|
||||
tags: ['#Bürger', '#Händler'],
|
||||
stats: {
|
||||
playtimeHours: 482,
|
||||
level: 45,
|
||||
role: 'Bürger',
|
||||
organizationId: 'org-3' // Moved to Old Haven
|
||||
},
|
||||
inventory: [
|
||||
{ id: 'minecraft:diamond_pickaxe', name: 'Behutsamkeit Hacke', count: 1, type: 'tool', rarity: 'epic', nbtSummary: 'Eff V, Rep.' },
|
||||
{ id: 'minecraft:stone_bricks', name: 'Polierter Andesit', count: 64, type: 'block' },
|
||||
{ id: 'minecraft:stone_bricks', name: 'Polierter Andesit', count: 32, type: 'block' },
|
||||
null, null,
|
||||
{ id: 'minecraft:cooked_beef', name: 'Steak', count: 12, type: 'consumable' },
|
||||
{ id: 'minecraft:compass', name: 'Stadtkarte', count: 1, type: 'misc', nbtSummary: 'Ziel: Spawn' },
|
||||
null, null
|
||||
],
|
||||
storyMarkdown: `
|
||||
# Der Bauplan von V
|
||||
|
||||
> "Stein erinnert sich an das, was Eisen vergisst."
|
||||
|
||||
**V** kam während der *Großen Expansion* ins Tal. Ursprünglich ein einfacher Maurer, stieg er nach dem Entwurf des **Aquädukt-Systems** schnell in den Rängen auf.
|
||||
|
||||
### Bekannte Verbindungen
|
||||
* Die Händlergilde
|
||||
* Nördliche Entdecker
|
||||
|
||||
### Aktuelles Ziel
|
||||
Restaurierung des verfallenen Wachturms am östlichen Grat.
|
||||
`
|
||||
},
|
||||
{
|
||||
uuid: '8984c0b5-d912-4462-b189-c864fba4a1af',
|
||||
username: 'DrKButz',
|
||||
isOnline: false,
|
||||
tags: ['#Bauunternehmer'],
|
||||
stats: {
|
||||
playtimeHours: 120,
|
||||
level: 12,
|
||||
role: 'Unternehmer',
|
||||
organizationId: 'org-4'
|
||||
},
|
||||
inventory: [
|
||||
{ id: 'minecraft:redstone', name: 'Redstone Staub', count: 64, type: 'misc' },
|
||||
{ id: 'minecraft:comparator', name: 'Komparator', count: 16, type: 'misc' },
|
||||
null,
|
||||
{ id: 'minecraft:book', name: 'Forschungsnotizen', count: 1, type: 'misc', nbtSummary: 'Seite 12: Logikgatter' },
|
||||
null, null, null, null, null
|
||||
],
|
||||
storyMarkdown: `
|
||||
# Forschungslogbuch:
|
||||
|
||||
Spezialisiert auf automatisierte Logistik. Gerüchten zufolge hat er eine Maschine gebaut, die Items schneller sortieren kann als jeder Mensch.
|
||||
|
||||
* Sucht aktuell: Quarz
|
||||
* Verkauft: Auto-Schmelzöfen
|
||||
`
|
||||
},
|
||||
{
|
||||
uuid: 'b3b84518-03a2-4b48-8551-448c3f7a7d77',
|
||||
username: 'ceratic',
|
||||
isOnline: true,
|
||||
tags: ['#Bürger', '#Händler'],
|
||||
stats: {
|
||||
playtimeHours: 50,
|
||||
level: 5,
|
||||
role: 'Bürger',
|
||||
organizationId: 'org-3'
|
||||
},
|
||||
inventory: [],
|
||||
storyMarkdown: 'Einfach nur eine einfache Bäckerin, die gutes Brot liebt.'
|
||||
},
|
||||
{
|
||||
uuid: '3c10008b-1eb2-4d89-8b8a-6d03c821eb09',
|
||||
username: 'merkursun',
|
||||
isOnline: true,
|
||||
tags: ['#Bürger'],
|
||||
stats: {
|
||||
playtimeHours: 50,
|
||||
level: 5,
|
||||
role: 'Bürger',
|
||||
organizationId: 'org-4'
|
||||
},
|
||||
inventory: [],
|
||||
storyMarkdown: 'Einfach nur eine einfache Bäckerin, die gutes Brot liebt.'
|
||||
}
|
||||
];
|
||||
|
||||
export const MOCK_ORGS: Organization[] = [
|
||||
/* {
|
||||
id: 'org-2',
|
||||
name: 'Butz Building GmbH',
|
||||
type: 'Company',
|
||||
description: 'Baugewerbe.',
|
||||
memberCount: 1,
|
||||
status: 'active'
|
||||
},*/
|
||||
{
|
||||
id: 'org-3',
|
||||
name: 'Provisorium Null',
|
||||
type: 'City',
|
||||
description: 'Die erste Siedlung, eingebettet zwischen den Zwillingsgipfeln. Bekannt für das geschäftige Marktviertel und die alten Steinmauern.',
|
||||
memberCount: 6,
|
||||
status: 'active',
|
||||
mayor: '',
|
||||
establishedYear: 'Day 0',
|
||||
bannerUrl: 'images/screenshots/2025-12-28_01.11.10.png',
|
||||
gallery: [
|
||||
'images/screenshots/2025-12-28_01.12.07.png', // Market
|
||||
'images/screenshots/2025-12-28_01.12.17.png', // Castle
|
||||
'images/screenshots/2025-12-28_01.12.24.png' // Walls
|
||||
],
|
||||
cityStats: {
|
||||
//taxRate: 3.5,
|
||||
biome: 'Ebene / Gebirge',
|
||||
//defenseRating: 8,
|
||||
government: 'Feudalmonarchie',
|
||||
specialty: 'Handel & Stein'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'org-4',
|
||||
name: 'Sakura',
|
||||
type: 'City',
|
||||
description: 'Eine dunkle, biolumineszente Hafenstadt in den tiefen Höhlen. Heimat von Schmugglern und Händlern seltener Mineralien.',
|
||||
memberCount: 2,
|
||||
status: 'active',
|
||||
mayor: 'Kampfzwerk',
|
||||
establishedYear: 'Ära 2, Jahr 10',
|
||||
bannerUrl: 'images/screenshots/2025-12-28_01.11.32.png',
|
||||
gallery: [
|
||||
'images/screenshots/2025-12-28_01.11.38.png',
|
||||
'images/screenshots/2025-12-28_01.11.44.png'
|
||||
],
|
||||
cityStats: {
|
||||
taxRate: 15.0,
|
||||
biome: 'Deep Dark / Lush Caves',
|
||||
defenseRating: 4,
|
||||
government: 'Syndikat',
|
||||
specialty: 'Schmuggelware & Erze'
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
export const MOCK_PROJECTS: Project[] = [
|
||||
{
|
||||
id: 'ven-1',
|
||||
title: 'DrkButz Architektur & Mauerwerk',
|
||||
description: 'Führendes Architekturbüro, spezialisiert auf gotische Strukturen und Verteidigungsmauern. Wir entwerfen dein Vermächtnis.',
|
||||
category: 'Enterprise',
|
||||
status: 'active',
|
||||
progress: 85, // Reputation
|
||||
owner: 'DrKButz',
|
||||
employees: [],
|
||||
hiring: false,
|
||||
foundedDate: 'Zyklus 12',
|
||||
associatedOrgId: 'org-4', // Old Haven
|
||||
bannerUrl: 'images/screenshots/2025-12-28_01.11.49.png',
|
||||
shopCatalog: [
|
||||
{ id: 's2', name: 'Geländevermessung', description: '1 Stunde Beratung zur Geländevorbereitung.', price: 20, currency: 'Gold', stock: 10, type: 'service' },
|
||||
{ id: 's3', name: 'Mauerbau', description: 'Wir bauen deine Außenmauern. Preis pro Chunk.', price: 100, currency: 'Gold', stock: 5, type: 'service', materialsRequired: 'Kunde stellt Steinziegel' },
|
||||
{ id: 's4', name: 'Terraforming', description: 'Einebnung und Landschaftsbau.', price: 75, currency: 'Gold', stock: 2, type: 'service' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'ven-5',
|
||||
title: 'Tinas Tavern',
|
||||
description: 'Täglich frisches Brot zum Spawn-Markt geliefert. Lieferant der königlichen Garde.',
|
||||
category: 'Enterprise',
|
||||
status: 'active',
|
||||
progress: 98,
|
||||
owner: 'kaiwastoshort',
|
||||
employees: [],
|
||||
hiring: false,
|
||||
associatedOrgId: 'org-3',
|
||||
bannerUrl: 'images/screenshots/tinas.png',
|
||||
shopCatalog: [
|
||||
{ id: 'f1', name: 'Redstone Block', description: '', price: 2, currency: 'Dias', stock: 45, type: 'item' },
|
||||
{ id: 'f2', name: 'Honey', description: '', price: 5, currency: 'Dias', stock: 12, type: 'item' },
|
||||
{ id: 'f3', name: 'Slime', description: '', price: 10, currency: 'Dias', stock: 4, type: 'item' },
|
||||
{ id: 'f4', name: 'Bier', description: '', price: 3, currency: 'Dias', stock: 20, type: 'item' }
|
||||
]
|
||||
},
|
||||
];
|
||||
74
index.html
Normal file
74
index.html
Normal file
@@ -0,0 +1,74 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Projekt: Vollidion | Server Plattform</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
background: '#0f0f12',
|
||||
surface: '#1a1a1f',
|
||||
surfaceHighlight: '#27272f',
|
||||
border: '#2e2e36',
|
||||
textMain: '#e6e6ff',
|
||||
textMuted: '#8f9099',
|
||||
accentSuccess: '#57C254',
|
||||
accentWarn: '#FF5555',
|
||||
accentInfo: '#5E81AC',
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ['Inter', 'sans-serif'],
|
||||
mono: ['JetBrains Mono', 'monospace'],
|
||||
},
|
||||
boxShadow: {
|
||||
'card': '0 4px 20px rgba(0,0,0,0.3)',
|
||||
'glow': '0 0 15px rgba(94, 129, 172, 0.15)',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
body {
|
||||
background-color: #0f0f12;
|
||||
background-image: radial-gradient(circle at 50% 0%, #1e1e2e 0%, #0f0f12 50%);
|
||||
background-attachment: fixed;
|
||||
color: #e6e6ff;
|
||||
}
|
||||
/* Custom Scrollbar for SaaS feel */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #2e2e36;
|
||||
border-radius: 3px;
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #3e3e4a;
|
||||
}
|
||||
</style>
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
"react": "https://esm.sh/react@^19.2.3",
|
||||
"react-dom/": "https://esm.sh/react-dom@^19.2.3/",
|
||||
"react/": "https://esm.sh/react@^19.2.3/"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
15
index.tsx
Normal file
15
index.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
|
||||
const rootElement = document.getElementById('root');
|
||||
if (!rootElement) {
|
||||
throw new Error("Could not find root element to mount to");
|
||||
}
|
||||
|
||||
const root = ReactDOM.createRoot(rootElement);
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
5
metadata.json
Normal file
5
metadata.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "Obsidian | RP Plattform",
|
||||
"description": "Eine professionelle, SaaS-ähnliche Oberfläche für Minecraft-Rollenspiel-Server. Bietet Inventar-Visualisierung, Markdown-Story-Rendering und eine neutrale, hochwertige Dark-Mode-Ästhetik.",
|
||||
"requestFramePermissions": []
|
||||
}
|
||||
21
package.json
Normal file
21
package.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "obsidian-|-rp-plattform",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^19.2.3",
|
||||
"react-dom": "^19.2.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.14.0",
|
||||
"@vitejs/plugin-react": "^5.0.0",
|
||||
"typescript": "~5.8.2",
|
||||
"vite": "^6.2.0"
|
||||
}
|
||||
}
|
||||
75
pages/Cities.tsx
Normal file
75
pages/Cities.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import React from 'react';
|
||||
import { MOCK_ORGS } from '../constants';
|
||||
import { Organization } from '../types';
|
||||
import { Icons } from '../components/IconSet';
|
||||
|
||||
interface CitiesProps {
|
||||
onSelectCity: (id: string) => void;
|
||||
}
|
||||
|
||||
const CityCard = ({ city, onClick }: { city: Organization; onClick: () => void }) => (
|
||||
<div
|
||||
onClick={onClick}
|
||||
className="group relative h-64 rounded-2xl overflow-hidden cursor-pointer border border-border hover:border-accentInfo/50 transition-all duration-300 shadow-card"
|
||||
>
|
||||
{/* Background Image */}
|
||||
<div className="absolute inset-0">
|
||||
<img
|
||||
src={city.bannerUrl || 'https://via.placeholder.com/800x400'}
|
||||
alt={city.name}
|
||||
className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-105"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-background via-background/60 to-transparent" />
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="absolute inset-0 p-6 flex flex-col justify-end">
|
||||
<div className="flex justify-between items-end">
|
||||
<div>
|
||||
<div className="text-xs font-bold text-accentInfo mb-1 uppercase tracking-widest">{city.establishedYear || 'Unbekannte Ära'}</div>
|
||||
<h2 className="text-3xl font-bold text-white mb-2 group-hover:translate-x-1 transition-transform">{city.name}</h2>
|
||||
<p className="text-sm text-gray-300 line-clamp-2 max-w-sm">
|
||||
{city.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-end gap-2">
|
||||
<div className="flex items-center gap-2 px-3 py-1 bg-black/40 backdrop-blur rounded-full border border-white/10">
|
||||
<Icons.Users className="w-4 h-4 text-textMuted" />
|
||||
<span className="text-sm font-medium text-white">{city.memberCount} Bürger</span>
|
||||
</div>
|
||||
<div className="px-3 py-1 bg-accentInfo/20 backdrop-blur rounded-full border border-accentInfo/30 text-xs font-bold text-accentInfo uppercase">
|
||||
{city.status}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const Cities: React.FC<CitiesProps> = ({ onSelectCity }) => {
|
||||
const cities = MOCK_ORGS.filter(org => org.type === 'City');
|
||||
|
||||
return (
|
||||
<div className="animate-in fade-in slide-in-from-bottom-2 space-y-8">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold mb-1">Große Siedlungen</h1>
|
||||
<p className="text-textMuted">Entdecke die blühenden Zentren der Zivilisation im Obsidian-Tal.</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-6">
|
||||
{cities.map(city => (
|
||||
<CityCard key={city.id} city={city} onClick={() => onSelectCity(city.id)} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{cities.length === 0 && (
|
||||
<div className="text-center py-20 text-textMuted">
|
||||
<p>Noch keine Städte gegründet.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Cities;
|
||||
247
pages/CityProfile.tsx
Normal file
247
pages/CityProfile.tsx
Normal file
@@ -0,0 +1,247 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Organization, Project, Player } from '../types';
|
||||
import { MOCK_PLAYERS, MOCK_PROJECTS } from '../constants';
|
||||
import { Icons } from '../components/IconSet';
|
||||
|
||||
interface CityProfileProps {
|
||||
city: Organization;
|
||||
onBack: () => void;
|
||||
backLabel?: string;
|
||||
onSelectPlayer: (id: string) => void;
|
||||
onSelectProject: (id: string) => void;
|
||||
}
|
||||
|
||||
const CityProfile: React.FC<CityProfileProps> = ({
|
||||
city,
|
||||
onBack,
|
||||
backLabel = 'Zurück',
|
||||
onSelectPlayer,
|
||||
onSelectProject
|
||||
}) => {
|
||||
const [activeTab, setActiveTab] = useState<'overview' | 'residents' | 'ventures'>('overview');
|
||||
|
||||
const residents = MOCK_PLAYERS.filter(p => p.stats.organizationId === city.id);
|
||||
const ventures = MOCK_PROJECTS.filter(p => p.associatedOrgId === city.id);
|
||||
|
||||
return (
|
||||
<div className="animate-in slide-in-from-right-4 duration-300">
|
||||
<button onClick={onBack} className="flex items-center gap-2 text-sm text-textMuted hover:text-textMain mb-6 transition-colors group">
|
||||
<span className="group-hover:-translate-x-1 transition-transform">←</span> {backLabel}
|
||||
</button>
|
||||
|
||||
{/* Hero Header */}
|
||||
<div className="relative h-64 md:h-80 rounded-2xl overflow-hidden border border-border mb-8 shadow-card">
|
||||
<img
|
||||
src={city.bannerUrl || 'https://images.unsplash.com/photo-1562774053-701939374585?q=80&w=2086&auto=format&fit=crop'}
|
||||
alt={city.name}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-background via-background/50 to-transparent" />
|
||||
|
||||
<div className="absolute bottom-0 left-0 right-0 p-8">
|
||||
<div className="flex flex-col md:flex-row md:items-end justify-between gap-4">
|
||||
<div>
|
||||
{city.establishedYear && (
|
||||
<div className="text-accentInfo font-mono text-xs mb-2 bg-black/40 inline-block px-2 py-1 rounded backdrop-blur border border-white/5">
|
||||
GEGR. {city.establishedYear}
|
||||
</div>
|
||||
)}
|
||||
<h1 className="text-4xl md:text-6xl font-bold text-white tracking-tight mb-2">{city.name}</h1>
|
||||
<div className="flex items-center gap-4 text-sm text-gray-300">
|
||||
<span className="flex items-center gap-1.5">
|
||||
<Icons.Users className="w-4 h-4" /> {city.memberCount} Mitglieder
|
||||
</span>
|
||||
<span className="flex items-center gap-1.5">
|
||||
<div className="w-2 h-2 rounded-full bg-accentSuccess animate-pulse" /> {city.status}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{city.mayor && (
|
||||
<div className="text-right">
|
||||
<div className="text-xs text-textMuted uppercase tracking-widest mb-1">Aktueller Anführer</div>
|
||||
<div className="flex items-center gap-2 text-lg font-medium text-amber-100 bg-surfaceHighlight/60 px-4 py-2 rounded-lg backdrop-blur border border-amber-500/30 shadow-[0_0_15px_rgba(245,158,11,0.15)] group hover:border-amber-500/50 transition-colors">
|
||||
<Icons.Crown className="w-5 h-5 text-amber-400 drop-shadow-md" />
|
||||
{city.mayor}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Navigation Tabs */}
|
||||
<div className="flex gap-1 border-b border-border mb-8 overflow-x-auto">
|
||||
<button
|
||||
onClick={() => setActiveTab('overview')}
|
||||
className={`px-6 py-3 text-sm font-medium border-b-2 transition-colors whitespace-nowrap ${activeTab === 'overview' ? 'border-accentInfo text-white' : 'border-transparent text-textMuted hover:text-white'}`}
|
||||
>
|
||||
Übersicht & Galerie
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('residents')}
|
||||
className={`px-6 py-3 text-sm font-medium border-b-2 transition-colors whitespace-nowrap ${activeTab === 'residents' ? 'border-accentInfo text-white' : 'border-transparent text-textMuted hover:text-white'}`}
|
||||
>
|
||||
Bewohner ({residents.length})
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('ventures')}
|
||||
className={`px-6 py-3 text-sm font-medium border-b-2 transition-colors whitespace-nowrap ${activeTab === 'ventures' ? 'border-accentInfo text-white' : 'border-transparent text-textMuted hover:text-white'}`}
|
||||
>
|
||||
Lokale Unternehmen ({ventures.length})
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content Area */}
|
||||
<div className="min-h-[400px]">
|
||||
{activeTab === 'overview' && (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
<div className="lg:col-span-2 space-y-8">
|
||||
<section>
|
||||
<h3 className="text-xl font-bold mb-4 flex items-center gap-2">
|
||||
<Icons.Map className="w-5 h-5 text-accentInfo" /> Über {city.name}
|
||||
</h3>
|
||||
<p className="text-textMuted leading-relaxed text-lg">
|
||||
{city.description}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{city.gallery && city.gallery.length > 0 && (
|
||||
<section>
|
||||
<h3 className="text-xl font-bold mb-4">Bildarchiv</h3>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
{city.gallery.map((url, idx) => (
|
||||
<div key={idx} className="rounded-xl overflow-hidden aspect-video border border-border group relative">
|
||||
<img src={url} alt={`Gallery ${idx}`} className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110" />
|
||||
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-colors" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{city.cityStats && (
|
||||
<div className="bg-surface border border-border rounded-xl p-6 h-fit">
|
||||
<h4 className="text-sm font-bold uppercase text-textMuted mb-6 flex items-center gap-2">
|
||||
<Icons.Terminal className="w-4 h-4" /> Stadt-Statistiken
|
||||
</h4>
|
||||
<div className="space-y-6">
|
||||
|
||||
{/* Tax Rate */}
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 bg-green-500/10 rounded text-green-400">
|
||||
<Icons.Coins className="w-4 h-4" />
|
||||
</div>
|
||||
<span className="text-sm text-gray-400">Steuersatz</span>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<span className="font-mono text-lg text-white font-medium">{city.cityStats.taxRate}%</span>
|
||||
<div className="text-[10px] text-textMuted">Pro Transaktion</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full bg-white/5 h-px" />
|
||||
|
||||
{/* Defense */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 bg-blue-500/10 rounded text-blue-400">
|
||||
<Icons.Shield className="w-4 h-4" />
|
||||
</div>
|
||||
<span className="text-sm text-gray-400">Verteidigungswert</span>
|
||||
</div>
|
||||
<span className="font-mono text-white font-medium">{city.cityStats.defenseRating}/10</span>
|
||||
</div>
|
||||
<div className="h-1.5 w-full bg-surfaceHighlight rounded-full overflow-hidden">
|
||||
<div className="h-full bg-blue-500/70" style={{ width: `${(city.cityStats.defenseRating / 10) * 100}%` }} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full bg-white/5 h-px" />
|
||||
|
||||
{/* Gov Type */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 bg-purple-500/10 rounded text-purple-400">
|
||||
<Icons.Scroll className="w-4 h-4" />
|
||||
</div>
|
||||
<span className="text-sm text-gray-400">Regierungsform</span>
|
||||
</div>
|
||||
<span className="text-sm text-white font-medium">{city.cityStats.government}</span>
|
||||
</div>
|
||||
|
||||
<div className="w-full bg-white/5 h-px" />
|
||||
|
||||
{/* Biome */}
|
||||
<div className="flex justify-between items-center py-1">
|
||||
<span className="text-sm text-gray-400">Biom</span>
|
||||
<span className="text-sm text-white">{city.cityStats.biome}</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center py-1">
|
||||
<span className="text-sm text-gray-400">Spezialität</span>
|
||||
<span className="text-sm text-white">{city.cityStats.specialty}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'residents' && (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||
{residents.map(player => (
|
||||
<div
|
||||
key={player.uuid}
|
||||
onClick={() => onSelectPlayer(player.uuid)}
|
||||
className="bg-surface border border-border p-4 rounded-xl flex items-center gap-4 hover:border-accentInfo/50 transition-colors cursor-pointer group hover:shadow-card hover:bg-surfaceHighlight/30"
|
||||
>
|
||||
<div className="w-10 h-10 bg-surfaceHighlight rounded flex items-center justify-center font-bold text-lg text-textMuted group-hover:text-textMain transition-colors">
|
||||
{player.username.charAt(0)}
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium text-white group-hover:text-accentInfo transition-colors">{player.username}</div>
|
||||
<div className="text-xs text-textMuted">{player.stats.role}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{residents.length === 0 && <div className="text-textMuted italic">Keine Bewohner öffentlich registriert.</div>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'ventures' && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{ventures.map(project => (
|
||||
<div
|
||||
key={project.id}
|
||||
onClick={() => onSelectProject(project.id)}
|
||||
className="bg-surface border border-border rounded-xl p-5 hover:border-accentInfo/50 transition-all cursor-pointer group hover:shadow-card hover:bg-surfaceHighlight/30"
|
||||
>
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<span className={`text-[10px] uppercase font-bold px-2 py-0.5 rounded ${
|
||||
project.category === 'Story Arc' ? 'bg-orange-500/10 text-orange-400' : 'bg-blue-500/10 text-blue-400'
|
||||
}`}>
|
||||
{project.category}
|
||||
</span>
|
||||
{project.hiring && (
|
||||
<span className="text-[10px] font-bold text-accentSuccess animate-pulse">STELLEN</span>
|
||||
)}
|
||||
</div>
|
||||
<h3 className="text-lg font-bold text-white mb-1 group-hover:text-accentInfo transition-colors">{project.title}</h3>
|
||||
<p className="text-sm text-textMuted mb-4 line-clamp-2">{project.description}</p>
|
||||
<div className="flex items-center justify-between pt-4 border-t border-white/5">
|
||||
<span className="text-xs text-textMuted">Geführt von {project.owner}</span>
|
||||
<div className="text-xs font-mono text-white">{project.progress}% Fortschr.</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{ventures.length === 0 && <div className="text-textMuted italic">Keine aktiven Unternehmen in dieser Organisation.</div>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CityProfile;
|
||||
111
pages/Dashboard.tsx
Normal file
111
pages/Dashboard.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
import React from 'react';
|
||||
import { MOCK_PLAYERS, MOCK_PROJECTS, MOCK_ORGS } from '../constants';
|
||||
import { Icons } from '../components/IconSet';
|
||||
|
||||
const StatCard = ({ label, value, trend, icon: Icon }: any) => (
|
||||
<div className="bg-surface/50 border border-border p-6 rounded-xl hover:border-accentInfo/30 transition-all duration-300 group">
|
||||
<div className="flex justify-between items-start mb-6">
|
||||
<div className="p-3 bg-surfaceHighlight rounded-lg text-textMuted group-hover:text-textMain transition-colors">
|
||||
<Icon className="w-6 h-6" />
|
||||
</div>
|
||||
{trend && (
|
||||
<span className={`text-xs font-medium px-2.5 py-1 rounded-full ${
|
||||
trend > 0 ? 'bg-accentSuccess/10 text-accentSuccess' : 'bg-accentWarn/10 text-accentWarn'
|
||||
}`}>
|
||||
{trend > 0 ? '+' : ''}{trend}%
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-4xl font-bold text-textMain font-mono mb-2 tracking-tight">{value}</div>
|
||||
<div className="text-sm text-textMuted font-medium">{label}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const ProjectCard = ({ project }: { project: any }) => (
|
||||
<div className="flex items-center gap-6 py-4 border-b border-white/5 last:border-0 group hover:bg-white/[0.02] px-2 rounded transition-colors -mx-2">
|
||||
<div className={`w-2.5 h-2.5 rounded-full shadow-sm ${
|
||||
project.status === 'active' ? 'bg-accentInfo' :
|
||||
project.status === 'recruiting' ? 'bg-accentSuccess' : 'bg-textMuted'
|
||||
}`} />
|
||||
<div className="flex-1">
|
||||
<div className="text-base font-medium text-textMain group-hover:text-accentInfo transition-colors">{project.title}</div>
|
||||
<div className="text-xs text-textMuted mt-0.5 uppercase tracking-wide">Inhaber: {project.owner}</div>
|
||||
</div>
|
||||
<div className="w-32 hidden sm:block">
|
||||
<div className="h-2 bg-surfaceHighlight rounded-full overflow-hidden">
|
||||
<div className="h-full bg-accentInfo/80" style={{ width: `${project.progress}%` }} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm font-mono text-textMuted w-12 text-right">{project.progress}%</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const Dashboard: React.FC = () => {
|
||||
return (
|
||||
<div className="space-y-12">
|
||||
{/* Intro Section */}
|
||||
<div className="flex flex-col md:flex-row md:items-end justify-between gap-4 pb-4 border-b border-border">
|
||||
<div>
|
||||
<h2 className="text-sm font-bold text-accentInfo tracking-widest uppercase mb-2">Live-Telemetrie</h2>
|
||||
<h1 className="text-4xl md:text-5xl font-bold tracking-tight text-white">Tal-Übersicht</h1>
|
||||
</div>
|
||||
<p className="text-textMuted text-right hidden md:block max-w-xs leading-relaxed">
|
||||
Echtzeit-Datenaggregation aus dem Zentralarchiv.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* KPI Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
<StatCard label="Registrierte Bürger" value={MOCK_PLAYERS.length} trend={12} icon={Icons.Users} />
|
||||
<StatCard label="Aktive Unternehmen" value={MOCK_PROJECTS.filter(p => p.status === 'active' || p.status === 'recruiting').length} trend={5} icon={Icons.Layers} />
|
||||
<StatCard label="Organisationen" value={MOCK_ORGS.length} trend={0} icon={Icons.Map} />
|
||||
</div>
|
||||
|
||||
{/* Content Grid */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-12">
|
||||
{/* Projects List - Wider */}
|
||||
<div className="lg:col-span-7">
|
||||
<div className="flex justify-between items-end mb-6">
|
||||
<h3 className="text-xl font-semibold">Top Unternehmen</h3>
|
||||
<button className="text-sm text-textMuted hover:text-accentInfo transition-colors">Verzeichnis ansehen →</button>
|
||||
</div>
|
||||
<div className="bg-surface/30 border border-border rounded-2xl p-6 backdrop-blur-sm">
|
||||
{MOCK_PROJECTS.slice(0, 5).map(p => <ProjectCard key={p.id} project={p} />)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Decree / News - Narrower */}
|
||||
<div className="lg:col-span-5 flex flex-col">
|
||||
<h3 className="text-xl font-semibold mb-6">Offizieller Erlass</h3>
|
||||
<div className="flex-1 bg-gradient-to-b from-surface/50 to-surface/20 border border-border rounded-2xl p-8 relative overflow-hidden group">
|
||||
{/* Decorative background element */}
|
||||
<div className="absolute -top-10 -right-10 w-40 h-40 bg-accentInfo/10 rounded-full blur-3xl group-hover:bg-accentInfo/20 transition-all duration-700" />
|
||||
|
||||
<div className="relative z-10">
|
||||
<div className="mb-6">
|
||||
<span className="text-xs font-mono px-2 py-1 bg-accentInfo/20 text-accentInfo rounded border border-accentInfo/20">NEU</span>
|
||||
<span className="text-xs font-mono px-2 py-1 ml-2 text-textMuted">VOR 12 MIN</span>
|
||||
</div>
|
||||
|
||||
<div className="prose prose-invert prose-lg text-textMuted leading-relaxed">
|
||||
<p className="text-textMain italic text-lg font-serif mb-6">
|
||||
"Im Auftrag des Bürgermeisters ist jeglicher Handel innerhalb der inneren Mauern für das kommende Fest steuerbefreit."
|
||||
</p>
|
||||
<p className="text-sm">
|
||||
Bürger werden ermutigt, ihre Stände im Marktviertel aufzubauen, bevor der Mond am 14. Tag aufgeht.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="absolute bottom-6 left-8 flex gap-2">
|
||||
<span className="text-xs text-textMuted hover:text-white cursor-pointer">#Wirtschaft</span>
|
||||
<span className="text-xs text-textMuted hover:text-white cursor-pointer">#Events</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
273
pages/DatapackGenerator.tsx
Normal file
273
pages/DatapackGenerator.tsx
Normal file
@@ -0,0 +1,273 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Icons } from '../components/IconSet';
|
||||
|
||||
interface FileBlock {
|
||||
name: string;
|
||||
path: string;
|
||||
language: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
const DatapackGenerator: React.FC = () => {
|
||||
const [activeFile, setActiveFile] = useState<string>('menu.mcfunction');
|
||||
|
||||
const files: FileBlock[] = [
|
||||
{
|
||||
name: 'pack.mcmeta',
|
||||
path: 'obsidian_core/pack.mcmeta',
|
||||
language: 'json',
|
||||
content: `{
|
||||
"pack": {
|
||||
"pack_format": 15,
|
||||
"description": "Obsidian Core - Verwaltung (v2.4 DE)"
|
||||
}
|
||||
}`
|
||||
},
|
||||
{
|
||||
name: 'load.json',
|
||||
path: 'obsidian_core/data/minecraft/tags/functions/load.json',
|
||||
language: 'json',
|
||||
content: `{
|
||||
"values": [
|
||||
"obsidian:load"
|
||||
]
|
||||
}`
|
||||
},
|
||||
{
|
||||
name: 'tick.json',
|
||||
path: 'obsidian_core/data/minecraft/tags/functions/tick.json',
|
||||
language: 'json',
|
||||
content: `{
|
||||
"values": [
|
||||
"obsidian:system/tick"
|
||||
]
|
||||
}`
|
||||
},
|
||||
{
|
||||
name: 'load.mcfunction',
|
||||
path: 'obsidian_core/data/obsidian/functions/load.mcfunction',
|
||||
language: 'mcfunction',
|
||||
content: `# Initialize Scoreboards
|
||||
scoreboard objectives add obs_action trigger
|
||||
scoreboard objectives add obs_id dummy
|
||||
|
||||
# Initialize Storage
|
||||
data modify storage obsidian:main selected set value {id:"Keine", type:"None"}
|
||||
tellraw @a {"text":"[Obsidian] System geladen. Trigger registriert.","color":"green"}`
|
||||
},
|
||||
{
|
||||
name: 'tick.mcfunction',
|
||||
path: 'obsidian_core/data/obsidian/functions/system/tick.mcfunction',
|
||||
language: 'mcfunction',
|
||||
content: `# Enable trigger for all players
|
||||
execute as @a run scoreboard players enable @s obs_action
|
||||
|
||||
# Detect triggers
|
||||
execute as @a[scores={obs_action=1..}] run function obsidian:system/handle_trigger`
|
||||
},
|
||||
{
|
||||
name: 'menu.mcfunction',
|
||||
path: 'obsidian_core/data/obsidian/functions/ui/menu.mcfunction',
|
||||
language: 'mcfunction',
|
||||
content: `tellraw @s ["",{"text":"\\n=== OBSIDIAN MENÜ ===\\n","color":"dark_aqua","bold":true}]
|
||||
|
||||
tellraw @s ["",{"text":"Neu erstellen:","color":"gray"}]
|
||||
tellraw @s [{"text":"[+ Stadt] ","color":"gold","clickEvent":{"action":"run_command","value":"/trigger obs_action set 10"},"hoverEvent":{"action":"show_text","value":"Eine neue Stadt gründen"}}]
|
||||
tellraw @s [{"text":" [+ Unternehmung] ","color":"green","clickEvent":{"action":"run_command","value":"/trigger obs_action set 20"},"hoverEvent":{"action":"show_text","value":"Ein neues Unternehmen registrieren"}}]
|
||||
|
||||
tellraw @s ["",{"text":"\\n\\nZielauswahl:","color":"gray"}]
|
||||
tellraw @s [{"text":"[Wähle per Name] ","color":"aqua","clickEvent":{"action":"run_command","value":"/trigger obs_action set 30"},"hoverEvent":{"action":"show_text","value":"Halte ein Item mit dem exakten Namen um den Eintrag zu wählen."}}]
|
||||
|
||||
tellraw @s ["",{"text":"\\nAktuell gewählt: ","color":"gray"},{"nbt":"selected.id","storage":"obsidian:main","color":"light_purple"}]
|
||||
|
||||
# Edit Controls
|
||||
tellraw @s ["",{"text":"Bearbeiten: ","color":"gray"}]
|
||||
tellraw @s [{"text":"[Umbenennen] ","color":"white","clickEvent":{"action":"run_command","value":"/trigger obs_action set 60"},"hoverEvent":{"action":"show_text","value":"Namen des Eintrags ändern"}}]
|
||||
tellraw @s [{"text":"[Beschr. ändern] ","color":"white","clickEvent":{"action":"run_command","value":"/trigger obs_action set 40"},"hoverEvent":{"action":"show_text","value":"Beschreibungstext aktualisieren"}}]
|
||||
tellraw @s [{"text":"[Status ändern] ","color":"white","clickEvent":{"action":"run_command","value":"/trigger obs_action set 50"},"hoverEvent":{"action":"show_text","value":"Wechseln zwischen Aktiv/Rekrutierend/Geschlossen"}}]
|
||||
|
||||
tellraw @s ["",{"text":"\\n\\n[?] Hilfe","color":"yellow","clickEvent":{"action":"run_command","value":"/trigger obs_action set 9"},"hoverEvent":{"action":"show_text","value":"Spieleranleitung lesen"}}]`
|
||||
},
|
||||
{
|
||||
name: 'handle_trigger.mcfunction',
|
||||
path: 'obsidian_core/data/obsidian/functions/system/handle_trigger.mcfunction',
|
||||
language: 'mcfunction',
|
||||
content: `# --- HANDLE TRIGGERS ---
|
||||
|
||||
# 1: Open Menu
|
||||
execute if score @s obs_action matches 1 run function obsidian:ui/menu
|
||||
# 9: Help
|
||||
execute if score @s obs_action matches 9 run function obsidian:ui/help
|
||||
|
||||
# --- CREATION ---
|
||||
# 10: Prompt City
|
||||
execute if score @s obs_action matches 10 run tellraw @s ["",{"text":"[Neue Stadt] ","color":"gold"},{"text":"Item in Stadt-Name umbenennen. Halten & ","color":"gray"},{"text":"[KLICK]","color":"green","bold":true,"clickEvent":{"action":"run_command","value":"/trigger obs_action set 11"}}]
|
||||
# 11: Create City
|
||||
execute if score @s obs_action matches 11 run tellraw @a [{"text":"Neue Stadt gegründet: ","color":"gold"},{"nbt":"SelectedItem.tag.display.Name","entity":"@s"}]
|
||||
execute if score @s obs_action matches 11 run data modify storage obsidian:db Cities append value {name:"New",status:"active"}
|
||||
execute if score @s obs_action matches 11 run data modify storage obsidian:db Cities[-1].name set from entity @s SelectedItem.tag.display.Name
|
||||
|
||||
# 20: Prompt Venture
|
||||
execute if score @s obs_action matches 20 run tellraw @s ["",{"text":"[Neues Unternehmen] ","color":"green"},{"text":"Item in Firmen-Name umbenennen. Halten & ","color":"gray"},{"text":"[KLICK]","color":"green","bold":true,"clickEvent":{"action":"run_command","value":"/trigger obs_action set 21"}}]
|
||||
# 21: Create Venture
|
||||
execute if score @s obs_action matches 21 run tellraw @a [{"text":"Neues Unternehmen registriert: ","color":"green"},{"nbt":"SelectedItem.tag.display.Name","entity":"@s"}]
|
||||
execute if score @s obs_action matches 21 run data modify storage obsidian:db Ventures append value {name:"New",status:"active"}
|
||||
execute if score @s obs_action matches 21 run data modify storage obsidian:db Ventures[-1].name set from entity @s SelectedItem.tag.display.Name
|
||||
|
||||
# --- SELECTION ---
|
||||
# 30: Select Target
|
||||
execute if score @s obs_action matches 30 run data modify storage obsidian:main selected.id set from entity @s SelectedItem.tag.display.Name
|
||||
execute if score @s obs_action matches 30 run tellraw @s [{"text":"Ziel erfasst: ","color":"aqua"},{"nbt":"selected.id","storage":"obsidian:main"}]
|
||||
|
||||
# --- EDITING ---
|
||||
|
||||
# 40/41: Description
|
||||
execute if score @s obs_action matches 40 run tellraw @s ["",{"text":"[Beschr. ändern] ","color":"light_purple"},{"text":"Item in neue Beschreibung umbenennen. Halten & ","color":"gray"},{"text":"[UPDATE]","color":"yellow","bold":true,"clickEvent":{"action":"run_command","value":"/trigger obs_action set 41"}}]
|
||||
execute if score @s obs_action matches 41 run tellraw @a [{"text":"LOG|UPDATE_DESC|","color":"dark_gray"},{"nbt":"selected.id","storage":"obsidian:main"},{"text":"|"},{"nbt":"SelectedItem.tag.display.Name","entity":"@s"}]
|
||||
|
||||
# 50: Toggle Status (Active -> Recruiting -> Closed)
|
||||
execute if score @s obs_action matches 50 run tellraw @a [{"text":"LOG|CYCLE_STATUS|","color":"dark_gray"},{"nbt":"selected.id","storage":"obsidian:main"}]
|
||||
|
||||
# 60/61: Rename Entry
|
||||
execute if score @s obs_action matches 60 run tellraw @s ["",{"text":"[Umbenennen] ","color":"red"},{"text":"Item in NEUEN Namen umbenennen. Halten & ","color":"gray"},{"text":"[BESTÄTIGEN]","color":"red","bold":true,"clickEvent":{"action":"run_command","value":"/trigger obs_action set 61"}}]
|
||||
execute if score @s obs_action matches 61 run tellraw @a [{"text":"LOG|RENAME_ENTRY|","color":"dark_gray"},{"nbt":"selected.id","storage":"obsidian:main"},{"text":"|"},{"nbt":"SelectedItem.tag.display.Name","entity":"@s"}]
|
||||
execute if score @s obs_action matches 61 run data modify storage obsidian:main selected.id set from entity @s SelectedItem.tag.display.Name
|
||||
|
||||
# Reset
|
||||
scoreboard players set @s obs_action 0
|
||||
scoreboard players enable @s obs_action`
|
||||
},
|
||||
{
|
||||
name: 'help.mcfunction',
|
||||
path: 'obsidian_core/data/obsidian/functions/ui/help.mcfunction',
|
||||
language: 'mcfunction',
|
||||
content: `tellraw @s ["",{"text":"\\n=== OBSIDIAN ANLEITUNG ===\\n","color":"dark_aqua","bold":true}]
|
||||
tellraw @s [{"text":"[!] ","color":"yellow"},{"text":"Texteingabe ohne Befehle:","color":"gray"}]
|
||||
tellraw @s [{"text":"\\n1. Item umbenennen: ","color":"white","bold":true},{"text":"Benenne ein Papier (oder Item) im Amboss um (Dein Text).","color":"gray"}]
|
||||
tellraw @s [{"text":"\\n2. Item halten: ","color":"white","bold":true},{"text":"Halte das Item in der Haupthand.","color":"gray"}]
|
||||
tellraw @s [{"text":"\\n3. Aktion klicken: ","color":"white","bold":true},{"text":"Klicke [Bestätigen] oder [Wählen] im Menü.","color":"gray"}]
|
||||
tellraw @s [{"text":"\\nDas System liest den Item-Namen als Eingabe.","color":"gray","italic":true}]`
|
||||
},
|
||||
{
|
||||
name: 'export.mcfunction',
|
||||
path: 'obsidian_core/data/obsidian/functions/system/export.mcfunction',
|
||||
language: 'mcfunction',
|
||||
content: `# Admin Only Export
|
||||
tellraw @a {"text":"JSON_EXPORT_START|DB_DUMP|", "color":"white"}
|
||||
tellraw @a {"nbt":"Cities","storage":"obsidian:db"}
|
||||
tellraw @a {"nbt":"Ventures","storage":"obsidian:db"}`
|
||||
}
|
||||
];
|
||||
|
||||
const currentFile = files.find(f => f.name === activeFile);
|
||||
|
||||
return (
|
||||
<div className="max-w-6xl mx-auto grid grid-cols-1 lg:grid-cols-12 gap-8 animate-in fade-in">
|
||||
{/* Intro Sidebar */}
|
||||
<div className="lg:col-span-4 space-y-6">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="bg-accentSuccess/10 text-accentSuccess border border-accentSuccess/20 text-xs font-bold px-2 py-1 rounded uppercase tracking-wider">v2.4 Non-OP (DE)</span>
|
||||
<span className="text-textMuted text-xs">Trigger System</span>
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold mb-2">Datapack Generator</h1>
|
||||
<p className="text-textMuted">
|
||||
Non-OP Management System. Nutzt <strong>Item-Umbenennung</strong> als sichere Eingabemethode für Spielertexte.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* PLAYER GUIDE SECTION */}
|
||||
<div className="bg-surface border border-border rounded-xl overflow-hidden shadow-card">
|
||||
<div className="p-4 bg-surfaceHighlight/30 border-b border-white/5">
|
||||
<h3 className="font-bold text-white flex items-center gap-2">
|
||||
<Icons.Users className="w-4 h-4 text-accentSuccess" />
|
||||
Spieler Anleitung
|
||||
</h3>
|
||||
</div>
|
||||
<div className="p-5 space-y-4">
|
||||
<p className="text-xs text-textMuted leading-relaxed">
|
||||
Verteile diese Anleitung an deine Spieler. Da normale Spieler keine Befehle nutzen können, nutzt dieses Pack ein immersives <strong>"Item Key"</strong> System.
|
||||
</p>
|
||||
|
||||
<div className="space-y-4 mt-2">
|
||||
<div className="flex gap-3">
|
||||
<div className="w-6 h-6 rounded bg-blue-500/10 text-blue-400 border border-blue-500/20 flex items-center justify-center font-bold text-xs shrink-0">1</div>
|
||||
<div className="text-sm">
|
||||
<strong className="block text-gray-200 text-xs uppercase tracking-wide mb-1">Erstellen</strong>
|
||||
<p className="text-textMuted text-xs leading-relaxed">Klicke [+ Unternehmung], benenne ein Item zu "Mein Shop", halte es, und klicke den Bestätigungs-Link.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<div className="w-6 h-6 rounded bg-blue-500/10 text-blue-400 border border-blue-500/20 flex items-center justify-center font-bold text-xs shrink-0">2</div>
|
||||
<div className="text-sm">
|
||||
<strong className="block text-gray-200 text-xs uppercase tracking-wide mb-1">Zum Bearbeiten wählen</strong>
|
||||
<p className="text-textMuted text-xs leading-relaxed">Halte ein Item mit dem <strong>exakten Namen</strong> deiner Firma. Klicke <strong>[Wähle per Name]</strong>. Das Menü visiert nun diesen Eintrag an.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<div className="w-6 h-6 rounded bg-blue-500/10 text-blue-400 border border-blue-500/20 flex items-center justify-center font-bold text-xs shrink-0">3</div>
|
||||
<div className="text-sm">
|
||||
<strong className="block text-gray-200 text-xs uppercase tracking-wide mb-1">Modifizieren</strong>
|
||||
<p className="text-textMuted text-xs leading-relaxed">
|
||||
Nutze <strong>[Umbenennen]</strong> oder <strong>[Beschr. ändern]</strong>. Du wirst aufgefordert, ein Item zum NEUEN Wert umzubenennen und zu bestätigen.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Admin Note */}
|
||||
<div className="p-4 bg-surfaceHighlight/10 border border-border rounded-lg text-xs text-textMuted">
|
||||
<strong className="text-accentInfo block mb-1">Wichtig:</strong>
|
||||
Ich habe <code>load.json</code> hinzugefügt, um sicherzustellen, dass das Scoreboard automatisch erstellt wird. Bitte führe <code>/reload</code> nach der Installation aus.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Code Viewer */}
|
||||
<div className="lg:col-span-8 bg-[#0b0b0d] border border-border rounded-xl overflow-hidden shadow-2xl flex flex-col min-h-[600px]">
|
||||
{/* Tabs */}
|
||||
<div className="flex overflow-x-auto border-b border-white/5 bg-surface/50">
|
||||
{files.map(file => (
|
||||
<button
|
||||
key={file.name}
|
||||
onClick={() => setActiveFile(file.name)}
|
||||
className={`px-4 py-3 text-xs font-mono border-r border-white/5 transition-colors whitespace-nowrap ${
|
||||
activeFile === file.name
|
||||
? 'bg-[#0b0b0d] text-accentInfo'
|
||||
: 'text-textMuted hover:bg-white/5 hover:text-gray-300'
|
||||
}`}
|
||||
>
|
||||
{file.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Editor Content */}
|
||||
<div className="flex-1 p-6 relative group overflow-y-auto custom-scrollbar">
|
||||
<div className="absolute top-0 right-0 p-4">
|
||||
<button
|
||||
onClick={() => currentFile && navigator.clipboard.writeText(currentFile.content)}
|
||||
className="text-xs bg-surfaceHighlight hover:bg-white/10 text-textMuted hover:text-white px-3 py-1.5 rounded transition-colors border border-white/10"
|
||||
>
|
||||
Inhalt kopieren
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{currentFile && (
|
||||
<div>
|
||||
<div className="text-xs text-gray-500 mb-4 font-mono select-none">
|
||||
Dateipfad: <span className="text-gray-400">{currentFile.path}</span>
|
||||
</div>
|
||||
<pre className="font-mono text-sm text-gray-300 leading-relaxed whitespace-pre-wrap">
|
||||
{currentFile.content}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DatapackGenerator;
|
||||
101
pages/Organizations.tsx
Normal file
101
pages/Organizations.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
import React, { useState } from 'react';
|
||||
import { MOCK_ORGS } from '../constants';
|
||||
import { Organization } from '../types';
|
||||
import { Icons } from '../components/IconSet';
|
||||
|
||||
const OrgCard = ({ org, onClick }: { org: Organization; onClick: () => void }) => (
|
||||
<div
|
||||
onClick={onClick}
|
||||
className="bg-surface border border-border rounded-xl p-6 hover:border-accentInfo/40 transition-all duration-200 hover:shadow-card group flex flex-col h-full relative overflow-hidden cursor-pointer"
|
||||
>
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<div className={`w-10 h-10 rounded-lg flex items-center justify-center text-lg font-bold border border-white/5 ${
|
||||
org.type === 'City' ? 'bg-blue-500/10 text-blue-400' :
|
||||
org.type === 'Guild' ? 'bg-amber-500/10 text-amber-400' :
|
||||
'bg-purple-500/10 text-purple-400'
|
||||
}`}>
|
||||
{org.name.charAt(0)}
|
||||
</div>
|
||||
<span className={`text-[10px] uppercase font-bold px-2 py-0.5 rounded-full border ${
|
||||
org.status === 'active' ? 'bg-accentSuccess/10 text-accentSuccess border-accentSuccess/20' : 'bg-textMuted/10 text-textMuted border-textMuted/20'
|
||||
}`}>
|
||||
{org.status}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h3 className="text-xl font-bold text-textMain mb-1 group-hover:text-accentInfo transition-colors">
|
||||
{org.name}
|
||||
</h3>
|
||||
<div className="text-xs text-textMuted font-mono mb-4">{org.type}</div>
|
||||
|
||||
<p className="text-sm text-textMuted mb-6 flex-1 leading-relaxed">
|
||||
{org.description}
|
||||
</p>
|
||||
|
||||
<div className="pt-4 border-t border-white/5 flex items-center gap-4 text-xs text-textMuted">
|
||||
<div className="flex items-center gap-2">
|
||||
<Icons.Users className="w-4 h-4" />
|
||||
<span>{org.memberCount} Mitglieder</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const Organizations: React.FC<{ onSelectOrg: (id: string) => void }> = ({ onSelectOrg }) => {
|
||||
const [filter, setFilter] = useState<'all' | Organization['type']>('all');
|
||||
|
||||
const filteredOrgs = MOCK_ORGS.filter(org =>
|
||||
filter === 'all' ? true : org.type === filter
|
||||
);
|
||||
|
||||
const tabs = [
|
||||
{ id: 'all', label: 'Alle Organisationen' },
|
||||
{ id: 'City', label: 'Städte' },
|
||||
{ id: 'Guild', label: 'Gilden' },
|
||||
{ id: 'Company', label: 'Firmen' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-8 animate-in fade-in slide-in-from-bottom-2">
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold mb-1">Organisationen</h1>
|
||||
<p className="text-textMuted">Offizielle Fraktionen, Städte und registrierte Gilden.</p>
|
||||
</div>
|
||||
<button className="bg-textMain text-background hover:bg-white font-medium px-4 py-2 rounded-lg text-sm transition-colors flex items-center gap-2">
|
||||
<span>+ Org registrieren</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2 border-b border-border pb-1">
|
||||
{tabs.map(tab => (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setFilter(tab.id as any)}
|
||||
className={`px-4 py-2 text-sm font-medium rounded-t-lg transition-colors relative top-[1px] ${
|
||||
filter === tab.id
|
||||
? 'text-textMain border-b-2 border-accentInfo bg-surfaceHighlight/20'
|
||||
: 'text-textMuted hover:text-textMain hover:bg-white/5'
|
||||
}`}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
|
||||
{filteredOrgs.map(org => (
|
||||
<OrgCard key={org.id} org={org} onClick={() => onSelectOrg(org.id)} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{filteredOrgs.length === 0 && (
|
||||
<div className="text-center py-20 text-textMuted">
|
||||
<p>Keine Organisationen gefunden.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Organizations;
|
||||
128
pages/PlayerProfile.tsx
Normal file
128
pages/PlayerProfile.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
import React from 'react';
|
||||
import { Player } from '../types';
|
||||
import { MOCK_ORGS } from '../constants';
|
||||
import InventoryGrid from '../components/InventoryGrid';
|
||||
import { Icons } from '../components/IconSet';
|
||||
|
||||
interface PlayerProfileProps {
|
||||
player: Player;
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
const PlayerProfile: React.FC<PlayerProfileProps> = ({ player, onBack }) => {
|
||||
const playerOrg = MOCK_ORGS.find(o => o.id === player.stats.organizationId);
|
||||
|
||||
// Simple markdown renderer replacement for demo purposes
|
||||
// In production, use 'react-markdown'
|
||||
const renderMarkdown = (text: string) => {
|
||||
return text.split('\n').map((line, i) => {
|
||||
if (line.startsWith('# ')) return <h1 key={i} className="text-2xl font-bold mt-6 mb-3 text-textMain border-b border-border pb-2">{line.replace('# ', '')}</h1>;
|
||||
if (line.startsWith('### ')) return <h3 key={i} className="text-lg font-semibold mt-4 mb-2 text-textMain">{line.replace('### ', '')}</h3>;
|
||||
if (line.startsWith('> ')) return <blockquote key={i} className="border-l-2 border-accentInfo pl-4 italic text-textMuted my-4 bg-surfaceHighlight/30 py-2 pr-2 rounded-r">{line.replace('> ', '')}</blockquote>;
|
||||
if (line.startsWith('* ')) return <li key={i} className="ml-4 list-disc text-textMuted mb-1 marker:text-accentInfo">{line.replace('* ', '')}</li>;
|
||||
return <p key={i} className="mb-2 text-textMuted leading-relaxed">{line}</p>;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto animate-in slide-in-from-right-4 duration-300">
|
||||
<button onClick={onBack} className="flex items-center gap-2 text-sm text-textMuted hover:text-textMain mb-6 transition-colors">
|
||||
<span className="text-lg">←</span> Zurück zur Liste
|
||||
</button>
|
||||
|
||||
{/* Header */}
|
||||
<div className="bg-surface border border-border rounded-xl p-6 shadow-card mb-6">
|
||||
<div className="flex flex-col md:flex-row gap-6 items-start md:items-center">
|
||||
<div className="w-20 h-20 rounded-lg flex items-center justify-center shadow-inner shrink-0">
|
||||
<img src={"https://minotar.net/armor/bust/"+player.username+"/500.png"}></img>
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
<div className="flex flex-wrap items-center gap-3 mb-1">
|
||||
<h1 className="text-3xl font-bold text-textMain tracking-tight">{player.username}</h1>
|
||||
{player.isOnline && (
|
||||
<span className="flex items-center gap-1.5 px-2 py-0.5 rounded-full bg-accentSuccess/10 border border-accentSuccess/20 text-accentSuccess text-xs font-medium">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-accentSuccess animate-pulse"></span>
|
||||
Online
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2 mb-4">
|
||||
{player.tags.map(tag => (
|
||||
<span key={tag} className="text-xs px-2 py-1 bg-surfaceHighlight rounded text-textMuted border border-white/5 font-mono">
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-6 text-sm">
|
||||
<div className="flex items-center gap-2 text-textMuted">
|
||||
<Icons.Terminal className="w-4 h-4" />
|
||||
<span className="font-mono text-textMain">{player.stats.playtimeHours}h</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-textMuted">
|
||||
<Icons.Box className="w-4 h-4" />
|
||||
<span className="font-mono text-textMain">Lvl {player.stats.level}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* Left Col: Inventory */}
|
||||
<div className="lg:col-span-1">
|
||||
{/* <InventoryGrid items={player.inventory} /> */}
|
||||
|
||||
<div className="bg-surface border border-border rounded-xl p-4 shadow-card">
|
||||
<h3 className="text-xs font-bold uppercase tracking-wider text-textMuted mb-3">Zugehörigkeit</h3>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`w-10 h-10 rounded flex items-center justify-center text-lg font-bold border border-white/5 ${
|
||||
playerOrg
|
||||
? (playerOrg.type === 'City' ? 'bg-blue-500/10 text-blue-400' :
|
||||
playerOrg.type === 'Guild' ? 'bg-amber-500/10 text-amber-400' :
|
||||
'bg-purple-500/10 text-purple-400')
|
||||
: 'bg-surfaceHighlight text-textMuted'
|
||||
}`}>
|
||||
{playerOrg ? playerOrg.name.charAt(0) : <Icons.Map className="w-5 h-5 opacity-50" />}
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm font-medium text-textMain">{player.stats.role}</div>
|
||||
<div className="text-xs text-textMuted">
|
||||
{playerOrg ? (
|
||||
<span className="group-hover:text-accentInfo transition-colors">{playerOrg.name}</span>
|
||||
) : (
|
||||
'Freiberufler / Keine Zugehörigkeit'
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{playerOrg && (
|
||||
<div className="mt-3 pt-3 border-t border-white/5 text-xs text-textMuted leading-relaxed">
|
||||
{playerOrg.type} • {playerOrg.description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Col: Story */}
|
||||
<div className="lg:col-span-2">
|
||||
<div className="bg-surface border border-border rounded-xl p-8 shadow-card min-h-[400px]">
|
||||
<div className="flex items-center justify-between mb-6 border-b border-border pb-4">
|
||||
<h2 className="text-lg font-semibold flex items-center gap-2">
|
||||
Charakter-Journal
|
||||
</h2>
|
||||
<span className="text-xs text-textMuted font-mono">Markdown Rendered</span>
|
||||
</div>
|
||||
<div className="prose-custom text-sm">
|
||||
{renderMarkdown(player.storyMarkdown)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PlayerProfile;
|
||||
350
pages/ProjectProfile.tsx
Normal file
350
pages/ProjectProfile.tsx
Normal file
@@ -0,0 +1,350 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Project, ShopItem } from '../types';
|
||||
import { MOCK_ORGS, MOCK_PLAYERS } from '../constants';
|
||||
import { Icons, ItemIcon } from '../components/IconSet';
|
||||
|
||||
interface ProjectProfileProps {
|
||||
project: Project;
|
||||
onBack: () => void;
|
||||
onSelectPlayer: (id: string) => void;
|
||||
onSelectOrg: (id: string) => void;
|
||||
}
|
||||
|
||||
const ProjectProfile: React.FC<ProjectProfileProps> = ({
|
||||
project,
|
||||
onBack,
|
||||
onSelectPlayer,
|
||||
onSelectOrg
|
||||
}) => {
|
||||
const [activeTab, setActiveTab] = useState<'overview' | 'shop'>('overview');
|
||||
|
||||
const org = project.associatedOrgId ? MOCK_ORGS.find(o => o.id === project.associatedOrgId) : null;
|
||||
const ownerPlayer = MOCK_PLAYERS.find(p => p.username === project.owner);
|
||||
const hasShop = project.shopCatalog && project.shopCatalog.length > 0;
|
||||
|
||||
// Group shop items
|
||||
const services = project.shopCatalog?.filter(i => i.type === 'service') || [];
|
||||
const products = project.shopCatalog?.filter(i => i.type !== 'service') || [];
|
||||
|
||||
return (
|
||||
<div className="animate-in slide-in-from-right-4 duration-300">
|
||||
<button onClick={onBack} className="flex items-center gap-2 text-sm text-textMuted hover:text-textMain mb-6 transition-colors group">
|
||||
<span className="group-hover:-translate-x-1 transition-transform">←</span> Zurück zum Verzeichnis
|
||||
</button>
|
||||
|
||||
{/* Banner Header */}
|
||||
<div className="relative h-64 md:h-80 rounded-2xl overflow-hidden border border-border mb-8 shadow-card group">
|
||||
<img
|
||||
src={project.bannerUrl || 'https://images.unsplash.com/photo-1542601906990-b4d3fb778b09?q=80&w=2070&auto=format&fit=crop'}
|
||||
alt={project.title}
|
||||
className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-105"
|
||||
/>
|
||||
|
||||
{/* Decorative elements for Black Market */}
|
||||
{project.category === 'Black Market' && (
|
||||
<div className="absolute inset-0 bg-red-900/20 mix-blend-overlay" />
|
||||
)}
|
||||
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-background via-background/80 to-transparent" />
|
||||
|
||||
<div className="absolute bottom-0 left-0 right-0 p-8">
|
||||
<div className="flex flex-col md:flex-row md:items-end justify-between gap-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className={`text-xs font-mono px-2 py-1 rounded border border-white/5 uppercase tracking-wider bg-black/40 backdrop-blur ${
|
||||
project.category === 'Story Arc' ? 'text-orange-400' :
|
||||
project.category === 'Black Market' ? 'text-red-400' :
|
||||
'text-accentInfo'
|
||||
}`}>
|
||||
{project.category}
|
||||
</span>
|
||||
{project.status === 'active' && (
|
||||
<span className="flex items-center gap-1.5 text-xs font-bold text-accentSuccess bg-black/40 px-2 py-1 rounded backdrop-blur border border-white/5">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-accentSuccess animate-pulse" />
|
||||
AKTIV
|
||||
</span>
|
||||
)}
|
||||
{project.hiring && (
|
||||
<span className="flex items-center gap-1.5 text-xs font-bold text-purple-400 bg-black/40 px-2 py-1 rounded backdrop-blur border border-white/5">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-purple-400 animate-pulse" />
|
||||
STELLEN
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<h1 className="text-4xl md:text-5xl font-bold text-white tracking-tight mb-2 drop-shadow-lg">{project.title}</h1>
|
||||
<div className="flex items-center gap-6 text-sm text-gray-300">
|
||||
<span
|
||||
className={`flex items-center gap-2 ${ownerPlayer ? 'cursor-pointer hover:text-white transition-colors group/owner' : ''}`}
|
||||
onClick={() => ownerPlayer && onSelectPlayer(ownerPlayer.uuid)}
|
||||
>
|
||||
<div className="w-5 h-5 rounded-full bg-gradient-to-br from-gray-700 to-gray-900 flex items-center justify-center text-[10px] border border-white/10 font-bold group-hover/owner:border-accentInfo group-hover/owner:text-accentInfo transition-colors">
|
||||
{project.owner.charAt(0)}
|
||||
</div>
|
||||
Inhaber: <span className="text-white font-medium group-hover/owner:underline decoration-accentInfo/50 underline-offset-4">{project.owner}</span>
|
||||
</span>
|
||||
{project.foundedDate && (
|
||||
<span className="flex items-center gap-1.5 text-textMuted">
|
||||
<Icons.Layers className="w-3.5 h-3.5" /> Gegr. {project.foundedDate}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{hasShop && (
|
||||
<button
|
||||
onClick={() => setActiveTab('shop')}
|
||||
className="flex items-center gap-2 bg-accentInfo hover:bg-accentInfo/90 text-white px-6 py-3 rounded-lg font-medium transition-colors shadow-lg shadow-accentInfo/20 border border-white/10 backdrop-blur-sm group/btn"
|
||||
>
|
||||
<Icons.ShoppingBag className="w-4 h-4 group-hover/btn:scale-110 transition-transform" />
|
||||
Katalog öffnen
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Navigation */}
|
||||
<div className="flex gap-8 border-b border-border mb-8 px-2">
|
||||
<button
|
||||
onClick={() => setActiveTab('overview')}
|
||||
className={`pb-4 text-sm font-medium transition-colors border-b-2 ${activeTab === 'overview' ? 'border-accentInfo text-white' : 'border-transparent text-textMuted hover:text-white'}`}
|
||||
>
|
||||
Übersicht
|
||||
</button>
|
||||
{hasShop && (
|
||||
<button
|
||||
onClick={() => setActiveTab('shop')}
|
||||
className={`pb-4 text-sm font-medium transition-colors border-b-2 ${activeTab === 'shop' ? 'border-accentInfo text-white' : 'border-transparent text-textMuted hover:text-white'}`}
|
||||
>
|
||||
Katalog ({project.shopCatalog?.length})
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="min-h-[400px]">
|
||||
{activeTab === 'overview' && (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
<div className="lg:col-span-2 space-y-8">
|
||||
<section className="bg-surface/50 border border-border rounded-xl p-6">
|
||||
<h3 className="text-lg font-bold text-white mb-4">Manifest</h3>
|
||||
<p className="text-textMuted leading-relaxed whitespace-pre-line text-lg">
|
||||
{project.description}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{/* Project Portfolio / Gallery */}
|
||||
{project.gallery && project.gallery.length > 0 && (
|
||||
<section>
|
||||
<h3 className="text-lg font-bold text-white mb-4 flex items-center gap-2">
|
||||
<Icons.Layers className="w-4 h-4 text-accentInfo" /> Portfolio
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
{project.gallery.map((url, idx) => (
|
||||
<div key={idx} className="rounded-xl overflow-hidden aspect-video border border-border group relative">
|
||||
<img src={url} alt={`Portfolio ${idx}`} className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110" />
|
||||
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-colors" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<section>
|
||||
<h3 className="text-lg font-bold text-white mb-4 flex items-center gap-2">
|
||||
<Icons.Users className="w-4 h-4 text-accentInfo" /> Personal
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
{project.employees.map((emp, idx) => {
|
||||
const empPlayer = MOCK_PLAYERS.find(p => p.username === emp);
|
||||
return (
|
||||
<div
|
||||
key={idx}
|
||||
onClick={() => empPlayer && onSelectPlayer(empPlayer.uuid)}
|
||||
className={`flex items-center gap-3 bg-surface border border-border p-3 rounded-lg ${empPlayer ? 'cursor-pointer hover:border-accentInfo/50 hover:bg-surfaceHighlight/30 transition-all group' : ''}`}
|
||||
>
|
||||
<div className="w-8 h-8 bg-surfaceHighlight rounded flex items-center justify-center text-xs font-bold text-textMuted group-hover:text-textMain transition-colors">
|
||||
{emp.charAt(0)}
|
||||
</div>
|
||||
<span className="text-sm font-medium text-gray-300 group-hover:text-accentInfo transition-colors">{emp}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{project.employees.length === 0 && (
|
||||
<div className="text-textMuted italic text-sm">Kein weiteres Personal registriert.</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{/* Right Column: Statistics */}
|
||||
<div className="space-y-6">
|
||||
<div className="bg-surface border border-border rounded-xl p-6 h-fit">
|
||||
<h3 className="text-xs font-bold uppercase text-textMuted mb-6 flex items-center gap-2">
|
||||
<Icons.Terminal className="w-4 h-4" /> Unternehmensdaten
|
||||
</h3>
|
||||
<div className="space-y-5">
|
||||
|
||||
{/* Industry */}
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-textMuted">Branche</span>
|
||||
<span className={`text-sm font-medium ${
|
||||
project.category === 'Black Market' ? 'text-red-400' : 'text-white'
|
||||
}`}>{project.category}</span>
|
||||
</div>
|
||||
<div className="w-full bg-white/5 h-px" />
|
||||
|
||||
{/* Founded */}
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-textMuted">Gegründet</span>
|
||||
<span className="text-sm font-medium text-white">{project.foundedDate || 'N/A'}</span>
|
||||
</div>
|
||||
<div className="w-full bg-white/5 h-px" />
|
||||
|
||||
{/* Reputation */}
|
||||
<div>
|
||||
<div className="flex justify-between text-xs mb-2">
|
||||
<span className="text-textMuted">Ruf</span>
|
||||
<span className="font-mono text-white">{project.progress}/100</span>
|
||||
</div>
|
||||
<div className="h-1.5 bg-surfaceHighlight rounded-full overflow-hidden">
|
||||
<div
|
||||
className={`h-full ${project.category === 'Black Market' ? 'bg-red-500' : 'bg-accentInfo'}`}
|
||||
style={{ width: `${project.progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full bg-white/5 h-px" />
|
||||
|
||||
{/* Workforce */}
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-textMuted">Belegschaft</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<Icons.Users className="w-3 h-3 text-textMuted" />
|
||||
<span className="text-sm font-medium text-white">{project.employees.length + 1}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Headquarters Link */}
|
||||
{org && (
|
||||
<div className="mt-4 pt-4 border-t border-white/5">
|
||||
<span className="text-xs text-textMuted block mb-2">Hauptsitz</span>
|
||||
<div
|
||||
onClick={() => onSelectOrg(org.id)}
|
||||
className="flex items-center gap-3 bg-surfaceHighlight/50 p-2 rounded-lg border border-white/5 cursor-pointer hover:border-accentInfo/50 hover:bg-surfaceHighlight transition-all group"
|
||||
>
|
||||
<div className="w-8 h-8 rounded bg-blue-500/20 text-blue-400 flex items-center justify-center font-bold text-xs border border-blue-500/20">
|
||||
{org.name.charAt(0)}
|
||||
</div>
|
||||
<span className="text-sm font-medium text-gray-200 group-hover:text-white transition-colors">{org.name}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'shop' && project.shopCatalog && (
|
||||
<div className="space-y-12">
|
||||
|
||||
{/* Services Section */}
|
||||
{services.length > 0 && (
|
||||
<section>
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<div className="p-2 bg-amber-500/10 rounded-lg text-amber-400">
|
||||
<Icons.Hammer className="w-5 h-5" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-white">Dienstleistungen & Verträge</h2>
|
||||
<p className="text-sm text-textMuted">Fachkräfte für Spezialaufträge anheuern.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{services.map(item => (
|
||||
<div key={item.id} className="bg-surface border border-border rounded-xl p-6 flex flex-col hover:border-accentInfo/30 transition-all">
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<h3 className="text-lg font-bold text-white">{item.name}</h3>
|
||||
<span className="font-mono text-accentInfo font-bold bg-accentInfo/10 px-3 py-1 rounded">
|
||||
{item.price} {item.currency}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-textMuted mb-4">{item.description}</p>
|
||||
|
||||
{item.materialsRequired && (
|
||||
<div className="mt-auto mb-4 bg-surfaceHighlight/50 border border-white/5 rounded p-3 text-xs">
|
||||
<span className="font-bold text-gray-300 block mb-1">Materialanforderungen:</span>
|
||||
<span className="text-textMuted">{item.materialsRequired}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button className="mt-2 w-full py-2 bg-white/5 hover:bg-white/10 rounded text-sm font-medium transition-colors border border-white/5">
|
||||
Angebot anfordern
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Products Section */}
|
||||
{products.length > 0 && (
|
||||
<section>
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<div className="p-2 bg-blue-500/10 rounded-lg text-blue-400">
|
||||
<Icons.Box className="w-5 h-5" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-white">Produktkatalog</h2>
|
||||
<p className="text-sm text-textMuted">Items, Bücher und Baupläne zur sofortigen Abholung.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{products.map(item => (
|
||||
<div key={item.id} className="bg-surface border border-border rounded-xl p-5 hover:border-accentInfo/30 transition-all group flex flex-col">
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<div className="w-12 h-12 bg-surfaceHighlight rounded-lg flex items-center justify-center text-textMuted group-hover:text-accentInfo transition-colors">
|
||||
{item.type === 'blueprint' ? <Icons.Scroll className="w-6 h-6" /> :
|
||||
<ItemIcon type="consumable" className="w-6 h-6" />}
|
||||
</div>
|
||||
<div className={`px-2 py-0.5 rounded text-[10px] font-bold uppercase border ${
|
||||
item.stock > 0 ? 'bg-green-500/10 text-green-400 border-green-500/20' : 'bg-red-500/10 text-red-400 border-red-500/20'
|
||||
}`}>
|
||||
{item.stock > 0 ? `${item.stock} auf Lager` : 'Ausverkauft'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 className="text-lg font-bold text-white mb-1">{item.name}</h3>
|
||||
<p className="text-sm text-textMuted mb-4 flex-1 line-clamp-2">{item.description}</p>
|
||||
|
||||
<div className="mt-auto pt-4 border-t border-white/5 flex items-center justify-between">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[10px] text-textMuted uppercase">Preis</span>
|
||||
<span className="font-mono font-bold text-accentInfo">
|
||||
{item.price} <span className="text-xs font-sans text-gray-400">{item.currency}</span>
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
disabled={item.stock === 0}
|
||||
className="px-4 py-2 bg-white/5 hover:bg-white/10 disabled:opacity-50 disabled:cursor-not-allowed rounded text-sm font-medium transition-colors"
|
||||
>
|
||||
Kaufen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectProfile;
|
||||
177
pages/Projects.tsx
Normal file
177
pages/Projects.tsx
Normal file
@@ -0,0 +1,177 @@
|
||||
import React, { useState } from 'react';
|
||||
import { MOCK_PROJECTS } from '../constants';
|
||||
import { Project } from '../types';
|
||||
import { Icons } from '../components/IconSet';
|
||||
|
||||
interface ProjectsProps {
|
||||
onSelectProject?: (id: string) => void;
|
||||
}
|
||||
|
||||
const StatusBadge = ({ status, hiring }: { status: Project['status'], hiring: boolean }) => {
|
||||
const styles = {
|
||||
'active': 'bg-accentInfo/10 text-accentInfo border-accentInfo/20',
|
||||
'recruiting': 'bg-accentSuccess/10 text-accentSuccess border-accentSuccess/20',
|
||||
'private': 'bg-textMuted/10 text-textMuted border-textMuted/20',
|
||||
'completed': 'bg-textMuted/10 text-textMuted border-textMuted/20',
|
||||
};
|
||||
|
||||
const labels = {
|
||||
'active': 'Aktives Geschäft',
|
||||
'recruiting': 'Offener Story-Arc',
|
||||
'private': 'Privat / Versteckt',
|
||||
'completed': 'Archiviert'
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex gap-2">
|
||||
<span className={`text-[10px] uppercase font-bold px-2 py-0.5 rounded-full border ${styles[status]}`}>
|
||||
{labels[status]}
|
||||
</span>
|
||||
{hiring && (
|
||||
<span className="text-[10px] uppercase font-bold px-2 py-0.5 rounded-full border bg-purple-500/10 text-purple-400 border-purple-500/20 animate-pulse">
|
||||
Stellen
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const VentureCard = ({ project, onClick }: { project: Project, onClick?: () => void }) => (
|
||||
<div
|
||||
onClick={onClick}
|
||||
className="bg-surface border border-border rounded-xl p-5 hover:border-accentInfo/40 transition-all duration-200 hover:shadow-card group flex flex-col h-full relative overflow-hidden cursor-pointer"
|
||||
>
|
||||
{/* Background accent for certain types */}
|
||||
{project.category === 'Black Market' && (
|
||||
<div className="absolute top-0 right-0 w-20 h-20 bg-red-900/10 blur-xl rounded-full -mr-10 -mt-10 pointer-events-none" />
|
||||
)}
|
||||
|
||||
<div className="flex justify-between items-start mb-3 relative z-10">
|
||||
<div className={`text-xs font-mono px-2 py-1 rounded border border-white/5 ${
|
||||
project.category === 'Story Arc' ? 'bg-orange-500/10 text-orange-400' :
|
||||
project.category === 'Black Market' ? 'bg-red-500/10 text-red-400' :
|
||||
'bg-surfaceHighlight text-textMuted'
|
||||
}`}>
|
||||
{project.category}
|
||||
</div>
|
||||
<StatusBadge status={project.status} hiring={project.hiring} />
|
||||
</div>
|
||||
|
||||
<h3 className="text-lg font-bold text-textMain mb-1 group-hover:text-accentInfo transition-colors relative z-10">
|
||||
{project.title}
|
||||
</h3>
|
||||
|
||||
<div className="flex items-center gap-2 mb-4 text-xs text-textMuted relative z-10">
|
||||
<span>Inhaber</span>
|
||||
<span className="text-textMain font-medium bg-white/5 px-1.5 py-0.5 rounded flex items-center gap-1.5">
|
||||
<div className="w-3 h-3 rounded-full bg-gradient-to-tr from-accentInfo to-blue-400"></div>
|
||||
{project.owner}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-textMuted mb-6 flex-1 leading-relaxed relative z-10">
|
||||
{project.description}
|
||||
</p>
|
||||
|
||||
<div className="pt-4 border-t border-white/5 space-y-3 relative z-10">
|
||||
{/* Reputation / Progress Bar */}
|
||||
<div>
|
||||
<div className="flex justify-between text-xs mb-1">
|
||||
<span className="text-textMuted">
|
||||
{project.category === 'Story Arc' ? 'Story Fortschritt' : 'Ruf'}
|
||||
</span>
|
||||
<span className="font-mono text-textMain">{project.progress}%</span>
|
||||
</div>
|
||||
<div className="h-1 bg-surfaceHighlight rounded-full overflow-hidden">
|
||||
<div
|
||||
className={`h-full rounded-full ${
|
||||
project.category === 'Black Market' ? 'bg-red-500/70' : 'bg-accentInfo/70'
|
||||
}`}
|
||||
style={{ width: `${project.progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between text-xs text-textMuted">
|
||||
<div className="flex items-center gap-2">
|
||||
<Icons.Users className="w-3 h-3" />
|
||||
<span>{project.employees.length} Mitglieder</span>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
{project.shopCatalog && project.shopCatalog.length > 0 && (
|
||||
<div className="flex items-center gap-1 text-accentInfo">
|
||||
<Icons.ShoppingBag className="w-3 h-3" />
|
||||
<span className="font-bold">Shop</span>
|
||||
</div>
|
||||
)}
|
||||
{project.foundedDate && <span>Gegr. {project.foundedDate}</span>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const Projects: React.FC<ProjectsProps> = ({ onSelectProject }) => {
|
||||
const [filter, setFilter] = useState<'all' | Project['status']>('all');
|
||||
|
||||
const filteredProjects = MOCK_PROJECTS.filter(p =>
|
||||
filter === 'all' ? true : p.status === filter
|
||||
);
|
||||
|
||||
const tabs = [
|
||||
{ id: 'all', label: 'Alle Unternehmen' },
|
||||
{ id: 'active', label: 'Aktive Firmen' },
|
||||
{ id: 'recruiting', label: 'Story Arcs' },
|
||||
{ id: 'private', label: 'Privat' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-8 animate-in fade-in slide-in-from-bottom-2">
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold mb-1">Unternehmen & Projekte</h1>
|
||||
<p className="text-textMuted">Spielergeführte Firmen, aktive Rollenspiel-Stränge und Dienstleister.</p>
|
||||
</div>
|
||||
<button className="bg-textMain text-background hover:bg-white font-medium px-4 py-2 rounded-lg text-sm transition-colors flex items-center gap-2">
|
||||
<span>+ Firma registrieren</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Filter Tabs */}
|
||||
<div className="flex flex-wrap gap-2 border-b border-border pb-1">
|
||||
{tabs.map(tab => (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setFilter(tab.id as any)}
|
||||
className={`px-4 py-2 text-sm font-medium rounded-t-lg transition-colors relative top-[1px] ${
|
||||
filter === tab.id
|
||||
? 'text-textMain border-b-2 border-accentInfo bg-surfaceHighlight/20'
|
||||
: 'text-textMuted hover:text-textMain hover:bg-white/5'
|
||||
}`}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
|
||||
{filteredProjects.map(project => (
|
||||
<VentureCard
|
||||
key={project.id}
|
||||
project={project}
|
||||
onClick={() => onSelectProject && onSelectProject(project.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{filteredProjects.length === 0 && (
|
||||
<div className="text-center py-20 text-textMuted">
|
||||
<p>Keine Unternehmen in dieser Kategorie gefunden.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Projects;
|
||||
128
pages/SetupGuide.tsx
Normal file
128
pages/SetupGuide.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
import React from 'react';
|
||||
import { Icons } from '../components/IconSet';
|
||||
|
||||
const CodeBlock = ({ label, lang, code }: { label: string, lang: string, code: string }) => (
|
||||
<div className="mb-8">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<span className="text-sm font-medium text-textMain flex items-center gap-2">
|
||||
<Icons.Terminal className="w-4 h-4 text-accentInfo" /> {label}
|
||||
</span>
|
||||
<span className="text-xs text-textMuted uppercase bg-surfaceHighlight px-2 py-1 rounded">{lang}</span>
|
||||
</div>
|
||||
<div className="bg-[#0b0b0d] border border-border rounded-lg p-4 overflow-x-auto shadow-inner group relative">
|
||||
<pre className="text-xs font-mono text-gray-300 leading-relaxed">
|
||||
<code>{code}</code>
|
||||
</pre>
|
||||
<button
|
||||
onClick={() => navigator.clipboard.writeText(code)}
|
||||
className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 bg-surfaceHighlight border border-border text-xs px-2 py-1 rounded text-textMain hover:bg-white/10 transition-all"
|
||||
>
|
||||
Kopieren
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const SetupGuide: React.FC = () => {
|
||||
return (
|
||||
<div className="max-w-3xl mx-auto space-y-8 pb-20">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold mb-4">Installationsanleitung</h1>
|
||||
<p className="text-textMuted leading-relaxed">
|
||||
Die Obsidian-Plattform nutzt einen leichtgewichtigen, serverunabhängigen Ansatz.
|
||||
Der Minecraft-Server generiert Daten über Vanilla-Befehle, und ein einfaches Shell-Skript lädt diese auf den Webserver hoch.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-surfaceHighlight/20 border border-accentInfo/20 rounded-lg text-sm text-textMuted">
|
||||
<strong className="text-accentInfo">Hinweis:</strong> Kein Python oder schweres Backend erforderlich. Funktioniert unter Windows (Batch) und Linux (Bash).
|
||||
</div>
|
||||
|
||||
<CodeBlock
|
||||
label="1. Datapack Funktion (export.mcfunction)"
|
||||
lang="mcfunction"
|
||||
code={`# File: data/obsidian/functions/export_player.mcfunction
|
||||
# Triggered by: execute as @a run function obsidian:export_player
|
||||
|
||||
# 1. Store Inventory Data to Storage
|
||||
data modify storage obsidian:temp Inventory set from entity @s Inventory
|
||||
|
||||
# 2. Log formatted JSON to server console
|
||||
# We use tellraw/log tricks or simply "data get" to print to log, which the script scrapes.
|
||||
# Ideally, use a macro to format this into a single line string.
|
||||
|
||||
# Simplified concept for log scraping:
|
||||
tellraw @a[tag=admin] [{"text":"JSON_EXPORT_START|"},{"selector":"@s"},{"text":"|"},{"nbt":"Inventory","entity":"@s"}]
|
||||
`}
|
||||
/>
|
||||
|
||||
<CodeBlock
|
||||
label="2. Upload Skript (Linux/Bash)"
|
||||
lang="bash"
|
||||
code={`#!/bin/bash
|
||||
# obsidian_sync.sh
|
||||
# Scrapes logs for JSON markers and uploads to Web Server
|
||||
|
||||
LOG_FILE="/path/to/minecraft/logs/latest.log"
|
||||
WEB_DIR="./web_data"
|
||||
FTP_HOST="ftp.yourserver.com"
|
||||
FTP_USER="user"
|
||||
FTP_PASS="pass"
|
||||
|
||||
mkdir -p $WEB_DIR
|
||||
|
||||
# 1. Parse Log (Simple Grep/Sed extraction)
|
||||
# Look for lines starting with JSON_EXPORT_START
|
||||
grep "JSON_EXPORT_START" $LOG_FILE | tail -n 10 > temp_extract.txt
|
||||
|
||||
while read p; do
|
||||
# Extract Name and JSON data (simplified regex)
|
||||
# In production, use 'jq' if available, otherwise pure bash string manipulation
|
||||
PLAYER_NAME=$(echo $p | cut -d'|' -f2)
|
||||
JSON_DATA=$(echo $p | cut -d'|' -f3)
|
||||
|
||||
echo "{ \"username\": \"$PLAYER_NAME\", \"inventory\": $JSON_DATA }" > "$WEB_DIR/$PLAYER_NAME.json"
|
||||
done < temp_extract.txt
|
||||
|
||||
# 2. Upload via lftp (High performance, parallel)
|
||||
lftp -u $FTP_USER,$FTP_PASS $FTP_HOST <<EOF
|
||||
mirror -R -v $WEB_DIR /public_html/api/players
|
||||
bye
|
||||
EOF
|
||||
|
||||
echo "Sync Complete at $(date)"
|
||||
`}
|
||||
/>
|
||||
|
||||
<CodeBlock
|
||||
label="3. Upload Skript (Windows Batch)"
|
||||
lang="batch"
|
||||
code={`@echo off
|
||||
:: obsidian_sync.bat
|
||||
:: Uses standard Windows FTP command
|
||||
|
||||
set LOCAL_DIR=C:\\Minecraft\\web_data
|
||||
set FTP_SCRIPT=ftp_script.txt
|
||||
|
||||
:: Create FTP script dynamically
|
||||
echo open ftp.yourserver.com > %FTP_SCRIPT%
|
||||
echo myuser >> %FTP_SCRIPT%
|
||||
echo mypassword >> %FTP_SCRIPT%
|
||||
echo cd public_html/api/players >> %FTP_SCRIPT%
|
||||
echo lcd %LOCAL_DIR% >> %FTP_SCRIPT%
|
||||
echo mput *.json >> %FTP_SCRIPT%
|
||||
echo disconnect >> %FTP_SCRIPT%
|
||||
echo quit >> %FTP_SCRIPT%
|
||||
|
||||
:: Run FTP
|
||||
ftp -i -s:%FTP_SCRIPT%
|
||||
del %FTP_SCRIPT%
|
||||
|
||||
echo Sync Complete.
|
||||
`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SetupGuide;
|
||||
29
tsconfig.json
Normal file
29
tsconfig.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"experimentalDecorators": true,
|
||||
"useDefineForClassFields": false,
|
||||
"module": "ESNext",
|
||||
"lib": [
|
||||
"ES2022",
|
||||
"DOM",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"skipLibCheck": true,
|
||||
"types": [
|
||||
"node"
|
||||
],
|
||||
"moduleResolution": "bundler",
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"allowJs": true,
|
||||
"jsx": "react-jsx",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./*"
|
||||
]
|
||||
},
|
||||
"allowImportingTsExtensions": true,
|
||||
"noEmit": true
|
||||
}
|
||||
}
|
||||
77
types.ts
Normal file
77
types.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
export interface Item {
|
||||
id: string;
|
||||
name: string;
|
||||
count: number;
|
||||
type: 'tool' | 'block' | 'consumable' | 'misc';
|
||||
rarity?: 'common' | 'uncommon' | 'rare' | 'epic';
|
||||
nbtSummary?: string;
|
||||
}
|
||||
|
||||
export interface PlayerStats {
|
||||
playtimeHours: number;
|
||||
level: number;
|
||||
role: string;
|
||||
organizationId?: string;
|
||||
}
|
||||
|
||||
export interface Player {
|
||||
uuid: string;
|
||||
username: string;
|
||||
skinUrl?: string; // Placeholder in a real app
|
||||
inventory: (Item | null)[];
|
||||
stats: PlayerStats;
|
||||
storyMarkdown: string;
|
||||
tags: string[];
|
||||
isOnline: boolean;
|
||||
}
|
||||
|
||||
export interface CityStats {
|
||||
taxRate: number;
|
||||
biome: string;
|
||||
defenseRating: number; // 0-10
|
||||
government: string;
|
||||
specialty: string;
|
||||
}
|
||||
|
||||
export interface Organization {
|
||||
id: string;
|
||||
name: string;
|
||||
type: 'City' | 'Guild' | 'Company';
|
||||
description: string;
|
||||
memberCount: number;
|
||||
status: 'active' | 'archived';
|
||||
bannerUrl?: string;
|
||||
gallery?: string[];
|
||||
establishedYear?: string;
|
||||
mayor?: string;
|
||||
cityStats?: CityStats;
|
||||
}
|
||||
|
||||
export interface ShopItem {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
price: number;
|
||||
currency: 'Gold' | 'Credits' | 'Barter' | 'Diamonds';
|
||||
stock: number;
|
||||
type: 'item' | 'service' | 'blueprint';
|
||||
imageUrl?: string;
|
||||
materialsRequired?: string; // e.g. "Customer provides Stone"
|
||||
}
|
||||
|
||||
export interface Project {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
category: 'Enterprise' | 'Service' | 'Story Arc' | 'Faction' | 'Black Market';
|
||||
status: 'active' | 'recruiting' | 'private' | 'completed';
|
||||
progress: number; // For story arcs completion or company reputation
|
||||
owner: string;
|
||||
employees: string[];
|
||||
hiring: boolean;
|
||||
foundedDate?: string;
|
||||
associatedOrgId?: string; // Links this project to a city or guild
|
||||
shopCatalog?: ShopItem[];
|
||||
gallery?: string[];
|
||||
bannerUrl?: string;
|
||||
}
|
||||
23
vite.config.ts
Normal file
23
vite.config.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import path from 'path';
|
||||
import { defineConfig, loadEnv } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig(({ mode }) => {
|
||||
const env = loadEnv(mode, '.', '');
|
||||
return {
|
||||
server: {
|
||||
port: 3000,
|
||||
host: '0.0.0.0',
|
||||
},
|
||||
plugins: [react()],
|
||||
define: {
|
||||
'process.env.API_KEY': JSON.stringify(env.GEMINI_API_KEY),
|
||||
'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY)
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, '.'),
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user