feat: Initialize project with Vite, React, and TypeScript

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

75
pages/Cities.tsx Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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;