feat: initialize dual identity author portfolio
Setup project structure with React, Tailwind CSS, and TypeScript for a multi-theme portfolio application. Includes mock data definitions, environment configuration, and Vite build tooling.
This commit is contained in:
parent
dec0559235
commit
d3bcf718ae
20 changed files with 2488 additions and 6 deletions
9
.env.example
Normal file
9
.env.example
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# GEMINI_API_KEY: Required for Gemini AI API calls.
|
||||
# AI Studio automatically injects this at runtime from user secrets.
|
||||
# Users configure this via the Secrets panel in the AI Studio UI.
|
||||
GEMINI_API_KEY="MY_GEMINI_API_KEY"
|
||||
|
||||
# APP_URL: The URL where this applet is hosted.
|
||||
# AI Studio automatically injects this at runtime with the Cloud Run service URL.
|
||||
# Used for self-referential links, OAuth callbacks, and API endpoints.
|
||||
APP_URL="MY_APP_URL"
|
||||
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
node_modules/
|
||||
build/
|
||||
dist/
|
||||
coverage/
|
||||
.DS_Store
|
||||
*.log
|
||||
.env*
|
||||
!.env.example
|
||||
40
Dockerfile
Normal file
40
Dockerfile
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
# Build Stage
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy dependency manifests
|
||||
COPY package*.json ./
|
||||
|
||||
# Install all dependencies
|
||||
RUN npm ci
|
||||
|
||||
# Copy full application codebase
|
||||
COPY . .
|
||||
|
||||
# Compile frontend and bundle backend files
|
||||
RUN npm run build
|
||||
|
||||
# Production alpine stage
|
||||
FROM node:20-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy configuration files
|
||||
COPY package*.json ./
|
||||
|
||||
# Install only production dependencies (Express, dotenv, @google/genai)
|
||||
RUN npm ci --only=production
|
||||
|
||||
# Copy compiled build resources from builder stage
|
||||
COPY --from=builder /app/dist ./dist
|
||||
COPY --from=builder /app/data ./data
|
||||
|
||||
# Expose port (Internal container routing to 3000)
|
||||
EXPOSE 3000
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV ADMIN_PASSWORD=autor2026
|
||||
|
||||
# Execute standalone node production entrypoint
|
||||
CMD ["node", "dist/server.cjs"]
|
||||
93
README.md
93
README.md
|
|
@ -1,11 +1,92 @@
|
|||
<div align="center">
|
||||
# 📚 Dual Identity Autoren-Zentrale (Selbstgehostet)
|
||||
|
||||
<img width="1200" height="475" alt="GHBanner" src="https://github.com/user-attachments/assets/0aa67016-6eaf-458a-adb2-6e31a0763ed6" />
|
||||
Willkommen in Ihrer modernen, reaktiven Autoren-Website für das 21. Jahrhundert! Dieses System ermöglicht es Ihnen, zwei völlig eigenständige Autorenprofile mit individuellen Themen, Biografien, Schreibprojekten und Bücherregalen über eine einzige, passwortgeschützte Administrationsoberfläche zu pflegen.
|
||||
|
||||
<h1>Built with AI Studio</h2>
|
||||
---
|
||||
|
||||
<p>The fastest path from prompt to production with Gemini.</p>
|
||||
## 🛠️ Warum kein klassischer LAMP-Stack? (Unser Vorschlag)
|
||||
|
||||
<a href="https://aistudio.google.com/apps">Start building</a>
|
||||
Bisher liefen klassische Portfolios oft auf einem standardmäßigen **LAMP-Stack** (Linux, Apache, MySQL, PHP). Für diese modernisierte Website schlagen wir einen **Node.JS + Docker-Stack** vor. Hier ist der Grund, warum das für Ihre Bedürfnisse die deutlich bessere Option ist:
|
||||
|
||||
</div>
|
||||
1. **Vollständige Kapselung (Kein Server-Wildwuchs):** Bei einem LAMP-Stack müssen Sie Apache/Nginx, PHP-Laufzeiten und eine MySQL-Datenbank auf dem Host installieren und managen. Unser Docker-Stack bündelt das gesamte Frontend, das Express-Backend und die Speicherrelevanz in einem **einzigen, autarken Container**.
|
||||
2. **Blitzschnelle, reaktive Benutzeroberfläche (SPA):** Das Frontend nutzt modernste Webtechnologie (React & Tailwind CSS). Seitenwechsel und Buchdetails laden augenblicklich im Browser des Nutzers, ohne das klassische, träge Neuladen von PHP-Seiten.
|
||||
3. **Einfachheit bei Datensicherungen (Backups):** Anstatt komplexe MySQL-Dumps durchzuführen, speichert unsere Anwendung alle Buchtitel, Projekte, Cover und Biografien in einer schlanken, robusten JSON-Datenbankdatei (`/data/database.json`). Ein Backup lässt sich durch einfaches Kopieren dieser Datei anfertigen!
|
||||
4. **KI-Unterstützung integriert:** Der integrierte **Gemini-Klappentext-Assistent** läuft nahtlos und sicher über das Node-Backend (ohne API-Schlüssel im Webbrowser zu exponieren).
|
||||
|
||||
---
|
||||
|
||||
## 🌌 Die Trennung der Profile
|
||||
|
||||
Das System ist von Grund auf so strukturiert, dass Besucher der Website **keine Verbindung** zwischen Ihren beiden Identitäten herstellen können:
|
||||
- **Echte Identität (Daniel Hesse — Sci-Fi & Fantasy):** Erreichbar auf der Standard-Startseite (`/`). Das Design kombiniert dunkelblaue Weltraum-Energie, Monospace-Einflüsse und kosmische Magie-Akzente.
|
||||
- **Pseudonym (M. S. Velvet — Sinnliche Literatur):** Erreichbar **ausschließlich** über den Pfad `/sensual-moments`. Sie verwendet ein warmes, edles "Champagner-Gold & Samt-Bordeaux"-Thema mit klassischer und anspruchsvoller Serif-Asthetik. Es gibt keinerlei gegenseitige Verlinkungen!
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Inbetriebnahme als Docker-Stack (Empfohlen)
|
||||
|
||||
Es wird dringend empfohlen, die Website mithilfe von **Docker und Docker-Compose** zu betreiben. Dies garantiert, dass die Anwendung sofort und unabhängig von installierten Node.JS-Versionen auf Ihrem Server funktioniert.
|
||||
|
||||
### 1. Dateien vorbereiten
|
||||
Stellen Sie sicher, dass sich folgende Dateien im gleichen Ordner auf Ihrem Server befinden:
|
||||
- `Dockerfile`
|
||||
- `docker-compose.yml`
|
||||
- Der gesamte Code-Ordner
|
||||
|
||||
### 2. Konfiguration anpassen
|
||||
Öffnen Sie die `docker-compose.yml` auf Ihrem Server und passen Sie folgende Umgebungsvariablen an:
|
||||
```yaml
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
# Das Passwort für Ihren Administrationsbereich (/admin):
|
||||
- ADMIN_PASSWORD=IhrSicheresLieblingsPasswort123
|
||||
# (Optional) Für den 21. Jahrhundert Schreibassistenten (Gemini 3.5 Flash)
|
||||
- GEMINI_API_KEY=Ihr_Gemini_API_Schluessel
|
||||
```
|
||||
|
||||
### 3. Container starten
|
||||
Führen Sie im entsprechenden Verzeichnis folgenden Befehl aus:
|
||||
```bash
|
||||
docker compose up -d --build
|
||||
```
|
||||
Die Anwendung baut das Image und startet die Autoren-Zentrale im Hintergrund. Sie ist nun direkt auf Port **`3000`** Ihres Webservers erreichbar!
|
||||
|
||||
#### Reverse Proxy Tipp:
|
||||
Sie können ganz hervorragend einen Reverse Proxy wie **Nginx Proxy Manager**, **Traefik** oder **Caddy** davorhängen, um SSL-Zertifikate (Let's Encrypt) zuzuweisen und Ihre Domain auf den Container-Port `3000` umzuleiten.
|
||||
|
||||
---
|
||||
|
||||
## 💾 Manuelle Installation ohne Docker (Alternativ)
|
||||
|
||||
Sollten Sie die Software direkt auf Ihrem Server (ohne Docker) starten wollen:
|
||||
|
||||
1. **Voraussetzung:** Installieren Sie **Node.js** (v18 oder neuer) auf Ihrem Linux- oder Windows-Server.
|
||||
2. **Abhängigkeiten installieren:**
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
3. **Produktions-Build ausführen:**
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
4. **Umgebungsvariablen setzen:**
|
||||
Erstellen Sie eine `.env`-Datei oder exportieren Sie diese im Terminal:
|
||||
```bash
|
||||
export ADMIN_PASSWORD="IhrSicheresPasswort"
|
||||
export GEMINI_API_KEY="Ihr_Gemini_API_Schlüssel"
|
||||
```
|
||||
5. **Starten:**
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧱 Datensicherung / Backups
|
||||
|
||||
Alle Ihre wertvollen Inhalte (Biografien, Projektfortschritte und alle Bücherregal-Einträge) liegen verschlüsselt strukturiert in Ihrem Projektordner unter:
|
||||
`./data/database.json`
|
||||
|
||||
Da wir in der `docker-compose.yml` ein **lokales Volume** gemountet haben (`./data:/app/data`), wird diese Datei direkt auf die Festplatte Ihres Servers geschrieben.
|
||||
- **Backup erstellen:** Sichern Sie einfach die Datei `./data/database.json`.
|
||||
- **Wiederherstellen:** Platzieren Sie eine gesicherte `database.json` in den `./data/`-Ordner vor dem Starten des Containers.
|
||||
|
|
|
|||
1
assets/.aistudio/.gitignore
vendored
Normal file
1
assets/.aistudio/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
*
|
||||
19
docker-compose.yml
Normal file
19
docker-compose.yml
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
version: "3.8"
|
||||
|
||||
services:
|
||||
author-suite:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: author_portfolio_suite
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "3000:3000"
|
||||
volumes:
|
||||
- ./data:/app/data
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
# Ändern Sie das Passwort für Ihren Administrationsbereich:
|
||||
- ADMIN_PASSWORD=IhrSicheresPasswort2026
|
||||
# (Optional) Für den 21. Jahrhundert KI-Klappentextassistenten (Gemini 3.5 Flash)
|
||||
- GEMINI_API_KEY=Ihr_Gemini_API_Schluessel
|
||||
13
index.html
Normal file
13
index.html
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>My Google AI Studio App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
6
metadata.json
Normal file
6
metadata.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "Dual Identity Author Portfolio",
|
||||
"description": "A high-performance modern portfolio for authors writing under multiple pen names. Completely separate visual themes, isolated routes, integrated Spotify player embeds, and an administrative panel. Docker self-hosting ready.",
|
||||
"requestFramePermissions": [],
|
||||
"majorCapabilities": ["MAJOR_CAPABILITY_SERVER_SIDE_GEMINI_API"]
|
||||
}
|
||||
35
package.json
Normal file
35
package.json
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"name": "react-example",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "tsx server.ts",
|
||||
"build": "vite build && esbuild server.ts --bundle --platform=node --format=cjs --packages=external --sourcemap --outfile=dist/server.cjs",
|
||||
"start": "node dist/server.cjs",
|
||||
"clean": "rm -rf dist",
|
||||
"lint": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@google/genai": "^2.4.0",
|
||||
"@tailwindcss/vite": "^4.1.14",
|
||||
"@vitejs/plugin-react": "^5.0.4",
|
||||
"lucide-react": "^0.546.0",
|
||||
"react": "^19.0.1",
|
||||
"react-dom": "^19.0.1",
|
||||
"vite": "^6.2.3",
|
||||
"express": "^4.21.2",
|
||||
"dotenv": "^17.2.3",
|
||||
"motion": "^12.23.24"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.14.0",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"esbuild": "^0.25.0",
|
||||
"tailwindcss": "^4.1.14",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "~5.8.2",
|
||||
"vite": "^6.2.3",
|
||||
"@types/express": "^4.17.21"
|
||||
}
|
||||
}
|
||||
186
server.ts
Normal file
186
server.ts
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
import express from "express";
|
||||
import path from "path";
|
||||
import fs from "fs/promises";
|
||||
import { fileURLToPath } from "url";
|
||||
import dotenv from "dotenv";
|
||||
import { GoogleGenAI } from "@google/genai";
|
||||
import { defaultAuthorData } from "./src/defaultData.js";
|
||||
import { AuthorData } from "./src/types.js";
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const app = express();
|
||||
const PORT = 3000;
|
||||
|
||||
// Path to durable local database file
|
||||
const DATA_DIR = path.join(process.cwd(), "data");
|
||||
const DATA_FILE = path.join(DATA_DIR, "database.json");
|
||||
|
||||
// Establish initial database of authors
|
||||
async function initDatabase(): Promise<AuthorData> {
|
||||
try {
|
||||
await fs.mkdir(DATA_DIR, { recursive: true });
|
||||
try {
|
||||
const existingData = await fs.readFile(DATA_FILE, "utf-8");
|
||||
return JSON.parse(existingData) as AuthorData;
|
||||
} catch {
|
||||
// If file doesn't exist, seed it with default data
|
||||
await fs.writeFile(DATA_FILE, JSON.stringify(defaultAuthorData, null, 2), "utf-8");
|
||||
return defaultAuthorData;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Database storage initialization failed, using default data in-memory:", err);
|
||||
return defaultAuthorData;
|
||||
}
|
||||
}
|
||||
|
||||
// In-memory runtime data cache, synced to active storage file
|
||||
let dbCache: AuthorData;
|
||||
|
||||
// Initialize on boot
|
||||
initDatabase().then((data) => {
|
||||
dbCache = data;
|
||||
});
|
||||
|
||||
// Configure middleware
|
||||
app.use(express.json({ limit: "10mb" }));
|
||||
|
||||
// Initialize Google GenAI if API key exists
|
||||
const getGeminiClient = () => {
|
||||
const apiKey = process.env.GEMINI_API_KEY;
|
||||
if (!apiKey) return null;
|
||||
return new GoogleGenAI({
|
||||
apiKey,
|
||||
httpOptions: {
|
||||
headers: {
|
||||
"User-Agent": "aistudio-build",
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// Admin authentication password helper
|
||||
// In production or self-hosted, they set ADMIN_PASSWORD in environment or docker-compose.
|
||||
// Default fallback is "autor2026"
|
||||
const getAdminPassword = () => {
|
||||
return process.env.ADMIN_PASSWORD || "autor2026";
|
||||
};
|
||||
|
||||
// Authorization verification middleware
|
||||
const verifyToken = (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
const authHeader = req.headers.authorization;
|
||||
if (!authHeader) {
|
||||
res.status(401).json({ error: "Kein Autorisierungs-Token bereitgestellt." });
|
||||
return;
|
||||
}
|
||||
const token = authHeader.replace("Bearer ", "");
|
||||
// To keep session simple, secure, and self-hosted, our auth token is just the password itself or adminPassword
|
||||
if (token === getAdminPassword()) {
|
||||
next();
|
||||
} else {
|
||||
res.status(403).json({ error: "Ungültiges Passwort oder Sitzungstoken." });
|
||||
}
|
||||
};
|
||||
|
||||
// --- API ROUTES ---
|
||||
|
||||
// 1. Get entire public author setup
|
||||
app.get("/api/author-data", (req, res) => {
|
||||
res.json(dbCache || defaultAuthorData);
|
||||
});
|
||||
|
||||
// 2. Manage Admin Login
|
||||
app.post("/api/admin/login", (req, res) => {
|
||||
const { password } = req.body;
|
||||
if (!password) {
|
||||
res.status(400).json({ error: "Passwort ist erforderlich." });
|
||||
return;
|
||||
}
|
||||
|
||||
if (password === getAdminPassword()) {
|
||||
// Return the token which client stores in localStorage
|
||||
res.json({ success: true, token: getAdminPassword() });
|
||||
} else {
|
||||
res.status(401).json({ error: "Ungültiges Passwort. Bitte versuchen Sie es erneut." });
|
||||
}
|
||||
});
|
||||
|
||||
// 3. Save modified profile configurations (About, Projects, Books)
|
||||
app.post("/api/admin/save-profile", verifyToken, async (req, res) => {
|
||||
const { profileKey, profileData } = req.body;
|
||||
if (profileKey !== "scifi" && profileKey !== "erotica") {
|
||||
res.status(400).json({ error: "Ungültiger Profilschlüssel." });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
dbCache[profileKey] = profileData;
|
||||
await fs.writeFile(DATA_FILE, JSON.stringify(dbCache, null, 2), "utf-8");
|
||||
res.json({ success: true, message: "Profil erfolgreich gespeichert." });
|
||||
} catch (err: any) {
|
||||
console.error("Failed to write to database.json:", err);
|
||||
res.status(500).json({ error: "Fehler beim persistenten Speichern der Formulardaten." });
|
||||
}
|
||||
});
|
||||
|
||||
// 4. Creative AI Blurb Assistant for book blurb updates
|
||||
app.post("/api/admin/generate-blurb", verifyToken, async (req, res) => {
|
||||
const { title, genre, ideas, tone } = req.body;
|
||||
|
||||
const ai = getGeminiClient();
|
||||
if (!ai) {
|
||||
res.status(503).json({
|
||||
error: "Mit dem integrierten KI-Schreibassistenten konnte keine Verbindung hergestellt werden. Bitte stellen Sie sicher, dass GEMINI_API_KEY konfiguriert ist."
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const prompt = `Du bist ein professioneller literarischer Marketing-Experte und Buch-Co-Autor.
|
||||
Schreibe eine fesselnde, hochkarätige Synopsis (Buchrückenblurb) auf Deutsch für folgendes Buch:
|
||||
- Buchtitel: "${title}"
|
||||
- Genre/Kategorie: "${genre}"
|
||||
- Eingebundene Ideen/Motive: "${ideas}"
|
||||
- Gewünschte Tonalität/Stilrichtung: "${tone}"
|
||||
|
||||
Die Synopsis soll neugierig machen, stark die visuelle Stimmung rüberbringen und frei von Klischees sein. Teile den Text in zwei bis max. drei Absätze auf, evtl. eingeleitet durch einen kurzen, fetten Einzeiler, der Aufmerksamkeit catched (z.B. ein prägnanter Satz). Antworte NUR mit dem generierten Blurb-Text in Deutsch.`;
|
||||
|
||||
const response = await ai.models.generateContent({
|
||||
model: "gemini-3.5-flash",
|
||||
contents: prompt,
|
||||
});
|
||||
|
||||
const generatedText = response.text || "Fehler beim Generieren der Synopsis.";
|
||||
res.json({ success: true, text: generatedText });
|
||||
} catch (error: any) {
|
||||
console.error("Gemini-Fehler aufgetreten:", error);
|
||||
res.status(500).json({ error: "KI-Generierungsfehler: " + error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Configure Vite middleware or static serve
|
||||
async function startServer() {
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
const { createServer: createViteServer } = await import("vite");
|
||||
const vite = await createViteServer({
|
||||
server: { middlewareMode: true },
|
||||
appType: "spa",
|
||||
});
|
||||
app.use(vite.middlewares);
|
||||
} else {
|
||||
const distPath = path.join(process.cwd(), "dist");
|
||||
app.use(express.static(distPath));
|
||||
app._router.get("*", (req: express.Request, res: express.Response) => {
|
||||
res.sendFile(path.join(distPath, "index.html"));
|
||||
});
|
||||
}
|
||||
|
||||
app.listen(PORT, "0.0.0.0", () => {
|
||||
console.log(`Server running on http://0.0.0.0:${PORT}`);
|
||||
});
|
||||
}
|
||||
|
||||
startServer();
|
||||
114
src/App.tsx
Normal file
114
src/App.tsx
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
import { useState, useEffect } from "react";
|
||||
import { AuthorData } from "./types";
|
||||
import SciFiPortfolio from "./components/SciFiPortfolio";
|
||||
import EroticaPortfolio from "./components/EroticaPortfolio";
|
||||
import AdminPanel from "./components/AdminPanel";
|
||||
import { Lock, Eye, Sparkles, RefreshCw } from "lucide-react";
|
||||
|
||||
export default function App() {
|
||||
const [authorData, setAuthorData] = useState<AuthorData | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [currentPath, setCurrentPath] = useState(window.location.pathname);
|
||||
|
||||
// Monitor location changes for custom routing
|
||||
useEffect(() => {
|
||||
const handleLocationChange = () => {
|
||||
setCurrentPath(window.location.pathname);
|
||||
};
|
||||
|
||||
// Listen to popstate for navigation back/forward support
|
||||
window.addEventListener("popstate", handleLocationChange);
|
||||
return () => {
|
||||
window.removeEventListener("popstate", handleLocationChange);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Fetch profiles configuration from server
|
||||
useEffect(() => {
|
||||
fetch("/api/author-data")
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
setAuthorData(data);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Fehler beim Abrufen der Autoren-Informationen:", err);
|
||||
setLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleAdminLogout = () => {
|
||||
// Relocate to landing site after administration work
|
||||
window.history.pushState({}, "", "/");
|
||||
setCurrentPath("/");
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-950 text-slate-100 flex flex-col items-center justify-center space-y-4">
|
||||
<RefreshCw className="w-8 h-8 animate-spin text-cyan-400" />
|
||||
<p className="font-mono text-xs text-slate-500 uppercase tracking-widest">Kryptografisches Terminal bootet...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!authorData) {
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-950 text-slate-100 flex flex-col items-center justify-center p-4">
|
||||
<div className="max-w-md p-6 bg-slate-900 border border-slate-800 rounded-xl text-center space-y-3">
|
||||
<h2 className="text-lg font-bold text-rose-450 text-rose-400">Verbindungsfehler</h2>
|
||||
<p className="text-xs text-slate-400 leading-relaxed">
|
||||
Die Verbindung zum lokalen Datenspeicher-Server konnte nicht hergestellt werden. Bitte vergewissern Sie sich, dass Ihr Express API-Server ordnungsgemäß läuft.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Purely route-based render isolation structure
|
||||
// 1. Administration panel
|
||||
if (currentPath === "/admin" || currentPath.startsWith("/admin/")) {
|
||||
return <AdminPanel onLogout={handleAdminLogout} />;
|
||||
}
|
||||
|
||||
// 2. Sensual/Erotica Portfolio pseudonym - accessed ONLY via /sensual-moments (Strict content separation)
|
||||
if (currentPath === "/sensual-moments" || currentPath.startsWith("/sensual-moments/")) {
|
||||
return <EroticaPortfolio data={authorData.erotica} />;
|
||||
}
|
||||
|
||||
// 3. Fallback: SciFi & Fantasy portfolio - accessed via `/` (Daniel Hesse identity)
|
||||
return (
|
||||
<>
|
||||
<SciFiPortfolio data={authorData.scifi} />
|
||||
|
||||
{/* Absolute floating administration bypass link hidden on the bottom right for convenience of testing/admin accessing */}
|
||||
<div className="fixed bottom-3 left-3 opacity-0 hover:opacity-100 transition-opacity z-40 bg-slate-950/80 border border-slate-800 p-2.5 rounded-xl flex items-center gap-4 text-xs font-mono">
|
||||
<a
|
||||
href="/admin"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
window.history.pushState({}, "", "/admin");
|
||||
setCurrentPath("/admin");
|
||||
}}
|
||||
className="text-slate-500 hover:text-cyan-400 font-bold flex items-center gap-1"
|
||||
>
|
||||
<Lock className="w-3.5 h-3.5" />
|
||||
<span>Admin-Zentrale</span>
|
||||
</a>
|
||||
<div className="w-px h-3 bg-slate-800" />
|
||||
<a
|
||||
href="/sensual-moments"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
window.history.pushState({}, "", "/sensual-moments");
|
||||
setCurrentPath("/sensual-moments");
|
||||
}}
|
||||
className="text-slate-500 hover:text-rose-400 font-bold flex items-center gap-1"
|
||||
>
|
||||
<Eye className="w-3.5 h-3.5" />
|
||||
<span>Sinnlich</span>
|
||||
</a>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
1001
src/components/AdminPanel.tsx
Normal file
1001
src/components/AdminPanel.tsx
Normal file
File diff suppressed because it is too large
Load diff
379
src/components/EroticaPortfolio.tsx
Normal file
379
src/components/EroticaPortfolio.tsx
Normal file
|
|
@ -0,0 +1,379 @@
|
|||
import { useState } from "react";
|
||||
import { Book, Project, AuthorProfile } from "../types";
|
||||
import { Heart, Key, Star, Sparkles, BookOpen, ExternalLink, ArrowRight } from "lucide-react";
|
||||
|
||||
interface EroticaPortfolioProps {
|
||||
data: AuthorProfile;
|
||||
}
|
||||
|
||||
export default function EroticaPortfolio({ data }: EroticaPortfolioProps) {
|
||||
const [selectedBook, setSelectedBook] = useState<Book | null>(null);
|
||||
const [selectedProject, setSelectedProject] = useState<Project | null>(null);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-neutral-950 text-neutral-200 font-sans selection:bg-orange-450 selection:bg-orange-500 selection:text-black overflow-x-hidden relative">
|
||||
{/* Decorative Warm background gradient and peach glow vibes */}
|
||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top,_var(--tw-gradient-stops))] from-amber-950/20 via-neutral-950 to-neutral-950 -z-10 pointer-events-none" />
|
||||
<div className="absolute top-1/4 left-1/3 w-96 h-96 bg-orange-500/5 rounded-full blur-3xl pointer-events-none" />
|
||||
<div className="absolute bottom-1/3 right-1/4 w-96 h-96 bg-amber-500/5 rounded-full blur-3xl pointer-events-none" />
|
||||
|
||||
{/* Header / Hero */}
|
||||
<header className="relative py-24 px-4 max-w-7xl mx-auto border-b border-orange-950/40">
|
||||
<div className="text-center max-w-4xl mx-auto space-y-6">
|
||||
<div className="inline-flex items-center gap-2 px-4 py-1.5 bg-orange-950/30 border border-orange-900/40 rounded-full text-orange-300 text-xs tracking-wider mb-2">
|
||||
<Heart className="w-3.5 h-3.5 text-orange-400 fill-orange-400/20 animate-pulse" />
|
||||
<span className="font-sans italic">Stilvolle Sinnlichkeit & Romantik</span>
|
||||
</div>
|
||||
|
||||
<h1 className="text-4xl md:text-6xl font-serif tracking-wide text-amber-100 uppercase font-light drop-shadow-[0_2px_10px_rgba(249,115,22,0.1)]">
|
||||
{data.heroTitle || "M. S. VELVET"}
|
||||
</h1>
|
||||
<p className="text-orange-400 font-sans text-xs md:text-sm tracking-[0.3em] uppercase">
|
||||
{data.heroSubtitle || "ELEGANT-SINNLICHE LITERATUR"}
|
||||
</p>
|
||||
<div className="h-[1px] w-36 bg-gradient-to-r from-transparent via-orange-450/60 via-orange-400 to-transparent mx-auto mt-6" />
|
||||
|
||||
{/* Quick Section Navigation (Peach Styled) */}
|
||||
<nav className="mt-8 flex flex-wrap justify-center gap-3 text-xs font-sans">
|
||||
<a
|
||||
href="#ueber-mich"
|
||||
className="px-4 py-2 rounded-full border border-orange-950/60 bg-orange-950/20 hover:border-orange-400 hover:text-orange-300 transition-all text-neutral-300 uppercase tracking-wider"
|
||||
>
|
||||
🍑 Über mich
|
||||
</a>
|
||||
<a
|
||||
href="#projekte"
|
||||
className="px-4 py-2 rounded-full border border-orange-950/60 bg-orange-950/20 hover:border-orange-400 hover:text-orange-300 transition-all text-neutral-300 uppercase tracking-wider"
|
||||
>
|
||||
✍️ In der Entstehung
|
||||
</a>
|
||||
<a
|
||||
href="#buecherregal"
|
||||
className="px-4 py-2 rounded-full border border-orange-950/60 bg-orange-950/20 hover:border-orange-400 hover:text-orange-300 transition-all text-neutral-300 uppercase tracking-wider"
|
||||
>
|
||||
📖 Das Samtregal
|
||||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Main Grid */}
|
||||
<main className="max-w-7xl mx-auto px-4 py-16 space-y-24">
|
||||
|
||||
{/* About writer */}
|
||||
<section id="ueber-mich" className="grid grid-cols-1 md:grid-cols-12 gap-12 items-center">
|
||||
<div className="md:col-span-4 flex justify-center">
|
||||
<div className="relative group">
|
||||
{/* Outer warm glow */}
|
||||
<div className="absolute -inset-1.5 bg-gradient-to-r from-orange-500 to-amber-600 rounded-2xl blur opacity-25 group-hover:opacity-60 transition duration-1000 group-hover:duration-200"></div>
|
||||
<div className="relative w-64 h-64 md:w-80 md:h-80 rounded-2xl overflow-hidden border border-orange-950 bg-neutral-900">
|
||||
<img
|
||||
src={data.avatarUrl || "https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?auto=format&fit=crop&q=80&w=400"}
|
||||
alt={data.name}
|
||||
className="w-full h-full object-cover grayscale brightness-90 group-hover:scale-105 group-hover:grayscale-0 transition-all duration-1000"
|
||||
referrerPolicy="no-referrer"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="md:col-span-8 space-y-6">
|
||||
<h2 className="text-2xl md:text-3xl font-serif text-amber-100 flex items-center gap-3">
|
||||
<Sparkles className="w-6 h-6 text-orange-400" />
|
||||
<span>Das Geheimnis hinter der Feder</span>
|
||||
</h2>
|
||||
<div className="prose prose-invert prose-stone max-w-none text-neutral-300 leading-relaxed font-sans space-y-4">
|
||||
<p className="whitespace-pre-wrap italic">{data.bio}</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-4 pt-4 text-xs">
|
||||
<div className="p-3 bg-neutral-900/50 border border-neutral-900 rounded-xl flex items-center gap-3">
|
||||
<Heart className="w-5 h-5 text-orange-450 text-orange-400 fill-orange-400/10" />
|
||||
<div>
|
||||
<div className="text-neutral-500 uppercase tracking-wider text-[10px]">Genre</div>
|
||||
<div className="text-neutral-200 font-semibold">Sinnliche Romance</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3 bg-neutral-900/50 border border-neutral-900 rounded-xl flex items-center gap-3">
|
||||
<Sparkles className="w-5 h-5 text-amber-400" />
|
||||
<div>
|
||||
<div className="text-neutral-500 uppercase tracking-wider text-[10px]">Tonalität</div>
|
||||
<div className="text-neutral-200 font-semibold">Elegant & Knisternd</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3 bg-neutral-900/50 border border-neutral-900 rounded-xl flex items-center gap-3 col-span-2 md:col-span-1">
|
||||
<Key className="w-5 h-5 text-orange-300" />
|
||||
<div>
|
||||
<div className="text-neutral-500 uppercase tracking-wider text-[10px]">Identität</div>
|
||||
<div className="text-neutral-200 font-semibold">Diskret / Pseudonym</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Dynamic ongoing work writing progress */}
|
||||
<section id="projekte" className="space-y-8">
|
||||
<div className="flex flex-col md:flex-row md:items-end justify-between gap-4 border-b border-orange-950/20 pb-4">
|
||||
<div>
|
||||
<h2 className="text-2xl md:text-3xl font-serif text-amber-100 flex items-center gap-3">
|
||||
<Heart className="w-6 h-6 text-orange-400" />
|
||||
<span>In der Entstehung...</span>
|
||||
</h2>
|
||||
<p className="text-neutral-400 text-sm mt-1">Ein diskreter Blick hinter die Samtvorhänge meiner aktuellen Manuskripte.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{data.projects.map((project) => (
|
||||
<button
|
||||
key={project.id}
|
||||
onClick={() => setSelectedProject(project)}
|
||||
className="p-6 bg-orange-950/10 border border-orange-950/20 hover:border-amber-500/40 rounded-xl transition-all duration-300 flex flex-col text-left w-full justify-between group cursor-pointer hover:shadow-[0_0_20px_rgba(249,115,22,0.15)] focus:outline-none"
|
||||
>
|
||||
<div className="space-y-4 w-full">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[10px] uppercase font-sans text-amber-400 bg-amber-950/40 px-2.5 py-1 rounded border border-amber-900/30 tracking-wider">
|
||||
{project.status || "In Arbeit"}
|
||||
</span>
|
||||
<span className="text-xs text-neutral-400 italic">
|
||||
Verlangen erfüllt: {project.progress}%
|
||||
</span>
|
||||
</div>
|
||||
<h3 className="text-lg font-serif text-amber-100 group-hover:text-orange-400 transition-colors">
|
||||
{project.title}
|
||||
</h3>
|
||||
<p className="text-neutral-300 text-sm italic line-clamp-3">
|
||||
{project.description}
|
||||
</p>
|
||||
<p className="text-orange-400 font-sans text-[11px] mt-2 group-hover:translate-x-1 transition-transform flex items-center gap-1 italic">
|
||||
Details einsehen <ArrowRight className="w-3 h-3" />
|
||||
</p>
|
||||
</div>
|
||||
{/* Luxurious peach-golden loading progress meter */}
|
||||
<div className="mt-6 space-y-1.5 w-full">
|
||||
<div className="h-1 w-full bg-neutral-900 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-gradient-to-r from-orange-500 via-amber-400 to-yellow-300 rounded-full transition-all duration-1000 ease-out shadow-[0_0_8px_rgba(249,115,22,0.3)]"
|
||||
style={{ width: `${project.progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Bookshelf display, strictly elegant and minimalist */}
|
||||
<section id="buecherregal" className="space-y-8">
|
||||
<div className="border-b border-orange-950/20 pb-4">
|
||||
<h2 className="text-2xl md:text-3xl font-serif text-amber-100 flex items-center gap-3">
|
||||
<BookOpen className="w-6 h-6 text-orange-400" />
|
||||
<span>Das Samtregal</span>
|
||||
</h2>
|
||||
<p className="text-neutral-400 text-sm mt-1">Wählen Sie ein Buch aus, um tiefer in die Versuchung einzutauchen.</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-6 justify-items-center">
|
||||
{data.books.map((book) => (
|
||||
<button
|
||||
key={book.id}
|
||||
onClick={() => setSelectedBook(book)}
|
||||
className="group relative focus:outline-none text-left w-full max-w-[210px] aspect-[2/3] rounded-lg overflow-hidden border border-neutral-900 bg-neutral-900 hover:border-orange-400/50 transition-all duration-300 hover:shadow-[0_0_20px_rgba(249,115,22,0.15)] cursor-pointer"
|
||||
>
|
||||
<div className="absolute inset-0 bg-orange-950/10 opacity-0 group-hover:opacity-100 transition-opacity z-10 pointer-events-none" />
|
||||
<img
|
||||
src={book.coverUrl || "https://images.unsplash.com/photo-1517841905240-472988babdf9?auto=format&fit=crop&q=80&w=300"}
|
||||
alt={book.title}
|
||||
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-700 z-0"
|
||||
referrerPolicy="no-referrer"
|
||||
/>
|
||||
|
||||
{/* Sensual overlay - elegant translucent readable overlay, fully visible on hover */}
|
||||
<div className="absolute inset-x-0 bottom-0 bg-gradient-to-t from-neutral-950 via-neutral-950/90 to-transparent p-4 opacity-80 group-hover:opacity-100 transition-all duration-350 z-20">
|
||||
<p className="text-amber-100 text-xs font-serif truncate">{book.title}</p>
|
||||
<p className="text-orange-400 text-[10px] font-sans mt-0.5 italic">Eintauchen <ArrowRight className="inline-block w-2.5 h-2.5 ml-0.5" /></p>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
|
||||
{/* Book details modular modal */}
|
||||
{selectedBook && (
|
||||
<div className="fixed inset-0 z-50 overflow-y-auto" role="dialog" aria-modal="true">
|
||||
<div
|
||||
className="fixed inset-0 bg-neutral-950/85 backdrop-blur-md transition-opacity"
|
||||
onClick={() => setSelectedBook(null)}
|
||||
/>
|
||||
|
||||
<div className="flex min-h-full items-center justify-center p-4 text-center sm:p-0">
|
||||
<div className="relative transform overflow-hidden rounded-2xl bg-neutral-950 border border-neutral-900 text-left shadow-2xl transition-all sm:my-8 sm:w-full sm:max-w-4xl p-6 md:p-8 space-y-6">
|
||||
|
||||
<button
|
||||
onClick={() => setSelectedBook(null)}
|
||||
className="absolute top-4 right-4 text-neutral-400 hover:text-amber-100 hover:bg-orange-950/80 px-2.5 py-1 rounded-md text-xs font-sans border border-neutral-800 transition-all cursor-pointer"
|
||||
>
|
||||
SCHLIEẞEN
|
||||
</button>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-12 gap-8 pt-4">
|
||||
|
||||
{/* Book Cover */}
|
||||
<div className="col-span-1 md:col-span-4 flex flex-col items-center">
|
||||
<div className="rounded-xl overflow-hidden shadow-2xl border border-orange-950/50 max-w-[260px] aspect-[2/3] w-full">
|
||||
<img
|
||||
src={selectedBook.coverUrl}
|
||||
alt={selectedBook.title}
|
||||
className="w-full h-full object-cover"
|
||||
referrerPolicy="no-referrer"
|
||||
/>
|
||||
</div>
|
||||
{selectedBook.buyLink && (
|
||||
<a
|
||||
href={selectedBook.buyLink}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="mt-6 w-full max-w-[260px] py-3 text-center bg-gradient-to-r from-orange-400 to-amber-500 hover:from-orange-300 hover:to-amber-400 text-neutral-950 font-bold font-sans rounded-xl flex items-center justify-center gap-2 transition-all hover:scale-[1.02] shadow-[0_4px_12px_rgba(249,115,22,0.25)] text-sm"
|
||||
>
|
||||
<span>Buch erwerben</span>
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Details text area */}
|
||||
<div className="col-span-1 md:col-span-8 flex flex-col justify-between space-y-6">
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="text-[10px] font-sans text-orange-300 bg-orange-950/30 px-2.5 py-0.5 rounded border border-orange-900/30">
|
||||
Sinnlich seit: {selectedBook.publishedYear}
|
||||
</span>
|
||||
{selectedBook.genres?.map((genre) => (
|
||||
<span key={genre} className="text-[10px] font-sans text-amber-300 bg-amber-950/30 px-2.5 py-0.5 rounded border border-amber-900/30">
|
||||
{genre}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<h3 className="text-3xl font-serif text-amber-100">
|
||||
{selectedBook.title}
|
||||
</h3>
|
||||
|
||||
{selectedBook.subtitle && (
|
||||
<h4 className="text-sm font-sans italic text-orange-400">
|
||||
{selectedBook.subtitle}
|
||||
</h4>
|
||||
)}
|
||||
|
||||
<div className="h-px bg-orange-950/40" />
|
||||
|
||||
<p className="text-neutral-300 text-sm leading-relaxed whitespace-pre-wrap italic">
|
||||
{selectedBook.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Embedded Spotify player */}
|
||||
{selectedBook.spotifyPlaylistId && selectedBook.spotifyPlaylistId.trim() !== "" && (
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs font-sans text-orange-400 flex items-center gap-2 italic">
|
||||
<Heart className="w-3.5 h-3.5 fill-orange-400 text-orange-400" />
|
||||
<span>Sinnliche Begleitmusik (Spotify Playlist)</span>
|
||||
</p>
|
||||
<div className="rounded-xl overflow-hidden bg-neutral-900 border border-neutral-900">
|
||||
<iframe
|
||||
src={`https://open.spotify.com/embed/playlist/${selectedBook.spotifyPlaylistId}?utm_source=generator&theme=0`}
|
||||
width="100%"
|
||||
height="152"
|
||||
frameBorder="0"
|
||||
allowFullScreen={false}
|
||||
allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Project details modular modal */}
|
||||
{selectedProject && (
|
||||
<div className="fixed inset-0 z-50 overflow-y-auto" role="dialog" aria-modal="true">
|
||||
<div
|
||||
className="fixed inset-0 bg-neutral-950/85 backdrop-blur-md transition-opacity"
|
||||
onClick={() => setSelectedProject(null)}
|
||||
/>
|
||||
|
||||
<div className="flex min-h-full items-center justify-center p-4 text-center sm:p-0">
|
||||
<div className="relative transform overflow-hidden rounded-2xl bg-neutral-950 border border-neutral-900 text-left shadow-2xl transition-all sm:my-8 sm:w-full sm:max-w-2xl p-6 md:p-8 space-y-6">
|
||||
|
||||
<button
|
||||
onClick={() => setSelectedProject(null)}
|
||||
className="absolute top-4 right-4 text-neutral-400 hover:text-amber-100 hover:bg-orange-950/80 px-2.5 py-1 rounded-md text-xs font-sans border border-neutral-800 transition-all cursor-pointer"
|
||||
>
|
||||
SCHLIEẞEN
|
||||
</button>
|
||||
|
||||
<div className="space-y-6 pt-4">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="text-[10px] font-sans text-orange-300 bg-orange-950/30 px-2.5 py-0.5 rounded border border-orange-900/30">
|
||||
Status: {selectedProject.status || "In Arbeit"}
|
||||
</span>
|
||||
<span className="text-[10px] font-sans text-amber-300 bg-amber-950/30 px-2.5 py-0.5 rounded border border-amber-900/30">
|
||||
Intime Einblicke
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h3 className="text-3xl font-serif text-amber-100">
|
||||
{selectedProject.title}
|
||||
</h3>
|
||||
|
||||
<div className="h-px bg-orange-950/40" />
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between items-center text-xs font-sans text-neutral-450 text-neutral-400">
|
||||
<span>REIFEGRAD DES WERKS:</span>
|
||||
<span className="text-orange-400 font-bold">{selectedProject.progress}%</span>
|
||||
</div>
|
||||
<div className="h-2 w-full bg-neutral-900 rounded-full overflow-hidden border border-neutral-800">
|
||||
<div
|
||||
className="h-full bg-gradient-to-r from-orange-500 via-amber-450 via-amber-400 to-yellow-300 rounded-full transition-all duration-1000 ease-out shadow-[0_0_8px_rgba(249,115,22,0.35)]"
|
||||
style={{ width: `${selectedProject.progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-orange-950/10 p-5 rounded-xl border border-orange-950/20 relative overflow-hidden">
|
||||
<div className="absolute top-0 right-0 p-2 text-neutral-800 text-[10px] font-mono select-none">
|
||||
FRAGMENT_REPORT_09
|
||||
</div>
|
||||
<h4 className="text-xs font-sans text-orange-400 uppercase tracking-wider mb-3 italic">Konzeptuelle Skizzen & Atmosphärische Auszüge</h4>
|
||||
<p className="text-neutral-200 text-sm leading-relaxed whitespace-pre-wrap italic">
|
||||
{selectedProject.description || "Für dieses diskrete Werk wurde noch kein detaillierter Auszug freigegeben."}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="bg-neutral-950 border-t border-orange-950/20 py-12 px-4 text-center text-xs text-neutral-500 italic">
|
||||
<div className="max-w-7xl mx-auto space-y-2">
|
||||
<p>© {new Date().getFullYear()} {data.name}. Alle Rechte vorbehalten.</p>
|
||||
<p className="text-neutral-600">Geschlossenes Refugium für feine, hochästhetische Phantasiereisen.</p>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
389
src/components/SciFiPortfolio.tsx
Normal file
389
src/components/SciFiPortfolio.tsx
Normal file
|
|
@ -0,0 +1,389 @@
|
|||
import { useState } from "react";
|
||||
import { Book, Project, AuthorProfile } from "../types";
|
||||
import { BookOpen, Star, HelpCircle, Award, Compass, ArrowRight, ExternalLink, Moon } from "lucide-react";
|
||||
|
||||
interface SciFiPortfolioProps {
|
||||
data: AuthorProfile;
|
||||
}
|
||||
|
||||
export default function SciFiPortfolio({ data }: SciFiPortfolioProps) {
|
||||
const [selectedBook, setSelectedBook] = useState<Book | null>(null);
|
||||
const [selectedProject, setSelectedProject] = useState<Project | null>(null);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-950 text-slate-100 font-sans selection:bg-purple-500 selection:text-white overflow-x-hidden relative">
|
||||
{/* Decorative starry background with magical purple aura */}
|
||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top,_var(--tw-gradient-stops))] from-indigo-950/40 via-slate-950 to-slate-950 -z-10 pointer-events-none" />
|
||||
<div className="absolute top-1/4 left-1/4 w-96 h-96 bg-cyan-500/5 rounded-full blur-3xl pointer-events-none" />
|
||||
<div className="absolute top-2/3 right-1/4 w-96 h-96 bg-purple-500/5 rounded-full blur-3xl pointer-events-none" />
|
||||
|
||||
{/* Hero Section */}
|
||||
<header className="relative py-24 px-4 max-w-7xl mx-auto border-b border-indigo-900/40">
|
||||
<div className="text-center max-w-4xl mx-auto">
|
||||
{/* Subtle magical/futuristic accent */}
|
||||
<div className="inline-flex items-center gap-2 px-3 py-1 bg-indigo-950/60 border border-indigo-700/50 rounded-full text-indigo-400 text-xs font-mono mb-6 tracking-widest uppercase">
|
||||
<Star className="w-3 h-3 text-cyan-400 animate-pulse" />
|
||||
<span>Cyber-Mystisches Archive</span>
|
||||
</div>
|
||||
|
||||
<h1 className="text-5xl md:text-7xl font-sans tracking-tight text-white mb-4 uppercase drop-shadow-[0_2px_10px_rgba(6,182,212,0.15)] font-bold">
|
||||
{data.heroTitle || "DANIEL HESSE"}
|
||||
</h1>
|
||||
<p className="text-cyan-400 font-mono text-xs md:text-sm tracking-widest uppercase mb-8">
|
||||
{data.heroSubtitle || "WELTENWANDLER & STERNENZEICHNER"}
|
||||
</p>
|
||||
<div className="h-0.5 w-24 bg-gradient-to-r from-cyan-400 via-indigo-500 to-purple-500 mx-auto rounded-full" />
|
||||
|
||||
{/* Quick Section Navigation */}
|
||||
<nav className="mt-8 flex flex-wrap justify-center gap-3 text-xs font-mono">
|
||||
<a
|
||||
href="#ueber-mich"
|
||||
className="px-4 py-2 rounded-full border border-indigo-900/40 bg-indigo-950/20 hover:border-cyan-400 hover:text-cyan-400 transition-all text-slate-300 uppercase tracking-wider"
|
||||
>
|
||||
🌌 Über mich
|
||||
</a>
|
||||
<a
|
||||
href="#projekte"
|
||||
className="px-4 py-2 rounded-full border border-indigo-900/40 bg-indigo-950/20 hover:border-cyan-400 hover:text-cyan-400 transition-all text-slate-300 uppercase tracking-wider"
|
||||
>
|
||||
🛰️ Aktuelle Projekte
|
||||
</a>
|
||||
<a
|
||||
href="#buecherregal"
|
||||
className="px-4 py-2 rounded-full border border-indigo-900/40 bg-indigo-950/20 hover:border-cyan-400 hover:text-cyan-400 transition-all text-slate-300 uppercase tracking-wider"
|
||||
>
|
||||
🪐 Bücherregal
|
||||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Main Container */}
|
||||
<main className="max-w-7xl mx-auto px-4 py-16 space-y-24">
|
||||
|
||||
{/* Section 1: Über mich */}
|
||||
<section id="ueber-mich" className="grid grid-cols-1 md:grid-cols-12 gap-12 items-center">
|
||||
<div className="md:col-span-4 flex justify-center">
|
||||
<div className="relative group">
|
||||
{/* Outer neon ring */}
|
||||
<div className="absolute -inset-1.5 bg-gradient-to-r from-cyan-500 to-purple-600 rounded-2xl blur opacity-30 group-hover:opacity-70 transition duration-1000 group-hover:duration-200"></div>
|
||||
<div className="relative w-64 h-64 md:w-80 md:h-80 rounded-2xl overflow-hidden border border-indigo-500/30 bg-slate-900">
|
||||
<img
|
||||
src={data.avatarUrl || "https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?auto=format&fit=crop&q=80&w=400"}
|
||||
alt={data.name}
|
||||
className="w-full h-full object-cover grayscale brightness-90 contrast-110 group-hover:scale-105 group-hover:grayscale-0 transition-all duration-700"
|
||||
referrerPolicy="no-referrer"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="md:col-span-8 space-y-6">
|
||||
<h2 className="text-3xl font-bold tracking-tight text-white flex items-center gap-3">
|
||||
<Compass className="w-7 h-7 text-cyan-400" />
|
||||
<span>Über den Autor</span>
|
||||
</h2>
|
||||
<div className="prose prose-invert prose-cyan max-w-none text-slate-300 leading-relaxed space-y-4">
|
||||
<p className="whitespace-pre-wrap">{data.bio}</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-4 pt-4 font-mono text-xs">
|
||||
<div className="p-3 bg-slate-900/60 border border-slate-800 rounded-lg flex items-center gap-3">
|
||||
<BookOpen className="w-5 h-5 text-indigo-400" />
|
||||
<div>
|
||||
<div className="text-slate-500 uppercase tracking-widest">Fokus</div>
|
||||
<div className="text-white font-semibold">Sci-Fi & Fantasy</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3 bg-slate-900/60 border border-slate-800 rounded-lg flex items-center gap-3">
|
||||
<Star className="w-5 h-5 text-cyan-400" />
|
||||
<div>
|
||||
<div className="text-slate-500 uppercase tracking-widest">Welten</div>
|
||||
<div className="text-white font-semibold">Techno-Magie</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3 bg-slate-900/60 border border-slate-800 rounded-lg flex items-center gap-3 col-span-2 md:col-span-1">
|
||||
<Award className="w-5 h-5 text-purple-400" />
|
||||
<div>
|
||||
<div className="text-slate-500 uppercase tracking-widest">Stil</div>
|
||||
<div className="text-white font-semibold">Episch & Atmosphärisch</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Section 2: Aktuelle Projekte */}
|
||||
<section id="projekte" className="space-y-8">
|
||||
<div className="flex flex-col md:flex-row md:items-end justify-between gap-4 border-b border-indigo-900/30 pb-4">
|
||||
<div>
|
||||
<h2 className="text-3xl font-bold tracking-tight text-white flex items-center gap-3">
|
||||
<Star className="w-7 h-7 text-indigo-400" />
|
||||
<span>Aktuelle Projekte</span>
|
||||
</h2>
|
||||
<p className="text-slate-400 text-sm mt-1">Ein exklusiver Blick auf das, was sich gerade auf meinem Schreib-Amboss befindet.</p>
|
||||
</div>
|
||||
<div className="text-xs font-mono text-cyan-400 bg-cyan-950/40 px-3 py-1 border border-cyan-800/40 rounded">
|
||||
Laufendes Telemetrie-Tracking
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{data.projects.map((project) => (
|
||||
<button
|
||||
key={project.id}
|
||||
onClick={() => setSelectedProject(project)}
|
||||
className="p-6 bg-slate-900/40 backdrop-blur-sm border border-indigo-950 hover:border-cyan-500/50 rounded-xl transition-all duration-300 flex flex-col text-left w-full justify-between group cursor-pointer hover:shadow-[0_0_20px_rgba(6,182,212,0.15)] focus:outline-none"
|
||||
>
|
||||
<div className="space-y-4 w-full">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-mono text-cyan-400 bg-cyan-950/60 px-2.5 py-1 rounded border border-cyan-900/50 uppercase tracking-wider">
|
||||
{project.status || "In Arbeit"}
|
||||
</span>
|
||||
<span className="text-xs text-slate-500 font-mono">
|
||||
Progression: {project.progress}%
|
||||
</span>
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-white group-hover:text-cyan-300 transition-colors">
|
||||
{project.title}
|
||||
</h3>
|
||||
<p className="text-slate-400 text-sm leading-relaxed line-clamp-3">
|
||||
{project.description}
|
||||
</p>
|
||||
<p className="text-cyan-400 font-mono text-[11px] mt-2 group-hover:translate-x-1 transition-transform flex items-center gap-1">
|
||||
Details anzeigen <ArrowRight className="w-3 h-3" />
|
||||
</p>
|
||||
</div>
|
||||
{/* Custom glowing progress bar */}
|
||||
<div className="mt-6 space-y-1.5 w-full">
|
||||
<div className="h-1.5 w-full bg-slate-800 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-gradient-to-r from-cyan-400 via-indigo-500 to-purple-600 rounded-full transition-all duration-1000 ease-out shadow-[0_0_8px_rgba(34,211,238,0.4)]"
|
||||
style={{ width: `${project.progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Section 3: Bücherregal */}
|
||||
<section id="buecherregal" className="space-y-8">
|
||||
<div className="border-b border-indigo-900/30 pb-4">
|
||||
<h2 className="text-3xl font-bold tracking-tight text-white flex items-center gap-3">
|
||||
<BookOpen className="w-7 h-7 text-purple-400" />
|
||||
<span>Das Bücherregal</span>
|
||||
</h2>
|
||||
<p className="text-slate-400 text-sm mt-1">Veröffentlichte Meilensteine. Klicken oder Tippen Sie auf ein Cover, um Geheimnisse zu enthüllen.</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-6 justify-items-center">
|
||||
{data.books.map((book) => (
|
||||
<button
|
||||
key={book.id}
|
||||
onClick={() => setSelectedBook(book)}
|
||||
className="group relative focus:outline-none text-left w-full max-w-[210px] aspect-[2/3] rounded-lg overflow-hidden border border-slate-800 bg-slate-900 hover:border-cyan-400 transition-all duration-300 hover:shadow-[0_0_20px_rgba(6,182,212,0.2)] cursor-pointer"
|
||||
>
|
||||
{/* Glowing edge highlight on hover */}
|
||||
<div className="absolute inset-0 bg-cyan-900/10 opacity-0 group-hover:opacity-100 transition-opacity z-10 pointer-events-none" />
|
||||
<img
|
||||
src={book.coverUrl || "https://images.unsplash.com/photo-1543002588-bfa74002ed7e?auto=format&fit=crop&q=80&w=300"}
|
||||
alt={book.title}
|
||||
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500 z-0"
|
||||
referrerPolicy="no-referrer"
|
||||
/>
|
||||
|
||||
{/* Transparent yet readable overlay, fully visible on hover */}
|
||||
<div className="absolute inset-x-0 bottom-0 bg-gradient-to-t from-slate-950 via-slate-950/90 to-transparent p-4 opacity-80 group-hover:opacity-100 transition-all duration-350 z-20">
|
||||
<p className="text-white text-xs font-bold truncate">{book.title}</p>
|
||||
<p className="text-cyan-400 text-[10px] font-mono mt-0.5">Details auslesen <ArrowRight className="inline-block w-2.5 h-2.5 ml-0.5" /></p>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
|
||||
{/* Book details modular interface modal */}
|
||||
{selectedBook && (
|
||||
<div className="fixed inset-0 z-50 overflow-y-auto" role="dialog" aria-modal="true">
|
||||
{/* Overlay background */}
|
||||
<div
|
||||
className="fixed inset-0 bg-slate-950/80 backdrop-blur-md transition-opacity"
|
||||
onClick={() => setSelectedBook(null)}
|
||||
/>
|
||||
|
||||
<div className="flex min-h-full items-center justify-center p-4 text-center sm:p-0">
|
||||
<div className="relative transform overflow-hidden rounded-2xl bg-slate-900 border border-slate-800 text-left shadow-2xl transition-all sm:my-8 sm:w-full sm:max-w-4xl p-6 md:p-8 space-y-6">
|
||||
|
||||
{/* Close Button */}
|
||||
<button
|
||||
onClick={() => setSelectedBook(null)}
|
||||
className="absolute top-4 right-4 text-slate-400 hover:text-white hover:bg-slate-800/80 px-2.5 py-1 rounded-md text-xs font-mono border border-slate-800 transition-all cursor-pointer"
|
||||
>
|
||||
SCHLIESSEN [ESC]
|
||||
</button>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-12 gap-8 pt-4">
|
||||
|
||||
{/* Book Cover */}
|
||||
<div className="col-span-1 md:col-span-4 flex flex-col items-center">
|
||||
<div className="rounded-xl overflow-hidden shadow-2xl border border-indigo-900/30 max-w-[260px] aspect-[2/3] w-full">
|
||||
<img
|
||||
src={selectedBook.coverUrl}
|
||||
alt={selectedBook.title}
|
||||
className="w-full h-full object-cover"
|
||||
referrerPolicy="no-referrer"
|
||||
/>
|
||||
</div>
|
||||
{selectedBook.buyLink && (
|
||||
<a
|
||||
href={selectedBook.buyLink}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="mt-6 w-full max-w-[260px] py-3 text-center bg-gradient-to-r from-cyan-500 to-indigo-600 hover:from-cyan-400 hover:to-indigo-500 text-slate-950 font-bold font-sans rounded-xl flex items-center justify-center gap-2 transition-all hover:scale-[1.02] shadow-[0_4px_12px_rgba(6,182,212,0.3)] text-sm"
|
||||
>
|
||||
<span>Invasion starten (Kauf-Link)</span>
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Book Details */}
|
||||
<div className="col-span-1 md:col-span-8 flex flex-col justify-between space-y-6">
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="text-xs font-mono text-cyan-400 bg-cyan-950/40 px-2.5 py-0.5 rounded border border-cyan-900/40">
|
||||
Erschienen: {selectedBook.publishedYear}
|
||||
</span>
|
||||
{selectedBook.genres?.map((genre) => (
|
||||
<span key={genre} className="text-[11px] font-mono text-purple-400 bg-purple-950/40 px-2.5 py-0.5 rounded border border-purple-900/40">
|
||||
{genre}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<h3 className="text-3xl font-extrabold text-white">
|
||||
{selectedBook.title}
|
||||
</h3>
|
||||
|
||||
{selectedBook.subtitle && (
|
||||
<h4 className="text-sm font-mono text-indigo-400 uppercase tracking-wider">
|
||||
{selectedBook.subtitle}
|
||||
</h4>
|
||||
)}
|
||||
|
||||
<div className="h-px bg-indigo-950" />
|
||||
|
||||
<p className="text-slate-300 text-sm leading-relaxed whitespace-pre-wrap">
|
||||
{selectedBook.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Integrated Spotify web player playlist if available */}
|
||||
{selectedBook.spotifyPlaylistId && selectedBook.spotifyPlaylistId.trim() !== "" && (
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs font-mono text-indigo-400 flex items-center gap-2">
|
||||
<Star className="w-3.5 h-3.5 animate-spin" />
|
||||
<span>INTEGRIERTES ATMOSPHÄREN-SYSTEM (SPOTIFY PLAYLIST)</span>
|
||||
</p>
|
||||
<div className="rounded-xl overflow-hidden bg-slate-950 border border-slate-800">
|
||||
<iframe
|
||||
src={`https://open.spotify.com/embed/playlist/${selectedBook.spotifyPlaylistId}?utm_source=generator&theme=0`}
|
||||
width="100%"
|
||||
height="152"
|
||||
frameBorder="0"
|
||||
allowFullScreen={false}
|
||||
allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture"
|
||||
loading="lazy"
|
||||
className="bg-slate-950"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Project details modular modal */}
|
||||
{selectedProject && (
|
||||
<div className="fixed inset-0 z-50 overflow-y-auto" role="dialog" aria-modal="true">
|
||||
{/* Overlay background */}
|
||||
<div
|
||||
className="fixed inset-0 bg-slate-950/80 backdrop-blur-md transition-opacity"
|
||||
onClick={() => setSelectedProject(null)}
|
||||
/>
|
||||
|
||||
<div className="flex min-h-full items-center justify-center p-4 text-center sm:p-0">
|
||||
<div className="relative transform overflow-hidden rounded-2xl bg-slate-900 border border-slate-800 text-left shadow-2xl transition-all sm:my-8 sm:w-full sm:max-w-2xl p-6 md:p-8 space-y-6">
|
||||
|
||||
{/* Close Button */}
|
||||
<button
|
||||
onClick={() => setSelectedProject(null)}
|
||||
className="absolute top-4 right-4 text-slate-400 hover:text-white hover:bg-slate-800/80 px-2.5 py-1 rounded-md text-xs font-mono border border-slate-800 transition-all cursor-pointer"
|
||||
>
|
||||
SCHLIESSEN [ESC]
|
||||
</button>
|
||||
|
||||
<div className="space-y-6 pt-4">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="text-xs font-mono text-cyan-400 bg-cyan-950/40 px-2.5 py-0.5 rounded border border-cyan-900/40">
|
||||
Status: {selectedProject.status || "In Arbeit"}
|
||||
</span>
|
||||
<span className="text-xs font-mono text-purple-400 bg-purple-950/40 px-2.5 py-0.5 rounded border border-purple-900/40">
|
||||
Übertragung aktiv
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h3 className="text-3xl font-extrabold text-white">
|
||||
{selectedProject.title}
|
||||
</h3>
|
||||
|
||||
<div className="h-px bg-indigo-950" />
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between items-center text-xs font-mono text-slate-400">
|
||||
<span>ORGANISCHER FORTSCHRITT:</span>
|
||||
<span className="text-cyan-400 font-bold">{selectedProject.progress}%</span>
|
||||
</div>
|
||||
<div className="h-2 w-full bg-slate-950 rounded-full overflow-hidden border border-slate-800">
|
||||
<div
|
||||
className="h-full bg-gradient-to-r from-cyan-400 via-indigo-500 to-purple-600 rounded-full transition-all duration-1000 ease-out shadow-[0_0_8px_rgba(34,211,238,0.4)]"
|
||||
style={{ width: `${selectedProject.progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-slate-950/60 p-5 rounded-xl border border-slate-800/60 relative overflow-hidden">
|
||||
<div className="absolute top-0 right-0 p-2 text-slate-800 text-[10px] font-mono select-none">
|
||||
SEC_REPORT_LOG
|
||||
</div>
|
||||
<h4 className="text-xs font-mono text-slate-500 uppercase tracking-widest mb-3">Projekt-Exzerpt & Fortschrittsnotizen</h4>
|
||||
<p className="text-slate-300 text-sm leading-relaxed whitespace-pre-wrap">
|
||||
{selectedProject.description || "Für dieses Schreibprojekt wurden noch keine detaillierten Chiffren abgelegt."}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Styled Footer */}
|
||||
<footer className="bg-slate-950 border-t border-indigo-900/30 py-12 px-4 text-center text-xs text-slate-500 font-mono">
|
||||
<div className="max-w-7xl mx-auto space-y-4">
|
||||
<p>© {new Date().getFullYear()} {data.name}. Alle Rechte vorbehalten.</p>
|
||||
<p className="text-slate-600">Dokumentation, Fragmente und stellare Chiffren. Verschlüsselt mit moderner Node.JS Technologie.</p>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
98
src/defaultData.ts
Normal file
98
src/defaultData.ts
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
import { AuthorData } from "./types";
|
||||
|
||||
export const defaultAuthorData: AuthorData = {
|
||||
scifi: {
|
||||
name: "Daniel Hesse",
|
||||
avatarUrl: "https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?auto=format&fit=crop&q=80&w=300",
|
||||
heroTitle: "DANIEL HESSE",
|
||||
heroSubtitle: "WELTENWANDLER & STERNENZEICHNER",
|
||||
bio: "Herzlich willkommen! Ich bin Daniel Hesse. Ich schreibe epische Fantasy und tiefgründige Science-Fiction, die dich an die Grenzen der Realität und weit darüber hinaus entführen. Schon als Kind faszinierten mich die unendlichen Weiten des Kosmos und die alten Sagen vergessener Völker. In meinen Büchern verschmelzen kalte, fortschrittliche Technologie und uralter, mystischer Zauber zu einer einzigartigen Atmosphäre. Wenn ich nicht gerade neue Galaxien am Laptop erschaffe, wandere ich durch dunkle Wälder oder beobachte mit meinem Teleskop den Nachthimmel.",
|
||||
projects: [
|
||||
{
|
||||
id: "proj_1",
|
||||
title: "Caelum: Asche und Staub (Arche-Chroniken II)",
|
||||
status: "Lektorat",
|
||||
progress: 85,
|
||||
description: "Der mit Spannung erwartete Nachfolger des Sci-Fi-Bestsellers. Die Reise der Arche-Menschheit nimmt eine dramatische Wendung."
|
||||
},
|
||||
{
|
||||
id: "proj_2",
|
||||
title: "Das Siegel der Runenmagier",
|
||||
status: "Designphase",
|
||||
progress: 30,
|
||||
description: "Ein neues High-Fantasy Epos über ein magisches Schriftsystem, das die physische Welt umschreiben kann."
|
||||
}
|
||||
],
|
||||
books: [
|
||||
{
|
||||
id: "book_1",
|
||||
title: "Caelum: Ruf der Sterne",
|
||||
subtitle: "Arche-Chroniken I",
|
||||
coverUrl: "https://images.unsplash.com/photo-1451187580459-43490279c0fa?auto=format&fit=crop&q=80&w=600",
|
||||
description: "Am Rande der bekannten Galaxie sucht die Besatzung der 'Caelum' nach einer neuen Heimat für die letzten Überlebenden der Erde. Doch der leere Raum zwischen den Sternen ist nicht so unbewohnt, wie sie dachten. Es wartet eine Bedrohung, die älter ist als die Zeit selbst, aber auch eine Macht, die das Verständnis der Menschheit revolutionieren könnte.",
|
||||
publishedYear: 2024,
|
||||
buyLink: "https://amazon.de",
|
||||
spotifyPlaylistId: "37i9dQZF1DX10zK7J2Y7gO",
|
||||
genres: ["Science-Fiction", "Space Opera", "Techno-Fantasy"]
|
||||
},
|
||||
{
|
||||
id: "book_2",
|
||||
title: "Die Schatten von ThalÃassia",
|
||||
subtitle: "Einzelband",
|
||||
coverUrl: "https://images.unsplash.com/photo-1518709268805-4e9042af9f23?auto=format&fit=crop&q=80&w=600",
|
||||
description: "In der versunkenen Unterwasser-Metropole Thalassia ringen Magokraten und Kybernetiker um ein uraltes Artefakt. Ein junger Taucher stößt in den abyssalen Gräben auf ein grauenvolles Geheimnis, das das Schicksal beider Fraktionen besiegeln könnte.",
|
||||
publishedYear: 2023,
|
||||
buyLink: "https://amazon.de",
|
||||
spotifyPlaylistId: "37i9dQZF1DX4g8Gs5YmN17",
|
||||
genres: ["Science-Fantasy", "Cyberpunk", "Abyssal"]
|
||||
}
|
||||
]
|
||||
},
|
||||
erotica: {
|
||||
name: "M. S. Velvet",
|
||||
avatarUrl: "https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?auto=format&fit=crop&q=80&w=300",
|
||||
heroTitle: "M. S. VELVET",
|
||||
heroSubtitle: "ELEGANT-SINNLICHE LITERATUR",
|
||||
bio: "Willkommen in meiner privaten Welt der Versuchung. Unter dem Pseudonym M. S. Velvet schreibe ich stilvolle, hocherotische Romane und prickelnde Erzählungen. Meine Leidenschaft liegt darin, das feine Spiel zwischen Verlangen und Hingabe, Macht und Vertrauen in all seinen schillernden Facetten zu beleuchten. Meine Geschichten richten sich an Kenner anspruchsvoller Erotikliteratur, die Wert auf tiefgründige Charaktere, knisternde Spannung und ästhetische Beschreibungen legen. Treten Sie näher und lassen Sie sich entführen.",
|
||||
projects: [
|
||||
{
|
||||
id: "proj_er_1",
|
||||
title: "Gefährliche Versprechen (Velvet Trust II)",
|
||||
status: "Schreibphase",
|
||||
progress: 60,
|
||||
description: "Der packende Fortsetzungsroman um verbotene Affären und verdeckte Sehnsüchte im Herzen von Paris."
|
||||
},
|
||||
{
|
||||
id: "proj_er_2",
|
||||
title: "Schattenlichter",
|
||||
status: "Konzeptionell",
|
||||
progress: 15,
|
||||
description: "Eine sinnliche Anthologie über geheimnisvolle nächtliche Begegnungen in einer exklusiven Loge."
|
||||
}
|
||||
],
|
||||
books: [
|
||||
{
|
||||
id: "book_er_1",
|
||||
title: "Flüstern im Samt",
|
||||
subtitle: "Velvet Trust I",
|
||||
coverUrl: "https://images.unsplash.com/photo-1517841905240-472988babdf9?auto=format&fit=crop&q=80&w=600",
|
||||
description: "Ein Blick in den überfüllten Ballsaal reicht aus, um Sophias wohlgeordnete Welt ins Wanken zu bringen. Der geheimnisvolle Kunsthändler Alexander bietet ihr einen Deal an, dem sie nicht widerstehen kann. Doch hinter den luxuriösen Fassaden verbirgt sich ein leidenschaftliches Abenteuer voller Geheimnisse, das ihre tiefsten Tabus herausfordern wird.",
|
||||
publishedYear: 2025,
|
||||
buyLink: "https://amazon.de",
|
||||
spotifyPlaylistId: "37i9dQZF1DX8V9b88fH3Wf",
|
||||
genres: ["Dark Romance", "Erotik", "Elegante Sinnlichkeit"]
|
||||
},
|
||||
{
|
||||
id: "book_er_2",
|
||||
title: "Die Maske des Begehrens",
|
||||
subtitle: "Erotischer Thriller",
|
||||
coverUrl: "https://images.unsplash.com/photo-1508700115892-45ecd05ae2ad?auto=format&fit=crop&q=80&w=600",
|
||||
description: "In Venedigs geheimem Karnevalskreisen kreuzen sich die Wege zweier Fremder, versteckt hinter venezianischen Seidenmasken. Ein sinnliches Versteckspiel beginnt, bei dem Identitäten verblassen und nur noch die unkontrollierbare sexuelle Spannung regiert.",
|
||||
publishedYear: 2024,
|
||||
buyLink: "https://amazon.de",
|
||||
spotifyPlaylistId: "37i9dQZF1DX2U76YeeG0Ug",
|
||||
genres: ["Sinnlicher Thriller", "BDSM-Touch", "Maskerade"]
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
7
src/index.css
Normal file
7
src/index.css
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
@import "tailwindcss";
|
||||
|
||||
@layer base {
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
}
|
||||
10
src/main.tsx
Normal file
10
src/main.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import {StrictMode} from 'react';
|
||||
import {createRoot} from 'react-dom/client';
|
||||
import App from './App.tsx';
|
||||
import './index.css';
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
);
|
||||
38
src/types.ts
Normal file
38
src/types.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
export interface Project {
|
||||
id: string;
|
||||
title: string;
|
||||
status: string; // e.g., "Konzept", "In Arbeit", "Lektorat", "Abgeschlossen"
|
||||
progress: number; // 0 to 100
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface Book {
|
||||
id: string;
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
coverUrl: string;
|
||||
description: string;
|
||||
publishedYear: number;
|
||||
buyLink?: string;
|
||||
spotifyPlaylistId?: string; // Just the playlist ID (e.g., "47R6ZAdk7Xf6M4i9mR0fP1")
|
||||
genres?: string[];
|
||||
}
|
||||
|
||||
export interface AuthorProfile {
|
||||
name: string;
|
||||
bio: string;
|
||||
avatarUrl: string;
|
||||
heroTitle: string;
|
||||
heroSubtitle: string;
|
||||
projects: Project[];
|
||||
books: Book[];
|
||||
}
|
||||
|
||||
export interface AuthorData {
|
||||
scifi: AuthorProfile;
|
||||
erotica: AuthorProfile;
|
||||
}
|
||||
|
||||
export interface AdminSettings {
|
||||
adminPasswordHash?: string; // Optional if we hash, or we check against .env
|
||||
}
|
||||
26
tsconfig.json
Normal file
26
tsconfig.json
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"experimentalDecorators": true,
|
||||
"useDefineForClassFields": false,
|
||||
"module": "ESNext",
|
||||
"lib": [
|
||||
"ES2022",
|
||||
"DOM",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"allowJs": true,
|
||||
"jsx": "react-jsx",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./*"
|
||||
]
|
||||
},
|
||||
"allowImportingTsExtensions": true,
|
||||
"noEmit": true
|
||||
}
|
||||
}
|
||||
22
vite.config.ts
Normal file
22
vite.config.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import tailwindcss from '@tailwindcss/vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import path from 'path';
|
||||
import {defineConfig} from 'vite';
|
||||
|
||||
export default defineConfig(() => {
|
||||
return {
|
||||
plugins: [react(), tailwindcss()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, '.'),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
// HMR is disabled in AI Studio via DISABLE_HMR env var.
|
||||
// Do not modifyâfile watching is disabled to prevent flickering during agent edits.
|
||||
hmr: process.env.DISABLE_HMR !== 'true',
|
||||
// Disable file watching when DISABLE_HMR is true to save CPU during agent edits.
|
||||
watch: process.env.DISABLE_HMR === 'true' ? null : {},
|
||||
},
|
||||
};
|
||||
});
|
||||
Loading…
Reference in a new issue