HEX
Server: LiteSpeed
System: Linux houston.panomity.com 6.8.0-100-generic #100-Ubuntu SMP PREEMPT_DYNAMIC Tue Jan 13 16:40:06 UTC 2026 x86_64
User: nudepix (1011)
PHP: 7.4.33
Disabled: pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,pcntl_unshare,
Upload Files
File: //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>
  );
};