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:
parent
dc841b4275
commit
27ad05b13b
@ -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>
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user