import { useState, useMemo } from 'react'; import { View, Text, Pressable, Image, ActivityIndicator, } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; import { useTranslation } from 'react-i18next'; import { SuccessAlert } from '../SuccessAlert'; import { ConfirmAlert } from '../ConfirmAlert'; import type { CustomDomain, Tier } from '../../hooks/useCustomDomains'; // ─── Helpers ───────────────────────────────────────────────────────────── function timeAgo(input?: string | Date): string { if (!input) return ''; const date = typeof input === 'string' ? new Date(input) : input; const diffMs = Date.now() - date.getTime(); const minutes = Math.floor(diffMs / 60_000); if (minutes < 1) return 'jetzt'; if (minutes < 60) return `${minutes}m`; const hours = Math.floor(minutes / 60); if (hours < 24) return `${hours}h`; const days = Math.floor(hours / 24); if (days < 7) return `${days}d`; const weeks = Math.floor(days / 7); if (weeks < 5) return `${weeks}w`; const months = Math.floor(days / 30); return `${months}mo`; } function timeSinceSubmit(input?: string | Date): string { if (!input) return ''; const date = typeof input === 'string' ? new Date(input) : input; const diffMs = Date.now() - date.getTime(); const hours = Math.floor(diffMs / 3_600_000); if (hours < 1) { const minutes = Math.max(1, Math.floor(diffMs / 60_000)); return `${minutes} min`; } if (hours < 24) return `${hours} Std`; const days = Math.floor(hours / 24); return `${days} Tag${days === 1 ? '' : 'e'}`; } type Props = { domains: CustomDomain[]; tier: Tier; onAdd?: () => void; onSubmit?: (id: string) => Promise<{ ok: boolean }>; onRemove?: (id: string) => Promise<{ ok: boolean }>; onUpgradePro?: () => void; }; // Sort-Reihenfolge: User sieht zuerst was Aufmerksamkeit braucht. // submitted (in Prüfung) > rejected (kann erneut) > active (settled OK) const STATUS_PRIORITY: Record = { submitted: 0, rejected: 1, active: 2, approved: 99, }; export function DomainGrid({ domains, tier, onAdd, onSubmit, onUpgradePro }: Props) { const { t } = useTranslation(); // Slot-relevante Domains (alles außer approved). Sortiert nach Status-Priority, // innerhalb gleicher Priority dann newest-first by addedAt. const visible = useMemo(() => { return domains .filter((d) => d.status !== 'approved') .slice() .sort((a, b) => { const pa = STATUS_PRIORITY[a.status] ?? 99; const pb = STATUS_PRIORITY[b.status] ?? 99; if (pa !== pb) return pa - pb; const ta = a.addedAt ? new Date(a.addedAt).getTime() : 0; const tb = b.addedAt ? new Date(b.addedAt).getTime() : 0; return tb - ta; }); }, [domains]); return ( {/* Header: Section-Title + Slot-Counter + Add-Button (inline, neben SlotPill) */} {t('blocker.domain_section_title')} {onAdd && ( )} {/* Progress-Bar — 3-stufige Color-Schwelle: <60% grün, 60-90% orange, >=90% rot */} {(() => { const pct = (tier.usedSlots / tier.domainLimit) * 100; const barColor = pct >= 90 ? '#dc2626' : pct >= 60 ? '#f59e0b' : '#16a34a'; return ( ); })()} {/* Limit-Reached Upsell (nur Free) */} {tier.atLimit && tier.plan === 'free' && ( ({ backgroundColor: '#eff6ff', borderWidth: 1, borderColor: '#bfdbfe', borderRadius: 12, padding: 12, flexDirection: 'row', alignItems: 'center', gap: 10, opacity: pressed ? 0.85 : 1, })} > {t('blocker.domain_limit_title')} {t('blocker.domain_limit_desc')} )} {/* Empty State */} {visible.length === 0 ? ( {t('blocker.domain_empty')} ) : ( )} ); } // ─── SlotPill ───────────────────────────────────────────────────────────── function SlotPill({ tier }: { tier: Tier }) { const bg = tier.atLimit ? '#fee2e2' : '#f5f5f5'; const fg = tier.atLimit ? '#dc2626' : '#525252'; return ( {tier.usedSlots}/{tier.domainLimit} ); } // ─── Tiles ──────────────────────────────────────────────────────────────── function DomainTilesGrid({ domains, tier, onSubmit, }: { domains: CustomDomain[]; tier: Tier; onSubmit?: (id: string) => Promise<{ ok: boolean }>; }) { // 3-Spalten-Grid via flex-wrap. Parent ScrollView (in blocker.tsx) handles scroll — // KEIN nested ScrollView hier, sonst collabiert der Layout-Pass weil ScrollView // inner-content-view keine definierte Width für %-basierte Tile-Widths hat. return ( {domains.map((d) => ( ))} ); } function DomainTile({ domain, tier, onSubmit, }: { domain: CustomDomain; tier: Tier; onSubmit?: (id: string) => Promise<{ ok: boolean }>; }) { const { t } = useTranslation(); const [submitting, setSubmitting] = useState(false); const [imgError, setImgError] = useState(false); const [successVisible, setSuccessVisible] = useState(false); const [successContent, setSuccessContent] = useState<{ title: string; message: string }>({ title: '', message: '', }); const [confirmVisible, setConfirmVisible] = useState(false); const stripped = domain.domain.replace(/^www\./, ''); const isLegend = tier.plan === 'legend'; // statusColor wird auf Badge + Button angewendet. // iOS-native: blue (active), orange (submitted), red (rejected). const statusColor = (() => { switch (domain.status) { case 'submitted': return '#f59e0b'; // orange (Voting/Prüfung) case 'rejected': return '#FF3B30'; // iOS-red default: return '#007AFF'; // iOS-blue (active, "freigeben"-CTA) } })(); // Time-Color: nur Status die Aufmerksamkeit brauchen (submitted/rejected) sind farbig. // Active = neutral gray (settled state, kein Alarm-Indikator nötig). const timeColor = domain.status === 'active' ? '#a3a3a3' : statusColor; const badgeLabel = (() => { switch (domain.status) { case 'submitted': return isLegend ? t('blocker.domain_badge_pruefung') : t('blocker.domain_badge_voting'); case 'rejected': return t('blocker.domain_badge_rejected'); default: return t('blocker.domain_badge_active'); } })(); // Tier-aware Confirm-Dialog vor Freigabe — Pro geht zu Community-Voting, // Legend direkt zum ReBreak-Team. Animiertes Modal statt nativem Alert. const isResubmit = domain.status === 'rejected'; const confirmTitle = isLegend ? isResubmit ? t('blocker.domain_confirm_legend_resubmit') : t('blocker.domain_confirm_legend_first') : isResubmit ? t('blocker.domain_confirm_community_resubmit') : t('blocker.domain_confirm_community_first'); const confirmMessage = isLegend ? t('blocker.domain_confirm_legend_message', { domain: stripped }) : t('blocker.domain_confirm_community_message', { domain: stripped }); function openConfirm() { if (!onSubmit) return; setConfirmVisible(true); } async function handleConfirm() { setConfirmVisible(false); if (!onSubmit) return; setSubmitting(true); try { const result = await onSubmit(domain.id); if (result.ok) { setSuccessContent({ title: isLegend ? t('blocker.domain_success_legend_title') : t('blocker.domain_success_community_title'), message: isLegend ? t('blocker.domain_success_legend_message') : t('blocker.domain_success_community_message'), }); setSuccessVisible(true); } } finally { setSubmitting(false); } } const isFreeAndUsed = tier.plan === 'free' && domain.status !== 'active'; const showSubmit = tier.canSubmit && domain.status === 'active'; const showResubmit = tier.canSubmit && domain.status === 'rejected'; const showInPruefungBtn = domain.status === 'submitted'; return ( {/* Top-Row: Zeit links · Badge rechts — beide in Status-Color (matcht Bottom-Button). */} {timeAgo(domain.addedAt)} {badgeLabel} {/* Mitte: Favicon + Domain-Name (zentriert, flex-1) */} {!imgError ? ( setImgError(true)} /> ) : ( {stripped.slice(0, 2).toUpperCase()} )} {stripped} {/* Bottom-Slot: ALWAYS rendered Container (32px), Inhalt je nach Status. * Garantiert konsistente Tile-Höhe + sichtbaren Button. */} {showInPruefungBtn && ( {isLegend ? t('blocker.domain_btn_rebreak_prueft') : t('blocker.domain_btn_in_abstimmung')} )} {showSubmit && ( {submitting ? ( ) : ( {t('blocker.domain_btn_freigeben')} )} )} {showResubmit && ( {submitting ? ( ) : ( {t('blocker.domain_btn_erneut')} )} )} {/* Confirm-Modal vor Submit (statt nativer Alert.alert — selber animation-Style wie SuccessAlert) */} setConfirmVisible(false)} /> {/* Success-Alert mit animiertem Check-Icon nach erfolgreichem Submit */} setSuccessVisible(false)} /> ); }