Compare commits

...

10 Commits

Author SHA1 Message Date
3d9b8601bf Update layout and localization for improved functionality
- Added AsyncStorage and utility functions for future enhancements.
- Commented out ServicesGrid component in HomeScreen for potential redesign.
- Updated English and Russian localization files with new phrases and translations for prayer times and services.
2025-09-01 18:34:22 +05:00
f519052b7b Enhance app functionality and localization
- Added new dependencies: expo-file-system and expo-sharing.
- Updated package versions for @babel/core and react-dom.
- Introduced a new index screen for displaying prayer times with city selection.
- Refactored ServicesGrid to accept a dynamic services array and improved layout.
- Updated localization for new phrases and service titles in Turkmen.
- Enhanced LostKeyModal with a close button and additional text.
- Improved PhrasebookModal to allow expandable phrases for better user interaction.
2025-09-01 13:11:31 +05:00
6797ab6d9e only use dark mode 2025-08-28 16:25:57 +05:00
213062bda4 WIP 2025-08-28 16:10:52 +05:00
a2a4591848 Add TranslatorModal and PhrasebookModal to ServicesScreen; update localization for new phrases 2025-08-21 18:28:08 +05:00
e1d9b688d9 Add HotelBusinessCardModal and LostKeyModal to ServicesScreen; update localization for 'Lost room key' 2025-08-21 17:57:35 +05:00
dc76633cb1 Update service titles in ServicesScreen to use localized strings from the i18n library
- Changed service titles to their corresponding localized versions in Turkmen.
- Added new localization keys for 'Money', 'Hotel', 'Lost room key', and 'Translator' in the tk.json file.
2025-08-21 17:36:43 +05:00
7ecbe23e0d Refactor ServicesScreen to enhance modal management and localization
- Updated modal state management by introducing separate states for each service modal.
- Changed service titles to localized versions in Turkmen.
- Improved CurrencyConverterModal integration by updating visibility state and placeholder text color for better user experience.
2025-08-21 17:35:03 +05:00
f796f832a8 Refactor ServicesScreen to use a dynamic services array and improve layout
- Introduced a services array to dynamically render service cards with localized titles and icons.
- Updated layout styles for a grid display of service cards, enhancing visual organization.
- Added new localization keys for services in English, Russian, and Turkmen.
2025-08-20 18:43:05 +05:00
b5d3133c24 Enhance ServicesScreen with Currency Converter Modal and localization updates
- Added a CurrencyConverterModal to the ServicesScreen, triggered by a new TouchableOpacity around the service card for currency calculation.
- Updated localization file to include a new key for the currency converter.
2025-08-20 18:34:31 +05:00
22 changed files with 1198 additions and 105 deletions

8
Umra.code-workspace Normal file
View File

@@ -0,0 +1,8 @@
{
"folders": [
{
"path": "."
}
],
"settings": {}
}

View File

@@ -26,7 +26,7 @@ export default function TabLayout() {
backgroundColor: Colors[colorScheme].secondary,
borderTopColor: Colors[colorScheme].secondary,
},
headerShown: false, // Hide header globally for tabs
headerShown: false, // hide header globally for tabs
}}>
<Tabs.Screen
name="home"
@@ -42,11 +42,18 @@ export default function TabLayout() {
tabBarIcon: ({ color }) => <TabBarIcon name="th-large" color={color} />,
}}
/>
<Tabs.Screen
name="index"
options={{
title: i18n.t('menuSalah'),
tabBarIcon: ({ color }) => <TabBarIcon name="moon-o" color={color} />,
}}
/>
<Tabs.Screen
name="programs"
options={{
title: i18n.t('programs'),
tabBarIcon: ({ color }) => <TabBarIcon name="briefcase" color={color} />,
title: i18n.t('Programs'),
tabBarIcon: ({ color }) => <TabBarIcon name="calendar" color={color} />,
}}
/>
</Tabs>

View File

@@ -31,14 +31,22 @@ export default function HomeScreen() {
const currentLanguage = languages.find(l => l.value === i18n.locale.substring(0, 2))?.label;
const services = [
{ name: 'quran', icon: 'book-open-variant' },
{ name: 'hadith', icon: 'book-open-page-variant' },
{ name: 'dua', icon: 'human-greeting' },
];
return (
<SafeAreaView style={styles.container}>
<View style={styles.header}>
<Text style={styles.title}>{i18n.t('home')}</Text>
<Pressable onPress={() => setModalVisible(true)} style={pickerSelectStyles.inputIOS}>
<Text style={{ color: 'white' }}>{currentLanguage}</Text>
</Pressable>
</View>
<Modal
animationType="slide"
transparent={true}
@@ -69,7 +77,8 @@ export default function HomeScreen() {
/>
<PrayerTimeCard />
<ServicesGrid />
{/* <ServicesGrid services={services} /> */}
</View>
</ScrollView>
</SafeAreaView>
@@ -85,6 +94,7 @@ const styles = StyleSheet.create({
paddingHorizontal: 15,
},
header: {
display: 'none', // hide for now, will show later
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',

View File

@@ -1,5 +1,179 @@
import { Redirect } from 'expo-router';
import { Pressable, SafeAreaView, ScrollView, StyleSheet, Text, View } from 'react-native';
import { useCallback, useEffect, useState } from 'react';
import { getPrayerTimes, cities } from '../../utils/prayerTimeCalculator';
import i18n from '../../i18n';
import { useColorScheme } from 'react-native';
import Colors from '../../constants/Colors';
type Prayer = {
name: string;
time: string;
};
type City = keyof typeof cities;
export default function TabIndex() {
return <Redirect href="/(tabs)/home" />;
const colorScheme = useColorScheme();
const theme = Colors[colorScheme ?? 'light'];
const [selectedCity, setSelectedCity] = useState<City>('Makkah');
const [prayerTimes, setPrayerTimes] = useState<Prayer[]>([]);
const [nextPrayerName, setNextPrayerName] = useState<string | null>(null);
const prayerNameMapping: { [key: string]: string } = {
fajr: i18n.t('fajr'),
dhuhr: i18n.t('dhuhr'),
asr: i18n.t('asr'),
maghrib: i18n.t('maghrib'),
isha: i18n.t('isha'),
};
const updatePrayerTimes = useCallback(() => {
const times = getPrayerTimes(selectedCity);
const prayers: Prayer[] = Object.keys(prayerNameMapping).map((key) => ({
name: prayerNameMapping[key],
time: times[key] || '-----',
}));
setPrayerTimes(prayers);
const now = new Date();
let nextPrayer: Prayer | null = null;
for (const prayer of prayers) {
if (prayer.time === '-----') continue;
const [hours, minutes] = prayer.time.split(':').map(Number);
const prayerDate = new Date(now.getFullYear(), now.getMonth(), now.getDate(), hours, minutes);
if (prayerDate > now) {
nextPrayer = prayer;
break;
}
}
if (!nextPrayer && prayers.length > 0) {
const firstPrayer = prayers.find((p) => p.time !== '-----');
if (firstPrayer) {
nextPrayer = firstPrayer;
}
}
setNextPrayerName(nextPrayer ? nextPrayer.name : null);
}, [selectedCity]);
useEffect(() => {
updatePrayerTimes();
const interval = setInterval(updatePrayerTimes, 60000); // Update every minute
return () => clearInterval(interval);
}, [updatePrayerTimes]);
const renderPrayerTime = (prayer: Prayer) => {
const isNextPrayer = prayer.name === nextPrayerName;
return (
<View
key={prayer.name}
style={[styles.prayerRow, { backgroundColor: theme.secondary }, isNextPrayer && [styles.nextPrayerRow, { backgroundColor: theme.tint }]]}>
<View>
<Text style={[styles.prayerName, { color: theme.text }, isNextPrayer && styles.nextPrayerText]}>
{prayer.name}
</Text>
</View>
<Text style={[styles.prayerTime, { color: theme.text }, isNextPrayer && styles.nextPrayerTimeText]}>
{new Date(`1970-01-01T${prayer.time}:00`).toLocaleTimeString('en-US', {
hour: 'numeric',
minute: 'numeric',
hour12: false,
})}
</Text>
</View>
);
};
return (
<SafeAreaView style={[styles.container, { backgroundColor: theme.background }]}>
<View style={[styles.citySelector, { backgroundColor: theme.background }]}>
{(Object.keys(cities) as City[]).map((city) => (
<Pressable
key={city}
onPress={() => setSelectedCity(city)}
style={[styles.cityButton, { backgroundColor: theme.secondary }, selectedCity === city && [styles.activeCityButton, { backgroundColor: theme.tint }]]}>
<Text
style={[
styles.cityButtonText,
{ color: theme.text },
selectedCity === city && styles.activeCityButtonText,
]}>
{i18n.t(city)}
</Text>
</Pressable>
))}
</View>
<ScrollView contentContainerStyle={styles.listContainer}>
{prayerTimes.map(renderPrayerTime)}
</ScrollView>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
citySelector: {
flexDirection: 'row',
justifyContent: 'center',
paddingVertical: 20,
paddingHorizontal: 10,
},
cityButton: {
paddingVertical: 10,
paddingHorizontal: 25,
borderRadius: 20,
marginHorizontal: 5,
},
activeCityButton: {},
cityButtonText: {
fontSize: 18,
fontWeight: '500',
},
activeCityButtonText: {
color: '#fff',
},
listContainer: {
paddingHorizontal: 20,
},
prayerRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingVertical: 25,
paddingHorizontal: 20,
borderRadius: 15,
marginBottom: 10,
shadowColor: '#000',
shadowOffset: {
width: 0,
height: 1,
},
shadowOpacity: 0.1,
shadowRadius: 3,
elevation: 2,
},
nextPrayerRow: {},
prayerName: {
fontSize: 28,
fontWeight: '600',
},
inCityText: {
fontSize: 18,
},
prayerTime: {
fontSize: 28,
fontWeight: 'bold',
},
nextPrayerText: {
color: '#fff',
},
nextPrayerTimeText: {
color: '#fff',
fontSize: 32,
},
});

View File

@@ -1,20 +1,74 @@
import { StyleSheet, SafeAreaView, FlatList, View } from 'react-native';
import { StyleSheet, SafeAreaView, View, TouchableOpacity, Dimensions } from 'react-native';
import { Text } from '@/components/Themed';
import i18n from '@/i18n';
import { FontAwesome5 } from '@expo/vector-icons';
import ServiceCard from '@/components/ServiceCard';
import React, { useState } from 'react';
import CurrencyConverterModal from '@/components/CurrencyConverterModal';
import HotelBusinessCardModal from '@/components/HotelBusinessCardModal';
import LostKeyModal from '@/components/LostKeyModal';
import PhrasebookModal from '@/components/PhrasebookModal';
export default function ServicesScreen() {
const [currencyModalVisible, setCurrencyModalVisible] = useState(false);
const [hotelModalVisible, setHotelModalVisible] = useState(false);
const [lostKeyModalVisible, setLostKeyModalVisible] = useState(false);
const [phrasebookModalVisible, setPhrasebookModalVisible] = useState(false);
const services = [
{
title: i18n.t('Money'),
name: 'currencyConverter',
icon: <FontAwesome5 name="dollar-sign" size={24} color="#D4AF37" />,
onPress: () => setCurrencyModalVisible(true),
},
{
title: i18n.t('Hotel'),
name: 'hotelCard',
icon: <FontAwesome5 name="hotel" size={24} color="#D4AF37" />,
onPress: () => setHotelModalVisible(true),
},
{
title: i18n.t('Lost room key'),
name: 'lostKey',
icon: <FontAwesome5 name="key" size={24} color="#D4AF37" />,
onPress: () => setLostKeyModalVisible(true),
},
{
title: i18n.t('Phrasebook'),
name: 'phrasebook',
icon: <FontAwesome5 name="book" size={24} color="#D4AF37" />,
onPress: () => setPhrasebookModalVisible(true),
},
];
return (
<SafeAreaView style={styles.container}>
<Text style={styles.title}>{i18n.t('services')}</Text>
<View style={styles.row}>
<ServiceCard title="Manat kursy" icon={<FontAwesome5 name="dollar-sign" size={24} color="#D4AF37" />} />
<ServiceCard title="Oteliň wizitkasy" icon={<FontAwesome5 name="hotel" size={24} color="#D4AF37" />} />
<ServiceCard title="Oteliň açary içinde galsa" icon={<FontAwesome5 name="key" size={24} color="#D4AF37" />} />
<ServiceCard title="Terjimeçi" icon={<FontAwesome5 name="language" size={24} color="#D4AF37" />} />
<View style={styles.grid}>
{services.map((service, index) => (
<TouchableOpacity key={index} style={styles.cardContainer} onPress={service.onPress}>
<ServiceCard title={service.title} icon={service.icon} />
</TouchableOpacity>
))}
</View>
<CurrencyConverterModal
visible={currencyModalVisible}
onClose={() => setCurrencyModalVisible(false)}
/>
<HotelBusinessCardModal
visible={hotelModalVisible}
onClose={() => setHotelModalVisible(false)}
/>
<LostKeyModal
visible={lostKeyModalVisible}
onClose={() => setLostKeyModalVisible(false)}
/>
<PhrasebookModal
visible={phrasebookModalVisible}
onClose={() => setPhrasebookModalVisible(false)}
/>
</SafeAreaView>
);
}
@@ -22,7 +76,6 @@ export default function ServicesScreen() {
const styles = StyleSheet.create({
container: {
flex: 1,
paddingHorizontal: 15,
},
title: {
fontSize: 22,
@@ -30,11 +83,13 @@ const styles = StyleSheet.create({
marginVertical: 15,
marginLeft: 15,
},
row: {
flex: 1,
justifyContent: 'space-around',
grid: {
flexDirection: 'row',
flexWrap: 'wrap',
rowGap: 0,
marginLeft: 15,
},
cardContainer: {
width: '50%',
marginBottom: 15,
},
});

View File

@@ -5,8 +5,11 @@ import { Stack } from 'expo-router';
import * as SplashScreen from 'expo-splash-screen';
import React, { useEffect } from 'react';
import 'react-native-reanimated';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { initializeLanguage } from '@/i18n';
import { makeRequest } from '@/utils/makeRequest';
import { CURRENCY_RATES_ENDPOINT } from '@/utils/api';
export {
// Catch any errors thrown by the Layout component.
@@ -40,6 +43,10 @@ export default function RootLayout() {
});
}, []);
// useEffect(() => {
// }, []);
useEffect(() => {
if (loaded && langLoaded) {
SplashScreen.hideAsync();

View File

@@ -0,0 +1,126 @@
import React, { useState } from 'react';
import { View, Text, TextInput, Modal, StyleSheet, TouchableOpacity, KeyboardAvoidingView, Platform } from 'react-native';
import { FontAwesome } from '@expo/vector-icons';
import i18n from '@/i18n';
interface CurrencyConverterModalProps {
visible: boolean;
onClose: () => void;
}
const CurrencyConverterModal: React.FC<CurrencyConverterModalProps> = ({ visible, onClose }) => {
const [sar, setSar] = useState('');
const [tmt, setTmt] = useState('');
const USD_TO_SAR = '3.70';
const USD_TO_TMT = '19.5';
const handleSarChange = (value: string) => {
setSar(value);
if (value) {
const tmtValue = ((Number(value) / Number(USD_TO_SAR)) * Number(USD_TO_TMT)).toFixed(2);
setTmt(tmtValue);
} else {
setTmt('');
}
};
return (
<Modal
animationType="slide"
transparent={true}
visible={visible}
onRequestClose={onClose}
>
<KeyboardAvoidingView
behavior={Platform.OS === "ios" ? "padding" : "height"}
style={styles.centeredView}
>
<View style={styles.modalView}>
<TouchableOpacity style={styles.closeButton} onPress={onClose}>
<FontAwesome name="close" size={24} color="black" />
</TouchableOpacity>
<Text style={styles.modalTitle}>{i18n.t('currencyConverter')}</Text>
<View style={styles.inputContainer}>
<Text style={styles.currencyLabel}>Riyal</Text>
<TextInput
style={styles.input}
keyboardType="numeric"
placeholder="0.00"
placeholderTextColor="#666"
value={sar}
onChangeText={handleSarChange}
/>
</View>
<View style={styles.inputContainer}>
<Text style={styles.currencyLabel}>Manat</Text>
<Text style={styles.resultText}>{tmt || '0.00'}</Text>
</View>
</View>
</KeyboardAvoidingView>
</Modal>
);
};
const styles = StyleSheet.create({
centeredView: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'rgba(0,0,0,0.5)',
},
modalView: {
margin: 20,
backgroundColor: 'white',
borderRadius: 20,
padding: 35,
alignItems: 'center',
shadowColor: '#000',
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.25,
shadowRadius: 4,
elevation: 5,
width: '80%',
},
closeButton: {
position: 'absolute',
right: 15,
top: 15,
},
modalTitle: {
fontSize: 20,
fontWeight: 'bold',
marginBottom: 20,
},
inputContainer: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 15,
width: '100%',
},
currencyLabel: {
fontSize: 18,
marginRight: 10,
width: 60,
},
input: {
borderBottomWidth: 1,
borderBottomColor: '#ccc',
fontSize: 18,
padding: 5,
flex: 1,
},
resultText: {
fontSize: 18,
padding: 5,
flex: 1,
}
});
export default CurrencyConverterModal;

View File

@@ -0,0 +1,77 @@
import React from 'react';
import { Modal, View, Image, StyleSheet, TouchableOpacity, Dimensions, SafeAreaView } from 'react-native';
import { FontAwesome5 } from '@expo/vector-icons';
const { width, height } = Dimensions.get('window');
interface HotelBusinessCardModalProps {
visible: boolean;
onClose: () => void;
}
const HotelBusinessCardModal: React.FC<HotelBusinessCardModalProps> = ({ visible, onClose }) => {
return (
<Modal
animationType="slide"
transparent={false}
visible={visible}
onRequestClose={onClose}
>
<SafeAreaView style={styles.container}>
<TouchableOpacity style={styles.closeButton} onPress={onClose}>
<FontAwesome5 name="times" size={30} color="#333" />
</TouchableOpacity>
<View style={styles.cardsContainer}>
<View style={styles.card}>
<Image source={require('@/assets/images/aisha.jpg')} style={styles.image} />
</View>
<View style={styles.card}>
<Image source={require('@/assets/images/aisha.jpg')} style={styles.image} />
</View>
</View>
</SafeAreaView>
</Modal>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#f0f0f0',
},
closeButton: {
position: 'absolute',
top: 50,
right: 20,
zIndex: 1,
},
cardsContainer: {
justifyContent: 'center',
alignItems: 'center',
width: '100%',
},
card: {
backgroundColor: '#e3e3e3',
borderRadius: 10,
padding: 10,
marginVertical: 15,
width: width * 0.8,
height: height * 0.3,
justifyContent: 'center',
alignItems: 'center',
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.25,
shadowRadius: 3.84,
elevation: 5,
},
image: {
width: '100%',
height: '100%',
resizeMode: 'contain',
},
});
export default HotelBusinessCardModal;

View File

@@ -0,0 +1,83 @@
import { FontAwesome } from '@expo/vector-icons';
import React from 'react';
import { Modal, View, Text, StyleSheet, TouchableOpacity, Dimensions } from 'react-native';
const { width, height } = Dimensions.get('window');
interface LostKeyModalProps {
visible: boolean;
onClose: () => void;
}
const LostKeyModal: React.FC<LostKeyModalProps> = ({ visible, onClose }) => {
return (
<Modal
animationType="fade"
transparent={true}
visible={visible}
onRequestClose={onClose}
>
<TouchableOpacity style={styles.overlay} activeOpacity={1} onPress={onClose}>
<View style={styles.modalContainer}>
<TouchableOpacity style={styles.closeButton} onPress={onClose}>
<FontAwesome name="close" size={24} color="black" />
</TouchableOpacity>
<Text style={styles.smallText} >Aşak resepşyna şul aşakdaky haty görkeziň</Text>
<Text
numberOfLines={1}
adjustsFontSizeToFit
style={styles.text}
>Mastercard</Text>
<Text style={[styles.text, styles.arabicText]}>ماستركارد</Text>
</View>
</TouchableOpacity>
</Modal>
);
};
const styles = StyleSheet.create({
overlay: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'rgba(0, 0, 0, 0.5)',
},
modalContainer: {
width: width * 0.9,
padding: 20,
backgroundColor: 'white',
borderRadius: 10,
alignItems: 'center',
shadowColor: '#000',
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.25,
shadowRadius: 3.84,
elevation: 5,
},
closeButton: {
position: 'absolute',
top: 15,
right: 15,
zIndex: 1,
},
text: {
fontSize: 40,
marginBottom: 10,
},
smallText: {
marginTop: 15,
fontSize: 20,
color: '#666',
textAlign: 'center',
},
arabicText: {
fontFamily: 'System',
writingDirection: 'rtl',
}
});
export default LostKeyModal;

View File

@@ -0,0 +1,132 @@
import React from 'react';
import { Modal, View, Text, StyleSheet, TouchableOpacity, FlatList, SafeAreaView } from 'react-native';
import { View as ThemedView } from './Themed';
import { FontAwesome } from '@expo/vector-icons';
import i18n from '@/i18n';
type PhrasebookModalProps = {
visible: boolean;
onClose: () => void;
};
const PHRASES = [
{ tk: 'Bu näçe?', ar: 'بكم هذا؟ (Bikam hadha?)' },
{ tk: 'Arzanladyň', ar: 'تَخْفيض Takfidun' },
{ tk: 'Salam', ar: 'مرحبا (Marhaban)' },
{ tk: 'Hawa', ar: 'نعم (Na\'am)' },
{ tk: 'Ýok', ar: 'لا (La)' },
{ tk: 'Sag boluň', ar: 'شكرا (Shukran)' },
{ tk: 'Minnetdar', ar: 'شكرا جزيلا (Shukran Gazilan)' },
{ tk: 'Haýyş edýärin', ar: 'من فضلك (Min fadlik)' },
{ tk: 'Bagyşlaň', ar: 'آسف (Asif)' },
{ tk: 'Men size nähili kömek edip bilerin?', ar: 'كيف يمكنني مساعدتك؟ (Kayfa yumkinuni musa\'adatuk?)' },
{ tk: 'Hajathana nirede?', ar: 'أين الحمام؟ (Ayna al-hammam?)' },
{ tk: 'Men ýolumy ýitirdim', ar: 'لقد ضللت طريقي (Laqad dalalt tariqi)' },
{ tk: 'Lukman çagyryň', ar: 'اتصل بطبيب (Ittasil bi-tabib)' },
];
const PhrasebookModal = ({ visible, onClose }: PhrasebookModalProps) => {
const [expandedIndex, setExpandedIndex] = React.useState<number | null>(null);
const renderItem = ({ item, index }: { item: { tk: string; ar: string }; index: number }) => {
const isExpanded = index === expandedIndex;
return (
<TouchableOpacity onPress={() => setExpandedIndex(isExpanded ? null : index)}>
<View style={styles.phraseItem}>
<Text style={styles.turkmenText}>{item.tk}</Text>
{isExpanded && <Text style={styles.arabicText}>{item.ar}</Text>}
</View>
</TouchableOpacity>
);
};
return (
<Modal
animationType="slide"
transparent={true}
visible={visible}
onRequestClose={onClose}
>
<ThemedView style={styles.centeredView}>
<View style={styles.modalView}>
<TouchableOpacity style={styles.closeButton} onPress={onClose}>
<FontAwesome name="close" size={24} color="black" />
</TouchableOpacity>
<Text style={styles.modalTitle}>{i18n.t('Phrasebook')}</Text>
<SafeAreaView style={styles.listContainer}>
<FlatList
data={PHRASES}
renderItem={renderItem}
keyExtractor={(item, index) => index.toString()}
ItemSeparatorComponent={() => <View style={styles.separator} />}
extraData={expandedIndex}
/>
</SafeAreaView>
</View>
</ThemedView>
</Modal>
);
};
const styles = StyleSheet.create({
centeredView: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'rgba(0,0,0,0.5)',
},
modalView: {
margin: 20,
backgroundColor: 'white',
borderRadius: 20,
padding: 20,
paddingTop: 40,
alignItems: 'center',
shadowColor: '#000',
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.25,
shadowRadius: 4,
elevation: 5,
width: '90%',
height: '80%',
},
closeButton: {
position: 'absolute',
top: 15,
right: 15,
zIndex: 1,
},
modalTitle: {
fontSize: 20,
fontWeight: 'bold',
marginBottom: 15,
},
listContainer: {
width: '100%',
flex: 1,
},
phraseItem: {
paddingVertical: 15,
paddingHorizontal: 10,
},
turkmenText: {
fontSize: 16,
marginBottom: 5,
},
arabicText: {
fontSize: 18,
textAlign: 'right',
color: '#333',
},
separator: {
height: 1,
backgroundColor: '#eee',
width: '100%',
},
});
export default PhrasebookModal;

View File

@@ -25,14 +25,14 @@ const styles = StyleSheet.create({
borderRadius: 15,
padding: 15,
marginVertical: 10,
width: '45%',
aspectRatio: 1,
justifyContent: 'center',
alignItems: 'center',
aspectRatio: 1,
},
content: {
justifyContent: 'center',
alignItems: 'center',
flex: 1,
},
iconContainer: {
marginBottom: 10,

View File

@@ -1,34 +1,42 @@
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
import Colors from '@/constants/Colors';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import { router } from 'expo-router';
import i18n from '@/i18n';
const services = [
{ name: 'quran', icon: 'book-open-variant' },
{ name: 'hadith', icon: 'book-open-page-variant' },
{ name: 'dua', icon: 'human-greeting' },
];
export default function ServicesGrid() {
const colorScheme = 'dark';
type ServicesGridProps = {
services: {
name: string;
icon: any;
}[];
};
const ServicesGrid = ({ services }: ServicesGridProps) => {
const handlePress = (name: string) => {
if (name === 'quran') {
}
};
return (
<View style={styles.container}>
<Text style={styles.title}>{i18n.t('servicesToEnrich')}</Text>
<View style={styles.grid}>
{services.map((service, index) => (
<View key={index} style={styles.serviceItem}>
<View style={[styles.iconContainer, { backgroundColor: Colors[colorScheme].secondary }]}>
<MaterialCommunityIcons name={service.icon} size={30} color={Colors[colorScheme].tint} />
{services.map((service) => (
<TouchableOpacity
key={service.name}
style={styles.serviceItem}
onPress={() => handlePress(service.name)}>
<View style={[styles.iconContainer, { backgroundColor: Colors['dark'].secondary }]}>
<MaterialCommunityIcons name={service.icon} size={30} color={Colors['dark'].tint} />
</View>
<Text style={styles.serviceName}>{i18n.t(service.name)}</Text>
</View>
</TouchableOpacity>
))}
</View>
</View>
);
}
};
const styles = StyleSheet.create({
container: {
@@ -37,8 +45,8 @@ const styles = StyleSheet.create({
title: {
fontSize: 18,
fontWeight: 'bold',
color: Colors.dark.text,
marginBottom: 15,
color: 'white',
marginBottom: 10,
},
grid: {
flexDirection: 'row',
@@ -50,13 +58,15 @@ const styles = StyleSheet.create({
iconContainer: {
width: 60,
height: 60,
borderRadius: 15,
borderRadius: 30,
justifyContent: 'center',
alignItems: 'center',
marginBottom: 10,
marginBottom: 5,
},
serviceName: {
color: Colors.dark.text,
fontSize: 14,
color: 'white',
textAlign: 'center',
},
});
export default ServicesGrid;

View File

@@ -0,0 +1,192 @@
import React, { useState } from 'react';
import { Modal, View, Text, TextInput, Button, StyleSheet, TouchableOpacity, KeyboardAvoidingView, Platform, ScrollView } from 'react-native';
import { View as ThemedView } from './Themed';
import { FontAwesome } from '@expo/vector-icons';
import i18n from '@/i18n';
type TranslatorModalProps = {
visible: boolean;
onClose: () => void;
};
const TranslatorModal = ({ visible, onClose }: TranslatorModalProps) => {
const [turkmenText, setTurkmenText] = useState('');
const [arabicText, setArabicText] = useState('');
const handleTranslate = async () => {
if (turkmenText.trim() === '') {
setArabicText('');
return;
}
// --- Replace this with your Google Cloud API Key ---
const API_KEY = 'YOUR_GOOGLE_CLOUD_API_KEY';
const API_URL = `https://translation.googleapis.com/language/translate/v2?key=${API_KEY}`;
try {
const response = await fetch(API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
q: turkmenText,
source: 'tk',
target: 'ar',
format: 'text',
}),
});
const json = await response.json();
if (json.data && json.data.translations && json.data.translations.length > 0) {
setArabicText(json.data.translations[0].translatedText);
} else {
throw new Error('Invalid response from translation API');
}
} catch (error) {
console.error("Translation error:", error);
setArabicText(i18n.t("Error: Could not translate."));
}
};
return (
<Modal
animationType="slide"
transparent={true}
visible={visible}
onRequestClose={onClose}
>
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={styles.keyboardAvoidingContainer}
>
<ThemedView style={styles.centeredView}>
<ScrollView contentContainerStyle={styles.scrollViewContent}>
<View style={styles.modalView}>
<TouchableOpacity style={styles.closeButton} onPress={onClose}>
<FontAwesome name="close" size={24} color="black" />
</TouchableOpacity>
<Text style={styles.modalTitle}>{i18n.t('Translator')}</Text>
<View style={styles.inputContainer}>
<Text style={styles.label}>Türkmençe</Text>
<TextInput
style={[styles.textInput, styles.turkmenInput]}
placeholder="Hat yaz"
placeholderTextColor="#888" // slightly grey
onChangeText={setTurkmenText}
value={turkmenText}
multiline
/>
</View>
<View style={styles.inputContainer}>
<Text style={styles.label}>Arapça</Text>
<View style={styles.arabicOutputContainer}>
<Text style={styles.arabicText}>{arabicText}</Text>
</View>
</View>
<TouchableOpacity onPress={handleTranslate} style={styles.translateButton}>
<Text style={styles.translateButtonText}>{i18n.t('Translate')}</Text>
</TouchableOpacity>
</View>
</ScrollView>
</ThemedView>
</KeyboardAvoidingView>
</Modal>
);
};
const styles = StyleSheet.create({
keyboardAvoidingContainer: {
flex: 1,
width: '100%',
},
centeredView: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'rgba(0,0,0,0.5)',
width: '100%',
},
scrollViewContent: {
flexGrow: 1,
justifyContent: 'center',
alignItems: 'center',
width: '100%',
},
modalView: {
margin: 20,
backgroundColor: 'white',
borderRadius: 20,
padding: 35,
alignItems: 'center',
shadowColor: '#000',
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.25,
shadowRadius: 4,
elevation: 5,
width: '100%',
minWidth: '90%',
},
closeButton: {
position: 'absolute',
top: 10,
right: 10,
},
modalTitle: {
fontSize: 20,
fontWeight: 'bold',
marginBottom: 20,
},
inputContainer: {
marginBottom: 15,
minWidth: '90%',
},
label: {
fontSize: 16,
marginBottom: 5,
color: '#333',
},
textInput: {
borderWidth: 1,
borderColor: '#ddd',
borderRadius: 10,
padding: 10,
fontSize: 16,
minWidth: '90%',
},
turkmenInput: {
height: 100,
textAlignVertical: 'top',
},
arabicOutputContainer: {
height: 100,
borderWidth: 1,
borderColor: '#ddd',
borderRadius: 10,
padding: 10,
backgroundColor: '#f9f9f9',
justifyContent: 'center',
},
arabicText: {
fontSize: 18,
textAlign: 'right', // For RTL text
},
translateButton: {
marginTop: 15,
},
translateButtonText: {
color: '#D4AF37',
fontSize: 18,
fontWeight: '500',
},
});
export default TranslatorModal;

View File

@@ -1 +1,6 @@
export { useColorScheme } from 'react-native';
// The useColorScheme value is always either light or dark, but the built-in
// type suggests that it can be null. This will not happen in practice, so this
// makes it a bit easier to work with.
export function useColorScheme(): 'light' | 'dark' {
return 'dark';
}

View File

@@ -10,13 +10,27 @@
"newExperience": "New Experience",
"prayerTimes": "Prayer Times",
"programs": "Programs",
"Programs": "Schedule",
"leftOnPrayer": "Left on {{prayerName}} prayer",
"servicesToEnrich": "Services to Enrich Your Spiritual Experience",
"quran": "Qur'an",
"hadith": "Hadith",
"dua": "Du'a",
"dua": "Dua",
"currencyConverter": "Currency Converter",
"hotelCard": "Hotel Card",
"lostKey": "Lost Key?",
"translator": "Translator",
"adhkar": "Adhkar",
"hisnAlMuslim": "Hisn Al-Muslim",
"Makkah": "Makkah",
"Medina": "Medina",
"Jeddah": "Jeddah",
"fajr": "Fajr",
"sunrise": "Sunrise",
"dhuhr": "Dhuhr",
"asr": "Asr",
"maghrib": "Maghrib",
"isha": "Isha",
"morningEveningThikr": "Thikr said in the morning and evening",
"beforeSleepingThikr": "Thikr before sleeping",
"afterSalamThikr": "Thikr after salam",
@@ -28,5 +42,12 @@
"sarToTmt": "SAR to TMT",
"hotelBusinessCard": "Hotel Business Card",
"masterkeyBox": "Masterkey Box",
"translator": "Translator"
"Money": "Money",
"Hotel": "Hotel",
"Lost room key": "Lost room key",
"Phrasebook": "Phrasebook",
"Enter text in Turkmen": "Enter text",
"Translate": "Translate",
"Salah": "Salah",
"menuSalah": "Salah"
}

View File

@@ -1,6 +1,7 @@
{
"home": "Главная",
"services": "Сервисы",
"supplications": "Молитвы",
"yourJourneyToHajj": "Ваше путешествие в Хадж",
"hajjEssentials": "Все, что вам нужно для Хаджа.",
"umrah": "Умра",
@@ -9,13 +10,27 @@
"newExperience": "Новый опыт",
"prayerTimes": "Время молитв",
"programs": "Программы",
"Programs": "Расписание",
"leftOnPrayer": "Осталось до молитвы {{prayerName}}",
"servicesToEnrich": "Услуги для обогащения вашего духовного опыта",
"quran": "Коран",
"hadith": "Хадисы",
"hadith": "Хадис",
"dua": "Дуа",
"currencyConverter": "Конвертер валют",
"hotelCard": "Карта отеля",
"lostKey": "Потеряли ключ?",
"translator": "Переводчик",
"adhkar": "Азкар",
"hisnAlMuslim": "Крепость мусульманина",
"Makkah": "Мекка",
"Medina": "Медина",
"Jeddah": "Джидда",
"fajr": "Фаджр",
"sunrise": "Восход",
"dhuhr": "Зухр",
"asr": "Аср",
"maghrib": "Магриб",
"isha": "Иша",
"morningEveningThikr": "Зикр, читаемый утром и вечером",
"beforeSleepingThikr": "Зикр перед сном",
"afterSalamThikr": "Зикр после салама",
@@ -27,5 +42,12 @@
"sarToTmt": "SAR в TMT",
"hotelBusinessCard": "Визитная карточка отеля",
"masterkeyBox": "Ящик для мастер-ключей",
"translator": "Переводчик"
"Money": "Деньги",
"Hotel": "Отель",
"Lost room key": "Ключ от номера утерян",
"Phrasebook": "Разговорник",
"Enter text in Turkmen": "Введите текст",
"Translate": "Перевести",
"Salah": "Намаз",
"menuSalah": "Намаз"
}

View File

@@ -1,5 +1,5 @@
{
"home": "Baş sahypa",
"home": "Öý",
"services": "Hyzmatlar",
"supplications": "Dogalar",
"yourJourneyToHajj": "Haj syýahatyňyz",
@@ -10,11 +10,16 @@
"newExperience": "Täze tejribe",
"prayerTimes": "Namaz wagtlary",
"programs": "Programmalar",
"Programs": "Respisaniýa",
"leftOnPrayer": "{{prayerName}} namazyna çenli galdy",
"servicesToEnrich": "Ruhy tejribäňizi baýlaşdyrmak üçin hyzmatlar",
"quran": "Kuran",
"hadith": "Hadys",
"dua": "Doga",
"currencyConverter": "Walýuta hasaplaýjy",
"hotelCard": "Myhmanhana kartasy",
"lostKey": "Açaryňyzy ýitirdiňizmi?",
"translator": "Terjimeçi",
"adhkar": "Zikir",
"hisnAlMuslim": "Musulmanyň goragy",
"Makkah": "Mekke",
@@ -35,7 +40,13 @@
"seeingFruitSupplication": "Irki ýa-da bişmedik miwäni göreniňde aýdylýan doga",
"sneezingSupplication": "Asgyranyňda aýdylýan doga",
"sarToTmt": "SAR-dan TMT-a",
"hotelBusinessCard": "Myhmanhananyň wizit karty",
"masterkeyBox": "Masterkey gutusy",
"translator": "Terjimeçi"
"Money": "Pul",
"Hotel": "Otel",
"Lost room key": "Açar otagyň içinde galdy",
"Translator": "Perewod",
"Phrasebook": "Sözlük",
"Enter text in Turkmen": "Türkmençe sözleri gir",
"Translate": "Terjime et",
"Salah": "Namaz",
"menuSalah": "Namaz"
}

205
package-lock.json generated
View File

@@ -13,10 +13,12 @@
"@react-navigation/native": "^7.1.6",
"adhan": "^4.4.3",
"expo": "~53.0.20",
"expo-file-system": "~18.1.11",
"expo-font": "~13.3.2",
"expo-linking": "~7.1.7",
"expo-localization": "^16.1.6",
"expo-router": "~5.1.4",
"expo-sharing": "~13.1.5",
"expo-splash-screen": "~0.30.10",
"expo-status-bar": "~2.2.3",
"expo-system-ui": "~5.0.10",
@@ -24,17 +26,15 @@
"expo-web-browser": "~14.2.0",
"i18n-js": "^4.5.1",
"react": "19.0.0",
"react-dom": "19.0.0",
"react-native": "0.79.5",
"react-native-picker-select": "^9.3.1",
"react-native-reanimated": "~3.17.4",
"react-native-safe-area-context": "5.4.0",
"react-native-screens": "~4.11.1",
"react-native-vector-icons": "^10.3.0",
"react-native-web": "~0.20.0"
"react-native-vector-icons": "^10.3.0"
},
"devDependencies": {
"@babel/core": "^7.25.2",
"@babel/core": "^7.20.0",
"@types/react": "~19.0.10",
"@types/react-native-vector-icons": "^6.4.18",
"jest": "^29.2.1",
@@ -4349,6 +4349,8 @@
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.2.0.tgz",
"integrity": "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==",
"optional": true,
"peer": true,
"dependencies": {
"node-fetch": "^2.7.0"
}
@@ -4378,6 +4380,8 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-3.1.0.tgz",
"integrity": "sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A==",
"optional": true,
"peer": true,
"dependencies": {
"hyphenate-style-name": "^1.0.3"
}
@@ -5179,6 +5183,14 @@
"node": ">=10"
}
},
"node_modules/expo-sharing": {
"version": "13.1.5",
"resolved": "https://registry.npmjs.org/expo-sharing/-/expo-sharing-13.1.5.tgz",
"integrity": "sha512-X/5sAEiWXL2kdoGE3NO5KmbfcmaCWuWVZXHu8OQef7Yig4ZgHFkGD11HKJ5KqDrDg+SRZe4ISd6MxE7vGUgm4w==",
"peerDependencies": {
"expo": "*"
}
},
"node_modules/expo-splash-screen": {
"version": "0.30.10",
"resolved": "https://registry.npmjs.org/expo-splash-screen/-/expo-splash-screen-0.30.10.tgz",
@@ -5291,6 +5303,13 @@
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="
},
"node_modules/fast-loops": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/fast-loops/-/fast-loops-1.1.4.tgz",
"integrity": "sha512-8dbd3XWoKCTms18ize6JmQF1SFnnfj5s0B7rRry22EofgMu7B6LKHVh+XfFqFGsqnbH54xgeO83PzpKI+ODhlg==",
"optional": true,
"peer": true
},
"node_modules/fast-uri": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz",
@@ -5318,6 +5337,8 @@
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/fbjs/-/fbjs-3.0.5.tgz",
"integrity": "sha512-ztsSx77JBtkuMrEypfhgc3cI0+0h+svqeie7xHbh1k/IKdcydnvadp/mUaGgjAOXQmQSxsqgaRhS3q9fy+1kxg==",
"optional": true,
"peer": true,
"dependencies": {
"cross-fetch": "^3.1.5",
"fbjs-css-vars": "^1.0.0",
@@ -5331,12 +5352,16 @@
"node_modules/fbjs-css-vars": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/fbjs-css-vars/-/fbjs-css-vars-1.0.2.tgz",
"integrity": "sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ=="
"integrity": "sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ==",
"optional": true,
"peer": true
},
"node_modules/fbjs/node_modules/promise": {
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
"integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==",
"optional": true,
"peer": true,
"dependencies": {
"asap": "~2.0.3"
}
@@ -5767,7 +5792,9 @@
"node_modules/hyphenate-style-name": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.1.0.tgz",
"integrity": "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw=="
"integrity": "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==",
"optional": true,
"peer": true
},
"node_modules/i18n-js": {
"version": "4.5.1",
@@ -5900,11 +5927,14 @@
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="
},
"node_modules/inline-style-prefixer": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-7.0.1.tgz",
"integrity": "sha512-lhYo5qNTQp3EvSSp3sRvXMbVQTLrvGV6DycRMJ5dm2BLMiJ30wpXKdDdgX+GmJZ5uQMucwRKHamXSst3Sj/Giw==",
"version": "6.0.4",
"resolved": "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-6.0.4.tgz",
"integrity": "sha512-FwXmZC2zbeeS7NzGjJ6pAiqRhXR0ugUShSNb6GApMl6da0/XGc4MOJsoWAywia52EEWbXNSy0pzkwz/+Y+swSg==",
"optional": true,
"peer": true,
"dependencies": {
"css-in-js-utils": "^3.1.0"
"css-in-js-utils": "^3.1.0",
"fast-loops": "^1.1.3"
}
},
"node_modules/invariant": {
@@ -6423,6 +6453,55 @@
"react-native": "*"
}
},
"node_modules/jest-expo/node_modules/react": {
"version": "19.1.1",
"resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz",
"integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==",
"dev": true,
"peer": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/jest-expo/node_modules/react-dom": {
"version": "19.1.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz",
"integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==",
"dev": true,
"peer": true,
"dependencies": {
"scheduler": "^0.26.0"
},
"peerDependencies": {
"react": "^19.1.1"
}
},
"node_modules/jest-expo/node_modules/react-server-dom-webpack": {
"version": "19.0.0",
"resolved": "https://registry.npmjs.org/react-server-dom-webpack/-/react-server-dom-webpack-19.0.0.tgz",
"integrity": "sha512-hLug9KEXLc8vnU9lDNe2b2rKKDaqrp5gNiES4uyu2Up3FZfZJZmdwLFXlWzdA9gTB/6/cWduSB2K1Lfag2pSvw==",
"dev": true,
"dependencies": {
"acorn-loose": "^8.3.0",
"neo-async": "^2.6.1",
"webpack-sources": "^3.2.0"
},
"engines": {
"node": ">=0.10.0"
},
"peerDependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0",
"webpack": "^5.59.0"
}
},
"node_modules/jest-expo/node_modules/scheduler": {
"version": "0.26.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz",
"integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==",
"dev": true,
"peer": true
},
"node_modules/jest-get-type": {
"version": "29.6.3",
"resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz",
@@ -8058,6 +8137,8 @@
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"optional": true,
"peer": true,
"dependencies": {
"whatwg-url": "^5.0.0"
},
@@ -8076,17 +8157,23 @@
"node_modules/node-fetch/node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"optional": true,
"peer": true
},
"node_modules/node-fetch/node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"optional": true,
"peer": true
},
"node_modules/node-fetch/node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"optional": true,
"peer": true,
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
@@ -8619,7 +8706,9 @@
"node_modules/postcss-value-parser": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"optional": true,
"peer": true
},
"node_modules/pretty-bytes": {
"version": "5.6.0",
@@ -8865,14 +8954,27 @@
}
},
"node_modules/react-dom": {
"version": "19.0.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz",
"integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==",
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
"optional": true,
"peer": true,
"dependencies": {
"scheduler": "^0.25.0"
"loose-envify": "^1.1.0",
"scheduler": "^0.23.2"
},
"peerDependencies": {
"react": "^19.0.0"
"react": "^18.3.1"
}
},
"node_modules/react-dom/node_modules/scheduler": {
"version": "0.23.2",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
"optional": true,
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
}
},
"node_modules/react-fast-compare": {
@@ -9121,33 +9223,39 @@
}
},
"node_modules/react-native-web": {
"version": "0.20.0",
"resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.20.0.tgz",
"integrity": "sha512-OOSgrw+aON6R3hRosCau/xVxdLzbjEcsLysYedka0ZON4ZZe6n9xgeN9ZkoejhARM36oTlUgHIQqxGutEJ9Wxg==",
"version": "0.19.13",
"resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.19.13.tgz",
"integrity": "sha512-etv3bN8rJglrRCp/uL4p7l8QvUNUC++QwDbdZ8CB7BvZiMvsxfFIRM1j04vxNldG3uo2puRd6OSWR3ibtmc29A==",
"optional": true,
"peer": true,
"dependencies": {
"@babel/runtime": "^7.18.6",
"@react-native/normalize-colors": "^0.74.1",
"fbjs": "^3.0.4",
"inline-style-prefixer": "^7.0.1",
"inline-style-prefixer": "^6.0.1",
"memoize-one": "^6.0.0",
"nullthrows": "^1.1.1",
"postcss-value-parser": "^4.2.0",
"styleq": "^0.1.3"
},
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
"react": "^18.0.0",
"react-dom": "^18.0.0"
}
},
"node_modules/react-native-web/node_modules/@react-native/normalize-colors": {
"version": "0.74.89",
"resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.74.89.tgz",
"integrity": "sha512-qoMMXddVKVhZ8PA1AbUCk83trpd6N+1nF2A6k1i6LsQObyS92fELuk8kU/lQs6M7BsMHwqyLCpQJ1uFgNvIQXg=="
"integrity": "sha512-qoMMXddVKVhZ8PA1AbUCk83trpd6N+1nF2A6k1i6LsQObyS92fELuk8kU/lQs6M7BsMHwqyLCpQJ1uFgNvIQXg==",
"optional": true,
"peer": true
},
"node_modules/react-native-web/node_modules/memoize-one": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
"integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw=="
"integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==",
"optional": true,
"peer": true
},
"node_modules/react-native/node_modules/@react-native/virtualized-lists": {
"version": "0.79.5",
@@ -9246,25 +9354,6 @@
"node": ">=0.10.0"
}
},
"node_modules/react-server-dom-webpack": {
"version": "19.0.0",
"resolved": "https://registry.npmjs.org/react-server-dom-webpack/-/react-server-dom-webpack-19.0.0.tgz",
"integrity": "sha512-hLug9KEXLc8vnU9lDNe2b2rKKDaqrp5gNiES4uyu2Up3FZfZJZmdwLFXlWzdA9gTB/6/cWduSB2K1Lfag2pSvw==",
"dev": true,
"dependencies": {
"acorn-loose": "^8.3.0",
"neo-async": "^2.6.1",
"webpack-sources": "^3.2.0"
},
"engines": {
"node": ">=0.10.0"
},
"peerDependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0",
"webpack": "^5.59.0"
}
},
"node_modules/react-test-renderer": {
"version": "19.0.0",
"resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-19.0.0.tgz",
@@ -9780,7 +9869,9 @@
"node_modules/setimmediate": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
"optional": true,
"peer": true
},
"node_modules/setprototypeof": {
"version": "1.2.0",
@@ -10178,7 +10269,9 @@
"node_modules/styleq": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/styleq/-/styleq-0.1.3.tgz",
"integrity": "sha512-3ZUifmCDCQanjeej1f6kyl/BeP/Vae5EYkQ9iJfUm/QwZvlgnZzyflqAsAWYURdtea8Vkvswu2GrC57h3qffcA=="
"integrity": "sha512-3ZUifmCDCQanjeej1f6kyl/BeP/Vae5EYkQ9iJfUm/QwZvlgnZzyflqAsAWYURdtea8Vkvswu2GrC57h3qffcA==",
"optional": true,
"peer": true
},
"node_modules/sucrase": {
"version": "3.35.0",
@@ -10250,13 +10343,17 @@
"dev": true
},
"node_modules/tapable": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz",
"integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==",
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.3.tgz",
"integrity": "sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==",
"dev": true,
"peer": true,
"engines": {
"node": ">=6"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/webpack"
}
},
"node_modules/tar": {
@@ -10591,6 +10688,8 @@
"url": "https://github.com/sponsors/faisalman"
}
],
"optional": true,
"peer": true,
"bin": {
"ua-parser-js": "script/cli.js"
},
@@ -10838,9 +10937,9 @@
}
},
"node_modules/webpack": {
"version": "5.101.2",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.101.2.tgz",
"integrity": "sha512-4JLXU0tD6OZNVqlwzm3HGEhAHufSiyv+skb7q0d2367VDMzrU1Q/ZeepvkcHH0rZie6uqEtTQQe0OEOOluH3Mg==",
"version": "5.101.3",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.101.3.tgz",
"integrity": "sha512-7b0dTKR3Ed//AD/6kkx/o7duS8H3f1a4w3BYpIriX4BzIhjkn4teo05cptsxvLesHFKK5KObnadmCHBwGc+51A==",
"dev": true,
"peer": true,
"dependencies": {

View File

@@ -18,10 +18,12 @@
"@react-navigation/native": "^7.1.6",
"adhan": "^4.4.3",
"expo": "~53.0.20",
"expo-file-system": "~18.1.11",
"expo-font": "~13.3.2",
"expo-linking": "~7.1.7",
"expo-localization": "^16.1.6",
"expo-router": "~5.1.4",
"expo-sharing": "~13.1.5",
"expo-splash-screen": "~0.30.10",
"expo-status-bar": "~2.2.3",
"expo-system-ui": "~5.0.10",
@@ -29,14 +31,12 @@
"expo-web-browser": "~14.2.0",
"i18n-js": "^4.5.1",
"react": "19.0.0",
"react-dom": "19.0.0",
"react-native": "0.79.5",
"react-native-picker-select": "^9.3.1",
"react-native-reanimated": "~3.17.4",
"react-native-safe-area-context": "5.4.0",
"react-native-screens": "~4.11.1",
"react-native-vector-icons": "^10.3.0",
"react-native-web": "~0.19.6"
"react-native-vector-icons": "^10.3.0"
},
"devDependencies": {
"@babel/core": "^7.20.0",

3
utils/api.ts Normal file
View File

@@ -0,0 +1,3 @@
export const BASE_URL = 'http://127.0.0.1:8000/';
export const CURRENCY_RATES_ENDPOINT = 'api/v1/currency-rates';

35
utils/makeRequest.ts Normal file
View File

@@ -0,0 +1,35 @@
import { BASE_URL } from './api';
export const makeRequest = async <T,>(
endpoint: string,
options?: RequestInit
): Promise<T> => {
const url = new URL(endpoint, BASE_URL).toString();
try {
const response = await fetch(url, options);
console.log(response);
if (!response.ok) {
let errorData;
try {
errorData = await response.json();
} catch (e) {
// Ignore JSON parsing errors if the body is empty
}
const errorMessage =
errorData?.message ||
`Request failed with status ${response.status}`;
throw new Error(errorMessage);
}
if (response.status === 204) {
return null as T;
}
return response.json();
} catch (error) {
console.error('API request error:', error);
throw error;
}
};

16
utils/pdf.ts Normal file
View File

@@ -0,0 +1,16 @@
import * as FileSystem from 'expo-file-system';
import * as Linking from 'expo-linking';
import { Asset } from 'expo-asset';
export const openPdf = async () => {
// const asset = Asset.fromModule(require('../assets/pdf/quran_in_turkmen.pdf'));
// const localUri = `${FileSystem.documentDirectory}${asset.name}`;
// const fileInfo = await FileSystem.getInfoAsync(localUri);
// if (!fileInfo.exists) {
// await FileSystem.downloadAsync(asset.uri, localUri);
// }
// await Linking.openURL(localUri);
};