From d1b797a320845167e76a487d4108e045fc189d64 Mon Sep 17 00:00:00 2001 From: Lars Behrends Date: Sun, 28 Dec 2025 02:15:09 +0100 Subject: [PATCH] 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. --- .gitignore | 24 +++ App.tsx | 168 +++++++++++++++++ README.md | 25 ++- components/IconSet.tsx | 91 +++++++++ components/InventoryGrid.tsx | 63 +++++++ components/Layout.tsx | 126 +++++++++++++ constants.ts | 191 +++++++++++++++++++ index.html | 74 ++++++++ index.tsx | 15 ++ metadata.json | 5 + package.json | 21 +++ pages/Cities.tsx | 75 ++++++++ pages/CityProfile.tsx | 247 ++++++++++++++++++++++++ pages/Dashboard.tsx | 111 +++++++++++ pages/DatapackGenerator.tsx | 273 +++++++++++++++++++++++++++ pages/Organizations.tsx | 101 ++++++++++ pages/PlayerProfile.tsx | 128 +++++++++++++ pages/ProjectProfile.tsx | 350 +++++++++++++++++++++++++++++++++++ pages/Projects.tsx | 177 ++++++++++++++++++ pages/SetupGuide.tsx | 128 +++++++++++++ tsconfig.json | 29 +++ types.ts | 77 ++++++++ vite.config.ts | 23 +++ 23 files changed, 2514 insertions(+), 8 deletions(-) create mode 100644 .gitignore create mode 100644 App.tsx create mode 100644 components/IconSet.tsx create mode 100644 components/InventoryGrid.tsx create mode 100644 components/Layout.tsx create mode 100644 constants.ts create mode 100644 index.html create mode 100644 index.tsx create mode 100644 metadata.json create mode 100644 package.json create mode 100644 pages/Cities.tsx create mode 100644 pages/CityProfile.tsx create mode 100644 pages/Dashboard.tsx create mode 100644 pages/DatapackGenerator.tsx create mode 100644 pages/Organizations.tsx create mode 100644 pages/PlayerProfile.tsx create mode 100644 pages/ProjectProfile.tsx create mode 100644 pages/Projects.tsx create mode 100644 pages/SetupGuide.tsx create mode 100644 tsconfig.json create mode 100644 types.ts create mode 100644 vite.config.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/.gitignore @@ -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? diff --git a/App.tsx b/App.tsx new file mode 100644 index 0000000..c394cd4 --- /dev/null +++ b/App.tsx @@ -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(null); + const [selectedCityId, setSelectedCityId] = useState(null); + const [selectedProjectId, setSelectedProjectId] = useState(null); + const [selectedOrgId, setSelectedOrgId] = useState(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 ; + + if (activeTab === 'projects') { + if (selectedProjectId) { + const project = MOCK_PROJECTS.find(p => p.id === selectedProjectId); + if (project) return ( + setSelectedProjectId(null)} + onSelectPlayer={navigateToPlayer} + onSelectOrg={navigateToOrg} + /> + ); + } + return ; + } + + if (activeTab === 'organizations') { + if (selectedOrgId) { + const org = MOCK_ORGS.find(o => o.id === selectedOrgId); + if (org) return ( + setSelectedOrgId(null)} + backLabel="Zurück zum Verzeichnis" + onSelectPlayer={navigateToPlayer} + onSelectProject={navigateToProject} + /> + ); + } + return ; + } + + if (activeTab === 'setup') return ; + + if (activeTab === 'datapack') return ; + + if (activeTab === 'cities') { + if (selectedCityId) { + const city = MOCK_ORGS.find(o => o.id === selectedCityId); + if (city) return ( + setSelectedCityId(null)} + backLabel="Zurück zu Städte" + onSelectPlayer={navigateToPlayer} + onSelectProject={navigateToProject} + /> + ); + } + return ; + } + + if (activeTab === 'players') { + if (selectedPlayerId) { + const player = MOCK_PLAYERS.find(p => p.uuid === selectedPlayerId); + if (player) return setSelectedPlayerId(null)} />; + } + + return ( +
+
+

Bürgerverzeichnis

+
+ + +
+
+ +
+ {MOCK_PLAYERS.map(player => ( +
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" + > +
+
+ +
+
+
{player.username}
+
+ {player.tags.slice(0, 2).map(t => {t})} +
+
+
+
+ ))} +
+
+ ); + } + + return ( +
+ +

Modul {activeTab} wird derzeit gewartet.

+
+ ); + }; + + return ( + + {renderContent()} + + ); +} + +export default App; \ No newline at end of file diff --git a/README.md b/README.md index 2241000..aa357e3 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,20 @@
- GHBanner - -

Built with AI Studio

- -

The fastest path from prompt to production with Gemini.

- - Start building -
+ +# 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` diff --git a/components/IconSet.tsx b/components/IconSet.tsx new file mode 100644 index 0000000..473f180 --- /dev/null +++ b/components/IconSet.tsx @@ -0,0 +1,91 @@ +import React from 'react'; + +// UI Icons (Lucide-style wrappers) +export const Icons = { + Home: ({ className }: { className?: string }) => ( + + ), + Users: ({ className }: { className?: string }) => ( + + ), + Map: ({ className }: { className?: string }) => ( + + ), + Layers: ({ className }: { className?: string }) => ( + + ), + Terminal: ({ className }: { className?: string }) => ( + + ), + Box: ({ className }: { className?: string }) => ( + + ), + Search: ({ className }: { className?: string }) => ( + + ), + Crown: ({ className }: { className?: string }) => ( + + ), + Shield: ({ className }: { className?: string }) => ( + + ), + Coins: ({ className }: { className?: string }) => ( + + ), + Scroll: ({ className }: { className?: string }) => ( + + ), + ShoppingBag: ({ className }: { className?: string }) => ( + + ), + Tag: ({ className }: { className?: string }) => ( + + ), + Hammer: ({ className }: { className?: string }) => ( + + ) +}; + +// 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 ( + + + + + + + ); + } + + if (type === 'block') { + return ( + + + + + + ); + } + + if (type === 'consumable') { + return ( + + + + + + ); + } + + // Misc / Default + return ( + + + + + ); +}; \ No newline at end of file diff --git a/components/InventoryGrid.tsx b/components/InventoryGrid.tsx new file mode 100644 index 0000000..299f3ed --- /dev/null +++ b/components/InventoryGrid.tsx @@ -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 = ({ items }) => { + return ( +
+
+

Inventar

+
+ {items.filter(i => i !== null).length} / {items.length} Plätze +
+
+ +
+ {items.map((item, index) => ( +
+ {item ? ( + <> +
+ +
+ + {/* Count Badge */} + {item.count > 1 && ( + + {item.count} + + )} + + {/* Tooltip */} +
+
+
+ {item.name} +
+ {item.nbtSummary && ( +
+ {item.nbtSummary} +
+ )} +
+
+ + ) : ( + // Empty slot styling +
+ )} +
+ ))} +
+
+ ); +}; + +export default InventoryGrid; \ No newline at end of file diff --git a/components/Layout.tsx b/components/Layout.tsx new file mode 100644 index 0000000..d598999 --- /dev/null +++ b/components/Layout.tsx @@ -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; +}) => ( + +); + +const Layout: React.FC = ({ children, activeTab, onNavigate }) => { + const [mobileMenuOpen, setMobileMenuOpen] = useState(false); + + return ( +
+ {/* Top Header - Website Style */} +
+
+
+ {/* Logo */} +
onNavigate('dashboard')} + > +
+ P.V. +
+ Projekt: Vollidion +
+ + {/* Desktop Nav */} + +
+ +
+ + + + {/* Mobile Menu Toggle */} + +
+
+ + {/* Mobile Nav Dropdown */} + {mobileMenuOpen && ( +
+
{ onNavigate('dashboard'); setMobileMenuOpen(false); }} className="block py-2 text-textMuted hover:text-textMain">Übersicht
+
{ onNavigate('cities'); setMobileMenuOpen(false); }} className="block py-2 text-textMuted hover:text-textMain">Städte
+
{ onNavigate('players'); setMobileMenuOpen(false); }} className="block py-2 text-textMuted hover:text-textMain">Bürger
+
{ onNavigate('organizations'); setMobileMenuOpen(false); }} className="block py-2 text-textMuted hover:text-textMain">Organisationen
+
{ onNavigate('projects'); setMobileMenuOpen(false); }} className="block py-2 text-textMuted hover:text-textMain">Unternehmen
+
{ onNavigate('datapack'); setMobileMenuOpen(false); }} className="block py-2 text-textMain">Datapack holen
+
{ onNavigate('setup'); setMobileMenuOpen(false); }} className="block py-2 text-accentInfo font-mono text-sm border-t border-white/5 pt-4">Admin Setup >_
+
+ )} +
+ + {/* Main Content - Page Flow */} +
+
+ {children} +
+
+ + {/* Footer - Adds to the "Website" feel */} +
+
+
+
+

© 2024 Obsidian Platform

+
+
+ Dokumentation + Server Status + Datenschutz +
+
+
+
+ ); +}; + +export default Layout; \ No newline at end of file diff --git a/constants.ts b/constants.ts new file mode 100644 index 0000000..ad4a683 --- /dev/null +++ b/constants.ts @@ -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' } + ] + }, +]; \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..3b30ea4 --- /dev/null +++ b/index.html @@ -0,0 +1,74 @@ + + + + + + Projekt: Vollidion | Server Plattform + + + + + + + + + +
+ + \ No newline at end of file diff --git a/index.tsx b/index.tsx new file mode 100644 index 0000000..6ca5361 --- /dev/null +++ b/index.tsx @@ -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( + + + +); \ No newline at end of file diff --git a/metadata.json b/metadata.json new file mode 100644 index 0000000..ed7465c --- /dev/null +++ b/metadata.json @@ -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": [] +} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..f3d0435 --- /dev/null +++ b/package.json @@ -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" + } +} diff --git a/pages/Cities.tsx b/pages/Cities.tsx new file mode 100644 index 0000000..ab17c46 --- /dev/null +++ b/pages/Cities.tsx @@ -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 }) => ( +
+ {/* Background Image */} +
+ {city.name} +
+
+ + {/* Content */} +
+
+
+
{city.establishedYear || 'Unbekannte Ära'}
+

{city.name}

+

+ {city.description} +

+
+ +
+
+ + {city.memberCount} Bürger +
+
+ {city.status} +
+
+
+
+
+); + +const Cities: React.FC = ({ onSelectCity }) => { + const cities = MOCK_ORGS.filter(org => org.type === 'City'); + + return ( +
+
+

Große Siedlungen

+

Entdecke die blühenden Zentren der Zivilisation im Obsidian-Tal.

+
+ +
+ {cities.map(city => ( + onSelectCity(city.id)} /> + ))} +
+ + {cities.length === 0 && ( +
+

Noch keine Städte gegründet.

+
+ )} +
+ ); +}; + +export default Cities; \ No newline at end of file diff --git a/pages/CityProfile.tsx b/pages/CityProfile.tsx new file mode 100644 index 0000000..2b2e7b7 --- /dev/null +++ b/pages/CityProfile.tsx @@ -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 = ({ + 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 ( +
+ + + {/* Hero Header */} +
+ {city.name} +
+ +
+
+
+ {city.establishedYear && ( +
+ GEGR. {city.establishedYear} +
+ )} +

{city.name}

+
+ + {city.memberCount} Mitglieder + + +
{city.status} + +
+
+ {city.mayor && ( +
+
Aktueller Anführer
+
+ + {city.mayor} +
+
+ )} +
+
+
+ + {/* Navigation Tabs */} +
+ + + +
+ + {/* Content Area */} +
+ {activeTab === 'overview' && ( +
+
+
+

+ Über {city.name} +

+

+ {city.description} +

+
+ + {city.gallery && city.gallery.length > 0 && ( +
+

Bildarchiv

+
+ {city.gallery.map((url, idx) => ( +
+ {`Gallery +
+
+ ))} +
+
+ )} +
+ + {city.cityStats && ( +
+

+ Stadt-Statistiken +

+
+ + {/* Tax Rate */} +
+
+
+ +
+ Steuersatz +
+
+ {city.cityStats.taxRate}% +
Pro Transaktion
+
+
+ +
+ + {/* Defense */} +
+
+
+
+ +
+ Verteidigungswert +
+ {city.cityStats.defenseRating}/10 +
+
+
+
+
+ +
+ + {/* Gov Type */} +
+
+
+ +
+ Regierungsform +
+ {city.cityStats.government} +
+ +
+ + {/* Biome */} +
+ Biom + {city.cityStats.biome} +
+
+ Spezialität + {city.cityStats.specialty} +
+
+
+ )} +
+ )} + + {activeTab === 'residents' && ( +
+ {residents.map(player => ( +
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" + > +
+ {player.username.charAt(0)} +
+
+
{player.username}
+
{player.stats.role}
+
+
+ ))} + {residents.length === 0 &&
Keine Bewohner öffentlich registriert.
} +
+ )} + + {activeTab === 'ventures' && ( +
+ {ventures.map(project => ( +
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" + > +
+ + {project.category} + + {project.hiring && ( + STELLEN + )} +
+

{project.title}

+

{project.description}

+
+ Geführt von {project.owner} +
{project.progress}% Fortschr.
+
+
+ ))} + {ventures.length === 0 &&
Keine aktiven Unternehmen in dieser Organisation.
} +
+ )} +
+
+ ); +}; + +export default CityProfile; \ No newline at end of file diff --git a/pages/Dashboard.tsx b/pages/Dashboard.tsx new file mode 100644 index 0000000..9e2d55b --- /dev/null +++ b/pages/Dashboard.tsx @@ -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) => ( +
+
+
+ +
+ {trend && ( + 0 ? 'bg-accentSuccess/10 text-accentSuccess' : 'bg-accentWarn/10 text-accentWarn' + }`}> + {trend > 0 ? '+' : ''}{trend}% + + )} +
+
{value}
+
{label}
+
+); + +const ProjectCard = ({ project }: { project: any }) => ( +
+
+
+
{project.title}
+
Inhaber: {project.owner}
+
+
+
+
+
+
+
{project.progress}%
+
+); + +const Dashboard: React.FC = () => { + return ( +
+ {/* Intro Section */} +
+
+

Live-Telemetrie

+

Tal-Übersicht

+
+

+ Echtzeit-Datenaggregation aus dem Zentralarchiv. +

+
+ + {/* KPI Grid */} +
+ + p.status === 'active' || p.status === 'recruiting').length} trend={5} icon={Icons.Layers} /> + +
+ + {/* Content Grid */} +
+ {/* Projects List - Wider */} +
+
+

Top Unternehmen

+ +
+
+ {MOCK_PROJECTS.slice(0, 5).map(p => )} +
+
+ + {/* Decree / News - Narrower */} +
+

Offizieller Erlass

+
+ {/* Decorative background element */} +
+ +
+
+ NEU + VOR 12 MIN +
+ +
+

+ "Im Auftrag des Bürgermeisters ist jeglicher Handel innerhalb der inneren Mauern für das kommende Fest steuerbefreit." +

+

+ Bürger werden ermutigt, ihre Stände im Marktviertel aufzubauen, bevor der Mond am 14. Tag aufgeht. +

+
+
+ +
+ #Wirtschaft + #Events +
+
+
+
+
+ ); +}; + +export default Dashboard; \ No newline at end of file diff --git a/pages/DatapackGenerator.tsx b/pages/DatapackGenerator.tsx new file mode 100644 index 0000000..f2a9169 --- /dev/null +++ b/pages/DatapackGenerator.tsx @@ -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('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 ( +
+ {/* Intro Sidebar */} +
+
+
+ v2.4 Non-OP (DE) + Trigger System +
+

Datapack Generator

+

+ Non-OP Management System. Nutzt Item-Umbenennung als sichere Eingabemethode für Spielertexte. +

+
+ + {/* PLAYER GUIDE SECTION */} +
+
+

+ + Spieler Anleitung +

+
+
+

+ Verteile diese Anleitung an deine Spieler. Da normale Spieler keine Befehle nutzen können, nutzt dieses Pack ein immersives "Item Key" System. +

+ +
+
+
1
+
+ Erstellen +

Klicke [+ Unternehmung], benenne ein Item zu "Mein Shop", halte es, und klicke den Bestätigungs-Link.

+
+
+
+
2
+
+ Zum Bearbeiten wählen +

Halte ein Item mit dem exakten Namen deiner Firma. Klicke [Wähle per Name]. Das Menü visiert nun diesen Eintrag an.

+
+
+
+
3
+
+ Modifizieren +

+ Nutze [Umbenennen] oder [Beschr. ändern]. Du wirst aufgefordert, ein Item zum NEUEN Wert umzubenennen und zu bestätigen. +

+
+
+
+
+
+ + {/* Admin Note */} +
+ Wichtig: + Ich habe load.json hinzugefügt, um sicherzustellen, dass das Scoreboard automatisch erstellt wird. Bitte führe /reload nach der Installation aus. +
+
+ + {/* Code Viewer */} +
+ {/* Tabs */} +
+ {files.map(file => ( + + ))} +
+ + {/* Editor Content */} +
+
+ +
+ + {currentFile && ( +
+
+ Dateipfad: {currentFile.path} +
+
+                        {currentFile.content}
+                    
+
+ )} +
+
+
+ ); +}; + +export default DatapackGenerator; \ No newline at end of file diff --git a/pages/Organizations.tsx b/pages/Organizations.tsx new file mode 100644 index 0000000..7147488 --- /dev/null +++ b/pages/Organizations.tsx @@ -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 }) => ( +
+
+
+ {org.name.charAt(0)} +
+ + {org.status} + +
+ +

+ {org.name} +

+
{org.type}
+ +

+ {org.description} +

+ +
+
+ + {org.memberCount} Mitglieder +
+
+
+); + +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 ( +
+
+
+

Organisationen

+

Offizielle Fraktionen, Städte und registrierte Gilden.

+
+ +
+ +
+ {tabs.map(tab => ( + + ))} +
+ +
+ {filteredOrgs.map(org => ( + onSelectOrg(org.id)} /> + ))} +
+ + {filteredOrgs.length === 0 && ( +
+

Keine Organisationen gefunden.

+
+ )} +
+ ); +}; + +export default Organizations; \ No newline at end of file diff --git a/pages/PlayerProfile.tsx b/pages/PlayerProfile.tsx new file mode 100644 index 0000000..8580280 --- /dev/null +++ b/pages/PlayerProfile.tsx @@ -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 = ({ 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

{line.replace('# ', '')}

; + if (line.startsWith('### ')) return

{line.replace('### ', '')}

; + if (line.startsWith('> ')) return
{line.replace('> ', '')}
; + if (line.startsWith('* ')) return
  • {line.replace('* ', '')}
  • ; + return

    {line}

    ; + }); + }; + + return ( +
    + + + {/* Header */} +
    +
    +
    + +
    + +
    +
    +

    {player.username}

    + {player.isOnline && ( + + + Online + + )} +
    + +
    + {player.tags.map(tag => ( + + {tag} + + ))} +
    + +
    +
    + + {player.stats.playtimeHours}h +
    +
    + + Lvl {player.stats.level} +
    +
    +
    +
    +
    + +
    + {/* Left Col: Inventory */} +
    + {/* */} + +
    +

    Zugehörigkeit

    +
    +
    + {playerOrg ? playerOrg.name.charAt(0) : } +
    +
    +
    {player.stats.role}
    +
    + {playerOrg ? ( + {playerOrg.name} + ) : ( + 'Freiberufler / Keine Zugehörigkeit' + )} +
    +
    +
    + {playerOrg && ( +
    + {playerOrg.type} • {playerOrg.description} +
    + )} +
    +
    + + {/* Right Col: Story */} +
    +
    +
    +

    + Charakter-Journal +

    + Markdown Rendered +
    +
    + {renderMarkdown(player.storyMarkdown)} +
    +
    +
    +
    +
    + ); +}; + +export default PlayerProfile; \ No newline at end of file diff --git a/pages/ProjectProfile.tsx b/pages/ProjectProfile.tsx new file mode 100644 index 0000000..589c481 --- /dev/null +++ b/pages/ProjectProfile.tsx @@ -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 = ({ + 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 ( +
    + + + {/* Banner Header */} +
    + {project.title} + + {/* Decorative elements for Black Market */} + {project.category === 'Black Market' && ( +
    + )} + +
    + +
    +
    +
    +
    + + {project.category} + + {project.status === 'active' && ( + + + AKTIV + + )} + {project.hiring && ( + + + STELLEN + + )} +
    +

    {project.title}

    +
    + ownerPlayer && onSelectPlayer(ownerPlayer.uuid)} + > +
    + {project.owner.charAt(0)} +
    + Inhaber: {project.owner} +
    + {project.foundedDate && ( + + Gegr. {project.foundedDate} + + )} +
    +
    + + {hasShop && ( + + )} +
    +
    +
    + + {/* Navigation */} +
    + + {hasShop && ( + + )} +
    + + {/* Content */} +
    + {activeTab === 'overview' && ( +
    +
    +
    +

    Manifest

    +

    + {project.description} +

    +
    + + {/* Project Portfolio / Gallery */} + {project.gallery && project.gallery.length > 0 && ( +
    +

    + Portfolio +

    +
    + {project.gallery.map((url, idx) => ( +
    + {`Portfolio +
    +
    + ))} +
    +
    + )} + +
    +

    + Personal +

    +
    + {project.employees.map((emp, idx) => { + const empPlayer = MOCK_PLAYERS.find(p => p.username === emp); + return ( +
    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' : ''}`} + > +
    + {emp.charAt(0)} +
    + {emp} +
    + ); + })} + {project.employees.length === 0 && ( +
    Kein weiteres Personal registriert.
    + )} +
    +
    +
    + + {/* Right Column: Statistics */} +
    +
    +

    + Unternehmensdaten +

    +
    + + {/* Industry */} +
    + Branche + {project.category} +
    +
    + + {/* Founded */} +
    + Gegründet + {project.foundedDate || 'N/A'} +
    +
    + + {/* Reputation */} +
    +
    + Ruf + {project.progress}/100 +
    +
    +
    +
    +
    +
    + + {/* Workforce */} +
    + Belegschaft +
    + + {project.employees.length + 1} +
    +
    + + {/* Headquarters Link */} + {org && ( +
    + Hauptsitz +
    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" + > +
    + {org.name.charAt(0)} +
    + {org.name} +
    +
    + )} + +
    +
    +
    +
    + )} + + {activeTab === 'shop' && project.shopCatalog && ( +
    + + {/* Services Section */} + {services.length > 0 && ( +
    +
    +
    + +
    +
    +

    Dienstleistungen & Verträge

    +

    Fachkräfte für Spezialaufträge anheuern.

    +
    +
    + +
    + {services.map(item => ( +
    +
    +

    {item.name}

    + + {item.price} {item.currency} + +
    +

    {item.description}

    + + {item.materialsRequired && ( +
    + Materialanforderungen: + {item.materialsRequired} +
    + )} + + +
    + ))} +
    +
    + )} + + {/* Products Section */} + {products.length > 0 && ( +
    +
    +
    + +
    +
    +

    Produktkatalog

    +

    Items, Bücher und Baupläne zur sofortigen Abholung.

    +
    +
    + +
    + {products.map(item => ( +
    +
    +
    + {item.type === 'blueprint' ? : + } +
    +
    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'} +
    +
    + +

    {item.name}

    +

    {item.description}

    + +
    +
    + Preis + + {item.price} {item.currency} + +
    + +
    +
    + ))} +
    +
    + )} +
    + )} +
    +
    + ); +}; + +export default ProjectProfile; \ No newline at end of file diff --git a/pages/Projects.tsx b/pages/Projects.tsx new file mode 100644 index 0000000..4b81048 --- /dev/null +++ b/pages/Projects.tsx @@ -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 ( +
    + + {labels[status]} + + {hiring && ( + + Stellen + + )} +
    + ); +}; + +const VentureCard = ({ project, onClick }: { project: Project, onClick?: () => void }) => ( +
    + {/* Background accent for certain types */} + {project.category === 'Black Market' && ( +
    + )} + +
    +
    + {project.category} +
    + +
    + +

    + {project.title} +

    + +
    + Inhaber + +
    + {project.owner} +
    +
    + +

    + {project.description} +

    + +
    + {/* Reputation / Progress Bar */} +
    +
    + + {project.category === 'Story Arc' ? 'Story Fortschritt' : 'Ruf'} + + {project.progress}% +
    +
    +
    +
    +
    + +
    +
    + + {project.employees.length} Mitglieder +
    +
    + {project.shopCatalog && project.shopCatalog.length > 0 && ( +
    + + Shop +
    + )} + {project.foundedDate && Gegr. {project.foundedDate}} +
    +
    +
    +
    +); + +const Projects: React.FC = ({ 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 ( +
    +
    +
    +

    Unternehmen & Projekte

    +

    Spielergeführte Firmen, aktive Rollenspiel-Stränge und Dienstleister.

    +
    + +
    + + {/* Filter Tabs */} +
    + {tabs.map(tab => ( + + ))} +
    + + {/* Grid */} +
    + {filteredProjects.map(project => ( + onSelectProject && onSelectProject(project.id)} + /> + ))} +
    + + {filteredProjects.length === 0 && ( +
    +

    Keine Unternehmen in dieser Kategorie gefunden.

    +
    + )} +
    + ); +}; + +export default Projects; \ No newline at end of file diff --git a/pages/SetupGuide.tsx b/pages/SetupGuide.tsx new file mode 100644 index 0000000..05b28f0 --- /dev/null +++ b/pages/SetupGuide.tsx @@ -0,0 +1,128 @@ +import React from 'react'; +import { Icons } from '../components/IconSet'; + +const CodeBlock = ({ label, lang, code }: { label: string, lang: string, code: string }) => ( +
    +
    + + {label} + + {lang} +
    +
    +
    +        {code}
    +      
    + +
    +
    +); + +const SetupGuide: React.FC = () => { + return ( +
    +
    +

    Installationsanleitung

    +

    + 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. +

    +
    + +
    + Hinweis: Kein Python oder schweres Backend erforderlich. Funktioniert unter Windows (Batch) und Linux (Bash). +
    + + + + 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 < + + %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. +`} + /> +
    + ); +}; + +export default SetupGuide; \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..2c6eed5 --- /dev/null +++ b/tsconfig.json @@ -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 + } +} \ No newline at end of file diff --git a/types.ts b/types.ts new file mode 100644 index 0000000..9539255 --- /dev/null +++ b/types.ts @@ -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; +} \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..ee5fb8d --- /dev/null +++ b/vite.config.ts @@ -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, '.'), + } + } + }; +});