Compare commits

..

26 Commits

Author SHA1 Message Date
Mekan1206
2c7884b9ff Refactor HomeScreen activity state and update UI labels
- Changed state management for next day's activity to use 'dayLabel' instead of 'dateStr'.
- Updated logic to determine the day label for next day's activity.
- Modified badge text in HomeScreen to reflect the new label and improved localization for today's activity.
2025-09-20 14:24:33 +05:00
Mekan1206
fa0465ee1a Update activity rendering and styles in Programs component
- Added conditional rendering for activity time to prevent displaying empty values.
- Adjusted styles for time text and activity title to improve layout consistency.
2025-09-20 14:09:33 +05:00
Mekan1206
558698a058 Add next day activity fetching and display in HomeScreen
- Introduced state management for next day's activity in HomeScreen.
- Implemented logic to fetch and set the next day's activity based on sorted dates.
- Updated the UI to conditionally render a FeatureCard for the next day's activity details.
2025-09-20 13:42:06 +05:00
Mekan1206
ae6ccd7f53 Implement activity fetching and display in HomeScreen
- Added useEffect to fetch today's activities from the getPrograms function.
- Introduced logic to determine the next activity based on current time.
- Updated the HomeScreen to conditionally render the FeatureCard with today's activity details.
2025-09-20 13:39:40 +05:00
Mekan1206
a1871510a8 Add stale data handling in Programs component
- Enhanced the getPrograms function to return a stale flag indicating if cached data is being used.
- Implemented a warning message in the Programs component to inform users when stale data is displayed.
- Updated styles for the warning message to improve visibility.
2025-09-20 13:23:55 +05:00
Mekan1206
3ee7ec87be Implement program fetching and loading state in Programs component
- Added asynchronous fetching of program activities using getPrograms.
- Introduced loading and error states to enhance user experience.
- Refactored activity rendering to handle dynamic data and improved icon rendering logic.
2025-09-20 13:21:20 +05:00
Mekan1206
803bbfc30f Sync cities between prayers 2025-09-20 12:06:56 +05:00
Mekan1206
30dd67ecdf Prayer city saved on click 2025-09-20 11:56:07 +05:00
Mekan1206
47420f9941 Update app.json to enforce dark mode and enhance Android navigation bar styling
- Changed userInterfaceStyle from 'automatic' to 'dark' for consistent theming.
- Added androidNavigationBar configuration with background color and bar style for improved UI on Android devices.
2025-09-20 11:42:59 +05:00
Mekan1206
68affb3d4b Update app configuration and scripts for improved build process
- Added package name to app.json for better identification.
- Updated start scripts in package.json to use 'expo run' commands for Android and iOS.
2025-09-20 11:24:49 +05:00
Mekan1206
9e0e285172 Remove temporary swap file for localization, cleaning up project structure. 2025-09-17 19:36:17 +05:00
Mekan1206
c2cd61c679 Refactor localization and simplify HomeScreen functionality
- Removed English and Russian localization files, focusing solely on Turkmen.
- Simplified language initialization in i18n.ts to default to Turkmen.
- Cleaned up HomeScreen by removing modal for language selection and related state management.
2025-09-17 19:25:49 +05:00
Mekan1206
acddbf48f0 Refactor TabIndex to use fixed dark mode and comment out hotel service in ServicesScreen
- Set color scheme in TabIndex to a fixed 'dark' value for consistent theming.
- Commented out the hotel service option in ServicesScreen for potential future redesign.
2025-09-17 19:20:03 +05:00
Mekan1206
4a92077786 hide homescreen activity 2025-09-17 19:15:48 +05:00
Mekan1206
e542371268 Add expo-asset dependency and update layout for safe area insets
- Included expo-asset in app.json and package.json for asset management.
- Refactored layout components in HomeScreen, TabIndex, Programs, and ServicesScreen to utilize safe area insets for better UI on different devices.
- Updated StatusBar style in RootLayoutNav for improved visibility.
2025-09-17 19:14:09 +05:00
Mekan1206
48295de3b7 ignore android and ios 2025-09-17 18:25:33 +05:00
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
29 changed files with 3000 additions and 835 deletions

6
.gitignore vendored
View File

@@ -35,3 +35,9 @@ yarn-error.*
# typescript # typescript
*.tsbuildinfo *.tsbuildinfo
# android
android/
# ios
ios/

8
Umra.code-workspace Normal file
View File

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

View File

@@ -6,7 +6,7 @@
"orientation": "portrait", "orientation": "portrait",
"icon": "./assets/images/icon.png", "icon": "./assets/images/icon.png",
"scheme": "umra", "scheme": "umra",
"userInterfaceStyle": "automatic", "userInterfaceStyle": "dark",
"newArchEnabled": true, "newArchEnabled": true,
"splash": { "splash": {
"image": "./assets/images/splash-icon.png", "image": "./assets/images/splash-icon.png",
@@ -21,7 +21,12 @@
"foregroundImage": "./assets/images/adaptive-icon.png", "foregroundImage": "./assets/images/adaptive-icon.png",
"backgroundColor": "#ffffff" "backgroundColor": "#ffffff"
}, },
"edgeToEdgeEnabled": true "edgeToEdgeEnabled": true,
"package": "com.nurmuhammet.ali.Umra",
"androidNavigationBar": {
"backgroundColor": "#1C1C1E",
"barStyle": "light-content"
}
}, },
"web": { "web": {
"bundler": "metro", "bundler": "metro",
@@ -29,7 +34,8 @@
"favicon": "./assets/images/favicon.png" "favicon": "./assets/images/favicon.png"
}, },
"plugins": [ "plugins": [
"expo-router" "expo-router",
"expo-asset"
], ],
"experiments": { "experiments": {
"typedRoutes": true "typedRoutes": true

View File

@@ -4,6 +4,7 @@ import { useColorScheme, View } from 'react-native';
import Colors from '@/constants/Colors'; import Colors from '@/constants/Colors';
import i18n from '@/i18n'; import i18n from '@/i18n';
import { CityProvider } from '../../context/CityContext';
/** /**
* You can explore the built-in icon families and icons on the web at https://icons.expo.fyi/ * You can explore the built-in icon families and icons on the web at https://icons.expo.fyi/
@@ -19,36 +20,45 @@ export default function TabLayout() {
const colorScheme = 'dark'; // Force dark mode const colorScheme = 'dark'; // Force dark mode
return ( return (
<Tabs <CityProvider>
screenOptions={{ <Tabs
tabBarActiveTintColor: Colors[colorScheme].tint, screenOptions={{
tabBarStyle: { tabBarActiveTintColor: Colors[colorScheme].tint,
backgroundColor: Colors[colorScheme].secondary, tabBarStyle: {
borderTopColor: Colors[colorScheme].secondary, 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" <Tabs.Screen
options={{ name="home"
title: i18n.t('home'), options={{
tabBarIcon: ({ color }) => <TabBarIcon name="home" color={color} />, title: i18n.t('home'),
}} tabBarIcon: ({ color }) => <TabBarIcon name="home" color={color} />,
/> }}
<Tabs.Screen />
name="services" <Tabs.Screen
options={{ name="services"
title: i18n.t('services'), options={{
tabBarIcon: ({ color }) => <TabBarIcon name="th-large" color={color} />, title: i18n.t('services'),
}} tabBarIcon: ({ color }) => <TabBarIcon name="th-large" color={color} />,
/> }}
<Tabs.Screen />
name="programs" <Tabs.Screen
options={{ name="index"
title: i18n.t('programs'), options={{
tabBarIcon: ({ color }) => <TabBarIcon name="briefcase" color={color} />, title: i18n.t('menuSalah'),
}} tabBarIcon: ({ color }) => <TabBarIcon name="moon-o" color={color} />,
/> }}
</Tabs> />
<Tabs.Screen
name="programs"
options={{
title: i18n.t('Programs'),
tabBarIcon: ({ color }) => <TabBarIcon name="calendar" color={color} />,
}}
/>
</Tabs>
</CityProvider>
); );
} }

View File

@@ -1,4 +1,4 @@
import { StyleSheet, SafeAreaView, ScrollView, I18nManager, Modal, Pressable } from 'react-native'; import { StyleSheet, ScrollView } from 'react-native';
import { Text, View } from '@/components/Themed'; import { Text, View } from '@/components/Themed';
import FeatureCard from '@/components/FeatureCard'; import FeatureCard from '@/components/FeatureCard';
import PrayerTimeCard from '@/components/PrayerTimeCard'; import PrayerTimeCard from '@/components/PrayerTimeCard';
@@ -7,19 +7,74 @@ import i18n from '@/i18n';
import * as Updates from 'expo-updates'; import * as Updates from 'expo-updates';
import AsyncStorage from '@react-native-async-storage/async-storage'; import AsyncStorage from '@react-native-async-storage/async-storage';
import Colors from '@/constants/Colors'; import Colors from '@/constants/Colors';
import { useState } from 'react'; import { useEffect, useState } from 'react';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { getPrograms, ProgramActivity } from '@/utils/programs';
export default function HomeScreen() { export default function HomeScreen() {
const [modalVisible, setModalVisible] = useState(false); const insets = useSafeAreaInsets();
const [todaysActivity, setTodaysActivity] = useState<ProgramActivity | null>(null);
const [nextDayActivity, setNextDayActivity] = useState<(ProgramActivity & { dayLabel: string }) | null>(null);
useEffect(() => {
const fetchAndSetActivities = async () => {
try {
const { activities } = await getPrograms();
const now = new Date();
const formatDate = (date: Date) => {
const day = String(date.getDate()).padStart(2, '0');
const month = String(date.getMonth() + 1).padStart(2, '0');
const year = date.getFullYear();
return `${day}.${month}.${year}`;
};
const todayStr = formatDate(now);
const todaysActivities = activities[todayStr] || [];
const nextActivity = todaysActivities.find((activity) => {
if (!activity.time) return false;
const [hours, minutes] = activity.time.split(':').map(Number);
const activityTime = new Date(now);
activityTime.setHours(hours, minutes, 0, 0);
return activityTime > now;
});
setTodaysActivity(nextActivity || todaysActivities[0] || null);
const sortedDates = Object.keys(activities).sort((a, b) => {
const dateA = new Date(a.split('.').reverse().join('-')).getTime();
const dateB = new Date(b.split('.').reverse().join('-')).getTime();
return dateA - dateB;
});
const todayDate = new Date(todayStr.split('.').reverse().join('-'));
const nextDateStr = sortedDates.find((dateStr) => {
const loopDate = new Date(dateStr.split('.').reverse().join('-'));
return loopDate > todayDate;
});
const tomorrow = new Date(now);
tomorrow.setDate(tomorrow.getDate() + 1);
const tomorrowStr = formatDate(tomorrow);
if (nextDateStr && activities[nextDateStr].length > 0) {
const dayLabel = nextDateStr === tomorrowStr ? 'Ertir' : nextDateStr;
setNextDayActivity({ ...activities[nextDateStr][0], dayLabel });
}
} catch (error) {
console.error('Failed to load activities for home screen:', error);
}
};
fetchAndSetActivities();
}, []);
const changeLanguage = async (lang: 'en' | 'tk' | 'ru') => { const changeLanguage = async (lang: 'en' | 'tk' | 'ru') => {
if (!lang) return; if (!lang) return;
setModalVisible(false);
if (lang === i18n.locale.substring(0, 2)) return; if (lang === i18n.locale.substring(0, 2)) return;
await AsyncStorage.setItem('user-language', lang); await AsyncStorage.setItem('user-language', lang);
i18n.locale = lang; i18n.locale = lang;
I18nManager.forceRTL(false); // Assuming LTR for all three languages
Updates.reloadAsync(); Updates.reloadAsync();
}; };
@@ -31,48 +86,45 @@ export default function HomeScreen() {
const currentLanguage = languages.find(l => l.value === i18n.locale.substring(0, 2))?.label; 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 ( return (
<SafeAreaView style={styles.container}> <View style={[styles.container, { paddingTop: insets.top }]}>
<View style={styles.header}> <View style={styles.header}>
<Text style={styles.title}>{i18n.t('home')}</Text> <Text style={styles.title}>{i18n.t('home')}</Text>
<Pressable onPress={() => setModalVisible(true)} style={pickerSelectStyles.inputIOS}>
<Text style={{ color: 'white' }}>{currentLanguage}</Text>
</Pressable>
</View> </View>
<Modal
animationType="slide"
transparent={true}
visible={modalVisible}
onRequestClose={() => {
setModalVisible(!modalVisible);
}}>
<Pressable style={styles.modalOverlay} onPress={() => setModalVisible(false)}>
<View style={styles.modalView}>
{languages.map((lang) => (
<Pressable
key={lang.value}
style={styles.modalButton}
onPress={() => changeLanguage(lang.value as 'en' | 'tk' | 'ru')}>
<Text style={styles.modalButtonText}>{lang.label}</Text>
</Pressable>
))}
</View>
</Pressable>
</Modal>
<ScrollView> <ScrollView>
<View style={styles.innerContainer}> <View style={styles.innerContainer}>
<FeatureCard {todaysActivity && (
badgeText="Şu gün, 07:00" <FeatureCard
title="Aýşe metjidi" badgeText={`Şu gün, ${todaysActivity.time}`}
description="Oteldan ugraýar, 1-njy etazda garaşmaly" title={todaysActivity.title}
image={require('@/assets/images/aisha.jpg')} description={todaysActivity.description}
/> image={require('@/assets/images/aisha.jpg')}
/>
)}
{nextDayActivity && (
<FeatureCard
badgeText={`${nextDayActivity.dayLabel}, ${nextDayActivity.time}`}
title={nextDayActivity.title}
description={nextDayActivity.description}
image={require('@/assets/images/aisha.jpg')}
/>
)}
<PrayerTimeCard /> <PrayerTimeCard />
<ServicesGrid />
{/* <ServicesGrid services={services} /> */}
</View> </View>
</ScrollView> </ScrollView>
</SafeAreaView> </View>
); );
} }
@@ -85,6 +137,7 @@ const styles = StyleSheet.create({
paddingHorizontal: 15, paddingHorizontal: 15,
}, },
header: { header: {
display: 'none', // hide for now, will show later
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'center', alignItems: 'center',

View File

@@ -1,5 +1,181 @@
import { Redirect } from 'expo-router'; import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
import { useCallback, useEffect, useState } from 'react';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { getPrayerTimes, cities } from '../../utils/prayerTimeCalculator';
import i18n from '../../i18n';
import Colors from '../../constants/Colors';
import { useCity } from '../../context/CityContext';
type Prayer = {
name: string;
time: string;
};
type City = keyof typeof cities;
export default function TabIndex() { export default function TabIndex() {
return <Redirect href="/(tabs)/home" />; const colorScheme = 'dark';
const theme = Colors[colorScheme ?? 'light'];
const { selectedCity, setSelectedCity } = useCity();
const [prayerTimes, setPrayerTimes] = useState<Prayer[]>([]);
const [nextPrayerName, setNextPrayerName] = useState<string | null>(null);
const insets = useSafeAreaInsets();
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 (
<View style={[styles.container, { backgroundColor: theme.background, paddingTop: insets.top }]}>
<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>
</View>
);
} }
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,67 +1,96 @@
import { useState } from 'react'; import { useState } from 'react';
import { StyleSheet, SafeAreaView, Text, View, TouchableOpacity, ScrollView } from 'react-native'; import { StyleSheet, Text, View, TouchableOpacity, ScrollView, ActivityIndicator } from 'react-native';
import Colors from '@/constants/Colors'; import Colors from '@/constants/Colors';
import { FontAwesome, FontAwesome5 } from '@expo/vector-icons'; import { FontAwesome, FontAwesome5 } from '@expo/vector-icons';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { getPrograms, ProgramActivities, ProgramActivity } from '@/utils/programs';
import { useEffect } from 'react';
type Activity = { const Icon = ({ iconSet, iconName }: { iconSet: ProgramActivity['iconSet']; iconName: ProgramActivity['iconName'] }) => {
time: string; const color = "black";
title: string; const size = 24;
description: string;
icon: React.ReactNode;
transport?: string;
};
type Activities = { switch (iconSet) {
[key: string]: Activity[]; case 'FontAwesome':
return <FontAwesome name={iconName as any} size={size} color={color} />;
case 'FontAwesome5':
return <FontAwesome5 name={iconName as any} size={size} color={color} />;
default:
return null;
}
}; };
export default function Programs() { export default function Programs() {
const colorScheme = 'dark'; const colorScheme = 'dark';
const [activeDay, setActiveDay] = useState('Day 1'); const [activeDay, setActiveDay] = useState<string | null>(null);
const insets = useSafeAreaInsets();
const [activities, setActivities] = useState<ProgramActivities>({});
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [isStale, setIsStale] = useState(false);
const activities: Activities = { useEffect(() => {
'Day 1': [ const fetchPrograms = async () => {
{ try {
time: '16:30 - 19:15', const { activities: data, isStale } = await getPrograms();
title: 'Depart for Mecca', setActivities(data);
description: 'From your location to Mecca', setIsStale(isStale);
icon: <FontAwesome name="plane" size={24} color="black" />, if (Object.keys(data).length > 0) {
transport: 'Transport to hotel', const today = new Date();
}, const day = String(today.getDate()).padStart(2, '0');
{ const month = String(today.getMonth() + 1).padStart(2, '0');
time: '21:00 - 23:00', const year = today.getFullYear();
title: 'Dinner at Al-Baik', const formattedDate = `${day}.${month}.${year}`;
description: 'Masjid al-',
icon: <FontAwesome name="cutlery" size={24} color="black" />, if (data[formattedDate]) {
}, setActiveDay(formattedDate);
{ } else {
time: '00:00 - 04:00', setActiveDay(Object.keys(data)[0]);
title: 'Tawaf', }
description: 'Kaaba', }
icon: <FontAwesome5 name="kaaba" size={24} color="black" />, } catch (e) {
}, setError('Failed to load program activities.');
], } finally {
'Day 2': [ setLoading(false);
{ }
time: '10:00 - 12:00', };
title: 'Shopping',
description: 'Zamzam Tower', fetchPrograms();
icon: <FontAwesome name="shopping-bag" size={24} color="black" />, }, []);
},
], const dayKeys = Object.keys(activities);
'Day 3': [],
'Day 4': [], if (loading) {
}; return (
<View style={[styles.container, styles.center]}>
<ActivityIndicator size="large" color={Colors[colorScheme].text} />
</View>
);
}
if (error) {
return (
<View style={[styles.container, styles.center]}>
<Text style={{ color: Colors[colorScheme].text }}>{error}</Text>
</View>
);
}
return ( return (
<SafeAreaView style={[styles.container, { backgroundColor: Colors[colorScheme].background }]}> <View style={[styles.container, { backgroundColor: Colors[colorScheme].background, paddingTop: insets.top }]}>
<View style={styles.header}> <View style={styles.header}>
<Text style={[styles.title, { color: Colors[colorScheme].text }]}>Umrah Pilgrimage</Text> <Text style={[styles.title, { color: Colors[colorScheme].text }]}>Respisaniýa</Text>
</View> </View>
{isStale && (
<View style={styles.warningContainer}>
<Text style={styles.warningText}>Soňky maglumatlary almak başartmady. Saklanan çäreler görkezilýär.</Text>
</View>
)}
<View> <View>
<ScrollView horizontal showsHorizontalScrollIndicator={false} style={styles.daysScroll}> <ScrollView horizontal showsHorizontalScrollIndicator={false} style={styles.daysScroll}>
{Object.keys(activities).map((day) => ( {dayKeys.map((day) => (
<TouchableOpacity <TouchableOpacity
key={day} key={day}
style={[styles.dayButton, activeDay === day && styles.activeDayButton]} style={[styles.dayButton, activeDay === day && styles.activeDayButton]}
@@ -74,28 +103,31 @@ export default function Programs() {
</View> </View>
<ScrollView style={styles.content}> <ScrollView style={styles.content}>
{activities[activeDay].map((activity, index) => ( {activeDay &&
<View key={index}> activities[activeDay]?.map((activity, index) => (
<View style={styles.activityCard}> <View key={index}>
<View style={styles.timeContainer}> <View style={styles.activityCard}>
<Text style={styles.timeText}>{activity.time}</Text> <View style={styles.timeContainer}>
<Text style={styles.activityTitle}>{activity.title}</Text> {activity.time && <Text style={styles.timeText}>{activity.time}</Text>}
<Text style={styles.activityDescription}>{activity.description}</Text> <Text style={styles.activityTitle}>{activity.title}</Text>
<Text style={styles.activityDescription}>{activity.description}</Text>
</View>
<View style={styles.iconContainer}>
<Icon iconSet={activity.iconSet} iconName={activity.iconName} />
</View>
</View> </View>
<View style={styles.iconContainer}>{activity.icon}</View> {activity.transport && (
<View style={styles.transportContainer}>
<View style={styles.dot} />
<View style={styles.dot} />
<View style={styles.dot} />
<Text style={styles.transportText}>{activity.transport}</Text>
</View>
)}
</View> </View>
{activity.transport && ( ))}
<View style={styles.transportContainer}>
<View style={styles.dot} />
<View style={styles.dot} />
<View style={styles.dot} />
<Text style={styles.transportText}>{activity.transport}</Text>
</View>
)}
</View>
))}
</ScrollView> </ScrollView>
</SafeAreaView> </View>
); );
} }
@@ -103,11 +135,24 @@ const styles = StyleSheet.create({
container: { container: {
flex: 1, flex: 1,
}, },
center: {
justifyContent: 'center',
alignItems: 'center',
},
header: { header: {
alignItems: 'center', alignItems: 'center',
paddingVertical: 10, paddingVertical: 10,
paddingHorizontal: 20, paddingHorizontal: 20,
}, },
warningContainer: {
backgroundColor: 'orange',
padding: 10,
alignItems: 'center',
},
warningText: {
color: 'white',
textAlign: 'center',
},
title: { title: {
fontSize: 24, fontSize: 24,
fontWeight: 'bold', fontWeight: 'bold',
@@ -151,12 +196,13 @@ const styles = StyleSheet.create({
timeText: { timeText: {
color: 'gray', color: 'gray',
fontSize: 14, fontSize: 14,
marginBottom: 4,
}, },
activityTitle: { activityTitle: {
color: 'white', color: 'white',
fontSize: 20, fontSize: 20,
fontWeight: 'bold', fontWeight: 'bold',
marginVertical: 4, marginBottom: 4,
}, },
activityDescription: { activityDescription: {
color: 'gray', color: 'gray',

View File

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

View File

@@ -5,8 +5,12 @@ import { Stack } from 'expo-router';
import * as SplashScreen from 'expo-splash-screen'; import * as SplashScreen from 'expo-splash-screen';
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import 'react-native-reanimated'; import 'react-native-reanimated';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { StatusBar } from 'expo-status-bar';
import { initializeLanguage } from '@/i18n'; import { initializeLanguage } from '@/i18n';
import { makeRequest } from '@/utils/makeRequest';
import { CURRENCY_RATES_ENDPOINT } from '@/utils/api';
export { export {
// Catch any errors thrown by the Layout component. // Catch any errors thrown by the Layout component.
@@ -40,6 +44,10 @@ export default function RootLayout() {
}); });
}, []); }, []);
// useEffect(() => {
// }, []);
useEffect(() => { useEffect(() => {
if (loaded && langLoaded) { if (loaded && langLoaded) {
SplashScreen.hideAsync(); SplashScreen.hideAsync();
@@ -60,6 +68,7 @@ function RootLayoutNav() {
<Stack.Screen name="(tabs)" options={{ headerShown: false }} /> <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen name="modal" options={{ presentation: 'modal' }} /> <Stack.Screen name="modal" options={{ presentation: 'modal' }} />
</Stack> </Stack>
<StatusBar style="light" />
</ThemeProvider> </ThemeProvider>
); );
} }

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

@@ -4,7 +4,7 @@ import Colors from '@/constants/Colors';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import { getPrayerTimes, cities } from '@/utils/prayerTimeCalculator'; import { getPrayerTimes, cities } from '@/utils/prayerTimeCalculator';
import i18n from '@/i18n'; import i18n from '@/i18n';
import { createIconSetFromFontello } from 'react-native-vector-icons'; import { useCity } from '../context/CityContext';
type Prayer = { type Prayer = {
name: string; name: string;
@@ -25,7 +25,7 @@ export default function PrayerTimeCard() {
const [prayerTimes, setPrayerTimes] = useState<Prayer[]>([]); const [prayerTimes, setPrayerTimes] = useState<Prayer[]>([]);
const [nextPrayer, setNextPrayer] = useState<{ name: string; time: string } | null>(null); const [nextPrayer, setNextPrayer] = useState<{ name: string; time: string } | null>(null);
const [remainingTime, setRemainingTime] = useState(''); const [remainingTime, setRemainingTime] = useState('');
const [selectedCity, setSelectedCity] = useState<keyof typeof cities>('Makkah'); const { selectedCity, setSelectedCity } = useCity();
useEffect(() => { useEffect(() => {
const times = getPrayerTimes(selectedCity); const times = getPrayerTimes(selectedCity);

View File

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

View File

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

54
context/CityContext.tsx Normal file
View File

@@ -0,0 +1,54 @@
import React, { createContext, useState, useEffect, useContext, ReactNode } from 'react';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { cities } from '../utils/prayerTimeCalculator';
type City = keyof typeof cities;
interface CityContextType {
selectedCity: City;
setSelectedCity: (city: City) => void;
}
const CityContext = createContext<CityContextType | undefined>(undefined);
export const CityProvider = ({ children }: { children: ReactNode }) => {
const [selectedCity, setSelectedCityState] = useState<City>('Makkah');
useEffect(() => {
const loadSelectedCity = async () => {
try {
const city = await AsyncStorage.getItem('selectedCity') as City;
if (city) {
setSelectedCityState(city);
}
} catch (error) {
console.error('Failed to load selected city:', error);
}
};
loadSelectedCity();
}, []);
const handleSetSelectedCity = async (city: City) => {
setSelectedCityState(city);
try {
await AsyncStorage.setItem('selectedCity', city);
} catch (error) {
console.error('Failed to save selected city:', error);
}
};
return (
<CityContext.Provider value={{ selectedCity, setSelectedCity: handleSetSelectedCity }}>
{children}
</CityContext.Provider>
);
};
export const useCity = () => {
const context = useContext(CityContext);
if (context === undefined) {
throw new Error('useCity must be used within a CityProvider');
}
return context;
};

17
i18n.ts
View File

@@ -1,29 +1,16 @@
import { I18n } from 'i18n-js'; import { I18n } from 'i18n-js';
import * as Localization from 'expo-localization';
import AsyncStorage from '@react-native-async-storage/async-storage';
import en from './locales/en.json';
import tk from './locales/tk.json'; import tk from './locales/tk.json';
import ru from './locales/ru.json';
const i18n = new I18n({ const i18n = new I18n({
en,
tk, tk,
ru,
}); });
i18n.locale = 'tk';
i18n.enableFallback = true; i18n.enableFallback = true;
// Function to initialize the language // Function to initialize the language
export const initializeLanguage = async () => { export const initializeLanguage = async () => {
const savedLanguage = await AsyncStorage.getItem('user-language'); i18n.locale = 'tk';
if (savedLanguage) {
i18n.locale = savedLanguage;
} else {
// If no language is saved, detect from device and default to Turkmen
const userLanguageCode = Localization.getLocales()[0]?.languageCode;
i18n.locale = ['en', 'tk', 'ru'].includes(userLanguageCode || '') ? userLanguageCode! : 'tk';
}
}; };
export default i18n; export default i18n;

View File

@@ -1,32 +0,0 @@
{
"home": "Home",
"services": "Services",
"supplications": "Supplications",
"yourJourneyToHajj": "Your Journey to Hajj",
"hajjEssentials": "Everything you need for Hajj essentials.",
"umrah": "Umrah",
"bookPermit": "Book Permit",
"nobleRawdah": "Noble Rawdah",
"newExperience": "New Experience",
"prayerTimes": "Prayer Times",
"programs": "Programs",
"leftOnPrayer": "Left on {{prayerName}} prayer",
"servicesToEnrich": "Services to Enrich Your Spiritual Experience",
"quran": "Qur'an",
"hadith": "Hadith",
"dua": "Du'a",
"adhkar": "Adhkar",
"hisnAlMuslim": "Hisn Al-Muslim",
"morningEveningThikr": "Thikr said in the morning and evening",
"beforeSleepingThikr": "Thikr before sleeping",
"afterSalamThikr": "Thikr after salam",
"breakingFastSupplication": "Upon breaking fast",
"fastingPersonSupplication": "Supplication said by one fasting when presented with food and does not break his fast",
"insultedWhileFasting": "When insulted while fasting",
"seeingFruitSupplication": "Supplication upon seeing the early or premature fruit",
"sneezingSupplication": "Supplication upon sneezing",
"sarToTmt": "SAR to TMT",
"hotelBusinessCard": "Hotel Business Card",
"masterkeyBox": "Masterkey Box",
"translator": "Translator"
}

View File

@@ -1,31 +0,0 @@
{
"home": "Главная",
"services": "Сервисы",
"yourJourneyToHajj": "Ваше путешествие в Хадж",
"hajjEssentials": "Все, что вам нужно для Хаджа.",
"umrah": "Умра",
"bookPermit": "Забронировать разрешение",
"nobleRawdah": "Благородная Равда",
"newExperience": "Новый опыт",
"prayerTimes": "Время молитв",
"programs": "Программы",
"leftOnPrayer": "Осталось до молитвы {{prayerName}}",
"servicesToEnrich": "Услуги для обогащения вашего духовного опыта",
"quran": "Коран",
"hadith": "Хадисы",
"dua": "Дуа",
"adhkar": "Азкар",
"hisnAlMuslim": "Крепость мусульманина",
"morningEveningThikr": "Зикр, читаемый утром и вечером",
"beforeSleepingThikr": "Зикр перед сном",
"afterSalamThikr": "Зикр после салама",
"breakingFastSupplication": "При разговении",
"fastingPersonSupplication": "Мольба, произносимая постящимся, когда ему преподносят еду, и он не прерывает свой пост",
"insultedWhileFasting": "Когда оскорбляют во время поста",
"seeingFruitSupplication": "Мольба при виде ранних или незрелых плодов",
"sneezingSupplication": "Мольба при чихании",
"sarToTmt": "SAR в TMT",
"hotelBusinessCard": "Визитная карточка отеля",
"masterkeyBox": "Ящик для мастер-ключей",
"translator": "Переводчик"
}

View File

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

2218
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,8 +4,8 @@
"version": "1.0.0", "version": "1.0.0",
"scripts": { "scripts": {
"start": "expo start", "start": "expo start",
"android": "expo start --android", "android": "expo run:android",
"ios": "expo start --ios", "ios": "expo run:ios",
"web": "expo start --web", "web": "expo start --web",
"test": "jest --watchAll" "test": "jest --watchAll"
}, },
@@ -18,10 +18,13 @@
"@react-navigation/native": "^7.1.6", "@react-navigation/native": "^7.1.6",
"adhan": "^4.4.3", "adhan": "^4.4.3",
"expo": "~53.0.20", "expo": "~53.0.20",
"expo-asset": "~11.1.7",
"expo-file-system": "~18.1.11",
"expo-font": "~13.3.2", "expo-font": "~13.3.2",
"expo-linking": "~7.1.7", "expo-linking": "~7.1.7",
"expo-localization": "^16.1.6", "expo-localization": "^16.1.6",
"expo-router": "~5.1.4", "expo-router": "~5.1.4",
"expo-sharing": "~13.1.5",
"expo-splash-screen": "~0.30.10", "expo-splash-screen": "~0.30.10",
"expo-status-bar": "~2.2.3", "expo-status-bar": "~2.2.3",
"expo-system-ui": "~5.0.10", "expo-system-ui": "~5.0.10",
@@ -29,14 +32,12 @@
"expo-web-browser": "~14.2.0", "expo-web-browser": "~14.2.0",
"i18n-js": "^4.5.1", "i18n-js": "^4.5.1",
"react": "19.0.0", "react": "19.0.0",
"react-dom": "19.0.0",
"react-native": "0.79.5", "react-native": "0.79.5",
"react-native-picker-select": "^9.3.1", "react-native-picker-select": "^9.3.1",
"react-native-reanimated": "~3.17.4", "react-native-reanimated": "~3.17.4",
"react-native-safe-area-context": "5.4.0", "react-native-safe-area-context": "5.4.0",
"react-native-screens": "~4.11.1", "react-native-screens": "~4.11.1",
"react-native-vector-icons": "^10.3.0", "react-native-vector-icons": "^10.3.0"
"react-native-web": "~0.19.6"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.20.0", "@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);
};

43
utils/programs.ts Normal file
View File

@@ -0,0 +1,43 @@
import { FontAwesome, FontAwesome5, MaterialCommunityIcons } from '@expo/vector-icons';
import AsyncStorage from '@react-native-async-storage/async-storage';
export type ProgramActivity = {
time: string;
title: string;
description: string;
iconSet: 'FontAwesome' | 'FontAwesome5' | 'MaterialCommunityIcons';
iconName: React.ComponentProps<typeof FontAwesome>['name'] | React.ComponentProps<typeof FontAwesome5>['name'] | React.ComponentProps<typeof MaterialCommunityIcons>['name'];
transport?: string;
};
export type ProgramActivities = {
[date: string]: ProgramActivity[];
};
export type ProgramData = {
activities: ProgramActivities;
isStale: boolean;
};
const PROGRAMS_URL = 'http://kepilhyzmat.com/assets/programs.json';
const PROGRAMS_CACHE_KEY = 'program_activities_cache';
export const getPrograms = async (): Promise<ProgramData> => {
try {
const response = await fetch(PROGRAMS_URL);
if (!response.ok) {
throw new Error(`Request failed with status ${response.status}`);
}
const data: ProgramActivities = await response.json();
await AsyncStorage.setItem(PROGRAMS_CACHE_KEY, JSON.stringify(data));
return { activities: data, isStale: false };
} catch (error) {
console.error('Failed to fetch programs:', error);
const cachedData = await AsyncStorage.getItem(PROGRAMS_CACHE_KEY);
if (cachedData) {
return { activities: JSON.parse(cachedData), isStale: true };
}
throw new Error('Failed to fetch programs and no cache available.');
}
};