Compare commits
16 Commits
3d9b8601bf
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c7884b9ff | ||
|
|
fa0465ee1a | ||
|
|
558698a058 | ||
|
|
ae6ccd7f53 | ||
|
|
a1871510a8 | ||
|
|
3ee7ec87be | ||
|
|
803bbfc30f | ||
|
|
30dd67ecdf | ||
|
|
47420f9941 | ||
|
|
68affb3d4b | ||
|
|
9e0e285172 | ||
|
|
c2cd61c679 | ||
|
|
acddbf48f0 | ||
|
|
4a92077786 | ||
|
|
e542371268 | ||
|
|
48295de3b7 |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -35,3 +35,9 @@ yarn-error.*
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
|
||||
# android
|
||||
android/
|
||||
|
||||
# ios
|
||||
ios/
|
||||
12
app.json
12
app.json
@@ -6,7 +6,7 @@
|
||||
"orientation": "portrait",
|
||||
"icon": "./assets/images/icon.png",
|
||||
"scheme": "umra",
|
||||
"userInterfaceStyle": "automatic",
|
||||
"userInterfaceStyle": "dark",
|
||||
"newArchEnabled": true,
|
||||
"splash": {
|
||||
"image": "./assets/images/splash-icon.png",
|
||||
@@ -21,7 +21,12 @@
|
||||
"foregroundImage": "./assets/images/adaptive-icon.png",
|
||||
"backgroundColor": "#ffffff"
|
||||
},
|
||||
"edgeToEdgeEnabled": true
|
||||
"edgeToEdgeEnabled": true,
|
||||
"package": "com.nurmuhammet.ali.Umra",
|
||||
"androidNavigationBar": {
|
||||
"backgroundColor": "#1C1C1E",
|
||||
"barStyle": "light-content"
|
||||
}
|
||||
},
|
||||
"web": {
|
||||
"bundler": "metro",
|
||||
@@ -29,7 +34,8 @@
|
||||
"favicon": "./assets/images/favicon.png"
|
||||
},
|
||||
"plugins": [
|
||||
"expo-router"
|
||||
"expo-router",
|
||||
"expo-asset"
|
||||
],
|
||||
"experiments": {
|
||||
"typedRoutes": true
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useColorScheme, View } from 'react-native';
|
||||
|
||||
import Colors from '@/constants/Colors';
|
||||
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/
|
||||
@@ -19,43 +20,45 @@ export default function TabLayout() {
|
||||
const colorScheme = 'dark'; // Force dark mode
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
screenOptions={{
|
||||
tabBarActiveTintColor: Colors[colorScheme].tint,
|
||||
tabBarStyle: {
|
||||
backgroundColor: Colors[colorScheme].secondary,
|
||||
borderTopColor: Colors[colorScheme].secondary,
|
||||
},
|
||||
headerShown: false, // hide header globally for tabs
|
||||
}}>
|
||||
<Tabs.Screen
|
||||
name="home"
|
||||
options={{
|
||||
title: i18n.t('home'),
|
||||
tabBarIcon: ({ color }) => <TabBarIcon name="home" color={color} />,
|
||||
}}
|
||||
/>
|
||||
<Tabs.Screen
|
||||
name="services"
|
||||
options={{
|
||||
title: i18n.t('services'),
|
||||
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="calendar" color={color} />,
|
||||
}}
|
||||
/>
|
||||
</Tabs>
|
||||
<CityProvider>
|
||||
<Tabs
|
||||
screenOptions={{
|
||||
tabBarActiveTintColor: Colors[colorScheme].tint,
|
||||
tabBarStyle: {
|
||||
backgroundColor: Colors[colorScheme].secondary,
|
||||
borderTopColor: Colors[colorScheme].secondary,
|
||||
},
|
||||
headerShown: false, // hide header globally for tabs
|
||||
}}>
|
||||
<Tabs.Screen
|
||||
name="home"
|
||||
options={{
|
||||
title: i18n.t('home'),
|
||||
tabBarIcon: ({ color }) => <TabBarIcon name="home" color={color} />,
|
||||
}}
|
||||
/>
|
||||
<Tabs.Screen
|
||||
name="services"
|
||||
options={{
|
||||
title: i18n.t('services'),
|
||||
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="calendar" color={color} />,
|
||||
}}
|
||||
/>
|
||||
</Tabs>
|
||||
</CityProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 FeatureCard from '@/components/FeatureCard';
|
||||
import PrayerTimeCard from '@/components/PrayerTimeCard';
|
||||
@@ -7,19 +7,74 @@ import i18n from '@/i18n';
|
||||
import * as Updates from 'expo-updates';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
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() {
|
||||
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') => {
|
||||
if (!lang) return;
|
||||
setModalVisible(false);
|
||||
if (lang === i18n.locale.substring(0, 2)) return;
|
||||
await AsyncStorage.setItem('user-language', lang);
|
||||
i18n.locale = lang;
|
||||
I18nManager.forceRTL(false); // Assuming LTR for all three languages
|
||||
Updates.reloadAsync();
|
||||
};
|
||||
|
||||
@@ -38,50 +93,38 @@ export default function HomeScreen() {
|
||||
];
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<View style={[styles.container, { paddingTop: insets.top }]}>
|
||||
|
||||
<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}
|
||||
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>
|
||||
<View style={styles.innerContainer}>
|
||||
<FeatureCard
|
||||
badgeText="Şu gün, 07:00"
|
||||
title="Aýşe metjidi"
|
||||
description="Oteldan ugraýar, 1-njy etazda garaşmaly"
|
||||
image={require('@/assets/images/aisha.jpg')}
|
||||
/>
|
||||
{todaysActivity && (
|
||||
<FeatureCard
|
||||
badgeText={`Şu gün, ${todaysActivity.time}`}
|
||||
title={todaysActivity.title}
|
||||
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 />
|
||||
|
||||
{/* <ServicesGrid services={services} /> */}
|
||||
</View>
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Pressable, SafeAreaView, ScrollView, StyleSheet, Text, View } from 'react-native';
|
||||
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 { useColorScheme } from 'react-native';
|
||||
import Colors from '../../constants/Colors';
|
||||
import { useCity } from '../../context/CityContext';
|
||||
|
||||
type Prayer = {
|
||||
name: string;
|
||||
@@ -14,11 +15,12 @@ type Prayer = {
|
||||
type City = keyof typeof cities;
|
||||
|
||||
export default function TabIndex() {
|
||||
const colorScheme = useColorScheme();
|
||||
const colorScheme = 'dark';
|
||||
const theme = Colors[colorScheme ?? 'light'];
|
||||
const [selectedCity, setSelectedCity] = useState<City>('Makkah');
|
||||
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'),
|
||||
@@ -88,7 +90,7 @@ export default function TabIndex() {
|
||||
};
|
||||
|
||||
return (
|
||||
<SafeAreaView style={[styles.container, { backgroundColor: theme.background }]}>
|
||||
<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
|
||||
@@ -109,7 +111,7 @@ export default function TabIndex() {
|
||||
<ScrollView contentContainerStyle={styles.listContainer}>
|
||||
{prayerTimes.map(renderPrayerTime)}
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,67 +1,96 @@
|
||||
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 { 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 = {
|
||||
time: string;
|
||||
title: string;
|
||||
description: string;
|
||||
icon: React.ReactNode;
|
||||
transport?: string;
|
||||
};
|
||||
const Icon = ({ iconSet, iconName }: { iconSet: ProgramActivity['iconSet']; iconName: ProgramActivity['iconName'] }) => {
|
||||
const color = "black";
|
||||
const size = 24;
|
||||
|
||||
type Activities = {
|
||||
[key: string]: Activity[];
|
||||
switch (iconSet) {
|
||||
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() {
|
||||
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 = {
|
||||
'Day 1': [
|
||||
{
|
||||
time: '16:30 - 19:15',
|
||||
title: 'Depart for Mecca',
|
||||
description: 'From your location to Mecca',
|
||||
icon: <FontAwesome name="plane" size={24} color="black" />,
|
||||
transport: 'Transport to hotel',
|
||||
},
|
||||
{
|
||||
time: '21:00 - 23:00',
|
||||
title: 'Dinner at Al-Baik',
|
||||
description: 'Masjid al-',
|
||||
icon: <FontAwesome name="cutlery" size={24} color="black" />,
|
||||
},
|
||||
{
|
||||
time: '00:00 - 04:00',
|
||||
title: 'Tawaf',
|
||||
description: 'Kaaba',
|
||||
icon: <FontAwesome5 name="kaaba" size={24} color="black" />,
|
||||
},
|
||||
],
|
||||
'Day 2': [
|
||||
{
|
||||
time: '10:00 - 12:00',
|
||||
title: 'Shopping',
|
||||
description: 'Zamzam Tower',
|
||||
icon: <FontAwesome name="shopping-bag" size={24} color="black" />,
|
||||
},
|
||||
],
|
||||
'Day 3': [],
|
||||
'Day 4': [],
|
||||
};
|
||||
useEffect(() => {
|
||||
const fetchPrograms = async () => {
|
||||
try {
|
||||
const { activities: data, isStale } = await getPrograms();
|
||||
setActivities(data);
|
||||
setIsStale(isStale);
|
||||
if (Object.keys(data).length > 0) {
|
||||
const today = new Date();
|
||||
const day = String(today.getDate()).padStart(2, '0');
|
||||
const month = String(today.getMonth() + 1).padStart(2, '0');
|
||||
const year = today.getFullYear();
|
||||
const formattedDate = `${day}.${month}.${year}`;
|
||||
|
||||
if (data[formattedDate]) {
|
||||
setActiveDay(formattedDate);
|
||||
} else {
|
||||
setActiveDay(Object.keys(data)[0]);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
setError('Failed to load program activities.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchPrograms();
|
||||
}, []);
|
||||
|
||||
const dayKeys = Object.keys(activities);
|
||||
|
||||
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 (
|
||||
<SafeAreaView style={[styles.container, { backgroundColor: Colors[colorScheme].background }]}>
|
||||
<View style={[styles.container, { backgroundColor: Colors[colorScheme].background, paddingTop: insets.top }]}>
|
||||
<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>
|
||||
|
||||
{isStale && (
|
||||
<View style={styles.warningContainer}>
|
||||
<Text style={styles.warningText}>Soňky maglumatlary almak başartmady. Saklanan çäreler görkezilýär.</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View>
|
||||
<ScrollView horizontal showsHorizontalScrollIndicator={false} style={styles.daysScroll}>
|
||||
{Object.keys(activities).map((day) => (
|
||||
{dayKeys.map((day) => (
|
||||
<TouchableOpacity
|
||||
key={day}
|
||||
style={[styles.dayButton, activeDay === day && styles.activeDayButton]}
|
||||
@@ -74,28 +103,31 @@ export default function Programs() {
|
||||
</View>
|
||||
|
||||
<ScrollView style={styles.content}>
|
||||
{activities[activeDay].map((activity, index) => (
|
||||
<View key={index}>
|
||||
<View style={styles.activityCard}>
|
||||
<View style={styles.timeContainer}>
|
||||
<Text style={styles.timeText}>{activity.time}</Text>
|
||||
<Text style={styles.activityTitle}>{activity.title}</Text>
|
||||
<Text style={styles.activityDescription}>{activity.description}</Text>
|
||||
{activeDay &&
|
||||
activities[activeDay]?.map((activity, index) => (
|
||||
<View key={index}>
|
||||
<View style={styles.activityCard}>
|
||||
<View style={styles.timeContainer}>
|
||||
{activity.time && <Text style={styles.timeText}>{activity.time}</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 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>
|
||||
{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>
|
||||
</SafeAreaView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -103,11 +135,24 @@ const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
center: {
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
header: {
|
||||
alignItems: 'center',
|
||||
paddingVertical: 10,
|
||||
paddingHorizontal: 20,
|
||||
},
|
||||
warningContainer: {
|
||||
backgroundColor: 'orange',
|
||||
padding: 10,
|
||||
alignItems: 'center',
|
||||
},
|
||||
warningText: {
|
||||
color: 'white',
|
||||
textAlign: 'center',
|
||||
},
|
||||
title: {
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold',
|
||||
@@ -151,12 +196,13 @@ const styles = StyleSheet.create({
|
||||
timeText: {
|
||||
color: 'gray',
|
||||
fontSize: 14,
|
||||
marginBottom: 4,
|
||||
},
|
||||
activityTitle: {
|
||||
color: 'white',
|
||||
fontSize: 20,
|
||||
fontWeight: 'bold',
|
||||
marginVertical: 4,
|
||||
marginBottom: 4,
|
||||
},
|
||||
activityDescription: {
|
||||
color: 'gray',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { StyleSheet, SafeAreaView, View, TouchableOpacity, Dimensions } from 'react-native';
|
||||
import { StyleSheet, View, TouchableOpacity, Dimensions } from 'react-native';
|
||||
import { Text } from '@/components/Themed';
|
||||
import i18n from '@/i18n';
|
||||
import { FontAwesome5 } from '@expo/vector-icons';
|
||||
@@ -8,12 +8,14 @@ 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() {
|
||||
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 = [
|
||||
{
|
||||
@@ -22,12 +24,12 @@ export default function ServicesScreen() {
|
||||
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('Hotel'),
|
||||
// name: 'hotelCard',
|
||||
// icon: <FontAwesome5 name="hotel" size={24} color="#D4AF37" />,
|
||||
// onPress: () => setHotelModalVisible(true),
|
||||
// },
|
||||
{
|
||||
title: i18n.t('Lost room key'),
|
||||
name: 'lostKey',
|
||||
@@ -43,7 +45,7 @@ export default function ServicesScreen() {
|
||||
];
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<View style={[styles.container, { paddingTop: insets.top }]}>
|
||||
<Text style={styles.title}>{i18n.t('services')}</Text>
|
||||
|
||||
<View style={styles.grid}>
|
||||
@@ -69,7 +71,7 @@ export default function ServicesScreen() {
|
||||
visible={phrasebookModalVisible}
|
||||
onClose={() => setPhrasebookModalVisible(false)}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ 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 { StatusBar } from 'expo-status-bar';
|
||||
|
||||
import { initializeLanguage } from '@/i18n';
|
||||
import { makeRequest } from '@/utils/makeRequest';
|
||||
@@ -67,6 +68,7 @@ function RootLayoutNav() {
|
||||
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="modal" options={{ presentation: 'modal' }} />
|
||||
</Stack>
|
||||
<StatusBar style="light" />
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import Colors from '@/constants/Colors';
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
import { getPrayerTimes, cities } from '@/utils/prayerTimeCalculator';
|
||||
import i18n from '@/i18n';
|
||||
import { createIconSetFromFontello } from 'react-native-vector-icons';
|
||||
import { useCity } from '../context/CityContext';
|
||||
|
||||
type Prayer = {
|
||||
name: string;
|
||||
@@ -25,7 +25,7 @@ export default function PrayerTimeCard() {
|
||||
const [prayerTimes, setPrayerTimes] = useState<Prayer[]>([]);
|
||||
const [nextPrayer, setNextPrayer] = useState<{ name: string; time: string } | null>(null);
|
||||
const [remainingTime, setRemainingTime] = useState('');
|
||||
const [selectedCity, setSelectedCity] = useState<keyof typeof cities>('Makkah');
|
||||
const { selectedCity, setSelectedCity } = useCity();
|
||||
|
||||
useEffect(() => {
|
||||
const times = getPrayerTimes(selectedCity);
|
||||
|
||||
54
context/CityContext.tsx
Normal file
54
context/CityContext.tsx
Normal 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
17
i18n.ts
@@ -1,29 +1,16 @@
|
||||
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 ru from './locales/ru.json';
|
||||
|
||||
const i18n = new I18n({
|
||||
en,
|
||||
tk,
|
||||
ru,
|
||||
});
|
||||
|
||||
i18n.locale = 'tk';
|
||||
i18n.enableFallback = true;
|
||||
|
||||
// Function to initialize the language
|
||||
export const initializeLanguage = async () => {
|
||||
const savedLanguage = await AsyncStorage.getItem('user-language');
|
||||
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';
|
||||
}
|
||||
i18n.locale = 'tk';
|
||||
};
|
||||
|
||||
export default i18n;
|
||||
|
||||
@@ -1,53 +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",
|
||||
"Programs": "Schedule",
|
||||
"leftOnPrayer": "Left on {{prayerName}} prayer",
|
||||
"servicesToEnrich": "Services to Enrich Your Spiritual Experience",
|
||||
"quran": "Qur'an",
|
||||
"hadith": "Hadith",
|
||||
"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",
|
||||
"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",
|
||||
"Money": "Money",
|
||||
"Hotel": "Hotel",
|
||||
"Lost room key": "Lost room key",
|
||||
"Phrasebook": "Phrasebook",
|
||||
"Enter text in Turkmen": "Enter text",
|
||||
"Translate": "Translate",
|
||||
"Salah": "Salah",
|
||||
"menuSalah": "Salah"
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
{
|
||||
"home": "Главная",
|
||||
"services": "Сервисы",
|
||||
"supplications": "Молитвы",
|
||||
"yourJourneyToHajj": "Ваше путешествие в Хадж",
|
||||
"hajjEssentials": "Все, что вам нужно для Хаджа.",
|
||||
"umrah": "Умра",
|
||||
"bookPermit": "Забронировать разрешение",
|
||||
"nobleRawdah": "Благородная Равда",
|
||||
"newExperience": "Новый опыт",
|
||||
"prayerTimes": "Время молитв",
|
||||
"programs": "Программы",
|
||||
"Programs": "Расписание",
|
||||
"leftOnPrayer": "Осталось до молитвы {{prayerName}}",
|
||||
"servicesToEnrich": "Услуги для обогащения вашего духовного опыта",
|
||||
"quran": "Коран",
|
||||
"hadith": "Хадис",
|
||||
"dua": "Дуа",
|
||||
"currencyConverter": "Конвертер валют",
|
||||
"hotelCard": "Карта отеля",
|
||||
"lostKey": "Потеряли ключ?",
|
||||
"translator": "Переводчик",
|
||||
"adhkar": "Азкар",
|
||||
"hisnAlMuslim": "Крепость мусульманина",
|
||||
"Makkah": "Мекка",
|
||||
"Medina": "Медина",
|
||||
"Jeddah": "Джидда",
|
||||
"fajr": "Фаджр",
|
||||
"sunrise": "Восход",
|
||||
"dhuhr": "Зухр",
|
||||
"asr": "Аср",
|
||||
"maghrib": "Магриб",
|
||||
"isha": "Иша",
|
||||
"morningEveningThikr": "Зикр, читаемый утром и вечером",
|
||||
"beforeSleepingThikr": "Зикр перед сном",
|
||||
"afterSalamThikr": "Зикр после салама",
|
||||
"breakingFastSupplication": "При разговении",
|
||||
"fastingPersonSupplication": "Мольба, произносимая постящимся, когда ему преподносят еду, и он не прерывает свой пост",
|
||||
"insultedWhileFasting": "Когда оскорбляют во время поста",
|
||||
"seeingFruitSupplication": "Мольба при виде ранних или незрелых плодов",
|
||||
"sneezingSupplication": "Мольба при чихании",
|
||||
"sarToTmt": "SAR в TMT",
|
||||
"hotelBusinessCard": "Визитная карточка отеля",
|
||||
"masterkeyBox": "Ящик для мастер-ключей",
|
||||
"Money": "Деньги",
|
||||
"Hotel": "Отель",
|
||||
"Lost room key": "Ключ от номера утерян",
|
||||
"Phrasebook": "Разговорник",
|
||||
"Enter text in Turkmen": "Введите текст",
|
||||
"Translate": "Перевести",
|
||||
"Salah": "Намаз",
|
||||
"menuSalah": "Намаз"
|
||||
}
|
||||
2175
package-lock.json
generated
2175
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -4,8 +4,8 @@
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"start": "expo start",
|
||||
"android": "expo start --android",
|
||||
"ios": "expo start --ios",
|
||||
"android": "expo run:android",
|
||||
"ios": "expo run:ios",
|
||||
"web": "expo start --web",
|
||||
"test": "jest --watchAll"
|
||||
},
|
||||
@@ -18,6 +18,7 @@
|
||||
"@react-navigation/native": "^7.1.6",
|
||||
"adhan": "^4.4.3",
|
||||
"expo": "~53.0.20",
|
||||
"expo-asset": "~11.1.7",
|
||||
"expo-file-system": "~18.1.11",
|
||||
"expo-font": "~13.3.2",
|
||||
"expo-linking": "~7.1.7",
|
||||
|
||||
43
utils/programs.ts
Normal file
43
utils/programs.ts
Normal 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.');
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user