Compare commits
8 Commits
47420f9941
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c7884b9ff | ||
|
|
fa0465ee1a | ||
|
|
558698a058 | ||
|
|
ae6ccd7f53 | ||
|
|
a1871510a8 | ||
|
|
3ee7ec87be | ||
|
|
803bbfc30f | ||
|
|
30dd67ecdf |
@@ -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,43 +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="index"
|
<Tabs.Screen
|
||||||
options={{
|
name="index"
|
||||||
title: i18n.t('menuSalah'),
|
options={{
|
||||||
tabBarIcon: ({ color }) => <TabBarIcon name="moon-o" color={color} />,
|
title: i18n.t('menuSalah'),
|
||||||
}}
|
tabBarIcon: ({ color }) => <TabBarIcon name="moon-o" color={color} />,
|
||||||
/>
|
}}
|
||||||
<Tabs.Screen
|
/>
|
||||||
name="programs"
|
<Tabs.Screen
|
||||||
options={{
|
name="programs"
|
||||||
title: i18n.t('Programs'),
|
options={{
|
||||||
tabBarIcon: ({ color }) => <TabBarIcon name="calendar" color={color} />,
|
title: i18n.t('Programs'),
|
||||||
}}
|
tabBarIcon: ({ color }) => <TabBarIcon name="calendar" color={color} />,
|
||||||
/>
|
}}
|
||||||
</Tabs>
|
/>
|
||||||
|
</Tabs>
|
||||||
|
</CityProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,12 +7,68 @@ 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 { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
|
import { getPrograms, ProgramActivity } from '@/utils/programs';
|
||||||
|
|
||||||
export default function HomeScreen() {
|
export default function HomeScreen() {
|
||||||
const insets = useSafeAreaInsets();
|
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;
|
||||||
@@ -45,12 +101,23 @@ export default function HomeScreen() {
|
|||||||
|
|
||||||
<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 />
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|||||||
import { getPrayerTimes, cities } from '../../utils/prayerTimeCalculator';
|
import { getPrayerTimes, cities } from '../../utils/prayerTimeCalculator';
|
||||||
import i18n from '../../i18n';
|
import i18n from '../../i18n';
|
||||||
import Colors from '../../constants/Colors';
|
import Colors from '../../constants/Colors';
|
||||||
|
import { useCity } from '../../context/CityContext';
|
||||||
|
|
||||||
type Prayer = {
|
type Prayer = {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -16,7 +17,7 @@ type City = keyof typeof cities;
|
|||||||
export default function TabIndex() {
|
export default function TabIndex() {
|
||||||
const colorScheme = 'dark';
|
const colorScheme = 'dark';
|
||||||
const theme = Colors[colorScheme ?? 'light'];
|
const theme = Colors[colorScheme ?? 'light'];
|
||||||
const [selectedCity, setSelectedCity] = useState<City>('Makkah');
|
const { selectedCity, setSelectedCity } = useCity();
|
||||||
const [prayerTimes, setPrayerTimes] = useState<Prayer[]>([]);
|
const [prayerTimes, setPrayerTimes] = useState<Prayer[]>([]);
|
||||||
const [nextPrayerName, setNextPrayerName] = useState<string | null>(null);
|
const [nextPrayerName, setNextPrayerName] = useState<string | null>(null);
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
|
|||||||
@@ -1,69 +1,96 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { StyleSheet, 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 { 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 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 (
|
||||||
<View style={[styles.container, { backgroundColor: Colors[colorScheme].background, paddingTop: insets.top }]}>
|
<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]}
|
||||||
@@ -76,26 +103,29 @@ 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>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
@@ -105,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',
|
||||||
@@ -153,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',
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
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;
|
||||||
|
};
|
||||||
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