diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx index bf91398..5887916 100644 --- a/app/(tabs)/index.tsx +++ b/app/(tabs)/index.tsx @@ -10,11 +10,9 @@ import { TextInput, ActivityIndicator, ScrollView, - Alert, } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { router } from 'expo-router'; -import { LinearGradient } from 'expo-linear-gradient'; import * as Haptics from 'expo-haptics'; import { IconSymbol } from '@/components/ui/icon-symbol'; import { SessionCard, SwipeableSessionCard, SessionCardSkeleton } from '@/components/jules/session-card'; @@ -23,6 +21,7 @@ import { useColorScheme } from '@/hooks/use-color-scheme'; import type { Session } from '@/constants/types'; import { useI18n } from '@/constants/i18n-context'; import { useApiKey } from '@/constants/api-key-context'; +import { useAlert } from '@/contexts/alert-context'; import { useSecureStorage, type SessionFilterPreset, @@ -63,6 +62,7 @@ export default function SessionsScreen() { const colorScheme = useColorScheme(); const isDark = colorScheme === 'dark'; const { t } = useI18n(); + const { showAlert } = useAlert(); const colors = isDark ? Colors.dark : Colors.light; const { apiKey } = useApiKey(); @@ -238,11 +238,11 @@ export default function SessionsScreen() { }, [approvePlan, fetchSessions]); const handleDeleteSession = useCallback((sessionName: string) => { - Alert.alert( + showAlert( t('deleteSession'), t('deleteSessionConfirm'), [ - { text: t('cancel'), style: 'cancel' }, + { text: t('cancel'), style: 'cancel', onPress: () => {} }, { text: t('delete'), style: 'destructive', @@ -253,7 +253,7 @@ export default function SessionsScreen() { }, ] ); - }, [deleteSession, t]); + }, [deleteSession, t, showAlert]); const renderSessionItem = useCallback(({ item }: { item: Session }) => ( handleDeleteSession(item.name)} delayLongPress={500}> @@ -313,25 +313,13 @@ export default function SessionsScreen() { return ( - {/* Modern Header with Gradient */} + {/* Modern Header */} - - + - + Jules Client @@ -550,14 +538,9 @@ export default function SessionsScreen() { accessibilityRole="button" accessibilityHint="Create a new coding task session" > - + - + )} diff --git a/app/(tabs)/settings.tsx b/app/(tabs)/settings.tsx index f0b2f31..ae777a0 100644 --- a/app/(tabs)/settings.tsx +++ b/app/(tabs)/settings.tsx @@ -6,14 +6,12 @@ import { TouchableOpacity, StyleSheet, ScrollView, - Alert, Switch, Appearance, Linking, } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { useRouter } from 'expo-router'; -import { LinearGradient } from 'expo-linear-gradient'; import * as Haptics from 'expo-haptics'; import Constants from 'expo-constants'; import { IconSymbol } from '@/components/ui/icon-symbol'; @@ -21,6 +19,7 @@ import { useSecureStorage } from '@/hooks/use-secure-storage'; import { useColorScheme } from '@/hooks/use-color-scheme'; import { useI18n } from '@/constants/i18n-context'; import { useApiKey } from '@/constants/api-key-context'; +import { useAlert } from '@/contexts/alert-context'; import { Colors } from '@/constants/theme'; import { isValidExternalLink } from '@/utils/url'; @@ -93,6 +92,7 @@ export default function SettingsScreen() { const colors = isDark ? Colors.dark : Colors.light; const { apiKey, setApiKey: saveApiKeyToContext } = useApiKey(); + const { showAlert } = useAlert(); const [localApiKey, setLocalApiKey] = useState(apiKey); const [isApiKeyVisible, setIsApiKeyVisible] = useState(false); const [manualDarkMode, setManualDarkMode] = useState(isDark); @@ -120,10 +120,10 @@ export default function SettingsScreen() { try { await saveApiKeyToContext(localApiKey); void Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); - Alert.alert(t('savedSuccess')); + showAlert(t('savedSuccess'), undefined, undefined, 'success'); } catch { void Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error); - Alert.alert(t('error'), t('savedError')); + showAlert(t('error'), t('savedError'), undefined, 'error'); } }; @@ -144,31 +144,24 @@ export default function SettingsScreen() { const openURL = async (url: string) => { try { if (!isValidExternalLink(url)) { - Alert.alert(t('error'), t('unableToOpenLink') || 'Unable to open this link. Please check your device settings.'); + showAlert(t('error'), t('unableToOpenLink') || 'Unable to open this link. Please check your device settings.', undefined, 'error'); return; } const supported = await Linking.canOpenURL(url); if (supported) { await Linking.openURL(url); } else { - Alert.alert(t('error'), t('unableToOpenLink') || 'Unable to open this link. Please check your device settings.'); + showAlert(t('error'), t('unableToOpenLink') || 'Unable to open this link. Please check your device settings.', undefined, 'error'); } } catch (error) { - Alert.alert(t('error'), t('unableToOpenLink') || 'Unable to open this link. Please try again later.'); + showAlert(t('error'), t('unableToOpenLink') || 'Unable to open this link. Please try again later.', undefined, 'error'); } }; return ( - {/* Modern Header with Gradient */} + {/* Modern Header */} - {t('settings')} @@ -212,15 +205,10 @@ export default function SettingsScreen() { onPress={handleSave} activeOpacity={0.9} > - + {t('save')} - + {/* テーマ切り替え */} diff --git a/app/_layout.tsx b/app/_layout.tsx index 6700c1c..b3c2237 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -5,6 +5,7 @@ import 'react-native-reanimated'; import { ApiKeyProvider } from '@/constants/api-key-context'; import { I18nProvider } from '@/constants/i18n-context'; +import { AlertProvider } from '@/contexts/alert-context'; import { useColorScheme } from '@/hooks/use-color-scheme'; export default function RootLayout() { @@ -13,15 +14,17 @@ export default function RootLayout() { return ( - - - - - - - - - + + + + + + + + + + + ); diff --git a/app/create-session.tsx b/app/create-session.tsx index a21c5db..dd2e955 100644 --- a/app/create-session.tsx +++ b/app/create-session.tsx @@ -4,7 +4,6 @@ import { Text, TouchableOpacity, ScrollView, - Alert, KeyboardAvoidingView, Platform, } from 'react-native'; @@ -19,6 +18,7 @@ import { useSecureStorage } from '@/hooks/use-secure-storage'; import { useSourcesCache } from '@/hooks/use-sources-cache'; import type { Source } from '@/constants/types'; import { Colors } from '@/constants/theme'; +import { useAlert } from '@/contexts/alert-context'; import { FormSkeleton } from '@/components/jules/create-session/form-skeleton'; import { SourceSelector } from '@/components/jules/create-session/source-selector'; @@ -38,6 +38,7 @@ export default function CreateSessionScreen() { const { apiKey } = useApiKey(); const { saveRecentRepo, getRecentRepos } = useSecureStorage(); const { getCachedSources, saveCachedSources } = useSourcesCache(); + const { showAlert } = useAlert(); const [selectedSource, setSelectedSource] = useState(''); const [prompt, setPrompt] = useState(''); const [requirePlanApproval, setRequirePlanApproval] = useState(false); // false = Start/Run, true = Review @@ -174,12 +175,12 @@ export default function CreateSessionScreen() { // Create session and save to recent repos const handleCreate = useCallback(async () => { if (!selectedSource || !prompt.trim()) { - Alert.alert(t('error'), t('inputError')); + showAlert(t('error'), t('inputError'), undefined, 'error'); return; } if (prompt.length > 50000) { - Alert.alert(t('error'), t('promptTooLong')); + showAlert(t('error'), t('promptTooLong'), undefined, 'error'); return; } @@ -191,14 +192,14 @@ export default function CreateSessionScreen() { if (session && source) { // Save to recent repos await saveRecentRepo(source); - Alert.alert(t('createSuccess'), '', [ + showAlert(t('createSuccess'), undefined, [ { text: 'OK', onPress: () => router.back(), }, - ]); + ], 'success'); } - }, [selectedSource, prompt, requirePlanApproval, sourcesMap, createSession, saveRecentRepo, t]); + }, [selectedSource, prompt, requirePlanApproval, sourcesMap, createSession, saveRecentRepo, t, showAlert]); return ( <> diff --git a/app/session/id.tsx b/app/session/id.tsx index 8634bb6..19a55eb 100644 --- a/app/session/id.tsx +++ b/app/session/id.tsx @@ -11,14 +11,12 @@ import { Platform, Animated, Linking, - Alert, ActionSheetIOS, } from 'react-native'; import { useLocalSearchParams, Stack } from 'expo-router'; import { useHeaderHeight } from '@react-navigation/elements'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import * as Haptics from 'expo-haptics'; -import { LinearGradient } from 'expo-linear-gradient'; import { IconSymbol } from '@/components/ui/icon-symbol'; import { ActivityItem, ActivityItemSkeleton } from '@/components/jules'; import { useJulesApi } from '@/hooks/use-jules-api'; @@ -27,6 +25,7 @@ import { shareSession } from '@/hooks/use-export-session'; import type { Activity, Session } from '@/constants/types'; import { useI18n } from '@/constants/i18n-context'; import { useApiKey } from '@/constants/api-key-context'; +import { useAlert } from '@/contexts/alert-context'; import { SessionHeaderRight } from '@/components/jules/session-header-right'; import { ErrorBanner } from '@/components/jules/error-banner'; import { ApprovalBanner } from '@/components/jules/approval-banner'; @@ -239,7 +238,7 @@ export default function SessionDetailScreen() { // Export session handler const handleExportSession = useCallback(async (format: 'markdown' | 'json') => { if (!currentSession || activities.length === 0) { - Alert.alert(t('error'), t('noSessionDataToExport')); + showAlert(t('error'), t('noSessionDataToExport'), undefined, 'error'); return; } @@ -250,12 +249,12 @@ export default function SessionDetailScreen() { } catch (err) { const errorMessage = err instanceof Error ? err.message : t('exportFailed'); if (errorMessage.includes('not available')) { - Alert.alert(t('error'), t('sharingNotAvailable')); + showAlert(t('error'), t('sharingNotAvailable'), undefined, 'error'); } else { - Alert.alert(t('error'), errorMessage); + showAlert(t('error'), errorMessage, undefined, 'error'); } } - }, [currentSession, activities, t]); + }, [currentSession, activities, t, showAlert]); // Show export menu const showExportMenu = useCallback(() => { @@ -277,17 +276,17 @@ export default function SessionDetailScreen() { ); } else { // Android - show simple alert - Alert.alert( + showAlert( t('exportSession'), t('chooseExportFormat'), [ - { text: t('cancel'), style: 'cancel' }, + { text: t('cancel'), style: 'cancel', onPress: () => {} }, { text: t('exportAsMarkdown'), onPress: () => void handleExportSession('markdown') }, { text: t('exportAsJSON'), onPress: () => void handleExportSession('json') }, ] ); } - }, [t, handleExportSession]); + }, [t, handleExportSession, showAlert]); return ( @@ -370,11 +369,7 @@ export default function SessionDetailScreen() { if (url) void Linking.openURL(url); }} > - + Pull Request Created! @@ -385,7 +380,7 @@ export default function SessionDetailScreen() { )} - + ) : null } diff --git a/app/statistics.tsx b/app/statistics.tsx index d563b16..182028e 100644 --- a/app/statistics.tsx +++ b/app/statistics.tsx @@ -2,7 +2,6 @@ import React, { useMemo } from 'react'; import { View, Text, StyleSheet, ScrollView } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { Stack } from 'expo-router'; -import { LinearGradient } from 'expo-linear-gradient'; import { IconSymbol } from '@/components/ui/icon-symbol'; import { useColorScheme } from '@/hooks/use-color-scheme'; import { useI18n } from '@/constants/i18n-context'; @@ -56,66 +55,46 @@ export default function StatisticsScreen() { {/* Total Sessions */} - + {t('totalSessions')} {stats.total} - + {/* Active Sessions */} - + {t('activeSessions')} {stats.active} - + {/* Completed Sessions */} - + {t('completedSessions')} {stats.completed} - + {/* Failed Sessions */} - + {t('failedSessions')} {stats.failed} - + {/* Info Section */} diff --git a/bun.lock b/bun.lock index 9232ab1..6b5960b 100644 --- a/bun.lock +++ b/bun.lock @@ -17,7 +17,6 @@ "expo-haptics": "~15.0.8", "expo-image": "~3.0.11", "expo-image-picker": "~17.0.10", - "expo-linear-gradient": "^15.0.8", "expo-linking": "~8.0.11", "expo-router": "~6.0.21", "expo-secure-store": "~15.0.8", @@ -937,8 +936,6 @@ "expo-keep-awake": ["expo-keep-awake@15.0.8", "", { "peerDependencies": { "expo": "*", "react": "*" } }, "sha512-YK9M1VrnoH1vLJiQzChZgzDvVimVoriibiDIFLbQMpjYBnvyfUeHJcin/Gx1a+XgupNXy92EQJLgI/9ZuXajYQ=="], - "expo-linear-gradient": ["expo-linear-gradient@15.0.8", "", { "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-V2d8Wjn0VzhPHO+rrSBtcl+Fo+jUUccdlmQ6OoL9/XQB7Qk3d9lYrqKDJyccwDxmQT10JdST3Tmf2K52NLc3kw=="], - "expo-linking": ["expo-linking@8.0.11", "", { "dependencies": { "expo-constants": "~18.0.12", "invariant": "^2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-+VSaNL5om3kOp/SSKO5qe6cFgfSIWnnQDSbA7XLs3ECkYzXRquk5unxNS3pg7eK5kNUmQ4kgLI7MhTggAEUBLA=="], "expo-modules-autolinking": ["expo-modules-autolinking@3.0.23", "", { "dependencies": { "@expo/spawn-async": "^1.7.2", "chalk": "^4.1.0", "commander": "^7.2.0", "require-from-string": "^2.0.2", "resolve-from": "^5.0.0" }, "bin": { "expo-modules-autolinking": "bin/expo-modules-autolinking.js" } }, "sha512-YZnaE0G+52xftjH5nsIRaWsoVBY38SQCECclpdgLisdbRY/6Mzo7ndokjauOv3mpFmzMZACHyJNu1YSAffQwTg=="], diff --git a/components/jules/activity-item.tsx b/components/jules/activity-item.tsx index d785db6..bb64141 100644 --- a/components/jules/activity-item.tsx +++ b/components/jules/activity-item.tsx @@ -1,7 +1,6 @@ import React, { useState, useEffect, useRef } from 'react'; import { View, Text, StyleSheet, TouchableOpacity, ScrollView, Animated, Image } from 'react-native'; import Markdown from 'react-native-markdown-display'; -import { LinearGradient } from 'expo-linear-gradient'; import * as Haptics from 'expo-haptics'; import { IconSymbol } from '@/components/ui/icon-symbol'; import { useColorScheme } from '@/hooks/use-color-scheme'; @@ -245,14 +244,9 @@ function AgentMessageActivity({ activity, isDark, colors, formatTime, getMarkdow - + - + @@ -272,12 +266,7 @@ function UserMessageActivity({ activity, isDark, colors, formatTime }: { activit return ( - + You {formatTime(activity.createTime)} @@ -285,7 +274,7 @@ function UserMessageActivity({ activity, isDark, colors, formatTime }: { activit {activity.userMessaged!.userMessage} - + @@ -302,12 +291,9 @@ function PlanGeneratedActivity({ activity, isDark, colors }: { activity: Activit - + - + Plan Generated {plan.steps?.map((step, index) => ( @@ -331,21 +317,11 @@ function PlanApprovalRequestedActivity({ activity, isDark, colors, onApprovePlan const planId = activity.planApprovalRequested!.planId; return ( - - + - + - + Approval Required @@ -360,15 +336,10 @@ function PlanApprovalRequestedActivity({ activity, isDark, colors, onApprovePlan }} activeOpacity={0.9} > - + Approve Plan - + )} diff --git a/components/jules/create-session/submit-button.tsx b/components/jules/create-session/submit-button.tsx index 86ce8ee..fe581e4 100644 --- a/components/jules/create-session/submit-button.tsx +++ b/components/jules/create-session/submit-button.tsx @@ -1,6 +1,5 @@ import React from 'react'; import { View, Text, TouchableOpacity, ActivityIndicator } from 'react-native'; -import { LinearGradient } from 'expo-linear-gradient'; import * as Haptics from 'expo-haptics'; import { IconSymbol } from '@/components/ui/icon-symbol'; import { styles } from './styles'; @@ -45,19 +44,14 @@ export function SubmitButton({ ) : ( - + {isLoading ? ( ) : ( )} {buttonLabel} - + )} ); diff --git a/components/jules/feedback-banner.tsx b/components/jules/feedback-banner.tsx index 0e0e539..32b1bdb 100644 --- a/components/jules/feedback-banner.tsx +++ b/components/jules/feedback-banner.tsx @@ -1,6 +1,5 @@ import React from 'react'; import { View, Text, StyleSheet } from 'react-native'; -import { LinearGradient } from 'expo-linear-gradient'; import { IconSymbol } from '@/components/ui/icon-symbol'; interface FeedbackBannerProps { @@ -13,13 +12,15 @@ export function FeedbackBanner({ sessionState, isDark, t }: FeedbackBannerProps) if (sessionState !== 'AWAITING_USER_FEEDBACK') return null; return ( - - + Jules is waiting for your response diff --git a/components/jules/pr-card.tsx b/components/jules/pr-card.tsx index 3814e80..c17ec9c 100644 --- a/components/jules/pr-card.tsx +++ b/components/jules/pr-card.tsx @@ -1,7 +1,8 @@ import React from 'react'; -import { View, Text, TouchableOpacity, StyleSheet, Linking, Alert } from 'react-native'; +import { View, Text, TouchableOpacity, StyleSheet, Linking } from 'react-native'; import { IconSymbol } from '@/components/ui/icon-symbol'; import { isValidExternalLink } from '@/utils/url'; +import { useAlert } from '@/contexts/alert-context'; import type { PullRequest } from '@/constants/types'; @@ -12,6 +13,8 @@ interface PrCardProps { } export function PrCard({ submittedPr, isDark, t }: PrCardProps) { + const { showAlert } = useAlert(); + if (!submittedPr) return null; const isObject = typeof submittedPr === 'object'; @@ -36,7 +39,7 @@ export function PrCard({ submittedPr, isDark, t }: PrCardProps) { if (url && isValidExternalLink(url)) { void Linking.openURL(url); } else { - Alert.alert(t('error'), t('unableToOpenLink')); + showAlert(t('error'), t('unableToOpenLink'), undefined, 'error'); } }} activeOpacity={0.7} diff --git a/components/jules/session-card.tsx b/components/jules/session-card.tsx index 3d3a3ed..c93f6d4 100644 --- a/components/jules/session-card.tsx +++ b/components/jules/session-card.tsx @@ -1,6 +1,5 @@ import React, { useEffect, useRef } from 'react'; import { View, Text, TouchableOpacity, StyleSheet, Animated, ActivityIndicator } from 'react-native'; -import { LinearGradient } from 'expo-linear-gradient'; import * as Haptics from 'expo-haptics'; import { Swipeable } from 'react-native-gesture-handler'; import { IconSymbol } from '@/components/ui/icon-symbol'; @@ -214,16 +213,10 @@ export const SessionCard = React.memo(function SessionCard({ session, onPress, o /> )} - {/* Gradient accent for active sessions */} + {/* Flat accent for active sessions */} {isActiveState && ( - )} diff --git a/components/jules/session-header-right.tsx b/components/jules/session-header-right.tsx index 144b3f5..384bfe2 100644 --- a/components/jules/session-header-right.tsx +++ b/components/jules/session-header-right.tsx @@ -1,7 +1,8 @@ import React from 'react'; -import { View, Text, TouchableOpacity, StyleSheet, Linking, Alert } from 'react-native'; +import { View, Text, TouchableOpacity, StyleSheet, Linking } from 'react-native'; import { IconSymbol } from '@/components/ui/icon-symbol'; import { isValidExternalLink } from '@/utils/url'; +import { useAlert } from '@/contexts/alert-context'; export function getSessionStateText(state: string | null, t: (key: string) => string): string { if (!state) return ''; @@ -29,6 +30,8 @@ interface SessionHeaderRightProps { } export function SessionHeaderRight({ sessionState, sessionUrl, isDark, t, showExportMenu, loadActivities }: SessionHeaderRightProps) { + const { showAlert } = useAlert(); + return ( {sessionState && ( @@ -55,7 +58,7 @@ export function SessionHeaderRight({ sessionState, sessionUrl, isDark, t, showEx if (isValidExternalLink(sessionUrl)) { void Linking.openURL(sessionUrl); } else { - Alert.alert(t('error'), t('unableToOpenLink')); + showAlert(t('error'), t('unableToOpenLink'), undefined, 'error'); } }} accessibilityLabel="Open in Web" diff --git a/components/ui/custom-alert.tsx b/components/ui/custom-alert.tsx new file mode 100644 index 0000000..1cc00ff --- /dev/null +++ b/components/ui/custom-alert.tsx @@ -0,0 +1,262 @@ +import React, { useEffect } from 'react'; +import { View, Text, StyleSheet, TouchableOpacity, Modal, Animated } from 'react-native'; +import { Colors } from '@/constants/theme'; +import { IconSymbol } from '@/components/ui/icon-symbol'; + +export type AlertType = 'success' | 'error' | 'info'; + +interface CustomAlertProps { + visible: boolean; + title: string; + message?: string; + type?: AlertType; + buttons?: { + text: string; + onPress: () => void; + style?: 'default' | 'cancel' | 'destructive'; + }[]; + onClose: () => void; + isDark: boolean; +} + +export function CustomAlert({ + visible, + title, + message, + type = 'info', + buttons, + onClose, + isDark, +}: CustomAlertProps) { + const scaleValue = React.useRef(new Animated.Value(0.9)).current; + const opacityValue = React.useRef(new Animated.Value(0)).current; + const [modalVisible, setModalVisible] = React.useState(visible); + + useEffect(() => { + if (visible) { + setModalVisible(true); + Animated.parallel([ + Animated.spring(scaleValue, { + toValue: 1, + useNativeDriver: true, + tension: 65, + friction: 7, + }), + Animated.timing(opacityValue, { + toValue: 1, + duration: 200, + useNativeDriver: true, + }), + ]).start(); + } else { + Animated.parallel([ + Animated.timing(scaleValue, { + toValue: 0.9, + duration: 150, + useNativeDriver: true, + }), + Animated.timing(opacityValue, { + toValue: 0, + duration: 150, + useNativeDriver: true, + }), + ]).start(() => { + setModalVisible(false); + }); + } + }, [visible, scaleValue, opacityValue]); + + if (!modalVisible) return null; + + const getIcon = () => { + switch (type) { + case 'success': + return ; + case 'error': + return ; + default: + return ; + } + }; + + const defaultButtons = buttons || [ + { text: 'OK', onPress: onClose, style: 'default' } + ]; + + return ( + + + + + + {getIcon()} + + + + {title} + + + {message ? ( + + {message} + + ) : null} + + 2 && styles.buttonContainerVertical]}> + {defaultButtons.map((btn, index) => { + const isDestructive = btn.style === 'destructive'; + const isCancel = btn.style === 'cancel'; + + return ( + 2) && styles.buttonFull + ]} + onPress={() => { + btn.onPress(); + if (btn.onPress !== onClose) { + onClose(); + } + }} + > + + {btn.text} + + + ); + })} + + + + + ); +} + +const styles = StyleSheet.create({ + overlay: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, + backdrop: { + ...StyleSheet.absoluteFillObject, + backgroundColor: 'rgba(0, 0, 0, 0.4)', + }, + alertBox: { + width: '85%', + maxWidth: 340, + borderRadius: 16, + padding: 24, + alignItems: 'center', + shadowColor: '#000', + shadowOffset: { width: 0, height: 4 }, + shadowOpacity: 0.15, + shadowRadius: 12, + elevation: 8, + }, + alertBoxLight: { + backgroundColor: '#ffffff', + }, + alertBoxDark: { + backgroundColor: '#1e293b', + borderWidth: 1, + borderColor: '#334155', + }, + iconContainer: { + marginBottom: 16, + }, + title: { + fontSize: 18, + fontWeight: '700', + marginBottom: 8, + textAlign: 'center', + }, + textLight: { + color: '#0f172a', + }, + textDark: { + color: '#f8fafc', + }, + message: { + fontSize: 15, + textAlign: 'center', + marginBottom: 24, + lineHeight: 22, + }, + messageLight: { + color: '#64748b', + }, + messageDark: { + color: '#94a3b8', + }, + buttonContainer: { + flexDirection: 'row', + width: '100%', + gap: 12, + }, + buttonContainerVertical: { + flexDirection: 'column', + gap: 8, + }, + button: { + backgroundColor: Colors.light.primary, + paddingVertical: 14, + borderRadius: 10, + alignItems: 'center', + justifyContent: 'center', + }, + buttonHalf: { + flex: 1, + }, + buttonFull: { + width: '100%', + }, + buttonDestructive: { + backgroundColor: Colors.light.error, + }, + buttonCancelLight: { + backgroundColor: '#f1f5f9', + }, + buttonCancelDark: { + backgroundColor: '#334155', + }, + buttonText: { + color: '#ffffff', + fontSize: 16, + fontWeight: '600', + }, + buttonTextDestructive: { + color: '#ffffff', + }, + buttonTextCancelLight: { + color: '#475569', + }, + buttonTextCancelDark: { + color: '#cbd5e1', + }, +}); diff --git a/contexts/alert-context.tsx b/contexts/alert-context.tsx new file mode 100644 index 0000000..546c829 --- /dev/null +++ b/contexts/alert-context.tsx @@ -0,0 +1,37 @@ +import React, { createContext, useContext, ReactNode } from 'react'; +import { useColorScheme } from 'react-native'; +import { CustomAlert } from '@/components/ui/custom-alert'; +import { useCustomAlert, type AlertState } from '@/hooks/use-custom-alert'; +import type { AlertType } from '@/components/ui/custom-alert'; + +interface AlertContextType { + showAlert: ( + title: string, + message?: string, + buttons?: AlertState['buttons'], + type?: AlertType + ) => void; + hideAlert: () => void; +} + +const AlertContext = createContext(undefined); + +export function AlertProvider({ children }: { children: ReactNode }) { + const { alertState, showAlert, hideAlert } = useCustomAlert(); + const isDark = useColorScheme() === 'dark'; + + return ( + + {children} + + + ); +} + +export function useAlert() { + const context = useContext(AlertContext); + if (!context) { + throw new Error('useAlert must be used within an AlertProvider'); + } + return context; +} diff --git a/hooks/use-custom-alert.ts b/hooks/use-custom-alert.ts new file mode 100644 index 0000000..3af0f9d --- /dev/null +++ b/hooks/use-custom-alert.ts @@ -0,0 +1,49 @@ +import { useState, useCallback } from 'react'; +import type { AlertType } from '@/components/ui/custom-alert'; + +export interface AlertState { + visible: boolean; + title: string; + message?: string; + type?: AlertType; + buttons?: { + text: string; + onPress: () => void; + style?: 'default' | 'cancel' | 'destructive'; + }[]; +} + +export function useCustomAlert() { + const [alertState, setAlertState] = useState({ + visible: false, + title: '', + }); + + const showAlert = useCallback( + ( + title: string, + message?: string, + buttons?: AlertState['buttons'], + type: AlertType = 'info' + ) => { + setAlertState({ + visible: true, + title, + message, + type, + buttons, + }); + }, + [] + ); + + const hideAlert = useCallback(() => { + setAlertState((prev) => ({ ...prev, visible: false })); + }, []); + + return { + alertState, + showAlert, + hideAlert, + }; +} diff --git a/output.log b/output.log new file mode 100644 index 0000000..4565272 --- /dev/null +++ b/output.log @@ -0,0 +1,15 @@ +Starting project at /app +React Compiler enabled +Starting Metro Bundler +The following packages should be updated for best compatibility with the installed expo version: + expo@54.0.30 - expected version: ~54.0.34 + expo-constants@18.0.12 - expected version: ~18.0.13 + expo-file-system@18.0.12 - expected version: ~19.0.22 + expo-font@14.0.10 - expected version: ~14.0.11 + expo-image-picker@17.0.10 - expected version: ~17.0.11 + expo-linking@8.0.11 - expected version: ~8.0.12 + expo-router@6.0.21 - expected version: ~6.0.23 + expo-web-browser@15.0.10 - expected version: ~15.0.11 +Your project may not work correctly until you install the expected versions of the packages. +Waiting on http://localhost:8081 +Logs for your project will appear below. diff --git a/package.json b/package.json index 7280aae..0a4942f 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,6 @@ "expo-haptics": "~15.0.8", "expo-image": "~3.0.11", "expo-image-picker": "~17.0.10", - "expo-linear-gradient": "^15.0.8", "expo-linking": "~8.0.11", "expo-router": "~6.0.21", "expo-secure-store": "~15.0.8", diff --git a/serve_output.log b/serve_output.log new file mode 100644 index 0000000..d194c81 Binary files /dev/null and b/serve_output.log differ