fix(vip): Swap-Dialog-Polish — Inline-Button, Sortierung, Badge-Farben

- VipSwapSheet: Ersetzen-Button inline an der gewählten Domain-Zeile
  statt Modal-Footer-CTA
- VipDomainList: zu ersetzende Domain nach oben sortiert, Cooldown-Badge
  deutlicher (Icon + Border)
- Status-Badge einheitlich grün, Swap-Domain orange (kein Blau mehr)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
chahinebrini 2026-05-22 21:09:00 +02:00
parent dc841b4275
commit 27ad05b13b
2 changed files with 122 additions and 134 deletions

View File

@ -475,7 +475,18 @@ export function VipDomainList({ domains, open, onToggle, colors }: VipListProps)
const list = vipList ?? [...customStatusMap.keys()];
const customDomains = list.filter((d) => customStatusMap.has(d));
const now = Date.now();
const customDomains = list
.filter((d) => customStatusMap.has(d))
.sort((a, b) => {
const evictA = customStatusMap.get(a)?.vipEvictAt;
const evictB = customStatusMap.get(b)?.vipEvictAt;
const aIsPending = evictA ? new Date(evictA).getTime() > now : false;
const bIsPending = evictB ? new Date(evictB).getTime() > now : false;
if (aIsPending && !bIsPending) return -1;
if (!aIsPending && bIsPending) return 1;
return 0;
});
const curatedDomains = list.filter((d) => !customStatusMap.has(d));
function getMeta(d: string): VipCustomMeta {
@ -637,18 +648,17 @@ function VipCustomTile({
return Math.ceil(ms / (1000 * 60 * 60));
})();
const isEvictPending = evictBadgeHours !== null;
const statusColor: string = (() => {
switch (status) {
case 'submitted': return colors.warning;
case 'approved': return '#22c55e';
default: return colors.brandOrange;
}
if (status === 'submitted') return colors.warning;
if (isEvictPending) return '#f97316';
return '#22c55e';
})();
const badgeLabel: string = (() => {
switch (status) {
case 'submitted': return t('blocker.domain_badge_pruefung');
case 'approved': return t('blocker.domain_badge_active');
default: return t('blocker.domain_badge_active');
}
})();
@ -722,16 +732,21 @@ function VipCustomTile({
{evictBadgeHours !== null && (
<View
style={{
backgroundColor: '#fef3c7',
backgroundColor: '#fff7ed',
borderRadius: 6,
paddingVertical: 2,
borderWidth: 1,
borderColor: '#fed7aa',
paddingVertical: 3,
paddingHorizontal: 4,
flexDirection: 'row',
alignItems: 'center',
gap: 2,
}}
>
<Ionicons name="time-outline" size={8} color="#c2410c" />
<Text
numberOfLines={1}
style={{ fontSize: 7, fontFamily: 'Nunito_600SemiBold', color: '#92400e' }}
style={{ fontSize: 8, fontFamily: 'Nunito_700Bold', color: '#c2410c' }}
>
{t('blocker.vip_evict_badge', { hours: evictBadgeHours })}
</Text>

View File

@ -39,11 +39,11 @@ export function VipSwapSheet({ visible, newDomainId, candidates, onClose, onSwap
onClose();
}
async function handleSwap() {
if (!selectedId || submitting) return;
async function handleSwap(evictedId: string) {
if (submitting) return;
setSubmitting(true);
setError(null);
const result = await onSwap(newDomainId, selectedId);
const result = await onSwap(newDomainId, evictedId);
setSubmitting(false);
if (result.ok) {
close();
@ -52,8 +52,6 @@ export function VipSwapSheet({ visible, newDomainId, candidates, onClose, onSwap
setError(t('blocker.vip_swap_error'));
}
const ctaEnabled = selectedId !== null && !submitting;
return (
<FormSheet
visible={visible}
@ -65,7 +63,6 @@ export function VipSwapSheet({ visible, newDomainId, candidates, onClose, onSwap
showsVerticalScrollIndicator={false}
contentContainerStyle={{ padding: 16, gap: 12 }}
>
{/* Erklärtext */}
<View
style={{
flexDirection: 'row',
@ -91,7 +88,6 @@ export function VipSwapSheet({ visible, newDomainId, candidates, onClose, onSwap
</Text>
</View>
{/* Pick-Label */}
<Text
style={{
fontSize: 13,
@ -103,7 +99,6 @@ export function VipSwapSheet({ visible, newDomainId, candidates, onClose, onSwap
{t('blocker.vip_swap_pick')}
</Text>
{/* Domain-Liste */}
{eligible.length === 0 ? (
<Text
style={{
@ -123,7 +118,9 @@ export function VipSwapSheet({ visible, newDomainId, candidates, onClose, onSwap
key={d.id}
domain={d}
selected={selectedId === d.id}
onSelect={() => setSelectedId(d.id)}
submitting={submitting && selectedId === d.id}
onSelect={() => setSelectedId(selectedId === d.id ? null : d.id)}
onConfirm={() => handleSwap(d.id)}
colors={colors}
/>
))}
@ -142,62 +139,6 @@ export function VipSwapSheet({ visible, newDomainId, candidates, onClose, onSwap
{error}
</Text>
)}
{/* Buttons */}
<View style={{ flexDirection: 'row', gap: 10, marginTop: 4 }}>
<TouchableOpacity onPress={close} activeOpacity={0.8} style={{ flex: 1 }}>
<View
style={{
borderRadius: 14,
paddingVertical: 14,
alignItems: 'center',
backgroundColor: colors.surfaceElevated,
borderWidth: 1,
borderColor: colors.border,
}}
>
<Text
style={{
fontSize: 15,
fontFamily: 'Nunito_600SemiBold',
color: colors.textMuted,
}}
>
{t('common.cancel')}
</Text>
</View>
</TouchableOpacity>
<TouchableOpacity
onPress={handleSwap}
disabled={!ctaEnabled}
activeOpacity={0.85}
style={{ flex: 2 }}
>
<View
style={{
backgroundColor: ctaEnabled ? '#c2410c' : '#d4d4d4',
borderRadius: 14,
paddingVertical: 14,
alignItems: 'center',
}}
>
{submitting ? (
<ActivityIndicator color="#fff" />
) : (
<Text
style={{
fontSize: 15,
fontFamily: 'Nunito_700Bold',
color: '#fff',
}}
>
{t('blocker.vip_swap_cta')}
</Text>
)}
</View>
</TouchableOpacity>
</View>
</ScrollView>
</FormSheet>
);
@ -206,87 +147,119 @@ export function VipSwapSheet({ visible, newDomainId, candidates, onClose, onSwap
function SwapCandidateTile({
domain,
selected,
submitting,
onSelect,
onConfirm,
colors,
}: {
domain: CustomDomain;
selected: boolean;
submitting: boolean;
onSelect: () => void;
onConfirm: () => void;
colors: ReturnType<typeof useColors>;
}) {
const { t } = useTranslation();
const [imgError, setImgError] = useState(false);
const stripped = domain.domain.replace(/^www\./, '');
return (
<TouchableOpacity
onPress={onSelect}
activeOpacity={0.75}
<View
style={{
flexDirection: 'row',
alignItems: 'center',
gap: 12,
padding: 12,
borderRadius: 12,
borderWidth: 1.5,
borderColor: selected ? '#c2410c' : colors.border,
backgroundColor: selected ? '#fff7ed' : colors.surface,
overflow: 'hidden',
}}
>
{!imgError ? (
<Image
source={{ uri: `https://www.google.com/s2/favicons?domain=${stripped}&sz=64` }}
style={{ width: 28, height: 28, borderRadius: 6 }}
onError={() => setImgError(true)}
/>
) : (
<View
style={{
width: 28,
height: 28,
borderRadius: 6,
backgroundColor: colors.surfaceElevated,
alignItems: 'center',
justifyContent: 'center',
}}
>
<Text
<TouchableOpacity
onPress={onSelect}
activeOpacity={0.75}
style={{
flexDirection: 'row',
alignItems: 'center',
gap: 12,
padding: 12,
}}
>
{!imgError ? (
<Image
source={{ uri: `https://www.google.com/s2/favicons?domain=${stripped}&sz=64` }}
style={{ width: 28, height: 28, borderRadius: 6 }}
onError={() => setImgError(true)}
/>
) : (
<View
style={{
fontSize: 9,
fontFamily: 'Nunito_700Bold',
color: colors.textMuted,
width: 28,
height: 28,
borderRadius: 6,
backgroundColor: colors.surfaceElevated,
alignItems: 'center',
justifyContent: 'center',
}}
>
{stripped.slice(0, 2).toUpperCase()}
</Text>
</View>
<Text
style={{
fontSize: 9,
fontFamily: 'Nunito_700Bold',
color: colors.textMuted,
}}
>
{stripped.slice(0, 2).toUpperCase()}
</Text>
</View>
)}
<Text
style={{
flex: 1,
fontSize: 14,
fontFamily: 'Nunito_600SemiBold',
color: colors.text,
}}
numberOfLines={1}
>
{stripped}
</Text>
<Ionicons
name={selected ? 'chevron-up' : 'chevron-down'}
size={16}
color={selected ? '#c2410c' : colors.textMuted}
/>
</TouchableOpacity>
{selected && (
<TouchableOpacity
onPress={onConfirm}
disabled={submitting}
activeOpacity={0.85}
style={{
marginHorizontal: 12,
marginBottom: 12,
borderRadius: 10,
backgroundColor: '#c2410c',
paddingVertical: 12,
alignItems: 'center',
}}
>
{submitting ? (
<ActivityIndicator color="#fff" />
) : (
<Text
style={{
fontSize: 14,
fontFamily: 'Nunito_700Bold',
color: '#fff',
}}
>
{t('blocker.vip_swap_cta')}
</Text>
)}
</TouchableOpacity>
)}
<Text
style={{
flex: 1,
fontSize: 14,
fontFamily: 'Nunito_600SemiBold',
color: colors.text,
}}
numberOfLines={1}
>
{stripped}
</Text>
<View
style={{
width: 22,
height: 22,
borderRadius: 11,
borderWidth: 1.5,
borderColor: selected ? '#c2410c' : colors.border,
backgroundColor: selected ? '#c2410c' : 'transparent',
alignItems: 'center',
justifyContent: 'center',
}}
>
{selected && <Ionicons name="checkmark" size={13} color="#fff" />}
</View>
</TouchableOpacity>
</View>
);
}