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: //proc/thread-self/root/tmp/bewertemich-ComparePage-loop.jsx
import React, { useEffect, useState } from 'react';
import { useLocation, useParams } from 'react-router-dom';
import { apiFetch } from '../app/api.js';
import { buildComparisonShareUrl, getInviteAttribution, getSessionId, getVisitorId, trackEvent } from '../../utils/tracking.js';
import { buildComparisonInvitePlaybook } from './sharePlaybooks.js';

const copyText = async (text) => {
  try {
    await navigator.clipboard.writeText(text);
    return true;
  } catch {
    return false;
  }
};

export const ComparePage = () => {
  const { id } = useParams();
  const location = useLocation();
  const [comparison, setComparison] = useState(null);
  const [stats, setStats] = useState({ leftVotes: 0, rightVotes: 0, totalVotes: 0 });
  const [me, setMe] = useState(null);
  const [msg, setMsg] = useState('');
  const [busy, setBusy] = useState(false);
  const numericId = Number(id);
  const inviteAttribution = getInviteAttribution(location.search);

  useEffect(() => {
    let cancelled = false;
    (async () => {
      try {
        const [compareRes, meRes] = await Promise.all([
          apiFetch(`/api/comparisons/${numericId}`),
          apiFetch('/api/auth/me')
        ]);
        const compareJson = await compareRes.json().catch(() => ({}));
        const meJson = await meRes.json().catch(() => ({}));
        if (cancelled) return;
        if (!compareRes.ok || !compareJson?.comparison) {
          setMsg(compareJson.error || 'Vergleich nicht gefunden.');
          return;
        }
        setComparison(compareJson.comparison);
        setStats(compareJson.stats || { leftVotes: 0, rightVotes: 0, totalVotes: 0 });
        setMe(meJson.user || null);
      } catch {
        if (!cancelled) setMsg('Vergleich nicht gefunden.');
      }
    })();
    return () => { cancelled = true; };
  }, [numericId]);

  useEffect(() => {
    if (!inviteAttribution.isInvite) return;
    void trackEvent('comparison_invite_landing_view', {
      source: inviteAttribution.source,
      surface: inviteAttribution.surface || 'compare_page',
      meta: { comparison_id: numericId }
    });
  }, [inviteAttribution.isInvite, inviteAttribution.source, inviteAttribution.surface, numericId]);

  const vote = async (chosenPhotoId) => {
    if (!comparison) return;
    setBusy(true);
    setMsg('');
    try {
      const res = await apiFetch(`/api/comparisons/${comparison.id}/vote`, {
        method: 'POST',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify({
          chosenPhotoId,
          visitorId: getVisitorId(),
          sessionId: getSessionId(),
          source: inviteAttribution.isInvite ? inviteAttribution.source : 'comparison',
          surface: inviteAttribution.isInvite ? inviteAttribution.surface || 'compare_page' : 'compare_page'
        })
      });
      const json = await res.json().catch(() => ({}));
      if (res.ok || (res.status === 409 && json.code === 'ALREADY_VOTED')) {
        setStats(json.stats || stats);
        setMsg(res.ok ? 'Danke für deine Auswahl.' : 'Du hast bereits abgestimmt.');
        return;
      }
      throw new Error(json.error || 'Abstimmung fehlgeschlagen.');
    } catch (err) {
      setMsg(String(err.message || err));
    } finally {
      setBusy(false);
    }
  };

  if (!comparison) {
    return <div className="mx-auto max-w-4xl px-4 py-10">{msg || 'Lade…'}</div>;
  }

  const isOwner = !!(me && comparison && String(me.id) === String(comparison.user_id));
  const leftPercent = stats.totalVotes > 0 ? Math.round((stats.leftVotes / stats.totalVotes) * 100) : 50;
  const rightPercent = stats.totalVotes > 0 ? 100 - leftPercent : 50;
  const sharePreviewUrl = `/s/compare/${comparison.id}/og.jpg`;
  const compareShareUrl = buildComparisonShareUrl(comparison.id, { source: 'copy', surface: 'compare_page' });
  const sharePlaybook = buildComparisonInvitePlaybook({
    comparisonId: comparison.id,
    title: comparison.title,
    leftPercent,
    rightPercent,
    totalVotes: stats.totalVotes,
    surface: 'compare_page'
  });

  const onCopy = async () => {
    const ok = await copyText(compareShareUrl);
    if (ok) {
      void trackEvent('share_click', {
        source: 'copy',
        surface: 'compare_page',
        meta: { comparison_id: comparison.id, lane: 'primary' }
      });
    }
    setMsg(ok ? 'Vergleichslink kopiert.' : 'Kopieren fehlgeschlagen.');
  };

  return (
    <div className="mx-auto max-w-6xl px-4 py-10 space-y-6">
      <section className="rounded-3xl border bg-gradient-to-br from-primary to-secondary p-6 text-white shadow-lg">
        <div className="badge border-white/20 bg-white/10 text-white">A/B-Vergleich</div>
        <h1 className="mt-3 text-3xl font-black tracking-tight">{comparison.title || 'Welches Profilbild wirkt besser?'}</h1>
        <p className="mt-2 max-w-3xl text-white/85">
          {isOwner
            ? 'Teile diesen Vergleich zuerst an direkte Kontakte. Das direkte Duell gibt dir schneller eine Entscheidung als ein einzelner Score.'
            : `${comparison.display_name} testet zwei Bilder direkt gegeneinander. Wähle das stärkere Foto.`}
        </p>
      </section>

      <section className="grid gap-6 lg:grid-cols-2">
        {[
          { key: 'A', photoId: comparison.left_photo_id, votes: stats.leftVotes, percent: leftPercent },
          { key: 'B', photoId: comparison.right_photo_id, votes: stats.rightVotes, percent: rightPercent }
        ].map((item) => (
          <div key={item.key} className="rounded-3xl border bg-white p-4 shadow-sm">
            <img src={`/api/photos/file/${item.photoId}`} alt={`Bild ${item.key}`} className="aspect-[4/5] w-full rounded-2xl object-cover" />
            <div className="mt-4 flex items-center justify-between gap-3">
              <div>
                <div className="text-xs uppercase tracking-[0.2em] text-gray-500">Bild {item.key}</div>
                <div className="mt-1 text-2xl font-black">{item.percent}%</div>
                <div className="text-sm text-gray-500">{item.votes} Stimmen</div>
              </div>
              {!isOwner ? (
                <button type="button" className={`btn btn-primary ${busy ? 'loading' : ''}`} disabled={busy} onClick={() => vote(item.photoId)}>Bild {item.key} wählen</button>
              ) : null}
            </div>
          </div>
        ))}
      </section>

      <section className="grid gap-6 lg:grid-cols-[1.1fr_0.9fr]">
        <div className="rounded-3xl border bg-white p-5 shadow-sm">
          <div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
            <div>
              <h2 className="text-xl font-bold">Zwischenstand</h2>
              <div className="mt-2 text-sm text-gray-600">{stats.totalVotes} Gesamtstimmen</div>
            </div>
            <div className="flex flex-wrap gap-3">
              <button type="button" className="btn btn-primary" onClick={onCopy}>Vergleichslink kopieren</button>
              {isOwner ? <a className="btn btn-ghost border" href={`/compare/new?photo=${comparison.left_photo_id}`}>Neuen Vergleich starten</a> : null}
            </div>
          </div>
          {msg && <div className="mt-3 text-sm text-gray-700">{msg}</div>}

          {isOwner ? (
            <div className="mt-5 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={async () => {
                          const ok = await copyText(action.copyText);
                          if (ok) {
                            void trackEvent('share_click', {
                              source: action.source,
                              surface: 'compare_page',
                              meta: { comparison_id: comparison.id, lane: 'copy' }
                            });
                          }
                          setMsg(ok ? 'Reminder kopiert.' : 'Kopieren fehlgeschlagen.');
                        }}
                      >
                        Kopieren
                      </button>
                    ) : (
                      <a
                        className="btn btn-sm btn-ghost border"
                        href={action.href}
                        target="_blank"
                        rel="noreferrer"
                        onClick={() => {
                          void trackEvent('share_click', {
                            source: action.source,
                            surface: 'compare_page',
                            meta: { comparison_id: comparison.id, lane: 'open' }
                          }, { transport: 'beacon' });
                        }}
                      >
                        Öffnen
                      </a>
                    )}
                  </div>
                  <div className="mt-2 text-sm text-gray-600">{action.description}</div>
                </div>
              ))}
            </div>
          ) : null}
        </div>

        <div className="rounded-3xl border bg-white p-5 shadow-sm">
          <div className="text-xs uppercase tracking-[0.2em] text-gray-500">OG-Karte</div>
          <h2 className="mt-2 text-xl font-bold">Share-Preview für den Vergleich</h2>
          <p className="mt-2 text-sm text-gray-600">
            Der Vergleich hat jetzt eine eigene Preview-Karte. Das macht A/B-Links deutlich klarer und klickstaerker in Messengern.
          </p>
          <img src={sharePreviewUrl} alt="Vergleichs-Preview" className="mt-4 w-full rounded-2xl border shadow-sm" />
        </div>
      </section>
    </div>
  );
};