File: //proc/self/root/tmp/bewertemich-UploadSuccess-loop.jsx
import React, { useEffect, useState } from 'react';
import { Link, useLocation, useParams } from 'react-router-dom';
import { buildShareUrl, buildUploadPath, getUploadEntry, trackEvent } from '../../utils/tracking.js';
import { buildPhotoInvitePlaybook } from './sharePlaybooks.js';
const copyText = async (text) => {
try {
await navigator.clipboard.writeText(text);
return true;
} catch {
const input = document.createElement('input');
input.value = text;
document.body.appendChild(input);
input.select();
const ok = document.execCommand('copy');
document.body.removeChild(input);
return ok;
}
};
export const UploadSuccessPage = () => {
const { id } = useParams();
const location = useLocation();
const [photo, setPhoto] = useState(null);
const [stats, setStats] = useState({ avg_score: null, votes: 0 });
const [msg, setMsg] = useState('');
const [copyState, setCopyState] = useState('');
const numericId = Number(id);
const entryPoint = getUploadEntry(location.search);
useEffect(() => {
let cancelled = false;
const load = async () => {
try {
const photoRes = await fetch(`/api/photos/${numericId}`, { credentials: 'include' });
const photoJson = await photoRes.json().catch(() => ({}));
const statsRes = await fetch(`/api/votes/${numericId}`, { credentials: 'include' });
const statsJson = await statsRes.json().catch(() => ({ avg_score: null, votes: 0 }));
if (cancelled) return;
if (!photoRes.ok || !photoJson?.photo) {
setMsg('Foto nicht gefunden.');
return;
}
setPhoto(photoJson.photo);
setStats(statsJson || { avg_score: null, votes: 0 });
} catch {
if (!cancelled) setMsg('Foto nicht gefunden.');
}
};
if (Number.isInteger(numericId) && numericId > 0) load();
return () => { cancelled = true; };
}, [numericId]);
const copyShareUrl = buildShareUrl(numericId, { source: 'copy', surface: 'upload_success' });
const nativeShareUrl = buildShareUrl(numericId, { source: 'native', surface: 'upload_success' });
const sharePreviewUrl = numericId > 0 ? `/s/${numericId}/og.jpg` : '';
const remainingVotes = Math.max(0, 5 - Number(stats?.votes || 0));
const progressPercent = Math.max(8, Math.min(100, Math.round((Number(stats?.votes || 0) / 5) * 100)));
const avgScore = stats.avg_score ? Number(stats.avg_score) : null;
const scoreHeadline = !avgScore || Number(stats?.votes || 0) < 3
? 'Noch kein klares Signal'
: avgScore >= 8
? 'Starker erster Eindruck'
: avgScore >= 6.5
? 'Solider Start'
: 'Da ist noch Luft nach oben';
const scoreHint = remainingVotes > 0
? `Noch ${remainingVotes} Stimme${remainingVotes === 1 ? '' : 'n'} bis zur ersten belastbaren Tendenz.`
: avgScore >= 8
? 'Das Bild wirkt aktuell stark. Jetzt kannst du es auch breiter teilen.'
: 'Jetzt lohnt sich der Blick auf den Score und eventuell ein zweites Testbild.';
const nextMove = Number(stats?.votes || 0) < 3
? 'Empfohlen jetzt: 3 direkte WhatsApp-Kontakte'
: Number(stats?.votes || 0) < 5
? 'Empfohlen jetzt: Gruppenlink oder kurzer Reminder'
: 'Empfohlen jetzt: breiter teilen oder A/B-Vergleich starten';
const shareText = avgScore && Number(stats?.votes || 0) >= 5
? `Bewerte bitte mein Profilbild ehrlich. Aktuell hat es ${avgScore.toFixed(1)} Punkte.`
: 'Bewerte bitte mein Profilbild ehrlich auf bewertemich.de.';
const canNativeShare = typeof navigator !== 'undefined' && typeof navigator.share === 'function';
const sharePlaybook = buildPhotoInvitePlaybook({
photoId: numericId,
avgScore,
votes: Number(stats?.votes || 0),
surface: 'upload_success'
});
const shareTemplates = [
{
id: 'freund',
label: 'Direkt an Freunde',
text: `Magst du mein Profilbild kurz ehrlich bewerten? Dauert nur ein paar Sekunden: ${copyShareUrl}`
},
{
id: 'dating',
label: 'Dating-Check',
text: `Ich teste gerade mein Dating-Profilbild. Was ist dein ehrlicher Eindruck? ${copyShareUrl}`
},
{
id: 'gruppe',
label: 'Gruppe oder Story',
text: `Kurzer Realitätscheck: Wie wirkt dieses Profilbild auf dich? ${copyShareUrl}`
}
];
const onCopy = async () => {
const ok = await copyText(copyShareUrl);
if (ok) {
void trackEvent('share_click', {
photoId: numericId,
source: 'copy',
surface: 'upload_success',
entryPoint
});
}
setCopyState(ok ? 'Link kopiert.' : 'Kopieren fehlgeschlagen.');
};
const onNativeShare = async () => {
if (!canNativeShare) return;
try {
await navigator.share({ title: 'Bewerte mein Foto', text: shareText, url: nativeShareUrl });
void trackEvent('share_click', {
photoId: numericId,
source: 'native',
surface: 'upload_success',
entryPoint
});
} catch {}
};
const copyTemplate = async (template, source = 'copy') => {
const ok = await copyText(template);
if (ok) {
void trackEvent('share_click', {
photoId: numericId,
source,
surface: 'upload_success',
entryPoint,
meta: { template: 'message' }
});
}
setCopyState(ok ? 'Textvorlage kopiert.' : 'Kopieren fehlgeschlagen.');
};
if (!photo) {
return (
<div className="mx-auto max-w-4xl px-4 py-10">
<div className="alert bg-base-100 border">
<span>{msg || 'Lade...'}</span>
</div>
</div>
);
}
return (
<div className="mx-auto max-w-5xl px-4 py-10 space-y-6">
<section className="rounded-3xl border bg-gradient-to-br from-primary to-secondary p-6 text-primary-content shadow-lg md:p-8">
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div className="max-w-2xl">
<div className="badge border-white/30 bg-white/10 text-white">Schritt 2 von 3</div>
<h1 className="mt-3 text-3xl font-black tracking-tight">Jetzt 5 Personen einladen</h1>
<p className="mt-2 text-white/90">
Der schnellste Weg zu aussagekräftigem Feedback ist dein persönlicher Einladungslink. Teile ihn erst in direkten Kontakten, dann in Gruppen oder Story. Die neue OG-Karte zeigt Score und klares Voting-CTA statt nur dein Rohbild.
</p>
</div>
<div className="min-w-[240px] rounded-2xl bg-white/12 px-4 py-3 text-sm text-white/95">
<div className="text-xs uppercase tracking-[0.2em] text-white/70">Fortschritt</div>
<div className="mt-2 h-2 overflow-hidden rounded-full bg-white/20">
<div className="h-full rounded-full bg-white" style={{ width: `${progressPercent}%` }} />
</div>
<div className="mt-2">{scoreHint}</div>
<div className="mt-2 text-xs uppercase tracking-[0.16em] text-white/70">{nextMove}</div>
</div>
</div>
</section>
<section className="grid gap-6 md:grid-cols-[1.1fr_0.9fr]">
<div className="space-y-4">
<div className="rounded-2xl border bg-white p-4 shadow-sm">
<img
src={`/api/photos/file/${photo.id}`}
alt="Dein hochgeladenes Foto"
className="max-h-[70vh] w-full rounded-2xl border bg-white object-contain"
/>
<div className="mt-4 flex flex-wrap items-center gap-3 text-sm text-gray-600">
<span className="rounded-full bg-gray-100 px-3 py-1">Ø {avgScore ? avgScore.toFixed(1) : '-'}</span>
<span className="rounded-full bg-gray-100 px-3 py-1">{stats.votes} Stimmen</span>
<span className="rounded-full bg-gray-100 px-3 py-1">Invite first</span>
</div>
</div>
<div className="rounded-3xl bg-slate-900 p-5 text-white shadow-lg">
<div className="text-xs uppercase tracking-[0.2em] text-white/60">Ergebnis-Karte</div>
<div className="mt-3 grid gap-4 md:grid-cols-[180px_1fr]">
<img
src={`/api/photos/file/${photo.id}`}
alt="Ergebnis-Karte"
className="aspect-square w-full rounded-2xl object-cover"
/>
<div>
<div className="text-2xl font-black">{scoreHeadline}</div>
<p className="mt-2 text-sm text-white/80">{scoreHint}</p>
<div className="mt-4 flex flex-wrap gap-2 text-sm">
<span className="rounded-full bg-white/10 px-3 py-1">Ø {avgScore ? avgScore.toFixed(1) : '-'}</span>
<span className="rounded-full bg-white/10 px-3 py-1">{stats.votes} Stimmen</span>
<span className="rounded-full bg-white/10 px-3 py-1">{remainingVotes > 0 ? `Noch ${remainingVotes}` : 'Signal vorhanden'}</span>
</div>
<div className="mt-4 text-sm text-white/75">
Beste Reihenfolge: zuerst 5 direkte Kontakte, danach Gruppe oder Story.
</div>
</div>
</div>
</div>
<div className="rounded-3xl border bg-white p-5 shadow-sm">
<div className="text-xs uppercase tracking-[0.2em] text-gray-500">Share-Preview</div>
<h2 className="mt-2 text-xl font-bold">So sieht deine OG-Karte beim Teilen aus</h2>
<p className="mt-2 text-sm text-gray-600">
Messenger und Social-Previews ziehen jetzt eine Score-Karte mit CTA. Das konvertiert deutlich besser als ein blanker Link oder nur dein Rohbild.
</p>
<img src={sharePreviewUrl} alt="Share-Vorschau" className="mt-4 w-full rounded-2xl border shadow-sm" />
</div>
</div>
<div className="space-y-4">
<div className="rounded-2xl border bg-white p-5 shadow-sm">
<h2 className="text-xl font-bold">Einladungslink</h2>
<p className="mt-2 text-sm text-gray-600">
Dieser Link zeigt beim Teilen eine echte Foto-Vorschau und fuehrt direkt zur Bewertung.
</p>
<input
readOnly
value={copyShareUrl}
className="mt-4 w-full rounded-xl border border-gray-200 bg-gray-50 px-4 py-3 text-sm"
/>
<div className="mt-4 grid gap-3 sm:grid-cols-2">
<button type="button" className="btn btn-primary" onClick={onCopy}>Link kopieren</button>
{canNativeShare ? (
<button type="button" className="btn btn-ghost border" onClick={onNativeShare}>Direkt teilen</button>
) : (
<a className="btn btn-ghost border" href={copyShareUrl}>Link öffnen</a>
)}
<a
className="btn btn-ghost border"
href={sharePlaybook[0]?.href}
target="_blank"
rel="noreferrer"
onClick={() => {
void trackEvent('share_click', {
photoId: numericId,
source: sharePlaybook[0]?.source,
surface: 'upload_success',
entryPoint,
meta: { lane: 'primary' }
}, { transport: 'beacon' });
}}
>
WhatsApp direkt
</a>
<a
className="btn btn-ghost border"
href={sharePlaybook[1]?.href}
target="_blank"
rel="noreferrer"
onClick={() => {
void trackEvent('share_click', {
photoId: numericId,
source: sharePlaybook[1]?.source,
surface: 'upload_success',
entryPoint,
meta: { lane: 'primary' }
}, { transport: 'beacon' });
}}
>
Telegram direkt
</a>
</div>
{copyState && <div className="mt-3 text-sm text-gray-600">{copyState}</div>}
</div>
<div className="rounded-2xl border bg-white p-5 shadow-sm">
<h2 className="text-xl font-bold">Kanal-Playbook für mehr Votes</h2>
<p className="mt-2 text-sm text-gray-600">
Erst direkte Kontakte, danach Gruppe, dann Reminder. So kommst du am schnellsten über die ersten 5 Stimmen.
</p>
<div className="mt-4 space-y-3">
{sharePlaybook.map((action) => (
<div key={action.id} className="rounded-2xl border border-gray-200 p-4">
<div className="flex items-center justify-between gap-3">
<div>
<div className="font-semibold">{action.label}</div>
<div className="text-xs uppercase tracking-[0.16em] text-gray-400">{action.goal}</div>
</div>
{'copyText' in action ? (
<button
type="button"
className="btn btn-sm btn-ghost border"
onClick={() => copyTemplate(action.copyText, action.source)}
>
Kopieren
</button>
) : (
<a
className="btn btn-sm btn-ghost border"
href={action.href}
target="_blank"
rel="noreferrer"
onClick={() => {
void trackEvent('share_click', {
photoId: numericId,
source: action.source,
surface: 'upload_success',
entryPoint,
meta: { lane: 'playbook_open' }
}, { transport: 'beacon' });
}}
>
Öffnen
</a>
)}
</div>
<div className="mt-2 text-sm text-gray-600">{action.description}</div>
</div>
))}
</div>
</div>
<div className="rounded-2xl border bg-white p-5 shadow-sm">
<h2 className="text-xl font-bold">Textvorlagen, die schnell funktionieren</h2>
<div className="mt-4 space-y-3">
{shareTemplates.map((template) => (
<div key={template.id} className="rounded-2xl border border-gray-200 p-4">
<div className="flex items-center justify-between gap-3">
<div className="font-semibold">{template.label}</div>
<button type="button" className="btn btn-sm btn-ghost border" onClick={() => copyTemplate(template.text, 'copy_template')}>Kopieren</button>
</div>
<div className="mt-2 text-sm text-gray-600">{template.text}</div>
</div>
))}
</div>
</div>
<div className="rounded-2xl border bg-white p-5 shadow-sm">
<h2 className="text-xl font-bold">Empfohlene Reihenfolge</h2>
<ol className="mt-3 space-y-3 text-sm text-gray-700">
<li>1. Schicke den Link zuerst an 3 direkte WhatsApp-Kontakte.</li>
<li>2. Hole dir 2 weitere direkte Stimmen über Telegram oder Copy-Link.</li>
<li>3. Wenn du noch unter 5 Stimmen bist, nutze Gruppe oder Reminder.</li>
</ol>
<div className="mt-4 flex flex-wrap gap-3">
<Link to={`/rate/${photo.id}`} className="btn btn-primary">Zur Bewertungsseite</Link>
<Link to={`/compare/new?photo=${photo.id}`} className="btn btn-ghost border">A/B-Vergleich starten</Link>
<Link to={buildUploadPath('upload_success_repeat')} className="btn btn-ghost border">Noch ein Foto hochladen</Link>
</div>
</div>
</div>
</section>
</div>
);
};