managarten/maerchenzauber/apps/landing/public/character.html
Till-JS e7f5f942f3 chore: initial commit - consolidate 4 projects into monorepo
Projects included:
- maerchenzauber (NestJS backend + Expo mobile + SvelteKit web + Astro landing)
- manacore (Expo mobile + SvelteKit web + Astro landing)
- manadeck (NestJS backend + Expo mobile + SvelteKit web)
- memoro (Expo mobile + SvelteKit web + Astro landing)

This commit preserves the current state before monorepo restructuring.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-22 23:38:24 +01:00

424 lines
13 KiB
HTML

<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Charakter teilen | Märchenzauber</title>
<!-- Meta tags will be updated dynamically -->
<meta name="description" content="Entdecke magische Charaktere in Märchenzauber">
<meta property="og:type" content="website">
<meta property="og:title" content="Charakter teilen | Märchenzauber">
<meta property="og:description" content="Entdecke magische Charaktere in Märchenzauber">
<meta property="og:image" content="/og-image.jpg">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Grandstander:wght@400;700&display=swap" rel="stylesheet">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', sans-serif;
background: linear-gradient(135deg, #f5f0ff 0%, #ffe5f0 50%, #e0f0ff 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 40px 20px;
}
.loader {
text-align: center;
padding: 60px 20px;
}
.spinner {
border: 4px solid #f3f3f3;
border-top: 4px solid #9333ea;
border-radius: 50%;
width: 50px;
height: 50px;
animation: spin 1s linear infinite;
margin: 0 auto 20px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.character-card {
background: white;
border-radius: 24px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
overflow: hidden;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 40px;
padding: 40px;
}
@media (max-width: 768px) {
.character-card {
grid-template-columns: 1fr;
gap: 20px;
padding: 20px;
}
}
h1 {
font-family: 'Grandstander', cursive;
font-size: 2.5rem;
color: #1a1a1a;
margin-bottom: 24px;
}
.images-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.image-container {
aspect-ratio: 1;
border-radius: 16px;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.image-container img {
width: 100%;
height: 100%;
object-fit: cover;
}
.character-info {
display: flex;
flex-direction: column;
justify-content: space-between;
}
.description {
margin-bottom: 24px;
}
.description h2 {
font-size: 1.25rem;
color: #333;
margin-bottom: 12px;
}
.description p {
color: #666;
line-height: 1.6;
}
.info-box {
background: linear-gradient(135deg, #f3e8ff 0%, #fce7f3 100%);
border-radius: 16px;
padding: 24px;
margin-bottom: 24px;
}
.info-box p {
color: #4a4a4a;
line-height: 1.6;
margin-bottom: 8px;
}
.info-box p:last-child {
margin-bottom: 0;
font-size: 0.9rem;
color: #666;
}
.buttons {
display: flex;
flex-direction: column;
gap: 12px;
}
.btn {
padding: 16px 24px;
border-radius: 16px;
font-weight: 600;
font-size: 1rem;
text-align: center;
text-decoration: none;
cursor: pointer;
border: none;
transition: all 0.3s ease;
}
.btn-primary {
background: linear-gradient(135deg, #9333ea 0%, #ec4899 100%);
color: white;
box-shadow: 0 4px 12px rgba(147, 51, 234, 0.3);
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(147, 51, 234, 0.4);
}
.btn-secondary {
background: white;
color: #9333ea;
border: 2px solid #9333ea;
}
.btn-secondary:hover {
background: #f9f5ff;
}
.error {
text-align: center;
padding: 60px 20px;
}
.error-icon {
font-size: 4rem;
margin-bottom: 20px;
}
.error h1 {
font-family: 'Grandstander', cursive;
margin-bottom: 16px;
}
.error p {
color: #666;
margin-bottom: 32px;
}
.how-it-works {
background: white;
border-radius: 24px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
padding: 40px;
margin-top: 40px;
}
.how-it-works h2 {
font-family: 'Grandstander', cursive;
font-size: 1.75rem;
margin-bottom: 32px;
text-align: center;
}
.steps {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 32px;
}
.step {
text-align: center;
}
.step-icon {
font-size: 3rem;
margin-bottom: 16px;
}
.step h3 {
font-size: 1.125rem;
margin-bottom: 12px;
color: #333;
}
.step p {
font-size: 0.9rem;
color: #666;
line-height: 1.5;
}
</style>
</head>
<body>
<div class="container">
<div id="loading" class="loader">
<div class="spinner"></div>
<p>Charakter wird geladen...</p>
</div>
<div id="content" style="display: none;"></div>
</div>
<script>
const BACKEND_URL = 'https://storyteller-backend-111768794939.europe-west3.run.app';
// Parse URL to get character ID and share code
// URL format: /character/{id}/{shareCode}/
const pathParts = window.location.pathname.split('/').filter(Boolean);
const characterId = pathParts[1]; // character is at index 0
const shareCode = pathParts[2];
console.log('Character ID:', characterId, 'Share Code:', shareCode);
async function loadCharacter() {
if (!characterId || !shareCode) {
showError('Ungültiger Link', 'Dieser Link ist ungültig oder unvollständig.');
return;
}
try {
const response = await fetch(`${BACKEND_URL}/characters/public/${characterId}/${shareCode}`);
if (!response.ok) {
throw new Error('Character not found');
}
const character = await response.json();
if (character.error) {
throw new Error(character.error);
}
// Update meta tags for social sharing
updateMetaTags(character);
// Display character
displayCharacter(character);
// Auto-redirect on mobile devices
if (isMobile()) {
setTimeout(() => {
tryOpenApp();
}, 500);
}
} catch (error) {
console.error('Error loading character:', error);
showError('Charakter nicht gefunden', 'Dieser Charakter existiert nicht oder der Freigabelink ist ungültig.');
} finally {
document.getElementById('loading').style.display = 'none';
document.getElementById('content').style.display = 'block';
}
}
function updateMetaTags(character) {
document.title = `${character.name} - Teile diesen Charakter | Märchenzauber`;
const description = `Schau dir '${character.name}' an - ein magischer Charakter aus Märchenzauber! Lade die App herunter und füge diesen Charakter zu deiner Bibliothek hinzu.`;
document.querySelector('meta[name="description"]').setAttribute('content', description);
document.querySelector('meta[property="og:title"]').setAttribute('content', `${character.name} | Märchenzauber`);
document.querySelector('meta[property="og:description"]').setAttribute('content', description);
if (character.imageUrls && character.imageUrls[0]) {
document.querySelector('meta[property="og:image"]').setAttribute('content', character.imageUrls[0]);
}
}
function displayCharacter(character) {
const content = document.getElementById('content');
const imagesHTML = character.imageUrls?.slice(0, 4).map(url => `
<div class="image-container">
<img src="${url}" alt="${character.name}" loading="lazy">
</div>
`).join('') || '';
content.innerHTML = `
<div class="character-card">
<div>
<h1>${character.name}</h1>
<div class="images-grid">
${imagesHTML}
</div>
</div>
<div class="character-info">
<div>
${character.description ? `
<div class="description">
<h2>Beschreibung</h2>
<p>${character.description}</p>
</div>
` : ''}
<div class="info-box">
<p>Dieser magische Charakter wurde mit Märchenzauber erstellt.</p>
<p>Lade die App herunter, um diesen Charakter zu deiner Bibliothek hinzuzufügen und eigene personalisierte Geschichten zu erstellen!</p>
</div>
</div>
<div class="buttons">
<button class="btn btn-primary" onclick="tryOpenApp()">
🎨 In App öffnen
</button>
<a href="/download" class="btn btn-secondary">
📱 App herunterladen
</a>
</div>
</div>
</div>
<div class="how-it-works">
<h2>So funktioniert's</h2>
<div class="steps">
<div class="step">
<div class="step-icon">📱</div>
<h3>1. App öffnen</h3>
<p>Tippe auf "In App öffnen" oder lade Märchenzauber herunter</p>
</div>
<div class="step">
<div class="step-icon">⭐</div>
<h3>2. Charakter hinzufügen</h3>
<p>Der Charakter wird automatisch zu deiner Bibliothek hinzugefügt</p>
</div>
<div class="step">
<div class="step-icon">📖</div>
<h3>3. Geschichten erstellen</h3>
<p>Nutze den Charakter in deinen eigenen magischen Geschichten</p>
</div>
</div>
</div>
`;
}
function showError(title, message) {
const content = document.getElementById('content');
content.innerHTML = `
<div class="error">
<div class="error-icon">😢</div>
<h1>${title}</h1>
<p>${message}</p>
<a href="/" class="btn btn-primary">Zur Startseite</a>
</div>
`;
}
function tryOpenApp() {
// Try custom scheme (universal links will intercept this on iOS/Android)
const deepLink = `maerchenzauber://share/character/${characterId}/${shareCode}`;
// iOS/Android will automatically intercept HTTPS URLs if app is installed
// For custom scheme fallback compatibility
window.location.href = deepLink;
// Fallback to download page after 2 seconds if app didn't open
setTimeout(() => {
if (!document.hidden) {
window.location.href = '/download';
}
}, 2000);
}
function isMobile() {
return /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
}
// Load character on page load
loadCharacter();
</script>
</body>
</html>