Compare commits
26 Commits
4b3aedbd96
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c7884b9ff | ||
|
|
fa0465ee1a | ||
|
|
558698a058 | ||
|
|
ae6ccd7f53 | ||
|
|
a1871510a8 | ||
|
|
3ee7ec87be | ||
|
|
803bbfc30f | ||
|
|
30dd67ecdf | ||
|
|
47420f9941 | ||
|
|
68affb3d4b | ||
|
|
9e0e285172 | ||
|
|
c2cd61c679 | ||
|
|
acddbf48f0 | ||
|
|
4a92077786 | ||
|
|
e542371268 | ||
|
|
48295de3b7 | ||
| 3d9b8601bf | |||
| f519052b7b | |||
| 6797ab6d9e | |||
| 213062bda4 | |||
| a2a4591848 | |||
| e1d9b688d9 | |||
| dc76633cb1 | |||
| 7ecbe23e0d | |||
| f796f832a8 | |||
| b5d3133c24 |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -35,3 +35,9 @@ yarn-error.*
|
|||||||
|
|
||||||
# typescript
|
# typescript
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# android
|
||||||
|
android/
|
||||||
|
|
||||||
|
# ios
|
||||||
|
ios/
|
||||||
8
Umra.code-workspace
Normal file
8
Umra.code-workspace
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"folders": [
|
||||||
|
{
|
||||||
|
"path": "."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": {}
|
||||||
|
}
|
||||||
12
app.json
12
app.json
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
126
components/CurrencyConverterModal.tsx
Normal file
126
components/CurrencyConverterModal.tsx
Normal 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;
|
||||||
77
components/HotelBusinessCardModal.tsx
Normal file
77
components/HotelBusinessCardModal.tsx
Normal 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;
|
||||||
83
components/LostKeyModal.tsx
Normal file
83
components/LostKeyModal.tsx
Normal 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;
|
||||||
132
components/PhrasebookModal.tsx
Normal file
132
components/PhrasebookModal.tsx
Normal 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;
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
192
components/TranslatorModal.tsx
Normal file
192
components/TranslatorModal.tsx
Normal 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;
|
||||||
@@ -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
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 { 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;
|
||||||
|
|||||||
@@ -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"
|
|
||||||
}
|
|
||||||
@@ -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": "Переводчик"
|
|
||||||
}
|
|
||||||
@@ -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
2218
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
11
package.json
11
package.json
@@ -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
3
utils/api.ts
Normal 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
35
utils/makeRequest.ts
Normal 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
16
utils/pdf.ts
Normal 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
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