feat(challenges): 添加自定义挑战功能和多语言支持
- 新增自定义挑战创建页面,支持设置挑战类型、时间范围、目标值等 - 实现挑战邀请码系统,支持通过邀请码加入自定义挑战 - 完善挑战详情页面的多语言翻译支持 - 优化用户认证状态检查逻辑,使用token作为主要判断依据 - 添加阿里字体文件支持,提升UI显示效果 - 改进确认弹窗组件,支持Liquid Glass效果和自定义内容 - 优化应用启动流程,直接读取onboarding状态而非预加载用户数据
This commit is contained in:
@@ -5,6 +5,7 @@ import { Colors } from '@/constants/Colors';
|
||||
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
||||
import { useAuthGuard } from '@/hooks/useAuthGuard';
|
||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||
import { ChallengeSource } from '@/services/challengesApi';
|
||||
import {
|
||||
fetchChallengeDetail,
|
||||
fetchChallengeRankings,
|
||||
@@ -23,13 +24,17 @@ import {
|
||||
} from '@/store/challengesSlice';
|
||||
import { Toast } from '@/utils/toast.utils';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import dayjs from 'dayjs';
|
||||
import { BlurView } from 'expo-blur';
|
||||
import * as Clipboard from 'expo-clipboard';
|
||||
import { GlassView, isLiquidGlassAvailable } from 'expo-glass-effect';
|
||||
import * as Haptics from 'expo-haptics';
|
||||
import { Image } from 'expo-image';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import { useLocalSearchParams, useRouter } from 'expo-router';
|
||||
import LottieView from 'lottie-react-native';
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Alert,
|
||||
@@ -87,6 +92,7 @@ export default function ChallengeDetailScreen() {
|
||||
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
|
||||
const colorTokens = Colors[theme];
|
||||
const insets = useSafeAreaInsets();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { ensureLoggedIn } = useAuthGuard();
|
||||
|
||||
@@ -155,6 +161,24 @@ export default function ChallengeDetailScreen() {
|
||||
}, [showCelebration]);
|
||||
|
||||
const progress = challenge?.progress;
|
||||
const isJoined = challenge?.isJoined ?? false;
|
||||
const isCustomChallenge = challenge?.source === ChallengeSource.CUSTOM;
|
||||
const lastProgressAt = useMemo(() => {
|
||||
const progressRecord = challenge?.progress as { lastProgressAt?: string; last_progress_at?: string } | undefined;
|
||||
return progressRecord?.lastProgressAt ?? progressRecord?.last_progress_at;
|
||||
}, [challenge?.progress]);
|
||||
const hasCheckedInToday = useMemo(() => {
|
||||
if (!challenge?.progress) {
|
||||
return false;
|
||||
}
|
||||
if (lastProgressAt) {
|
||||
const lastDate = dayjs(lastProgressAt);
|
||||
if (lastDate.isValid() && lastDate.isSame(dayjs(), 'day')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return challenge.progress.checkedInToday ?? false;
|
||||
}, [challenge?.progress, lastProgressAt]);
|
||||
|
||||
const rankingData = useMemo(() => {
|
||||
const source = rankingList?.items ?? challenge?.rankings ?? [];
|
||||
@@ -165,6 +189,7 @@ export default function ChallengeDetailScreen() {
|
||||
() => rankingData.filter((item) => item.avatar).map((item) => item.avatar as string).slice(0, 6),
|
||||
[rankingData],
|
||||
);
|
||||
const showShareCode = isJoined && Boolean(challenge?.shareCode);
|
||||
|
||||
const handleViewAllRanking = () => {
|
||||
if (!id) {
|
||||
@@ -192,7 +217,7 @@ export default function ChallengeDetailScreen() {
|
||||
try {
|
||||
Toast.show({
|
||||
type: 'info',
|
||||
text1: '正在生成分享卡片...',
|
||||
text1: t('challengeDetail.share.generating'),
|
||||
});
|
||||
|
||||
// 捕获分享卡片视图
|
||||
@@ -203,8 +228,8 @@ export default function ChallengeDetailScreen() {
|
||||
|
||||
// 分享图片
|
||||
const shareMessage = isJoined && progress
|
||||
? `我正在参与「${challenge.title}」挑战,已完成 ${progress.completed}/${progress.target} 天!一起加入吧!`
|
||||
: `发现一个很棒的挑战「${challenge.title}」,一起来参与吧!`;
|
||||
? t('challengeDetail.share.messageJoined', { title: challenge.title, completed: progress.completed, target: progress.target })
|
||||
: t('challengeDetail.share.messageNotJoined', { title: challenge.title });
|
||||
|
||||
await Share.share({
|
||||
title: challenge.title,
|
||||
@@ -213,7 +238,7 @@ export default function ChallengeDetailScreen() {
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn('分享失败', error);
|
||||
Toast.error('分享失败,请稍后重试');
|
||||
Toast.error(t('challengeDetail.share.failed'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -234,7 +259,7 @@ export default function ChallengeDetailScreen() {
|
||||
await dispatch(fetchChallengeRankings({ id }));
|
||||
setShowCelebration(true)
|
||||
} catch (error) {
|
||||
Toast.error('加入挑战失败')
|
||||
Toast.error(t('challengeDetail.alert.joinFailed'))
|
||||
}
|
||||
};
|
||||
|
||||
@@ -246,7 +271,7 @@ export default function ChallengeDetailScreen() {
|
||||
await dispatch(leaveChallenge(id)).unwrap();
|
||||
await dispatch(fetchChallengeDetail(id)).unwrap();
|
||||
} catch (error) {
|
||||
Toast.error('退出挑战失败');
|
||||
Toast.error(t('challengeDetail.alert.leaveFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -254,34 +279,76 @@ export default function ChallengeDetailScreen() {
|
||||
if (!id || leaveStatus === 'loading') {
|
||||
return;
|
||||
}
|
||||
Alert.alert('确认退出挑战?', '退出后需要重新加入才能继续坚持。', [
|
||||
{ text: '取消', style: 'cancel' },
|
||||
{
|
||||
text: '退出挑战',
|
||||
style: 'destructive',
|
||||
onPress: () => {
|
||||
void handleLeave();
|
||||
Alert.alert(
|
||||
t('challengeDetail.alert.leaveConfirm.title'),
|
||||
t('challengeDetail.alert.leaveConfirm.message'),
|
||||
[
|
||||
{ text: t('challengeDetail.alert.leaveConfirm.cancel'), style: 'cancel' },
|
||||
{
|
||||
text: t('challengeDetail.alert.leaveConfirm.confirm'),
|
||||
style: 'destructive',
|
||||
onPress: () => {
|
||||
void handleLeave();
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
]
|
||||
);
|
||||
};
|
||||
|
||||
const handleProgressReport = () => {
|
||||
const handleProgressReport = async () => {
|
||||
if (!id || progressStatus === 'loading') {
|
||||
return;
|
||||
}
|
||||
dispatch(reportChallengeProgress({ id }));
|
||||
|
||||
if (hasCheckedInToday) {
|
||||
Toast.info(t('challengeDetail.checkIn.toast.alreadyChecked'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (challenge?.status === 'upcoming') {
|
||||
Toast.info(t('challengeDetail.checkIn.toast.notStarted'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (challenge?.status === 'expired') {
|
||||
Toast.info(t('challengeDetail.checkIn.toast.expired'));
|
||||
return;
|
||||
}
|
||||
|
||||
const isLoggedIn = await ensureLoggedIn();
|
||||
if (!isLoggedIn) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isJoined) {
|
||||
Toast.info(t('challengeDetail.checkIn.toast.mustJoin'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await dispatch(reportChallengeProgress({ id, value: 1 })).unwrap();
|
||||
Toast.success(t('challengeDetail.checkIn.toast.success'));
|
||||
} catch (error) {
|
||||
Toast.error(t('challengeDetail.checkIn.toast.failed'));
|
||||
}
|
||||
};
|
||||
|
||||
const handleCopyShareCode = async () => {
|
||||
if (!challenge?.shareCode) return;
|
||||
await Clipboard.setStringAsync(challenge.shareCode);
|
||||
// 添加震动反馈
|
||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
|
||||
Toast.success(t('challengeDetail.shareCode.copied'));
|
||||
};
|
||||
|
||||
const isJoined = challenge?.isJoined ?? false;
|
||||
const isLoadingInitial = detailStatus === 'loading' && !challenge;
|
||||
|
||||
if (!id) {
|
||||
return (
|
||||
<SafeAreaView style={[styles.safeArea, { backgroundColor: colorTokens.background }]}>
|
||||
<HeaderBar title="挑战详情" onBack={() => router.back()} withSafeTop transparent={false} />
|
||||
<HeaderBar title={t('challengeDetail.title')} onBack={() => router.back()} withSafeTop transparent={false} />
|
||||
<View style={styles.missingContainer}>
|
||||
<Text style={[styles.missingText, { color: colorTokens.textSecondary }]}>未找到该挑战,稍后再试试吧。</Text>
|
||||
<Text style={[styles.missingText, { color: colorTokens.textSecondary }]}>{t('challengeDetail.notFound')}</Text>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
@@ -290,10 +357,10 @@ export default function ChallengeDetailScreen() {
|
||||
if (isLoadingInitial) {
|
||||
return (
|
||||
<SafeAreaView style={[styles.safeArea, { backgroundColor: colorTokens.background }]}>
|
||||
<HeaderBar title="挑战详情" onBack={() => router.back()} withSafeTop transparent={false} />
|
||||
<HeaderBar title={t('challengeDetail.title')} onBack={() => router.back()} withSafeTop transparent={false} />
|
||||
<View style={styles.missingContainer}>
|
||||
<ActivityIndicator color={colorTokens.primary} />
|
||||
<Text style={[styles.missingText, { color: colorTokens.textSecondary, marginTop: 16 }]}>加载挑战详情中…</Text>
|
||||
<Text style={[styles.missingText, { color: colorTokens.textSecondary, marginTop: 16 }]}>{t('challengeDetail.loading')}</Text>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
@@ -302,43 +369,43 @@ export default function ChallengeDetailScreen() {
|
||||
if (!challenge) {
|
||||
return (
|
||||
<SafeAreaView style={[styles.safeArea, { backgroundColor: colorTokens.background }]}>
|
||||
<HeaderBar title="挑战详情" onBack={() => router.back()} withSafeTop transparent={false} />
|
||||
<HeaderBar title={t('challengeDetail.title')} onBack={() => router.back()} withSafeTop transparent={false} />
|
||||
<View style={styles.missingContainer}>
|
||||
<Text style={[styles.missingText, { color: colorTokens.textSecondary }]}>
|
||||
{detailError ?? '未找到该挑战,稍后再试试吧。'}
|
||||
{detailError ?? t('challengeDetail.notFound')}
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
style={[styles.retryButton, { borderColor: colorTokens.primary }]}
|
||||
activeOpacity={0.9}
|
||||
onPress={() => dispatch(fetchChallengeDetail(id))}
|
||||
>
|
||||
<Text style={[styles.retryText, { color: colorTokens.primary }]}>重新加载</Text>
|
||||
<Text style={[styles.retryText, { color: colorTokens.primary }]}>{t('challengeDetail.retry')}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
const highlightTitle = challenge.highlightTitle ?? '立即加入挑战';
|
||||
const highlightSubtitle = challenge.highlightSubtitle ?? '邀请好友一起坚持,更容易收获成果';
|
||||
const joinCtaLabel = joinStatus === 'loading' ? '加入中…' : challenge.ctaLabel ?? '立即加入挑战';
|
||||
const highlightTitle = challenge.highlightTitle ?? t('challengeDetail.highlight.join.title');
|
||||
const highlightSubtitle = challenge.highlightSubtitle ?? t('challengeDetail.highlight.join.subtitle');
|
||||
const joinCtaLabel = joinStatus === 'loading' ? t('challengeDetail.cta.joining') : challenge.ctaLabel ?? t('challengeDetail.cta.join');
|
||||
const isUpcoming = challenge.status === 'upcoming';
|
||||
const isExpired = challenge.status === 'expired';
|
||||
const upcomingStartLabel = formatMonthDay(challenge.startAt);
|
||||
const upcomingHighlightTitle = '挑战即将开始';
|
||||
const upcomingHighlightTitle = t('challengeDetail.highlight.upcoming.title');
|
||||
const upcomingHighlightSubtitle = upcomingStartLabel
|
||||
? `${upcomingStartLabel} 开始,敬请期待`
|
||||
: '挑战即将开启,敬请期待';
|
||||
const upcomingCtaLabel = '挑战即将开始';
|
||||
? t('challengeDetail.highlight.upcoming.subtitle', { date: upcomingStartLabel })
|
||||
: t('challengeDetail.highlight.upcoming.subtitleFallback');
|
||||
const upcomingCtaLabel = t('challengeDetail.cta.upcoming');
|
||||
const expiredEndLabel = formatMonthDay(challenge.endAt);
|
||||
const expiredHighlightTitle = '挑战已结束';
|
||||
const expiredHighlightTitle = t('challengeDetail.highlight.expired.title');
|
||||
const expiredHighlightSubtitle = expiredEndLabel
|
||||
? `${expiredEndLabel} 已截止,期待下一次挑战`
|
||||
: '本轮挑战已结束,期待下一次挑战';
|
||||
const expiredCtaLabel = '挑战已结束';
|
||||
const leaveHighlightTitle = '先别急着离开';
|
||||
const leaveHighlightSubtitle = '再坚持一下,下一个里程碑就要出现了';
|
||||
const leaveCtaLabel = leaveStatus === 'loading' ? '退出中…' : '退出挑战';
|
||||
? t('challengeDetail.highlight.expired.subtitle', { date: expiredEndLabel })
|
||||
: t('challengeDetail.highlight.expired.subtitleFallback');
|
||||
const expiredCtaLabel = t('challengeDetail.cta.expired');
|
||||
const leaveHighlightTitle = t('challengeDetail.highlight.leave.title');
|
||||
const leaveHighlightSubtitle = t('challengeDetail.highlight.leave.subtitle');
|
||||
const leaveCtaLabel = leaveStatus === 'loading' ? t('challengeDetail.cta.leaving') : t('challengeDetail.cta.leave');
|
||||
|
||||
let floatingHighlightTitle = highlightTitle;
|
||||
let floatingHighlightSubtitle = highlightSubtitle;
|
||||
@@ -349,8 +416,10 @@ export default function ChallengeDetailScreen() {
|
||||
let isDisabledButtonState = false;
|
||||
|
||||
if (isJoined) {
|
||||
floatingHighlightTitle = leaveHighlightTitle;
|
||||
floatingHighlightSubtitle = leaveHighlightSubtitle;
|
||||
floatingHighlightTitle = showShareCode
|
||||
? `分享码 ${challenge?.shareCode ?? ''}`
|
||||
: leaveHighlightTitle;
|
||||
floatingHighlightSubtitle = showShareCode ? '' : leaveHighlightSubtitle;
|
||||
floatingCtaLabel = leaveCtaLabel;
|
||||
floatingOnPress = handleLeaveConfirm;
|
||||
floatingDisabled = leaveStatus === 'loading';
|
||||
@@ -380,6 +449,23 @@ export default function ChallengeDetailScreen() {
|
||||
const participantsLabel = formatParticipantsLabel(challenge.participantsCount);
|
||||
|
||||
const inlineErrorMessage = detailStatus === 'failed' && detailError ? detailError : undefined;
|
||||
const checkInDisabled =
|
||||
progressStatus === 'loading' || hasCheckedInToday || !isJoined || isUpcoming || isExpired;
|
||||
const checkInButtonLabel =
|
||||
progressStatus === 'loading'
|
||||
? t('challengeDetail.checkIn.button.checking')
|
||||
: hasCheckedInToday
|
||||
? t('challengeDetail.checkIn.button.checked')
|
||||
: !isJoined
|
||||
? t('challengeDetail.checkIn.button.notJoined')
|
||||
: isUpcoming
|
||||
? t('challengeDetail.checkIn.button.upcoming')
|
||||
: isExpired
|
||||
? t('challengeDetail.checkIn.button.expired')
|
||||
: t('challengeDetail.checkIn.button.checkIn');
|
||||
const checkInSubtitle = hasCheckedInToday
|
||||
? t('challengeDetail.checkIn.subtitleChecked')
|
||||
: t('challengeDetail.checkIn.subtitle');
|
||||
|
||||
return (
|
||||
<View style={styles.safeArea}>
|
||||
@@ -411,9 +497,9 @@ export default function ChallengeDetailScreen() {
|
||||
// 已加入:显示个人进度
|
||||
<View style={styles.shareProgressContainer}>
|
||||
<View style={styles.shareProgressHeader}>
|
||||
<Text style={styles.shareProgressLabel}>我的坚持进度</Text>
|
||||
<Text style={styles.shareProgressLabel}>{t('challengeDetail.shareCard.progress.label')}</Text>
|
||||
<Text style={styles.shareProgressValue}>
|
||||
{progress.completed} / {progress.target} 天
|
||||
{t('challengeDetail.shareCard.progress.days', { completed: progress.completed, target: progress.target })}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
@@ -429,8 +515,8 @@ export default function ChallengeDetailScreen() {
|
||||
|
||||
<Text style={styles.shareProgressSubtext}>
|
||||
{progress.completed === progress.target
|
||||
? '🎉 已完成挑战!'
|
||||
: `还差 ${progress.target - progress.completed} 天完成挑战`}
|
||||
? t('challengeDetail.shareCard.progress.completed')
|
||||
: t('challengeDetail.shareCard.progress.remaining', { remaining: progress.target - progress.completed })}
|
||||
</Text>
|
||||
</View>
|
||||
) : (
|
||||
@@ -454,7 +540,7 @@ export default function ChallengeDetailScreen() {
|
||||
</View>
|
||||
<View style={styles.shareInfoTextWrapper}>
|
||||
<Text style={styles.shareInfoLabel}>{challenge.requirementLabel}</Text>
|
||||
<Text style={styles.shareInfoMeta}>按日打卡自动累计</Text>
|
||||
<Text style={styles.shareInfoMeta}>{t('challengeDetail.shareCard.info.checkInDaily')}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -464,7 +550,7 @@ export default function ChallengeDetailScreen() {
|
||||
</View>
|
||||
<View style={styles.shareInfoTextWrapper}>
|
||||
<Text style={styles.shareInfoLabel}>{participantsLabel}</Text>
|
||||
<Text style={styles.shareInfoMeta}>快来一起坚持吧</Text>
|
||||
<Text style={styles.shareInfoMeta}>{t('challengeDetail.shareCard.info.joinUs')}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
@@ -472,7 +558,7 @@ export default function ChallengeDetailScreen() {
|
||||
|
||||
{/* 底部标识 */}
|
||||
<View style={styles.shareCardFooter}>
|
||||
<Text style={styles.shareCardFooterText}>Out Live · 超越生命</Text>
|
||||
<Text style={styles.shareCardFooterText}>{t('challengeDetail.shareCard.footer')}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
@@ -568,7 +654,7 @@ export default function ChallengeDetailScreen() {
|
||||
</View>
|
||||
<View style={styles.detailTextWrapper}>
|
||||
<Text style={styles.detailLabel}>{challenge.requirementLabel}</Text>
|
||||
<Text style={styles.detailMeta}>按日打卡自动累计</Text>
|
||||
<Text style={styles.detailMeta}>{t('challengeDetail.detail.requirement')}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -590,19 +676,50 @@ export default function ChallengeDetailScreen() {
|
||||
))}
|
||||
{challenge.participantsCount && challenge.participantsCount > participantAvatars.length ? (
|
||||
<TouchableOpacity style={styles.moreAvatarButton}>
|
||||
<Text style={styles.moreAvatarText}>更多</Text>
|
||||
<Text style={styles.moreAvatarText}>{t('challengeDetail.participants.more')}</Text>
|
||||
</TouchableOpacity>
|
||||
) : null}
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{isCustomChallenge ? (
|
||||
<View style={styles.checkInCard}>
|
||||
<View style={styles.checkInCopy}>
|
||||
<Text style={styles.checkInTitle}>{hasCheckedInToday ? t('challengeDetail.checkIn.todayChecked') : t('challengeDetail.checkIn.title')}</Text>
|
||||
<Text style={styles.checkInSubtitle}>{checkInSubtitle}</Text>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.9}
|
||||
onPress={handleProgressReport}
|
||||
disabled={checkInDisabled}
|
||||
style={styles.checkInButton}
|
||||
>
|
||||
<LinearGradient
|
||||
colors={checkInDisabled ? CTA_DISABLED_GRADIENT : CTA_GRADIENT}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 1 }}
|
||||
style={styles.checkInButtonBackground}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.checkInButtonLabel,
|
||||
checkInDisabled && styles.checkInButtonLabelDisabled,
|
||||
]}
|
||||
>
|
||||
{checkInButtonLabel}
|
||||
</Text>
|
||||
</LinearGradient>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
<View style={styles.sectionHeader}>
|
||||
<Text style={styles.sectionTitle}>排行榜</Text>
|
||||
<Text style={styles.sectionTitle}>{t('challengeDetail.ranking.title')}</Text>
|
||||
<TouchableOpacity activeOpacity={0.8} onPress={handleViewAllRanking}>
|
||||
<Text style={styles.sectionAction}>查看全部</Text>
|
||||
<Text style={styles.sectionAction}>{t('challengeDetail.detail.viewAllRanking')}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
@@ -623,7 +740,7 @@ export default function ChallengeDetailScreen() {
|
||||
))
|
||||
) : (
|
||||
<View style={styles.emptyRanking}>
|
||||
<Text style={styles.emptyRankingText}>榜单即将开启,快来抢占席位。</Text>
|
||||
<Text style={styles.emptyRankingText}>{t('challengeDetail.ranking.empty')}</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
@@ -632,11 +749,30 @@ export default function ChallengeDetailScreen() {
|
||||
<View pointerEvents="box-none" style={[styles.floatingCTAContainer, { paddingBottom: insets.bottom }]}>
|
||||
<BlurView intensity={10} tint="light" style={styles.floatingCTABlur}>
|
||||
<View style={styles.floatingCTAContent}>
|
||||
<View style={styles.highlightCopy}>
|
||||
<Text style={styles.highlightTitle}>{floatingHighlightTitle}</Text>
|
||||
<Text style={styles.highlightSubtitle}>{floatingHighlightSubtitle}</Text>
|
||||
{floatingError ? <Text style={styles.ctaErrorText}>{floatingError}</Text> : null}
|
||||
</View>
|
||||
{showShareCode ? (
|
||||
<View style={[styles.highlightCopy, styles.highlightCopyCompact]}>
|
||||
<View style={styles.shareCodeRow}>
|
||||
<Text style={styles.highlightTitle}>{floatingHighlightTitle}</Text>
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.85}
|
||||
style={styles.shareCodeIconButton}
|
||||
onPress={handleCopyShareCode}
|
||||
>
|
||||
<Ionicons name="copy-outline" size={18} color="#4F5BD5" />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
{floatingHighlightSubtitle ? (
|
||||
<Text style={styles.highlightSubtitle}>{floatingHighlightSubtitle}</Text>
|
||||
) : null}
|
||||
{floatingError ? <Text style={styles.ctaErrorText}>{floatingError}</Text> : null}
|
||||
</View>
|
||||
) : (
|
||||
<View style={styles.highlightCopy}>
|
||||
<Text style={styles.highlightTitle}>{floatingHighlightTitle}</Text>
|
||||
<Text style={styles.highlightSubtitle}>{floatingHighlightSubtitle}</Text>
|
||||
{floatingError ? <Text style={styles.ctaErrorText}>{floatingError}</Text> : null}
|
||||
</View>
|
||||
)}
|
||||
<TouchableOpacity
|
||||
style={styles.highlightButton}
|
||||
activeOpacity={0.9}
|
||||
@@ -732,6 +868,19 @@ const styles = StyleSheet.create({
|
||||
flex: 1,
|
||||
marginRight: 16,
|
||||
},
|
||||
highlightCopyCompact: {
|
||||
marginRight: 12,
|
||||
gap: 4,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
shareCodeRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
flex: 1,
|
||||
},
|
||||
headerTextBlock: {
|
||||
paddingHorizontal: 24,
|
||||
marginTop: HERO_HEIGHT - 60,
|
||||
@@ -834,6 +983,49 @@ const styles = StyleSheet.create({
|
||||
color: '#4F5BD5',
|
||||
fontWeight: '600',
|
||||
},
|
||||
checkInCard: {
|
||||
marginTop: 4,
|
||||
padding: 14,
|
||||
borderRadius: 18,
|
||||
backgroundColor: '#f5f6ff',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
},
|
||||
checkInCopy: {
|
||||
flex: 1,
|
||||
},
|
||||
checkInTitle: {
|
||||
fontSize: 14,
|
||||
fontWeight: '700',
|
||||
color: '#1c1f3a',
|
||||
},
|
||||
checkInSubtitle: {
|
||||
marginTop: 4,
|
||||
fontSize: 12,
|
||||
color: '#6f7ba7',
|
||||
lineHeight: 18,
|
||||
},
|
||||
checkInButton: {
|
||||
borderRadius: 18,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
checkInButtonBackground: {
|
||||
paddingVertical: 10,
|
||||
paddingHorizontal: 14,
|
||||
borderRadius: 18,
|
||||
minWidth: 96,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
checkInButtonLabel: {
|
||||
fontSize: 13,
|
||||
fontWeight: '700',
|
||||
color: '#ffffff',
|
||||
},
|
||||
checkInButtonLabelDisabled: {
|
||||
color: '#6f7799',
|
||||
},
|
||||
sectionHeader: {
|
||||
marginTop: 36,
|
||||
marginHorizontal: 24,
|
||||
@@ -889,6 +1081,10 @@ const styles = StyleSheet.create({
|
||||
color: '#5f6a97',
|
||||
lineHeight: 18,
|
||||
},
|
||||
shareCodeIconButton: {
|
||||
paddingHorizontal: 4,
|
||||
paddingVertical: 4,
|
||||
},
|
||||
ctaErrorText: {
|
||||
marginTop: 8,
|
||||
fontSize: 12,
|
||||
@@ -1084,4 +1280,3 @@ const styles = StyleSheet.create({
|
||||
fontWeight: '600',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user