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 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));
|
const curatedDomains = list.filter((d) => !customStatusMap.has(d));
|
||||||
|
|
||||||
function getMeta(d: string): VipCustomMeta {
|
function getMeta(d: string): VipCustomMeta {
|
||||||
@ -637,18 +648,17 @@ function VipCustomTile({
|
|||||||
return Math.ceil(ms / (1000 * 60 * 60));
|
return Math.ceil(ms / (1000 * 60 * 60));
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
const isEvictPending = evictBadgeHours !== null;
|
||||||
|
|
||||||
const statusColor: string = (() => {
|
const statusColor: string = (() => {
|
||||||
switch (status) {
|
if (status === 'submitted') return colors.warning;
|
||||||
case 'submitted': return colors.warning;
|
if (isEvictPending) return '#f97316';
|
||||||
case 'approved': return '#22c55e';
|
return '#22c55e';
|
||||||
default: return colors.brandOrange;
|
|
||||||
}
|
|
||||||
})();
|
})();
|
||||||
|
|
||||||
const badgeLabel: string = (() => {
|
const badgeLabel: string = (() => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'submitted': return t('blocker.domain_badge_pruefung');
|
case 'submitted': return t('blocker.domain_badge_pruefung');
|
||||||
case 'approved': return t('blocker.domain_badge_active');
|
|
||||||
default: return t('blocker.domain_badge_active');
|
default: return t('blocker.domain_badge_active');
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
@ -722,16 +732,21 @@ function VipCustomTile({
|
|||||||
{evictBadgeHours !== null && (
|
{evictBadgeHours !== null && (
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: '#fef3c7',
|
backgroundColor: '#fff7ed',
|
||||||
borderRadius: 6,
|
borderRadius: 6,
|
||||||
paddingVertical: 2,
|
borderWidth: 1,
|
||||||
|
borderColor: '#fed7aa',
|
||||||
|
paddingVertical: 3,
|
||||||
paddingHorizontal: 4,
|
paddingHorizontal: 4,
|
||||||
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
gap: 2,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<Ionicons name="time-outline" size={8} color="#c2410c" />
|
||||||
<Text
|
<Text
|
||||||
numberOfLines={1}
|
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 })}
|
{t('blocker.vip_evict_badge', { hours: evictBadgeHours })}
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@ -39,11 +39,11 @@ export function VipSwapSheet({ visible, newDomainId, candidates, onClose, onSwap
|
|||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleSwap() {
|
async function handleSwap(evictedId: string) {
|
||||||
if (!selectedId || submitting) return;
|
if (submitting) return;
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
const result = await onSwap(newDomainId, selectedId);
|
const result = await onSwap(newDomainId, evictedId);
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
if (result.ok) {
|
if (result.ok) {
|
||||||
close();
|
close();
|
||||||
@ -52,8 +52,6 @@ export function VipSwapSheet({ visible, newDomainId, candidates, onClose, onSwap
|
|||||||
setError(t('blocker.vip_swap_error'));
|
setError(t('blocker.vip_swap_error'));
|
||||||
}
|
}
|
||||||
|
|
||||||
const ctaEnabled = selectedId !== null && !submitting;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormSheet
|
<FormSheet
|
||||||
visible={visible}
|
visible={visible}
|
||||||
@ -65,7 +63,6 @@ export function VipSwapSheet({ visible, newDomainId, candidates, onClose, onSwap
|
|||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
contentContainerStyle={{ padding: 16, gap: 12 }}
|
contentContainerStyle={{ padding: 16, gap: 12 }}
|
||||||
>
|
>
|
||||||
{/* Erklärtext */}
|
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
@ -91,7 +88,6 @@ export function VipSwapSheet({ visible, newDomainId, candidates, onClose, onSwap
|
|||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Pick-Label */}
|
|
||||||
<Text
|
<Text
|
||||||
style={{
|
style={{
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
@ -103,7 +99,6 @@ export function VipSwapSheet({ visible, newDomainId, candidates, onClose, onSwap
|
|||||||
{t('blocker.vip_swap_pick')}
|
{t('blocker.vip_swap_pick')}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
{/* Domain-Liste */}
|
|
||||||
{eligible.length === 0 ? (
|
{eligible.length === 0 ? (
|
||||||
<Text
|
<Text
|
||||||
style={{
|
style={{
|
||||||
@ -123,7 +118,9 @@ export function VipSwapSheet({ visible, newDomainId, candidates, onClose, onSwap
|
|||||||
key={d.id}
|
key={d.id}
|
||||||
domain={d}
|
domain={d}
|
||||||
selected={selectedId === d.id}
|
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}
|
colors={colors}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@ -142,62 +139,6 @@ export function VipSwapSheet({ visible, newDomainId, candidates, onClose, onSwap
|
|||||||
{error}
|
{error}
|
||||||
</Text>
|
</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>
|
</ScrollView>
|
||||||
</FormSheet>
|
</FormSheet>
|
||||||
);
|
);
|
||||||
@ -206,87 +147,119 @@ export function VipSwapSheet({ visible, newDomainId, candidates, onClose, onSwap
|
|||||||
function SwapCandidateTile({
|
function SwapCandidateTile({
|
||||||
domain,
|
domain,
|
||||||
selected,
|
selected,
|
||||||
|
submitting,
|
||||||
onSelect,
|
onSelect,
|
||||||
|
onConfirm,
|
||||||
colors,
|
colors,
|
||||||
}: {
|
}: {
|
||||||
domain: CustomDomain;
|
domain: CustomDomain;
|
||||||
selected: boolean;
|
selected: boolean;
|
||||||
|
submitting: boolean;
|
||||||
onSelect: () => void;
|
onSelect: () => void;
|
||||||
|
onConfirm: () => void;
|
||||||
colors: ReturnType<typeof useColors>;
|
colors: ReturnType<typeof useColors>;
|
||||||
}) {
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [imgError, setImgError] = useState(false);
|
const [imgError, setImgError] = useState(false);
|
||||||
const stripped = domain.domain.replace(/^www\./, '');
|
const stripped = domain.domain.replace(/^www\./, '');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<View
|
||||||
onPress={onSelect}
|
|
||||||
activeOpacity={0.75}
|
|
||||||
style={{
|
style={{
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 12,
|
|
||||||
padding: 12,
|
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
borderWidth: 1.5,
|
borderWidth: 1.5,
|
||||||
borderColor: selected ? '#c2410c' : colors.border,
|
borderColor: selected ? '#c2410c' : colors.border,
|
||||||
backgroundColor: selected ? '#fff7ed' : colors.surface,
|
backgroundColor: selected ? '#fff7ed' : colors.surface,
|
||||||
|
overflow: 'hidden',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{!imgError ? (
|
<TouchableOpacity
|
||||||
<Image
|
onPress={onSelect}
|
||||||
source={{ uri: `https://www.google.com/s2/favicons?domain=${stripped}&sz=64` }}
|
activeOpacity={0.75}
|
||||||
style={{ width: 28, height: 28, borderRadius: 6 }}
|
style={{
|
||||||
onError={() => setImgError(true)}
|
flexDirection: 'row',
|
||||||
/>
|
alignItems: 'center',
|
||||||
) : (
|
gap: 12,
|
||||||
<View
|
padding: 12,
|
||||||
style={{
|
}}
|
||||||
width: 28,
|
>
|
||||||
height: 28,
|
{!imgError ? (
|
||||||
borderRadius: 6,
|
<Image
|
||||||
backgroundColor: colors.surfaceElevated,
|
source={{ uri: `https://www.google.com/s2/favicons?domain=${stripped}&sz=64` }}
|
||||||
alignItems: 'center',
|
style={{ width: 28, height: 28, borderRadius: 6 }}
|
||||||
justifyContent: 'center',
|
onError={() => setImgError(true)}
|
||||||
}}
|
/>
|
||||||
>
|
) : (
|
||||||
<Text
|
<View
|
||||||
style={{
|
style={{
|
||||||
fontSize: 9,
|
width: 28,
|
||||||
fontFamily: 'Nunito_700Bold',
|
height: 28,
|
||||||
color: colors.textMuted,
|
borderRadius: 6,
|
||||||
|
backgroundColor: colors.surfaceElevated,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{stripped.slice(0, 2).toUpperCase()}
|
<Text
|
||||||
</Text>
|
style={{
|
||||||
</View>
|
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>
|
||||||
)}
|
)}
|
||||||
|
</View>
|
||||||
<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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user