File: //proc/self/root/tmp/bewertemich-HomePage-hubs.jsx
import React, { useEffect, useMemo, useState } from 'react';
import { Link } from 'react-router-dom';
import { Hero } from './Hero.jsx';
import { HomePageMeta } from './HomePageMeta.jsx';
import { SeoLandingTeaser } from './SeoLandingTeaser.jsx';
import { SeoGrowthWidget } from './SeoGrowthWidget.jsx';
import { getPhotoUrl } from '../../utils/imageUrl.js';
import { buildUploadPath, trackEvent } from '../../utils/tracking.js';
import { featuredSeoLinks } from '../seo/catalog.js';
const routeCards = [
{
title: 'Dating',
text: 'Teste, welches Foto im Dating-Kontext sympathisch und offen wirkt.',
image: '/landing-media/community-dating.webp',
slug: 'dating-foto-ratgeber',
entry: 'dating',
accent: 'text-coral-600',
cta: 'Dating-Foto testen'
},
{
title: 'Business',
text: 'Finde ein Profilbild, das kompetent, nahbar und professionell ankommt.',
image: '/landing-media/community-business.webp',
slug: 'business-foto-ratgeber',
entry: 'business',
accent: 'text-emerald-700',
cta: 'Business-Foto testen'
},
{
title: 'Social',
text: 'Optimiere dein Foto für Social-Profile, Storys und öffentliche Auftritte.',
image: '/landing-media/community-social.webp',
slug: 'social-profilbild-ratgeber',
entry: 'social',
accent: 'text-amber-700',
cta: 'Social-Foto testen'
}
];
const toManifestEntries = (seoManifest) => {
const pages = Array.isArray(seoManifest?.landingPages) ? seoManifest.landingPages : [];
if (pages.length === 0) return featuredSeoLinks.slice(0, 4);
return pages
.map((entry) => {
const slug = String(entry?.slug || '').trim();
if (!slug) return null;
const featured = featuredSeoLinks.find((item) => item.slug === slug);
return {
slug,
title: String(entry.title || featured?.title || slug),
tags: Array.isArray(entry.tags) ? entry.tags : [],
faqCount: Number(entry.faqCount || 0)
};
})
.filter(Boolean)
.slice(0, 6);
};
const toEntryToken = (value) => String(value || '').replace(/-/g, '_');
const isProfileLikeItem = (item) => {
if (!item) return false;
if (String(item.title || '').trim()) return true;
const score = Number(item.avg_score || 0);
if (Number.isFinite(score) && score >= 5) return true;
const fileName = String(item.file_name || '').toLowerCase();
return fileName.includes('nano') || fileName.includes('portrait') || fileName.includes('profil');
};
const CommunityFeed = ({ items, loading }) => (
<section className="mt-10">
<div className="flex flex-wrap items-end justify-between gap-4">
<div>
<h2 className="text-2xl font-black text-slate-950">Gerade in der Community</h2>
<p className="mt-2 max-w-2xl text-slate-600">Neue Fotos, echte Stimmen und direkte Routen zum Mitbewerten.</p>
</div>
<Link to={buildUploadPath('home_feed')} className="btn border-0 bg-coral-500 text-white hover:bg-coral-600">Foto hochladen</Link>
</div>
<div className="mt-5 grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
{loading && Array.from({ length: 4 }).map((_, index) => (
<div key={index} className="overflow-hidden rounded-xl border border-slate-200 bg-white shadow-sm">
<div className="skeleton aspect-[4/5] w-full rounded-none" />
<div className="space-y-3 p-4">
<div className="skeleton h-4 w-2/3" />
<div className="skeleton h-2 w-full" />
</div>
</div>
))}
{!loading && (items || []).slice(0, 4).map((item, index) => {
const score = Number(item.avg_score || 0) > 0 ? Math.round(Number(item.avg_score) * 10) : 72 + index * 4;
const votes = Number(item.votes || 0);
return (
<Link
key={item.id}
to={`/rate/${item.id}`}
className="group overflow-hidden rounded-xl border border-slate-200 bg-white shadow-sm transition hover:-translate-y-0.5 hover:shadow-md"
onClick={() => trackEvent('home_feed_card_click', {
source: 'home',
surface: 'community_feed',
entryPoint: String(item.id)
})}
>
<div className="relative aspect-[4/5] overflow-hidden bg-slate-100">
<img src={getPhotoUrl(item)} alt={item.title || 'Profilfoto bewerten'} loading="lazy" decoding="async" className="h-full w-full object-cover transition duration-500 group-hover:scale-105" />
<span className="absolute bottom-3 left-3 rounded-md bg-coral-500 px-2.5 py-1 text-xs font-bold text-white shadow">{votes > 0 ? votes : 'Neu'} Stimmen</span>
</div>
<div className="space-y-3 p-4">
<div className="flex items-center justify-between gap-3">
<h3 className="truncate font-black text-slate-950">{item.title || 'Neuer Fototest'}</h3>
<span className="font-black text-emerald-600">{score}%</span>
</div>
<div className="h-2 rounded-full bg-slate-100">
<div className="h-full rounded-full bg-emerald-500" style={{ width: `${Math.max(38, Math.min(96, score))}%` }} />
</div>
<p className="text-sm text-slate-500">Jetzt bewerten und der Person beim Entscheiden helfen.</p>
</div>
</Link>
);
})}
{!loading && (!items || items.length === 0) ? (
<div className="rounded-xl border border-slate-200 bg-white p-6 text-slate-600 sm:col-span-2 lg:col-span-4">
Noch keine neuen Fotos im Feed. Starte den ersten Community-Test.
</div>
) : null}
</div>
</section>
);
const AudienceRoutes = () => (
<section className="mt-10 rounded-2xl border border-slate-200 bg-white p-4 shadow-sm sm:p-6">
<div className="text-center">
<h2 className="text-2xl font-black text-slate-950">Finde das richtige Foto für deinen Zweck</h2>
<p className="mx-auto mt-2 max-w-2xl text-slate-600">Jede Situation braucht andere Signale. Wähle deinen Kontext und starte direkt passend.</p>
</div>
<div className="mt-6 grid gap-4 lg:grid-cols-3">
{routeCards.map((card) => (
<article key={card.entry} className="overflow-hidden rounded-xl border border-slate-200 bg-white">
<img src={card.image} alt={`${card.title} Foto-Feedback`} loading="lazy" decoding="async" className="aspect-[4/3] w-full object-cover" />
<div className="p-4">
<h3 className={`text-xl font-black ${card.accent}`}>{card.title}</h3>
<p className="mt-2 min-h-12 text-sm leading-6 text-slate-600">{card.text}</p>
<div className="mt-4 flex flex-wrap gap-2">
<Link
to={buildUploadPath(`audience_${card.entry}`)}
className="btn btn-sm border-0 bg-slate-950 text-white hover:bg-slate-800"
onClick={() => trackEvent('home_audience_upload_click', {
source: 'home',
surface: 'audience_routes',
entryPoint: card.entry
})}
>
{card.cta}
</Link>
<Link
to={`/${card.slug}/`}
className="btn btn-sm border-slate-200 bg-white text-slate-700 hover:bg-slate-50"
onClick={() => trackEvent('home_audience_landing_click', {
source: 'home',
surface: 'audience_routes',
entryPoint: card.slug
})}
>
Ratgeber
</Link>
</div>
</div>
</article>
))}
</div>
</section>
);
const IntentSearch = ({ intentQuery, setIntentQuery, intentMatches }) => (
<section className="mt-10 rounded-2xl border border-slate-200 bg-white p-4 shadow-sm sm:p-6">
<div className="flex flex-wrap items-center justify-between gap-4">
<div>
<h2 className="text-xl font-black text-slate-950">Direkt zur passenden Suchintention</h2>
<p className="mt-1 text-sm text-slate-600">Profilbild, Bewerbung, Dating oder Social: Longtail-Seiten führen direkt in den Upload.</p>
</div>
<form
className="w-full md:w-auto"
onSubmit={(event) => {
event.preventDefault();
const entry = intentMatches[0];
if (!entry) return;
trackEvent('home_intent_search_submit', {
source: 'home',
surface: 'intent_search',
entryPoint: 'intent_search_form',
meta: {
query: intentQuery.trim().toLowerCase(),
matchedCount: String(intentMatches.length)
}
});
window.location.assign('/' + entry.slug + '/');
}}
>
<div className="join w-full md:w-96">
<input
className="join-item input input-bordered w-full"
value={intentQuery}
onChange={(event) => setIntentQuery(event.target.value)}
placeholder="z. B. Profilbild für LinkedIn"
aria-label="Suchintention"
/>
<button className="btn join-item bg-coral-500 text-white hover:bg-coral-600" type="submit">Los</button>
</div>
</form>
</div>
<div className="mt-4 flex flex-wrap gap-2">
{intentMatches.map((item) => (
<button
key={item.slug}
type="button"
className="btn btn-sm border-slate-200 bg-slate-50 text-slate-700 hover:bg-slate-100"
onClick={() => {
trackEvent('home_intent_chip_click', {
source: 'home',
surface: 'intent_search',
entryPoint: item.slug,
meta: { slug: item.slug }
});
window.location.assign('/' + item.slug + '/');
}}
>
{item.title}
</button>
))}
</div>
</section>
);
const SeoRouteStrip = ({ manifestCards, manifestLoading }) => (
<section className="mt-10 rounded-2xl border border-slate-200 bg-slate-950 p-5 text-white shadow-sm sm:p-6">
<div className="flex flex-wrap items-center justify-between gap-4">
<div>
<h2 className="text-xl font-black">Organische Einstiege, die direkt zum Test führen</h2>
<p className="mt-1 text-sm text-slate-300">SEO bleibt sichtbar, aber die Community-Erfahrung bleibt der erste Eindruck.</p>
</div>
<Link to="/profilbild-ratgeber/" className="btn btn-sm border-white/20 bg-white text-slate-950 hover:bg-slate-100">SEO-Hub öffnen</Link>
</div>
<div className="mt-5 grid gap-3 md:grid-cols-2 lg:grid-cols-3">
{manifestLoading ? Array.from({ length: 3 }).map((_, index) => (
<div key={index} className="skeleton h-24 rounded-xl bg-white/10" />
)) : manifestCards.slice(0, 6).map((item) => (
<div key={item.slug} className="rounded-xl border border-white/10 bg-white/10 p-4">
<h3 className="font-black">{item.title}</h3>
<p className="mt-1 text-xs text-slate-300">{item.tags?.length ? item.tags.join(' · ') : item.slug}</p>
<div className="mt-3 flex gap-2">
<Link
to={`/${item.slug}/`}
className="btn btn-xs border-white/20 bg-white text-slate-950 hover:bg-slate-100"
onClick={() => trackEvent('home_manifest_tile_click', {
source: 'home',
surface: 'seo_manifest',
entryPoint: item.slug,
meta: { position: 'strip', type: 'landing_page' }
})}
>
Landingpage
</Link>
<a
href={`/upload?entry=seo_manifest_${toEntryToken(item.slug)}`}
className="btn btn-xs border-white/20 bg-transparent text-white hover:bg-white/10"
onClick={() => trackEvent('home_manifest_upload_click', {
source: 'home',
surface: 'seo_manifest',
entryPoint: item.slug,
meta: { step: 'upload_cta' }
})}
>
Upload
</a>
</div>
</div>
))}
</div>
</section>
);
export const HomePage = () => {
const [items, setItems] = useState([]);
const [online, setOnline] = useState(0);
const [feedLoading, setFeedLoading] = useState(true);
const [intentQuery, setIntentQuery] = useState('');
const [seoManifest, setSeoManifest] = useState(null);
const [manifestLoading, setManifestLoading] = useState(true);
const intentMatches = useMemo(() => {
const query = intentQuery.trim().toLowerCase();
if (!query) return featuredSeoLinks.slice(0, 5);
return featuredSeoLinks.filter((item) => {
const haystack = (item.slug + ' ' + item.title).toLowerCase();
return haystack.includes(query);
}).slice(0, 6);
}, [intentQuery]);
const manifestCards = useMemo(() => toManifestEntries(seoManifest), [seoManifest]);
const profileItems = useMemo(() => {
const filtered = (items || []).filter(isProfileLikeItem);
return filtered.length > 0 ? filtered : items;
}, [items]);
useEffect(() => {
let cancelled = false;
setFeedLoading(true);
setManifestLoading(true);
fetch('/api/photos/feed')
.then(r => r.json())
.then(d => {
if (cancelled) return;
setItems(d.items || []);
})
.catch(() => {})
.finally(() => {
if (cancelled) return;
setFeedLoading(false);
});
const wsProto = location.protocol === 'https:' ? 'wss' : 'ws';
const ws = new WebSocket(`${wsProto}://${location.host}/ws`);
ws.addEventListener('open', () => {
ws.send(JSON.stringify({ type: 'subscribe', channel: 'leaderboard:all' }));
});
ws.addEventListener('message', (ev) => {
try {
const m = JSON.parse(ev.data);
if (m.type === 'presence') setOnline(m.online || 0);
} catch {}
});
fetch('/seo-manifest.json')
.then((res) => (res.ok ? res.json() : null))
.then((json) => {
if (cancelled) return;
if (json) {
setSeoManifest(json);
trackEvent('home_seo_manifest_view', {
source: 'home',
surface: 'seo_manifest',
entryPoint: 'home_intent_surface',
meta: {
count: String(Array.isArray(json.landingPages) ? json.landingPages.length : 0)
}
});
}
})
.catch(() => {
if (!cancelled) setSeoManifest(null);
})
.finally(() => {
if (!cancelled) setManifestLoading(false);
});
return () => {
cancelled = true;
try { ws.close(); } catch {}
};
}, []);
return (
<div className="mx-auto max-w-7xl px-4 py-5 sm:px-6 md:py-8">
<HomePageMeta />
<Hero items={profileItems} online={online} />
<AudienceRoutes />
<CommunityFeed items={profileItems} loading={feedLoading} />
<IntentSearch intentQuery={intentQuery} setIntentQuery={setIntentQuery} intentMatches={intentMatches} />
<SeoRouteStrip manifestCards={manifestCards} manifestLoading={manifestLoading} />
<div className="mt-10 grid gap-6 xl:grid-cols-[0.95fr_1.05fr]">
<SeoGrowthWidget />
<SeoLandingTeaser />
</div>
</div>
);
};