Added app to repo.

This commit is contained in:
2026-02-19 15:43:11 -06:00
commit de34558cb1
61 changed files with 18893 additions and 0 deletions

138
.gitignore vendored Normal file
View File

@@ -0,0 +1,138 @@
# Expo
.expo
__generated__
web-build
bare-apps
# macOS
.DS_Store
# Node
node_modules
npm-debug.log
yarn-error.log
# Ruby
.direnv
# Env
.envrc.local
# Emacs
*~
# Vim
.*.swp
.*.swo
.*.swn
.*.swm
# VS Code
.vscode/launch.json
# Sublime Text
*.sublime-project
*.sublime-workspace
# Xcode
*.pbxuser
!default.pbxuser
*.xccheckout
*.xcscmblueprint
xcuserdata
# IDEA / Android Studio
*.iml
.gradle
.idea
# Eclipse
.project
.settings
# VSCode
.history/
/vscode/launch.json
# Android
*.apk
*.hprof
ReactAndroid-temp.aar
# Tools
jarjar-rules.txt
# Dynamic Macros
.kernel-ngrok-url
# Template files
/apps/bare-expo/android/app/google-services.json
/apps/bare-expo/ios/BareExpo/GoogleService-Info.plist
# Template projects
templates/**/android/**/generated/*
templates/**/android/app/build
templates/**/Pods/**
templates/**/Podfile.lock
templates/**/yarn.lock
templates/expo-template-bare-minimum/*.tgz
# Codemod
.codemod.bookmark
# Fastlane
/*.cer
/fastlane/report.xml
/fastlane/Preview.html
/fastlane/Deployment
/fastlane/test_output
/Preview.html
/gc_keys.json
/fastlane/gc_keys.json
# CI
/android/logcat.txt
# Shell apps
android-shell-app
shellAppBase-*
shellAppIntermediates
shellAppWorkspaces
/artifacts/*
# Expo Client builds
/client-builds
# Expo web env
.env.local
.env.development.local
.env.test.local
.env.production.local
apps/bare-expo/deploy-url.txt
# Temporary files created by Metro to check the health of the file watcher
.metro-health-check*
# Expo Doc merging
docs/pages/versions/*/react-native/ADDED_*.md
docs/pages/versions/*/react-native/REMOVED_*.md
docs/pages/versions/*/react-native/*.diff
# Expo Go
/apps/expo-go/src/dist
# Prebuilds
/packages/**/*.xcframework
/packages/**/*.spec.json
/packages/**/Info-generated.plist
!crsqlite.xcframework
# iOS
**/ios/.xcode.env.local
# TypeScript state
tsconfig.tsbuildinfo
# Secrets - contains real secrets fetched from GCP
/secrets/keys.json
/secrets/expotools.env

1
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1 @@
{ "recommendations": ["expo.vscode-expo-tools"] }

7
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,7 @@
{
"editor.codeActionsOnSave": {
"source.fixAll": "explicit",
"source.organizeImports": "explicit",
"source.sortMembers": "explicit"
}
}

40
actions/getCity.ts Normal file
View File

@@ -0,0 +1,40 @@
import * as Location from "expo-location";
interface CityLocation {
city: string;
country: string;
};
export async function getCity(): Promise<CityLocation | null> {
// Ask permission
const { status } = await Location.requestForegroundPermissionsAsync();
if (status !== "granted") {
console.log("Permission denied");
return null;
}
// Get current position
const location = await Location.getCurrentPositionAsync({
accuracy: Location.Accuracy.Balanced,
});
const { latitude, longitude } = location.coords;
// Reverse geocode (convert lat/lng -> address)
const address = await Location.reverseGeocodeAsync({
latitude,
longitude,
});
if (address.length > 0) {
const city = address[0].city ?? undefined;
const country = address[0].country ?? undefined;
if (city && country) {
return { city, country };
}
}
return null;
}

41
actions/getPrayerTimes.ts Normal file
View File

@@ -0,0 +1,41 @@
import { PrayerTimes, PrayerTimesResponse } from "@/types";
import { useQuery } from "@tanstack/react-query";
import { getCity } from "./getCity";
export function usePrayerTimes() {
// Get today's date in DD-MM-YYYY format (required by Aladhan API)
const today = new Date();
const date = `${String(today.getDate()).padStart(2, '0')}-${String(today.getMonth() + 1).padStart(2, '0')}-${today.getFullYear()}`;
return useQuery<PrayerTimes>({
queryKey: ['prayerTimes'],
queryFn: async () => {
const location = await getCity();
if (!location) {
throw new Error('City not available');
}
console.log(`Fetching prayer times for ${location.city}, ${location.country} on ${date}`);
const response = await fetch(
`https://api.aladhan.com/v1/timingsByCity/${date}?city=${encodeURIComponent(location.city)}&country=${encodeURIComponent(location.country)}`
);
if (!response.ok) {
throw new Error(response.statusText || 'Failed to fetch prayer times');
}
const data: PrayerTimesResponse = await response.json();
return {
Fajr: data.data.timings.Fajr,
Sunrise: data.data.timings.Sunrise,
Dhuhr: data.data.timings.Dhuhr,
Asr: data.data.timings.Asr,
Maghrib: data.data.timings.Maghrib,
Isha: data.data.timings.Isha,
};
},
});
};

View File

@@ -0,0 +1,30 @@
import { Coordinates } from "@/types";
const KAABA = {
latitude: 21.4225,
longitude: 39.8262,
} satisfies Coordinates;
export function getQiblaBearing(
userLat: number,
userLon: number
): number {
const kaabaLat = KAABA.latitude * (Math.PI / 180)
const kaabaLon = KAABA.longitude * (Math.PI / 180)
const φ1 = userLat * (Math.PI / 180)
const λ1 = userLon * (Math.PI / 180)
const Δλ = kaabaLon - λ1
const x = Math.sin(Δλ)
const y =
Math.cos(φ1) * Math.tan(kaabaLat) -
Math.sin(φ1) * Math.cos(Δλ)
let bearing = Math.atan2(x, y)
bearing = (bearing * 180) / Math.PI
return (bearing + 360) % 360
}

61
app.json Normal file
View File

@@ -0,0 +1,61 @@
{
"expo": {
"name": "Miqat",
"slug": "miqat",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/images/icon.png",
"scheme": "prayerapp",
"userInterfaceStyle": "automatic",
"newArchEnabled": true,
"splash": {
"image": "./assets/images/splash-icon.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
"ios": {
"supportsTablet": true,
"bundleIdentifier": "com.anonymous.miqat",
"infoPlist": {
"ITSAppUsesNonExemptEncryption": false
}
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/images/adaptive-icon.png",
"backgroundColor": "#ffffff"
},
"edgeToEdgeEnabled": true,
"predictiveBackGestureEnabled": false,
"permissions": [
"android.permission.ACCESS_COARSE_LOCATION",
"android.permission.ACCESS_FINE_LOCATION"
],
"package": "com.anonymous.miqat"
},
"web": {
"bundler": "metro",
"output": "static",
"favicon": "./assets/images/favicon.png"
},
"plugins": [
"expo-router",
[
"expo-location",
{
"locationWhenInUsePermission": "Allow Miqat to use your location to determine prayer times based on your current location."
}
]
],
"experiments": {
"typedRoutes": true
},
"extra": {
"router": {},
"eas": {
"projectId": "4c32053e-c596-4300-969b-65c8f2a07c45"
}
},
"owner": "coolestdude_28"
}
}

50
app/(tabs)/_layout.tsx Normal file
View File

@@ -0,0 +1,50 @@
import { Header } from '@/components/Header';
import { useClientOnlyValue } from '@/components/useClientOnlyValue';
import { useColorScheme } from '@/components/useColorScheme';
import Colors from '@/constants/Colors';
import { Tabs } from 'expo-router';
import { Box, HandHelping } from 'lucide-react-native';
import React, { Suspense } from 'react';
import { Text } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
export default function TabLayout() {
const colorScheme = useColorScheme();
const theme = Colors[colorScheme ?? 'light'];
return (
<Tabs
screenOptions={{
tabBarActiveTintColor: Colors[colorScheme ?? 'light'].tint,
tabBarStyle: {
backgroundColor: theme.background,
borderTopColor: theme.border,
},
// Disable the static render of the header on web
// to prevent a hydration error in React Navigation v6.
headerShown: useClientOnlyValue(false, true),
header: () => (
<SafeAreaView edges={['top']} style={{ backgroundColor: theme.background }}>
<Suspense fallback={<Text style={{ color: theme.text, paddingHorizontal: 20 }}>Loading...</Text>}>
<Header />
</Suspense>
</SafeAreaView>
),
}}>
<Tabs.Screen
name="index"
options={{
title: 'Prayer Times',
tabBarIcon: ({ color }) => <HandHelping color={color} />,
}}
/>
<Tabs.Screen
name="qibla"
options={{
title: 'Qibla',
tabBarIcon: ({ color }) => <Box color={color} />,
}}
></Tabs.Screen>
</Tabs>
);
}

355
app/(tabs)/index.tsx Normal file
View File

@@ -0,0 +1,355 @@
import { usePrayerTimes } from '@/actions/getPrayerTimes';
import { useColorScheme } from '@/components/useColorScheme';
import Colors from '@/constants/Colors';
import { PrayerTimes } from '@/types';
import { Cloud, Moon, Sun, Sunrise, Sunset } from 'lucide-react-native';
import React, { useEffect, useState } from 'react';
import {
ActivityIndicator,
ScrollView,
StyleSheet,
Text,
View
} from 'react-native';
type Status = 'done' | 'active' | 'upcoming';
function getNextPrayer(prayers: PrayerTimes) {
const now = new Date();
const currentTime = now.getHours() * 60 + now.getMinutes(); // Convert current time to minutes
const prayerTimes = Object.entries(prayers).map(([name, time]) => {
const [hour, minute] = time.split(':').map(Number);
const prayerTime = hour * 60 + minute; // Convert prayer time to minutes
return { name, time, prayerTime };
});
// Find the next prayer
for (const prayer of prayerTimes) {
if (currentTime < prayer.prayerTime) {
return prayer;
}
}
// If all prayers have passed, return the first one for the next day
return prayerTimes[0];
}
function determineStatus(prayerTime: number): Status {
const now = new Date();
const currentTime = now.getHours() * 60 + now.getMinutes();
if (currentTime > prayerTime) {
return 'done';
} else if (currentTime === prayerTime) {
return 'active';
} else {
return 'upcoming';
}
}
function getCountdownToPrayer(time: string) {
const now = new Date();
const currentMinutes = now.getHours() * 60 + now.getMinutes();
const [hour, minute] = time.split(':').map(Number);
const targetMinutes = hour * 60 + minute;
let diffMinutes = targetMinutes - currentMinutes;
if (diffMinutes < 0) {
diffMinutes += 24 * 60;
}
const hours = Math.floor(diffMinutes / 60);
const minutes = diffMinutes % 60;
return `${hours}H ${minutes}m`;
}
export default function MaqitScreen() {
const { data: prayers, isLoading, error } = usePrayerTimes();
const [nextPrayer, setNextPrayer] = useState<{ name: string; time: string } | null>(null);
const colorScheme = useColorScheme();
const theme = Colors[colorScheme ?? 'light'];
const styles = React.useMemo(() => createStyles(theme), [theme]);
useEffect(() => {
if (error) {
console.error('Error fetching prayer times:', error);
}
}, [error]);
useEffect(() => {
if (prayers) {
const next = getNextPrayer(prayers);
setNextPrayer(next);
}
}, [prayers, isLoading]);
const countdown = nextPrayer ? getCountdownToPrayer(nextPrayer.time) : null;
return (
<View style={styles.safe}>
<ScrollView
style={styles.scroll}
contentContainerStyle={styles.container}
showsVerticalScrollIndicator={false}
>
{/* ── Next Prayer Banner ─────────────────────────── */}
<View style={styles.banner}>
<View>
<Text style={styles.bannerLabel}>NEXT PRAYER</Text>
<Text style={styles.bannerName}>{nextPrayer?.name}</Text>
<Text style={styles.bannerDate}>Thu, 19 February</Text>
</View>
<View style={styles.bannerRight}>
<Text style={styles.bannerTime}>{nextPrayer?.time}</Text>
<Text style={styles.bannerCountdown}>{countdown ? `in ${countdown}` : ''}</Text>
</View>
</View>
{/* ── Prayer List ────────────────────────────────── */}
<View style={styles.list}>
{!isLoading && prayers ? (
<>
<PrayerRow name="Fajr" time={prayers.Fajr} Icon={Sunrise} label="Pre-dawn" theme={theme} />
<PrayerRow name="Dhuhr" time={prayers.Dhuhr} Icon={Sun} label="Noon" theme={theme} />
<PrayerRow name="Asr" time={prayers.Asr} Icon={Cloud} label="Afternoon" theme={theme} />
<PrayerRow name="Maghrib" time={prayers.Maghrib} Icon={Sunset} label="Sunset" theme={theme} />
<PrayerRow name="Isha" time={prayers.Isha} Icon={Moon} label="Night" theme={theme} />
</>
) : (
<ActivityIndicator
size="small"
color={theme.textDim}
style={styles.loadingSpinner}
/>
)}
</View>
{/* ── Tagline ───────────────────────────────────── */}
</ScrollView >
</View >
);
}
function PrayerRow({
name,
time,
Icon,
label,
theme,
}: {
name: string;
time: string;
Icon: React.ComponentType<{ size: number; color: string }>;
label: string;
theme: (typeof Colors)['light'];
}) {
const status = determineStatus(parseInt(time.split(':')[0]) * 60 + parseInt(time.split(':')[1]));
const isActive = status === 'active';
const isDone = status === 'done';
const styles = React.useMemo(() => createStyles(theme), [theme]);
const iconColor = isActive ? theme.green : isDone ? theme.textDim : theme.textMuted;
return (
<View
style={[
styles.row,
isActive && styles.rowActive,
isDone && styles.rowDone,
]}
>
{/* Icon pill */}
<View style={[styles.iconPill, isActive && styles.iconPillActive]}>
<Icon
size={16}
color={iconColor}
/>
</View>
{/* Name + label */}
<View style={styles.rowMid}>
<Text
style={[
styles.prayerName,
isActive && styles.prayerNameActive,
isDone && styles.prayerNameDone,
]}
>
{name}
</Text>
<Text style={styles.prayerLabel}>{label}</Text>
</View>
{/* Time + badge */}
<View style={styles.rowRight}>
<Text
style={[
styles.prayerTime,
isActive && styles.prayerTimeActive,
isDone && styles.prayerTimeDone,
]}
>
{time}
</Text>
{isActive && <Text style={styles.badgeActive}>NEXT</Text>}
{isDone && <Text style={styles.badgeDone}>Done</Text>}
</View>
</View>
);
}
const createStyles = (theme: (typeof Colors)['light']) =>
StyleSheet.create({
safe: {
flex: 1,
backgroundColor: theme.background,
},
scroll: {
flex: 1,
},
container: {
paddingHorizontal: 20,
paddingBottom: 40,
},
// Banner
banner: {
backgroundColor: theme.bannerBg,
borderWidth: 1,
borderColor: theme.bannerBorder,
borderRadius: 20,
padding: 18,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 20,
},
bannerLabel: {
fontSize: 10,
color: theme.textMuted,
fontWeight: '700',
letterSpacing: 1,
marginBottom: 4,
},
bannerName: {
fontSize: 20,
fontWeight: '800',
color: theme.text,
letterSpacing: -0.8,
},
bannerDate: {
fontSize: 12,
color: theme.textDim,
marginTop: 4,
},
bannerRight: {
alignItems: 'flex-end',
},
bannerTime: {
fontSize: 36,
fontWeight: '800',
color: theme.green,
letterSpacing: -2,
},
bannerCountdown: {
fontSize: 12,
color: theme.textDim,
marginTop: 4,
},
// Prayer list
list: {
gap: 9,
},
row: {
flexDirection: 'row',
alignItems: 'center',
gap: 12,
borderRadius: 16,
padding: 12,
paddingHorizontal: 14,
backgroundColor: theme.overlay,
borderWidth: 1,
borderColor: theme.border,
},
rowActive: {
backgroundColor: theme.bannerBg,
borderColor: theme.bannerBorder,
},
rowDone: {
backgroundColor: theme.surfaceAlt,
borderColor: theme.border,
},
iconPill: {
width: 36,
height: 36,
borderRadius: 10,
backgroundColor: theme.overlay,
alignItems: 'center',
justifyContent: 'center',
},
iconPillActive: {
backgroundColor: theme.bannerBg,
},
rowMid: {
flex: 1,
},
prayerName: {
fontSize: 15,
fontWeight: '500',
color: theme.textMuted,
letterSpacing: -0.3,
},
prayerNameActive: {
fontWeight: '700',
color: theme.text,
},
prayerNameDone: {
color: theme.textDim,
},
prayerLabel: {
fontSize: 11,
color: theme.textDim,
marginTop: 1,
},
rowRight: {
alignItems: 'flex-end',
},
prayerTime: {
fontSize: 16,
fontWeight: '700',
letterSpacing: -0.5,
color: theme.textDim,
},
prayerTimeActive: {
color: theme.green,
},
prayerTimeDone: {
color: theme.textDim,
},
badgeActive: {
fontSize: 10,
fontWeight: '700',
color: theme.green,
marginTop: 2,
letterSpacing: 0.5,
},
badgeDone: {
fontSize: 10,
color: theme.textDim,
marginTop: 2,
},
loadingText: {
textAlign: 'center',
fontSize: 12,
color: theme.textDim,
marginVertical: 8,
},
loadingSpinner: {
marginVertical: 10,
},
});

592
app/(tabs)/qibla.tsx Normal file
View File

@@ -0,0 +1,592 @@
import { getQiblaBearing } from "@/actions/getQiblaBearing";
import { useColorScheme } from "@/components/useColorScheme";
import Colors from "@/constants/Colors";
import * as Location from "expo-location";
import { Magnetometer } from "expo-sensors";
import React, { useEffect, useRef, useState } from "react";
import {
Animated,
Dimensions,
Platform,
StyleSheet,
Text,
View,
} from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
type Palette = {
bg: string;
card: string;
cardBorder: string;
green: string;
greenDim: string;
greenGlow: string;
text: string;
textMuted: string;
textDim: string;
};
function toRad(deg: number) {
return (deg * Math.PI) / 180;
}
// ─── Degree tick marks ────────────────────────────────────────────────────────
const { width } = Dimensions.get('window');
const DIAL_SIZE = width * 0.78;
const TICK_COUNT = 72; // every 5°
const COMPASS_RADIUS = DIAL_SIZE / 2 - 12;
// ─── Cardinal Labels ──────────────────────────────────────────────────────────
const CARDINALS = ['N', 'E', 'S', 'W'];
const CARDINAL_RADIUS = DIAL_SIZE / 2 - 40;
export default function QiblaCompass() {
const [heading, setHeading] = useState(0);
const [qibla, setQibla] = useState(0);
const [aligned, setAligned] = useState(false);
const colorScheme = useColorScheme();
const theme = Colors[colorScheme ?? 'light'];
const palette: Palette = {
bg: theme.background,
card: theme.card,
cardBorder: theme.cardBorder,
green: theme.green,
greenDim: theme.greenDim,
greenGlow: theme.greenGlow,
text: theme.text,
textMuted: theme.textMuted,
textDim: theme.textDim,
};
const styles = React.useMemo(() => createStyles(palette), [palette]);
const compassRotation = useRef(new Animated.Value(0)).current;
const needleRotation = useRef(new Animated.Value(0)).current;
const glowAnim = useRef(new Animated.Value(0)).current;
const lastHeading = useRef(0);
useEffect(() => {
async function setup() {
const { status } =
await Location.requestForegroundPermissionsAsync()
if (status !== "granted") return
const location =
await Location.getCurrentPositionAsync({})
const bearing = getQiblaBearing(
location.coords.latitude,
location.coords.longitude
)
setQibla(Math.round(bearing))
}
setup()
Magnetometer.setUpdateInterval(100)
const subscription = Magnetometer.addListener(
(data) => {
let angle = Math.atan2(data.y, data.x) * (180 / Math.PI);
angle = (angle + 360) % 360;
// Smooth out jitter
const diff = angle - lastHeading.current;
const smoothed =
lastHeading.current +
(Math.abs(diff) > 180 ? diff - Math.sign(diff) * 360 : diff) * 0.3;
lastHeading.current = smoothed;
setHeading(Math.round((smoothed + 360) % 360));
}
)
return () => subscription.remove()
}, [])
// Animate compass dial rotation (opposite of heading)
useEffect(() => {
Animated.spring(compassRotation, {
toValue: -heading,
useNativeDriver: true,
tension: 40,
friction: 8,
}).start();
}, [heading]);
// Animate needle to qibla direction
useEffect(() => {
Animated.spring(needleRotation, {
toValue: qibla - heading,
useNativeDriver: true,
tension: 40,
friction: 8,
}).start();
}, [qibla, heading]);
// Check alignment
useEffect(() => {
const diff = Math.abs(((qibla - heading + 540) % 360) - 180);
const isAligned = diff < 5;
setAligned(isAligned);
if (isAligned) {
Animated.loop(
Animated.sequence([
Animated.timing(glowAnim, {
toValue: 1,
duration: 700,
useNativeDriver: true,
}),
Animated.timing(glowAnim, {
toValue: 0,
duration: 700,
useNativeDriver: true,
}),
])
).start();
} else {
glowAnim.setValue(0);
}
}, [heading, qibla]);
const compassDeg = compassRotation.interpolate({
inputRange: [-360, 0, 360],
outputRange: ['-360deg', '0deg', '360deg'],
});
const needleDeg = needleRotation.interpolate({
inputRange: [-720, 0, 720],
outputRange: ['-720deg', '0deg', '720deg'],
});
const glowOpacity = glowAnim.interpolate({
inputRange: [0, 1],
outputRange: [0.4, 1],
});
const TickMarks = () => {
const ticks = [];
for (let i = 0; i < TICK_COUNT; i++) {
const angle = (i / TICK_COUNT) * 360;
const isMajor = i % 9 === 0; // every 45°
const isCardinal = i % 18 === 0; // every 90°
ticks.push(
<View
key={i}
style={[
styles.tick,
{
transform: [
{ rotate: `${angle}deg` },
{ translateY: -COMPASS_RADIUS },
],
height: isCardinal ? 16 : isMajor ? 10 : 5,
backgroundColor: isCardinal
? palette.green
: isMajor
? palette.greenDim
: palette.cardBorder,
width: isCardinal ? 2.5 : 1.5,
},
]}
/>
);
}
return <View style={styles.tickContainer}>{ticks}</View>;
};
const CardinalLabels = ({ rotation }: { rotation: Animated.Value }) => {
const center = DIAL_SIZE / 2;
return (
<Animated.View
style={[
styles.cardinalContainer,
{
transform: [
{
rotate: rotation.interpolate({
inputRange: [0, 360],
outputRange: ['0deg', '360deg'],
}),
},
],
},
]}
>
{CARDINALS.map((label, i) => {
const angle = toRad(i * 90);
const x = center + CARDINAL_RADIUS * Math.sin(angle);
const y = center - CARDINAL_RADIUS * Math.cos(angle);
return (
<Text
key={label}
style={[
styles.cardinalText,
{
position: 'absolute',
left: x - 7,
top: y - 9,
color: label === 'N' ? palette.green : palette.textMuted,
fontWeight: label === 'N' ? '700' : '500',
},
]}
>
{label}
</Text>
);
})}
</Animated.View>
);
};
return (
<SafeAreaView style={styles.container} edges={['bottom', 'left', 'right']}>
{/* Qibla Card */}
<View style={styles.card}>
<Text style={styles.cardLabel}>QIBLA DIRECTION</Text>
<View style={styles.cardRow}>
<Text style={styles.cardPrimary}>Makkah</Text>
<Text style={[styles.cardValue, aligned && styles.cardValueAligned]}>
{qibla}°
</Text>
</View>
<Text style={styles.cardSub}>
{aligned ? '✓ Facing Qibla' : `Rotate ${qibla}° from North`}
</Text>
</View>
{/* Compass */}
<View style={styles.compassOuter}>
{/* Glow ring when aligned */}
{aligned && (
<Animated.View
style={[styles.glowRing, { opacity: glowOpacity }]}
/>
)}
{/* Rotating compass dial */}
<Animated.View
style={[
styles.compassDial,
{ transform: [{ rotate: compassDeg }] },
]}
>
<TickMarks />
<CardinalLabels rotation={new Animated.Value(0)} />
</Animated.View>
{/* Outer ring */}
<View style={styles.compassRing} />
{/* Fixed Kaaba marker at top */}
<Animated.View
style={[
styles.qiblaMarkerContainer,
{ transform: [{ rotate: needleDeg }] },
]}
>
<View style={styles.qiblaMarkerLine} />
<View style={styles.qiblaMarkerTip} />
</Animated.View>
{/* Center circle with Kaaba icon */}
<View style={styles.centerCircle}>
<Text style={styles.kaabaIcon}>🕋</Text>
</View>
</View>
{/* Heading info */}
<View style={styles.infoRow}>
<View style={styles.infoItem}>
<Text style={styles.infoLabel}>HEADING</Text>
<Text style={styles.infoValue}>{heading}°</Text>
</View>
<View style={styles.infoDivider} />
<View style={styles.infoItem}>
<Text style={styles.infoLabel}>QIBLA</Text>
<Text style={[styles.infoValue, { color: palette.green }]}>
{qibla}°
</Text>
</View>
<View style={styles.infoDivider} />
<View style={styles.infoItem}>
<Text style={styles.infoLabel}>OFFSET</Text>
<Text style={styles.infoValue}>
{Math.round(Math.abs(((qibla - heading + 540) % 360) - 180))}°
</Text>
</View>
</View>
<Text style={styles.footer}>Simple, easy, and ad free always.</Text>
</SafeAreaView>
);
}
// ─── Styles ───────────────────────────────────────────────────────────────────
const createStyles = (palette: Palette) =>
StyleSheet.create({
container: {
flex: 1,
backgroundColor: palette.bg,
paddingHorizontal: 20,
paddingTop: 16,
},
// Card
card: {
backgroundColor: palette.card,
borderRadius: 16,
padding: 16,
borderWidth: 1,
borderColor: palette.cardBorder,
marginBottom: 40,
},
cardLabel: {
color: palette.textMuted,
fontSize: 11,
fontWeight: '700',
letterSpacing: 1.2,
marginBottom: 6,
},
cardRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
cardPrimary: {
color: palette.text,
fontSize: 26,
fontWeight: '800',
},
cardValue: {
color: palette.green,
fontSize: 32,
fontWeight: '800',
fontVariant: ['tabular-nums'],
},
cardValueAligned: {
color: palette.green,
textShadowColor: palette.greenGlow,
textShadowOffset: { width: 0, height: 0 },
textShadowRadius: 8,
},
cardSub: {
color: palette.textDim,
fontSize: 13,
marginTop: 4,
},
// Compass outer shell
compassOuter: {
width: DIAL_SIZE,
height: DIAL_SIZE,
alignSelf: 'center',
alignItems: 'center',
justifyContent: 'center',
marginBottom: 28,
},
glowRing: {
position: 'absolute',
width: DIAL_SIZE + 16,
height: DIAL_SIZE + 16,
borderRadius: (DIAL_SIZE + 16) / 2,
borderWidth: 2,
borderColor: palette.green,
},
compassRing: {
position: 'absolute',
width: DIAL_SIZE,
height: DIAL_SIZE,
borderRadius: DIAL_SIZE / 2,
borderWidth: 1.5,
borderColor: palette.cardBorder,
backgroundColor: 'transparent',
},
compassDial: {
position: 'absolute',
width: DIAL_SIZE,
height: DIAL_SIZE,
borderRadius: DIAL_SIZE / 2,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: palette.card,
overflow: 'hidden',
},
// Tick marks
tickContainer: {
position: 'absolute',
width: DIAL_SIZE,
height: DIAL_SIZE,
alignItems: 'center',
justifyContent: 'center',
},
tick: {
position: 'absolute',
borderRadius: 2,
transformOrigin: 'center center',
},
// Cardinal labels
cardinalContainer: {
position: 'absolute',
width: DIAL_SIZE,
height: DIAL_SIZE,
alignItems: 'center',
justifyContent: 'center',
},
cardinalText: {
fontSize: 15,
fontWeight: '600',
},
// Qibla needle / marker
qiblaMarkerContainer: {
position: 'absolute',
width: DIAL_SIZE,
height: DIAL_SIZE,
alignItems: 'center',
justifyContent: 'center',
},
qiblaMarkerLine: {
position: 'absolute',
width: 2,
height: DIAL_SIZE * 0.38,
backgroundColor: palette.green,
top: DIAL_SIZE * 0.05,
borderRadius: 2,
opacity: 0.8,
},
qiblaMarkerTip: {
position: 'absolute',
top: DIAL_SIZE * 0.03,
width: 10,
height: 10,
borderRadius: 5,
backgroundColor: palette.green,
},
// Needle
needleWrapper: {
width: 14,
height: 100,
alignItems: 'center',
position: 'absolute',
},
needleHalf: {
width: 6,
height: 50,
},
needleNorth: {
backgroundColor: palette.green,
borderTopLeftRadius: 3,
borderTopRightRadius: 3,
},
needleSouth: {
backgroundColor: palette.cardBorder,
borderBottomLeftRadius: 3,
borderBottomRightRadius: 3,
},
needleDot: {
position: 'absolute',
top: '50%',
marginTop: -6,
width: 12,
height: 12,
borderRadius: 6,
backgroundColor: palette.bg,
borderWidth: 2,
borderColor: palette.green,
},
// Center
centerCircle: {
width: 64,
height: 64,
borderRadius: 32,
backgroundColor: palette.card,
borderWidth: 2,
borderColor: palette.cardBorder,
alignItems: 'center',
justifyContent: 'center',
zIndex: 10,
},
kaabaIcon: {
fontSize: 28,
},
// Info row
infoRow: {
flexDirection: 'row',
backgroundColor: palette.card,
borderRadius: 14,
padding: 14,
borderWidth: 1,
borderColor: palette.cardBorder,
marginBottom: 16,
},
infoItem: {
flex: 1,
alignItems: 'center',
},
infoLabel: {
color: palette.textDim,
fontSize: 10,
fontWeight: '700',
letterSpacing: 1,
marginBottom: 4,
},
infoValue: {
color: palette.text,
fontSize: 18,
fontWeight: '700',
fontVariant: ['tabular-nums'],
},
infoDivider: {
width: 1,
backgroundColor: palette.cardBorder,
marginHorizontal: 8,
},
footer: {
color: palette.textDim,
fontSize: 12,
textAlign: 'center',
marginBottom: 8,
opacity: 0.6,
},
// Tab bar
tabBar: {
flexDirection: 'row',
borderTopWidth: 1,
borderTopColor: palette.cardBorder,
paddingTop: 12,
paddingBottom: Platform.OS === 'ios' ? 28 : 12,
marginHorizontal: -20,
paddingHorizontal: 20,
},
tabItem: {
flex: 1,
alignItems: 'center',
gap: 4,
},
tabItemActive: {
opacity: 1,
},
tabIcon: {
fontSize: 22,
},
tabLabel: {
fontSize: 11,
color: palette.textDim,
fontWeight: '500',
},
tabLabelActive: {
color: palette.green,
fontWeight: '600',
},
});

38
app/+html.tsx Normal file
View File

@@ -0,0 +1,38 @@
import { ScrollViewStyleReset } from 'expo-router/html';
// This file is web-only and used to configure the root HTML for every
// web page during static rendering.
// The contents of this function only run in Node.js environments and
// do not have access to the DOM or browser APIs.
export default function Root({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta httpEquiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
{/*
Disable body scrolling on web. This makes ScrollView components work closer to how they do on native.
However, body scrolling is often nice to have for mobile web. If you want to enable it, remove this line.
*/}
<ScrollViewStyleReset />
{/* Using raw CSS styles as an escape-hatch to ensure the background color never flickers in dark-mode. */}
<style dangerouslySetInnerHTML={{ __html: responsiveBackground }} />
{/* Add any additional <head> elements that you want globally available on web... */}
</head>
<body>{children}</body>
</html>
);
}
const responsiveBackground = `
body {
background-color: #fff;
}
@media (prefers-color-scheme: dark) {
body {
background-color: #000;
}
}`;

40
app/+not-found.tsx Normal file
View File

@@ -0,0 +1,40 @@
import { Link, Stack } from 'expo-router';
import { StyleSheet } from 'react-native';
import { Text, View } from '@/components/Themed';
export default function NotFoundScreen() {
return (
<>
<Stack.Screen options={{ title: 'Oops!' }} />
<View style={styles.container}>
<Text style={styles.title}>This screen doesn't exist.</Text>
<Link href="/" style={styles.link}>
<Text style={styles.linkText}>Go to home screen!</Text>
</Link>
</View>
</>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
padding: 20,
},
title: {
fontSize: 20,
fontWeight: 'bold',
},
link: {
marginTop: 15,
paddingVertical: 15,
},
linkText: {
fontSize: 14,
color: '#2e78b7',
},
});

73
app/_layout.tsx Normal file
View File

@@ -0,0 +1,73 @@
import QueryProvider from '@/components/QueryProvider';
import FontAwesome from '@expo/vector-icons/FontAwesome';
import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native';
import { useFonts } from 'expo-font';
import { Stack } from 'expo-router';
import * as SplashScreen from 'expo-splash-screen';
import { useEffect } from 'react';
import 'react-native-reanimated';
import { ThemePreferenceProvider, useThemePreference } from '@/components/ThemeProvider';
export {
// Catch any errors thrown by the Layout component.
ErrorBoundary
} from 'expo-router';
export const unstable_settings = {
// Ensure that reloading on `/modal` keeps a back button present.
initialRouteName: '(tabs)',
};
// Prevent the splash screen from auto-hiding before asset loading is complete.
SplashScreen.preventAutoHideAsync();
function RootLayout() {
const [loaded, error] = useFonts({
SpaceMono: require('../assets/fonts/SpaceMono-Regular.ttf'),
...FontAwesome.font,
});
// Expo Router uses Error Boundaries to catch errors in the navigation tree.
useEffect(() => {
if (error) throw error;
}, [error]);
useEffect(() => {
if (loaded) {
SplashScreen.hideAsync();
}
}, [loaded]);
if (!loaded) {
return null;
}
return <RootLayoutNav />;
}
function RootLayoutNav() {
const { effectiveScheme } = useThemePreference();
return (
<ThemeProvider value={effectiveScheme === 'dark' ? DarkTheme : DefaultTheme}>
<QueryProvider>
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false, title: 'Maqit' }} />
<Stack.Screen
name="settings"
options={{ headerTitle: 'Appearance', headerBackTitle: 'Home' }}
/>
</Stack>
</QueryProvider>
</ThemeProvider>
);
}
export default function RootLayoutWithProviders() {
return (
<ThemePreferenceProvider>
<RootLayout />
</ThemePreferenceProvider>
);
}

109
app/settings.tsx Normal file
View File

@@ -0,0 +1,109 @@
import { useThemePreference } from '@/components/ThemeProvider';
import { useColorScheme } from '@/components/useColorScheme';
import Colors from '@/constants/Colors';
import React from 'react';
import { Pressable, StyleSheet, Text, View } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
type ThemeOption = 'system' | 'light' | 'dark';
const OPTIONS: Array<{ label: string; value: ThemeOption }> = [
{ label: 'System', value: 'system' },
{ label: 'Light', value: 'light' },
{ label: 'Dark', value: 'dark' },
];
export default function SettingsScreen() {
const colorScheme = useColorScheme();
const { preference, setPreference } = useThemePreference();
const theme = Colors[colorScheme ?? 'light'];
return (
<SafeAreaView style={[styles.container, { backgroundColor: theme.background }]}>
<View style={[styles.card, { backgroundColor: theme.card, borderColor: theme.cardBorder }]}
>
<Text style={[styles.label, { color: theme.textMuted }]}>APPEARANCE</Text>
<Text style={[styles.title, { color: theme.text }]}>Theme</Text>
<View style={[styles.segment, { backgroundColor: theme.surfaceAlt, borderColor: theme.border }]}
>
{OPTIONS.map((option) => {
const isActive = preference === option.value;
return (
<Pressable
key={option.value}
onPress={() => setPreference(option.value)}
style={({ pressed }) => [
styles.segmentButton,
{
backgroundColor: isActive ? theme.surface : 'transparent',
borderColor: isActive ? theme.borderStrong : 'transparent',
opacity: pressed ? 0.7 : 1,
},
]}
>
<Text
style={[
styles.segmentLabel,
{ color: isActive ? theme.text : theme.textDim },
]}
>
{option.label}
</Text>
</Pressable>
);
})}
</View>
<Text style={[styles.helper, { color: theme.textDim }]}
>
System follows your device appearance. Light and Dark override it.
</Text>
</View>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
},
card: {
borderRadius: 16,
borderWidth: 1,
padding: 16,
},
label: {
fontSize: 11,
fontWeight: '700',
letterSpacing: 1,
marginBottom: 8,
},
title: {
fontSize: 18,
fontWeight: '700',
marginBottom: 16,
},
segment: {
flexDirection: 'row',
borderRadius: 12,
borderWidth: 1,
padding: 4,
gap: 6,
},
segmentButton: {
flex: 1,
borderRadius: 10,
borderWidth: 1,
paddingVertical: 10,
alignItems: 'center',
},
segmentLabel: {
fontSize: 13,
fontWeight: '600',
},
helper: {
marginTop: 12,
fontSize: 12,
lineHeight: 18,
},
});

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
assets/images/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
assets/images/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -0,0 +1,77 @@
import React from 'react';
import { StyleSheet } from 'react-native';
import { ExternalLink } from './ExternalLink';
import { MonoText } from './StyledText';
import { Text, View } from './Themed';
import Colors from '@/constants/Colors';
export default function EditScreenInfo({ path }: { path: string }) {
return (
<View>
<View style={styles.getStartedContainer}>
<Text
style={styles.getStartedText}
lightColor="rgba(0,0,0,0.8)"
darkColor="rgba(255,255,255,0.8)">
Open up the code for this screen:
</Text>
<View
style={[styles.codeHighlightContainer, styles.homeScreenFilename]}
darkColor="rgba(255,255,255,0.05)"
lightColor="rgba(0,0,0,0.05)">
<MonoText>{path}</MonoText>
</View>
<Text
style={styles.getStartedText}
lightColor="rgba(0,0,0,0.8)"
darkColor="rgba(255,255,255,0.8)">
Change any of the text, save the file, and your app will automatically update.
</Text>
</View>
<View style={styles.helpContainer}>
<ExternalLink
style={styles.helpLink}
href="https://docs.expo.io/get-started/create-a-new-app/#opening-the-app-on-your-phonetablet">
<Text style={styles.helpLinkText} lightColor={Colors.light.tint}>
Tap here if your app doesn't automatically update after making changes
</Text>
</ExternalLink>
</View>
</View>
);
}
const styles = StyleSheet.create({
getStartedContainer: {
alignItems: 'center',
marginHorizontal: 50,
},
homeScreenFilename: {
marginVertical: 7,
},
codeHighlightContainer: {
borderRadius: 3,
paddingHorizontal: 4,
},
getStartedText: {
fontSize: 17,
lineHeight: 24,
textAlign: 'center',
},
helpContainer: {
marginTop: 15,
marginHorizontal: 20,
alignItems: 'center',
},
helpLink: {
paddingVertical: 15,
},
helpLinkText: {
textAlign: 'center',
},
});

View File

@@ -0,0 +1,25 @@
import { Link } from 'expo-router';
import * as WebBrowser from 'expo-web-browser';
import React from 'react';
import { Platform } from 'react-native';
export function ExternalLink(
props: Omit<React.ComponentProps<typeof Link>, 'href'> & { href: string }
) {
return (
<Link
target="_blank"
{...props}
// @ts-expect-error: External URLs are not typed.
href={props.href}
onPress={(e) => {
if (Platform.OS !== 'web') {
// Prevent the default behavior of linking to the default browser on native.
e.preventDefault();
// Open the link in an in-app browser.
WebBrowser.openBrowserAsync(props.href as string);
}
}}
/>
);
}

130
components/Header.tsx Normal file
View File

@@ -0,0 +1,130 @@
import { useColorScheme } from '@/components/useColorScheme';
import Colors from '@/constants/Colors';
import { useRouter } from 'expo-router';
import { Settings } from 'lucide-react-native';
import React, { useMemo } from 'react';
import {
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native';
export function Header() {
const router = useRouter();
const colorScheme = useColorScheme();
const theme = Colors[colorScheme ?? 'light'];
const styles = useMemo(() => createStyles(theme), [theme]);
const iconColor = theme.textDim;
return (
<View style={styles.wrapper}>
{/* Top row: title + icon buttons */}
<View style={styles.headerRow}>
<View>
<Text style={styles.appName}>Maqit</Text>
<Text style={styles.tagline}>Simple and ad-free -- always.</Text>
</View>
<View style={styles.iconButtons}>
<TouchableOpacity
style={styles.iconBtn}
onPress={() => router.push('/settings')}
>
<Settings size={14} color={iconColor} />
</TouchableOpacity>
</View>
</View>
</View>
);
}
const createStyles = (theme: (typeof Colors)['light']) =>
StyleSheet.create({
wrapper: {
backgroundColor: theme.background,
paddingBottom: 12,
},
headerRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'flex-start',
paddingHorizontal: 20,
marginBottom: 16,
},
appName: {
fontSize: 30,
fontWeight: '800',
color: theme.text,
letterSpacing: -1.2,
},
iconButtons: {
flexDirection: 'row',
gap: 8,
marginTop: 4,
},
iconBtn: {
width: 34,
height: 34,
borderRadius: 10,
backgroundColor: theme.overlay,
borderWidth: 1,
borderColor: theme.border,
alignItems: 'center',
justifyContent: 'center',
},
// Banner
banner: {
backgroundColor: theme.bannerBg,
borderWidth: 1,
borderColor: theme.bannerBorder,
borderRadius: 20,
padding: 18,
paddingHorizontal: 24,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginHorizontal: 20,
},
bannerLabel: {
fontSize: 10,
color: theme.textMuted,
fontWeight: '700',
letterSpacing: 1,
marginBottom: 4,
},
bannerName: {
fontSize: 20,
fontWeight: '800',
color: theme.text,
letterSpacing: -0.8,
},
bannerDate: {
fontSize: 12,
color: theme.textDim,
marginTop: 4,
},
bannerRight: {
alignItems: 'flex-end',
},
bannerTime: {
fontSize: 36,
fontWeight: '800',
color: theme.green,
letterSpacing: -2,
},
bannerCountdown: {
fontSize: 12,
color: theme.textDim,
marginTop: 4,
},
// Tagline
tagline: {
textAlign: 'center',
paddingTop: 5,
fontSize: 11,
color: theme.textDim,
letterSpacing: 0.3,
},
});

View File

@@ -0,0 +1,11 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const queryClient = new QueryClient();
export default function QueryProvider({ children }: { children: React.ReactNode }) {
return (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
)
}

View File

@@ -0,0 +1,5 @@
import { Text, TextProps } from './Themed';
export function MonoText(props: TextProps) {
return <Text {...props} style={[props.style, { fontFamily: 'SpaceMono' }]} />;
}

View File

@@ -0,0 +1,67 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
import React, { createContext, useContext, useEffect, useMemo, useState } from 'react';
import { useColorScheme as useSystemColorScheme } from 'react-native';
type ThemePreference = 'system' | 'light' | 'dark';
type ThemePreferenceContextValue = {
preference: ThemePreference;
setPreference: (next: ThemePreference) => void;
effectiveScheme: 'light' | 'dark';
};
const THEME_PREFERENCE_KEY = 'themePreference';
const ThemePreferenceContext = createContext<ThemePreferenceContextValue | undefined>(undefined);
export function ThemePreferenceProvider({ children }: { children: React.ReactNode }) {
const systemScheme = useSystemColorScheme() ?? 'light';
const [preference, setPreferenceState] = useState<ThemePreference>('system');
useEffect(() => {
let isMounted = true;
AsyncStorage.getItem(THEME_PREFERENCE_KEY)
.then((value) => {
if (!isMounted || !value) return;
if (value === 'system' || value === 'light' || value === 'dark') {
setPreferenceState(value);
}
})
.catch(() => {
// Non-fatal: stick with the default preference.
});
return () => {
isMounted = false;
};
}, []);
const setPreference = (next: ThemePreference) => {
setPreferenceState(next);
AsyncStorage.setItem(THEME_PREFERENCE_KEY, next).catch(() => {
// Non-fatal: preference will remain for this session.
});
};
const effectiveScheme = preference === 'system' ? systemScheme : preference;
const value = useMemo(
() => ({ preference, setPreference, effectiveScheme }),
[preference, effectiveScheme]
);
return (
<ThemePreferenceContext.Provider value={value}>
{children}
</ThemePreferenceContext.Provider>
);
}
export function useThemePreference() {
const context = useContext(ThemePreferenceContext);
if (!context) {
throw new Error('useThemePreference must be used within ThemePreferenceProvider');
}
return context;
}

45
components/Themed.tsx Normal file
View File

@@ -0,0 +1,45 @@
/**
* Learn more about Light and Dark modes:
* https://docs.expo.io/guides/color-schemes/
*/
import { Text as DefaultText, View as DefaultView } from 'react-native';
import Colors from '@/constants/Colors';
import { useColorScheme } from './useColorScheme';
type ThemeProps = {
lightColor?: string;
darkColor?: string;
};
export type TextProps = ThemeProps & DefaultText['props'];
export type ViewProps = ThemeProps & DefaultView['props'];
export function useThemeColor(
props: { light?: string; dark?: string },
colorName: keyof typeof Colors.light & keyof typeof Colors.dark
) {
const theme = useColorScheme() ?? 'light';
const colorFromProps = props[theme];
if (colorFromProps) {
return colorFromProps;
} else {
return Colors[theme][colorName];
}
}
export function Text(props: TextProps) {
const { style, lightColor, darkColor, ...otherProps } = props;
const color = useThemeColor({ light: lightColor, dark: darkColor }, 'text');
return <DefaultText style={[{ color }, style]} {...otherProps} />;
}
export function View(props: ViewProps) {
const { style, lightColor, darkColor, ...otherProps } = props;
const backgroundColor = useThemeColor({ light: lightColor, dark: darkColor }, 'background');
return <DefaultView style={[{ backgroundColor }, style]} {...otherProps} />;
}

View File

@@ -0,0 +1,10 @@
import * as React from 'react';
import renderer from 'react-test-renderer';
import { MonoText } from '../StyledText';
it(`renders correctly`, () => {
const tree = renderer.create(<MonoText>Snapshot test!</MonoText>).toJSON();
expect(tree).toMatchSnapshot();
});

View File

@@ -0,0 +1,4 @@
// This function is web-only as native doesn't currently support server (or build-time) rendering.
export function useClientOnlyValue<S, C>(server: S, client: C): S | C {
return client;
}

View File

@@ -0,0 +1,12 @@
import React from 'react';
// `useEffect` is not invoked during server rendering, meaning
// we can use this to determine if we're on the server or not.
export function useClientOnlyValue<S, C>(server: S, client: C): S | C {
const [value, setValue] = React.useState<S | C>(server);
React.useEffect(() => {
setValue(client);
}, [client]);
return value;
}

View File

@@ -0,0 +1,12 @@
import { useColorScheme as useSystemColorScheme } from 'react-native';
import { useThemePreference } from './ThemeProvider';
export function useColorScheme() {
try {
const { effectiveScheme } = useThemePreference();
return effectiveScheme;
} catch {
return useSystemColorScheme() ?? 'light';
}
}

View File

@@ -0,0 +1,15 @@
import { useThemePreference } from './ThemeProvider';
// NOTE: The default React Native styling doesn't support server rendering.
// Server rendered styles should not change between the first render of the HTML
// and the first render on the client. Typically, web developers will use CSS media queries
// to render different styles on the client and server, these aren't directly supported in React Native
// but can be achieved using a styling library like Nativewind.
export function useColorScheme() {
try {
const { effectiveScheme } = useThemePreference();
return effectiveScheme;
} catch {
return 'light';
}
}

48
constants/Colors.ts Normal file
View File

@@ -0,0 +1,48 @@
const tintColorLight = '#15803d';
const tintColorDark = '#4ade80';
const brandGreen = '#22c55e';
export default {
light: {
text: '#0f1410',
background: '#f5f7f1',
tint: tintColorLight,
tabIconDefault: '#9aa7a0',
tabIconSelected: tintColorLight,
surface: '#ffffff',
surfaceAlt: '#eef2ea',
card: '#ffffff',
cardBorder: '#d8e0d1',
border: '#d8e0d1',
borderStrong: '#bfcab7',
textMuted: '#4b5a4b',
textDim: '#6b7a6b',
green: brandGreen,
greenDim: '#16a34a',
greenGlow: '#15803d',
bannerBg: 'rgba(34,197,94,0.12)',
bannerBorder: 'rgba(34,197,94,0.25)',
overlay: 'rgba(15,20,16,0.05)',
},
dark: {
text: '#fff',
background: '#080f08',
tint: tintColorDark,
tabIconDefault: '#6b7280',
tabIconSelected: tintColorDark,
surface: '#122012',
surfaceAlt: '#0f1a0f',
card: '#122012',
cardBorder: '#1e3a1e',
border: '#1e3a1e',
borderStrong: '#2b4a2b',
textMuted: '#4ade80',
textDim: '#6b7280',
green: brandGreen,
greenDim: '#16a34a',
greenGlow: '#15803d',
bannerBg: 'rgba(34,197,94,0.12)',
bannerBorder: 'rgba(74,222,128,0.22)',
overlay: 'rgba(255,255,255,0.06)',
},
};

21
eas.json Normal file
View File

@@ -0,0 +1,21 @@
{
"cli": {
"version": ">= 18.0.1",
"appVersionSource": "remote"
},
"build": {
"development": {
"developmentClient": true,
"distribution": "internal"
},
"preview": {
"distribution": "internal"
},
"production": {
"autoIncrement": true
}
},
"submit": {
"production": {}
}
}

3
expo-env.d.ts vendored Normal file
View File

@@ -0,0 +1,3 @@
/// <reference types="expo/types" />
// NOTE: This file should not be edited and should be in your git ignore

30
ios/.gitignore vendored Normal file
View File

@@ -0,0 +1,30 @@
# OSX
#
.DS_Store
# Xcode
#
build/
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
xcuserdata
*.xccheckout
*.moved-aside
DerivedData
*.hmap
*.ipa
*.xcuserstate
project.xcworkspace
.xcode.env.local
# Bundle artifacts
*.jsbundle
# CocoaPods
/Pods/

11
ios/.xcode.env Normal file
View File

@@ -0,0 +1,11 @@
# This `.xcode.env` file is versioned and is used to source the environment
# used when running script phases inside Xcode.
# To customize your local environment, you can create an `.xcode.env.local`
# file that is not versioned.
# NODE_BINARY variable contains the PATH to the node executable.
#
# Customize the NODE_BINARY variable here.
# For example, to use nvm with brew, add the following line
# . "$(brew --prefix nvm)/nvm.sh" --no-use
export NODE_BINARY=$(command -v node)

View File

@@ -0,0 +1,544 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
26E34BF6D680A403348562FA /* libPods-Miqat.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 336A6D73FC3E956530852915 /* libPods-Miqat.a */; };
3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */; };
45011B058265ECAF05F7AA5D /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A24CDB7836CFE841F1E8E236 /* ExpoModulesProvider.swift */; };
BB2F792D24A3F905000567C9 /* Expo.plist in Resources */ = {isa = PBXBuildFile; fileRef = BB2F792C24A3F905000567C9 /* Expo.plist */; };
C26066B1CD8249234B5560FD /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 7AA207893103188B74290865 /* PrivacyInfo.xcprivacy */; };
F11748422D0307B40044C1D9 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F11748412D0307B40044C1D9 /* AppDelegate.swift */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
13B07F961A680F5B00A75B9A /* Miqat.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Miqat.app; sourceTree = BUILT_PRODUCTS_DIR; };
13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = Miqat/Images.xcassets; sourceTree = "<group>"; };
13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = Miqat/Info.plist; sourceTree = "<group>"; };
336A6D73FC3E956530852915 /* libPods-Miqat.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Miqat.a"; sourceTree = BUILT_PRODUCTS_DIR; };
418B2782E6CA591EC35961C3 /* Pods-Miqat.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Miqat.release.xcconfig"; path = "Target Support Files/Pods-Miqat/Pods-Miqat.release.xcconfig"; sourceTree = "<group>"; };
7AA207893103188B74290865 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; name = PrivacyInfo.xcprivacy; path = Miqat/PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
A24CDB7836CFE841F1E8E236 /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-Miqat/ExpoModulesProvider.swift"; sourceTree = "<group>"; };
AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = SplashScreen.storyboard; path = Miqat/SplashScreen.storyboard; sourceTree = "<group>"; };
B8FC97B4516CECF14A75E781 /* Pods-Miqat.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Miqat.debug.xcconfig"; path = "Target Support Files/Pods-Miqat/Pods-Miqat.debug.xcconfig"; sourceTree = "<group>"; };
BB2F792C24A3F905000567C9 /* Expo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Expo.plist; sourceTree = "<group>"; };
ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
F11748412D0307B40044C1D9 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = Miqat/AppDelegate.swift; sourceTree = "<group>"; };
F11748442D0722820044C1D9 /* Miqat-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "Miqat-Bridging-Header.h"; path = "Miqat/Miqat-Bridging-Header.h"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
13B07F8C1A680F5B00A75B9A /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
26E34BF6D680A403348562FA /* libPods-Miqat.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
13B07FAE1A68108700A75B9A /* Miqat */ = {
isa = PBXGroup;
children = (
F11748412D0307B40044C1D9 /* AppDelegate.swift */,
F11748442D0722820044C1D9 /* Miqat-Bridging-Header.h */,
BB2F792B24A3F905000567C9 /* Supporting */,
13B07FB51A68108700A75B9A /* Images.xcassets */,
13B07FB61A68108700A75B9A /* Info.plist */,
AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */,
7AA207893103188B74290865 /* PrivacyInfo.xcprivacy */,
);
name = Miqat;
sourceTree = "<group>";
};
2B00996CCF777F82201ECA76 /* ExpoModulesProviders */ = {
isa = PBXGroup;
children = (
DAC81D176BADD033A9F95099 /* Miqat */,
);
name = ExpoModulesProviders;
sourceTree = "<group>";
};
2D16E6871FA4F8E400B85C8A /* Frameworks */ = {
isa = PBXGroup;
children = (
ED297162215061F000B7C4FE /* JavaScriptCore.framework */,
336A6D73FC3E956530852915 /* libPods-Miqat.a */,
);
name = Frameworks;
sourceTree = "<group>";
};
5376E0F625A2B9E7BACD4DC1 /* Pods */ = {
isa = PBXGroup;
children = (
B8FC97B4516CECF14A75E781 /* Pods-Miqat.debug.xcconfig */,
418B2782E6CA591EC35961C3 /* Pods-Miqat.release.xcconfig */,
);
name = Pods;
path = Pods;
sourceTree = "<group>";
};
832341AE1AAA6A7D00B99B32 /* Libraries */ = {
isa = PBXGroup;
children = (
);
name = Libraries;
sourceTree = "<group>";
};
83CBB9F61A601CBA00E9B192 = {
isa = PBXGroup;
children = (
13B07FAE1A68108700A75B9A /* Miqat */,
832341AE1AAA6A7D00B99B32 /* Libraries */,
83CBBA001A601CBA00E9B192 /* Products */,
2D16E6871FA4F8E400B85C8A /* Frameworks */,
2B00996CCF777F82201ECA76 /* ExpoModulesProviders */,
5376E0F625A2B9E7BACD4DC1 /* Pods */,
);
indentWidth = 2;
sourceTree = "<group>";
tabWidth = 2;
usesTabs = 0;
};
83CBBA001A601CBA00E9B192 /* Products */ = {
isa = PBXGroup;
children = (
13B07F961A680F5B00A75B9A /* Miqat.app */,
);
name = Products;
sourceTree = "<group>";
};
BB2F792B24A3F905000567C9 /* Supporting */ = {
isa = PBXGroup;
children = (
BB2F792C24A3F905000567C9 /* Expo.plist */,
);
name = Supporting;
path = Miqat/Supporting;
sourceTree = "<group>";
};
DAC81D176BADD033A9F95099 /* Miqat */ = {
isa = PBXGroup;
children = (
A24CDB7836CFE841F1E8E236 /* ExpoModulesProvider.swift */,
);
name = Miqat;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
13B07F861A680F5B00A75B9A /* Miqat */ = {
isa = PBXNativeTarget;
buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "Miqat" */;
buildPhases = (
76B124A5ACD9960BDC707454 /* [CP] Check Pods Manifest.lock */,
7046B60279DFE207423D8A50 /* [Expo] Configure project */,
13B07F871A680F5B00A75B9A /* Sources */,
13B07F8C1A680F5B00A75B9A /* Frameworks */,
13B07F8E1A680F5B00A75B9A /* Resources */,
00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */,
D74A08DA029B99BDC61D3A46 /* [CP] Embed Pods Frameworks */,
A89BC7BDC0E1721321B41805 /* [CP] Copy Pods Resources */,
);
buildRules = (
);
dependencies = (
);
name = Miqat;
productName = Miqat;
productReference = 13B07F961A680F5B00A75B9A /* Miqat.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
83CBB9F71A601CBA00E9B192 /* Project object */ = {
isa = PBXProject;
attributes = {
LastUpgradeCheck = 1130;
TargetAttributes = {
13B07F861A680F5B00A75B9A = {
LastSwiftMigration = 1250;
};
};
};
buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "Miqat" */;
compatibilityVersion = "Xcode 3.2";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 83CBB9F61A601CBA00E9B192;
productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
13B07F861A680F5B00A75B9A /* Miqat */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
13B07F8E1A680F5B00A75B9A /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
BB2F792D24A3F905000567C9 /* Expo.plist in Resources */,
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */,
C26066B1CD8249234B5560FD /* PrivacyInfo.xcprivacy in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"$(SRCROOT)/.xcode.env",
"$(SRCROOT)/.xcode.env.local",
);
name = "Bundle React Native code and images";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "if [[ -f \"$PODS_ROOT/../.xcode.env\" ]]; then\n source \"$PODS_ROOT/../.xcode.env\"\nfi\nif [[ -f \"$PODS_ROOT/../.xcode.env.local\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.local\"\nfi\n\n# The project root by default is one level up from the ios directory\nexport PROJECT_ROOT=\"$PROJECT_DIR\"/..\n\nif [[ \"$CONFIGURATION\" = *Debug* ]]; then\n export SKIP_BUNDLING=1\nfi\nif [[ -z \"$ENTRY_FILE\" ]]; then\n # Set the entry JS file using the bundler's entry resolution.\n export ENTRY_FILE=\"$(\"$NODE_BINARY\" -e \"require('expo/scripts/resolveAppEntry')\" \"$PROJECT_ROOT\" ios absolute | tail -n 1)\"\nfi\n\nif [[ -z \"$CLI_PATH\" ]]; then\n # Use Expo CLI\n export CLI_PATH=\"$(\"$NODE_BINARY\" --print \"require.resolve('@expo/cli', { paths: [require.resolve('expo/package.json')] })\")\"\nfi\nif [[ -z \"$BUNDLE_COMMAND\" ]]; then\n # Default Expo CLI command for bundling\n export BUNDLE_COMMAND=\"export:embed\"\nfi\n\n# Source .xcode.env.updates if it exists to allow\n# SKIP_BUNDLING to be unset if needed\nif [[ -f \"$PODS_ROOT/../.xcode.env.updates\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.updates\"\nfi\n# Source local changes to allow overrides\n# if needed\nif [[ -f \"$PODS_ROOT/../.xcode.env.local\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.local\"\nfi\n\n`\"$NODE_BINARY\" --print \"require('path').dirname(require.resolve('react-native/package.json')) + '/scripts/react-native-xcode.sh'\"`\n\n";
};
7046B60279DFE207423D8A50 /* [Expo] Configure project */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"$(SRCROOT)/.xcode.env",
"$(SRCROOT)/.xcode.env.local",
"$(SRCROOT)/Miqat/Miqat.entitlements",
"$(SRCROOT)/Pods/Target Support Files/Pods-Miqat/expo-configure-project.sh",
);
name = "[Expo] Configure project";
outputFileListPaths = (
);
outputPaths = (
"$(SRCROOT)/Pods/Target Support Files/Pods-Miqat/ExpoModulesProvider.swift",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "# This script configures Expo modules and generates the modules provider file.\nbash -l -c \"./Pods/Target\\ Support\\ Files/Pods-Miqat/expo-configure-project.sh\"\n";
};
76B124A5ACD9960BDC707454 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Miqat-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
A89BC7BDC0E1721321B41805 /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Miqat/Pods-Miqat-resources.sh",
"${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/EXConstants.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/ExpoConstants_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoFileSystem/ExpoFileSystem_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/RNCAsyncStorage/RNCAsyncStorage_resources.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/RNSVG/RNSVGFilters.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/React-Core/React-Core_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/React-cxxreact/React-cxxreact_privacy.bundle",
);
name = "[CP] Copy Pods Resources";
outputPaths = (
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXConstants.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoConstants_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoFileSystem_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNCAsyncStorage_resources.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNSVGFilters.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-Core_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-cxxreact_privacy.bundle",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Miqat/Pods-Miqat-resources.sh\"\n";
showEnvVarsInLog = 0;
};
D74A08DA029B99BDC61D3A46 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Miqat/Pods-Miqat-frameworks.sh",
"${PODS_XCFRAMEWORKS_BUILD_DIR}/React-Core-prebuilt/React.framework/React",
"${PODS_XCFRAMEWORKS_BUILD_DIR}/ReactNativeDependencies/ReactNativeDependencies.framework/ReactNativeDependencies",
"${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermes.framework/hermes",
);
name = "[CP] Embed Pods Frameworks";
outputPaths = (
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/React.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ReactNativeDependencies.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Miqat/Pods-Miqat-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
13B07F871A680F5B00A75B9A /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
F11748422D0307B40044C1D9 /* AppDelegate.swift in Sources */,
45011B058265ECAF05F7AA5D /* ExpoModulesProvider.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin XCBuildConfiguration section */
13B07F941A680F5B00A75B9A /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = B8FC97B4516CECF14A75E781 /* Pods-Miqat.debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Miqat/Miqat.entitlements;
CURRENT_PROJECT_VERSION = 1;
ENABLE_BITCODE = NO;
GCC_PREPROCESSOR_DEFINITIONS = (
"$(inherited)",
"FB_SONARKIT_ENABLED=1",
);
INFOPLIST_FILE = Miqat/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
"-lc++",
);
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
PRODUCT_BUNDLE_IDENTIFIER = com.anonymous.miqat;
PRODUCT_NAME = Miqat;
SWIFT_OBJC_BRIDGING_HEADER = "Miqat/Miqat-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
};
13B07F951A680F5B00A75B9A /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 418B2782E6CA591EC35961C3 /* Pods-Miqat.release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Miqat/Miqat.entitlements;
CURRENT_PROJECT_VERSION = 1;
INFOPLIST_FILE = Miqat/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
"-lc++",
);
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.anonymous.miqat;
PRODUCT_NAME = Miqat;
SWIFT_OBJC_BRIDGING_HEADER = "Miqat/Miqat-Bridging-Header.h";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
};
83CBBA201A601CBA00E9B192 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
CLANG_CXX_LANGUAGE_STANDARD = "c++20";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_SYMBOLS_PRIVATE_EXTERN = NO;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
LD_RUNPATH_SEARCH_PATHS = (
/usr/lib/swift,
"$(inherited)",
);
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\"";
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.29.0_@react-native-community+cli@12.3.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native";
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
SWIFT_ENABLE_EXPLICIT_MODULES = NO;
USE_HERMES = true;
};
name = Debug;
};
83CBBA211A601CBA00E9B192 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
CLANG_CXX_LANGUAGE_STANDARD = "c++20";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = YES;
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
LD_RUNPATH_SEARCH_PATHS = (
/usr/lib/swift,
"$(inherited)",
);
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\"";
MTL_ENABLE_DEBUG_INFO = NO;
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.29.0_@react-native-community+cli@12.3.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native";
SDKROOT = iphoneos;
SWIFT_ENABLE_EXPLICIT_MODULES = NO;
USE_HERMES = true;
VALIDATE_PRODUCT = YES;
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "Miqat" */ = {
isa = XCConfigurationList;
buildConfigurations = (
13B07F941A680F5B00A75B9A /* Debug */,
13B07F951A680F5B00A75B9A /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "Miqat" */ = {
isa = XCConfigurationList;
buildConfigurations = (
83CBBA201A601CBA00E9B192 /* Debug */,
83CBBA211A601CBA00E9B192 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 83CBB9F71A601CBA00E9B192 /* Project object */;
}

View File

@@ -0,0 +1,88 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1130"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
BuildableName = "Miqat.app"
BlueprintName = "Miqat"
ReferencedContainer = "container:Miqat.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "00E356ED1AD99517003FC87E"
BuildableName = "MiqatTests.xctest"
BlueprintName = "MiqatTests"
ReferencedContainer = "container:Miqat.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
BuildableName = "Miqat.app"
BlueprintName = "Miqat"
ReferencedContainer = "container:Miqat.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
BuildableName = "Miqat.app"
BlueprintName = "Miqat"
ReferencedContainer = "container:Miqat.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Miqat.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>

View File

@@ -0,0 +1,70 @@
import Expo
import React
import ReactAppDependencyProvider
@UIApplicationMain
public class AppDelegate: ExpoAppDelegate {
var window: UIWindow?
var reactNativeDelegate: ExpoReactNativeFactoryDelegate?
var reactNativeFactory: RCTReactNativeFactory?
public override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
let delegate = ReactNativeDelegate()
let factory = ExpoReactNativeFactory(delegate: delegate)
delegate.dependencyProvider = RCTAppDependencyProvider()
reactNativeDelegate = delegate
reactNativeFactory = factory
bindReactNativeFactory(factory)
#if os(iOS) || os(tvOS)
window = UIWindow(frame: UIScreen.main.bounds)
factory.startReactNative(
withModuleName: "main",
in: window,
launchOptions: launchOptions)
#endif
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
// Linking API
public override func application(
_ app: UIApplication,
open url: URL,
options: [UIApplication.OpenURLOptionsKey: Any] = [:]
) -> Bool {
return super.application(app, open: url, options: options) || RCTLinkingManager.application(app, open: url, options: options)
}
// Universal Links
public override func application(
_ application: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
) -> Bool {
let result = RCTLinkingManager.application(application, continue: userActivity, restorationHandler: restorationHandler)
return super.application(application, continue: userActivity, restorationHandler: restorationHandler) || result
}
}
class ReactNativeDelegate: ExpoReactNativeFactoryDelegate {
// Extension point for config-plugins
override func sourceURL(for bridge: RCTBridge) -> URL? {
// needed to return the correct URL for expo-dev-client.
bridge.bundleURL ?? bundleURL()
}
override func bundleURL() -> URL? {
#if DEBUG
return RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: ".expo/.virtual-metro-entry")
#else
return Bundle.main.url(forResource: "main", withExtension: "jsbundle")
#endif
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

View File

@@ -0,0 +1,14 @@
{
"images": [
{
"filename": "App-Icon-1024x1024@1x.png",
"idiom": "universal",
"platform": "ios",
"size": "1024x1024"
}
],
"info": {
"version": 1,
"author": "expo"
}
}

View File

@@ -0,0 +1,6 @@
{
"info" : {
"version" : 1,
"author" : "expo"
}
}

View File

@@ -0,0 +1,20 @@
{
"colors": [
{
"color": {
"components": {
"alpha": "1.000",
"blue": "1.00000000000000",
"green": "1.00000000000000",
"red": "1.00000000000000"
},
"color-space": "srgb"
},
"idiom": "universal"
}
],
"info": {
"version": 1,
"author": "expo"
}
}

View File

@@ -0,0 +1,23 @@
{
"images": [
{
"idiom": "universal",
"filename": "image.png",
"scale": "1x"
},
{
"idiom": "universal",
"filename": "image@2x.png",
"scale": "2x"
},
{
"idiom": "universal",
"filename": "image@3x.png",
"scale": "3x"
}
],
"info": {
"version": 1,
"author": "expo"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

89
ios/Miqat/Info.plist Normal file
View File

@@ -0,0 +1,89 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Miqat</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.0.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>prayerapp</string>
<string>com.anonymous.miqat</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSMinimumSystemVersion</key>
<string>12.0</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<false/>
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>Allow $(PRODUCT_NAME) to access your location</string>
<key>NSLocationAlwaysUsageDescription</key>
<string>Allow $(PRODUCT_NAME) to access your location</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>Allow Miqat to use your location to determine prayer times based on your current location.</string>
<key>NSMotionUsageDescription</key>
<string>Allow $(PRODUCT_NAME) to access your device motion</string>
<key>NSUserActivityTypes</key>
<array>
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
</array>
<key>RCTNewArchEnabled</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>SplashScreen</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>arm64</string>
</array>
<key>UIRequiresFullScreen</key>
<false/>
<key>UIStatusBarStyle</key>
<string>UIStatusBarStyleDefault</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIUserInterfaceStyle</key>
<string>Automatic</string>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
</dict>
</plist>

View File

@@ -0,0 +1,3 @@
//
// Use this file to import your target's public headers that you would like to expose to Swift.
//

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict/>
</plist>

View File

@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>CA92.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>0A2A.1</string>
<string>3B52.1</string>
<string>C617.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryDiskSpace</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>E174.1</string>
<string>85F4.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategorySystemBootTime</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>35F9.1</string>
</array>
</dict>
</array>
<key>NSPrivacyCollectedDataTypes</key>
<array/>
<key>NSPrivacyTracking</key>
<false/>
</dict>
</plist>

View File

@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="24093.7" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="EXPO-VIEWCONTROLLER-1">
<device id="retina6_12" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="24053.1"/>
<capability name="Named colors" minToolsVersion="9.0"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<scene sceneID="EXPO-SCENE-1">
<objects>
<viewController storyboardIdentifier="SplashScreenViewController" id="EXPO-VIEWCONTROLLER-1" sceneMemberID="viewController">
<view key="view" userInteractionEnabled="NO" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="EXPO-ContainerView" userLabel="ContainerView">
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<imageView id="EXPO-SplashScreen" userLabel="SplashScreenLegacy" image="SplashScreenLegacy" contentMode="scaleAspectFit" clipsSubviews="true" userInteractionEnabled="false" translatesAutoresizingMaskIntoConstraints="false">
<rect key="frame" x="0" y="0" width="414" height="736"/>
</imageView>
</subviews>
<viewLayoutGuide key="safeArea" id="Rmq-lb-GrQ"/>
<constraints>
<constraint firstItem="EXPO-SplashScreen" firstAttribute="top" secondItem="EXPO-ContainerView" secondAttribute="top" id="83fcb9b545b870ba44c24f0feeb116490c499c52"/>
<constraint firstItem="EXPO-SplashScreen" firstAttribute="leading" secondItem="EXPO-ContainerView" secondAttribute="leading" id="61d16215e44b98e39d0a2c74fdbfaaa22601b12c"/>
<constraint firstItem="EXPO-SplashScreen" firstAttribute="trailing" secondItem="EXPO-ContainerView" secondAttribute="trailing" id="f934da460e9ab5acae3ad9987d5b676a108796c1"/>
<constraint firstItem="EXPO-SplashScreen" firstAttribute="bottom" secondItem="EXPO-ContainerView" secondAttribute="bottom" id="d6a0be88096b36fb132659aa90203d39139deda9"/>
</constraints>
<color key="backgroundColor" name="SplashScreenBackground"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="EXPO-PLACEHOLDER-1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="0.0" y="0.0"/>
</scene>
</scenes>
<resources>
<image name="SplashScreenLegacy" width="414" height="736"/>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
<namedColor name="SplashScreenBackground">
<color alpha="1.000" blue="1.00000000000000" green="1.00000000000000" red="1.00000000000000" customColorSpace="sRGB" colorSpace="custom"/>
</namedColor>
</resources>
</document>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>EXUpdatesCheckOnLaunch</key>
<string>ALWAYS</string>
<key>EXUpdatesEnabled</key>
<false/>
<key>EXUpdatesLaunchWaitMs</key>
<integer>0</integer>
</dict>
</plist>

63
ios/Podfile Normal file
View File

@@ -0,0 +1,63 @@
require File.join(File.dirname(`node --print "require.resolve('expo/package.json')"`), "scripts/autolinking")
require File.join(File.dirname(`node --print "require.resolve('react-native/package.json')"`), "scripts/react_native_pods")
require 'json'
podfile_properties = JSON.parse(File.read(File.join(__dir__, 'Podfile.properties.json'))) rescue {}
def ccache_enabled?(podfile_properties)
# Environment variable takes precedence
return ENV['USE_CCACHE'] == '1' if ENV['USE_CCACHE']
# Fall back to Podfile properties
podfile_properties['apple.ccacheEnabled'] == 'true'
end
ENV['RCT_NEW_ARCH_ENABLED'] ||= '0' if podfile_properties['newArchEnabled'] == 'false'
ENV['EX_DEV_CLIENT_NETWORK_INSPECTOR'] ||= podfile_properties['EX_DEV_CLIENT_NETWORK_INSPECTOR']
ENV['RCT_USE_RN_DEP'] ||= '1' if podfile_properties['ios.buildReactNativeFromSource'] != 'true' && podfile_properties['newArchEnabled'] != 'false'
ENV['RCT_USE_PREBUILT_RNCORE'] ||= '1' if podfile_properties['ios.buildReactNativeFromSource'] != 'true' && podfile_properties['newArchEnabled'] != 'false'
platform :ios, podfile_properties['ios.deploymentTarget'] || '15.1'
prepare_react_native_project!
target 'Miqat' do
use_expo_modules!
if ENV['EXPO_USE_COMMUNITY_AUTOLINKING'] == '1'
config_command = ['node', '-e', "process.argv=['', '', 'config'];require('@react-native-community/cli').run()"];
else
config_command = [
'node',
'--no-warnings',
'--eval',
'require(\'expo/bin/autolinking\')',
'expo-modules-autolinking',
'react-native-config',
'--json',
'--platform',
'ios'
]
end
config = use_native_modules!(config_command)
use_frameworks! :linkage => podfile_properties['ios.useFrameworks'].to_sym if podfile_properties['ios.useFrameworks']
use_frameworks! :linkage => ENV['USE_FRAMEWORKS'].to_sym if ENV['USE_FRAMEWORKS']
use_react_native!(
:path => config[:reactNativePath],
:hermes_enabled => podfile_properties['expo.jsEngine'] == nil || podfile_properties['expo.jsEngine'] == 'hermes',
# An absolute path to your application root.
:app_path => "#{Pod::Config.instance.installation_root}/..",
:privacy_file_aggregation_enabled => podfile_properties['apple.privacyManifestAggregationEnabled'] != 'false',
)
post_install do |installer|
react_native_post_install(
installer,
config[:reactNativePath],
:mac_catalyst_enabled => false,
:ccache_enabled => ccache_enabled?(podfile_properties),
)
end
end

2390
ios/Podfile.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,5 @@
{
"expo.jsEngine": "hermes",
"EX_DEV_CLIENT_NETWORK_INSPECTOR": "true",
"newArchEnabled": "true"
}

45
package.json Normal file
View File

@@ -0,0 +1,45 @@
{
"name": "prayer-app",
"main": "expo-router/entry",
"version": "1.0.0",
"scripts": {
"start": "expo start",
"android": "expo run:android",
"ios": "expo run:ios",
"web": "expo start --web"
},
"dependencies": {
"@expo/vector-icons": "^15.0.3",
"@react-native-async-storage/async-storage": "^2.2.0",
"@react-navigation/native": "^7.1.28",
"@tanstack/react-query": "^5.90.21",
"eas-cli": "^18.0.1",
"expo": "~54.0.33",
"expo-constants": "~18.0.13",
"expo-font": "~14.0.11",
"expo-linking": "~8.0.11",
"expo-location": "~19.0.8",
"expo-router": "~6.0.23",
"expo-sensors": "^15.0.8",
"expo-splash-screen": "~31.0.13",
"expo-status-bar": "~3.0.9",
"expo-web-browser": "~15.0.10",
"lucide-react-native": "^0.574.0",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-native": "0.81.5",
"react-native-async-storage": "^0.0.1",
"react-native-qibla-compass": "^1.3.0",
"react-native-reanimated": "~4.1.6",
"react-native-safe-area-context": "~5.6.2",
"react-native-screens": "~4.16.0",
"react-native-web": "~0.21.2",
"react-native-worklets": "0.5.1"
},
"devDependencies": {
"@types/react": "~19.1.17",
"react-test-renderer": "19.1.0",
"typescript": "~5.9.3"
},
"private": true
}

13180
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

21
tsconfig.json Normal file
View File

@@ -0,0 +1,21 @@
{
"extends": "expo/tsconfig.base",
"compilerOptions": {
"strict": true,
"jsx": "react-native",
"esModuleInterop": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"paths": {
"@/*": [
"./*"
]
}
},
"include": [
"**/*.ts",
"**/*.tsx",
".expo/types/**/*.ts",
"expo-env.d.ts"
]
}

108
types.ts Normal file
View File

@@ -0,0 +1,108 @@
export interface PrayerTimesResponse {
code: number
status: string
data: {
timings: {
Fajr: string
Sunrise: string
Dhuhr: string
Asr: string
Sunset: string
Maghrib: string
Isha: string
Imsak: string
Midnight: string
Firstthird: string
Lastthird: string
}
date: {
readable: string
timestamp: string
hijri: {
date: string
format: string
day: string
weekday: {
en: string
ar: string
}
month: {
number: number
en: string
ar: string
days: number
}
year: string
designation: {
abbreviated: string
expanded: string
}
holidays: [any]
adjustedHolidays: [string]
method: string
}
gregorian: {
date: string
format: string
day: string
weekday: {
en: string
}
month: {
number: number
en: string
}
year: string
designation: {
abbreviated: string
expanded: string
}
lunarSighting: boolean
}
}
meta: {
latitude: number
longitude: number
timezone: string
method: {
id: number
name: string
params: {
Fajr: number
Isha: number
}
location: {
latitude: number
longitude: number
}
}
latitudeAdjustmentMethod: string
midnightMode: string
school: string
offset: {
Imsak: number
Fajr: number
Sunrise: number
Dhuhr: number
Asr: number
Sunset: number
Maghrib: number
Isha: number
Midnight: number
}
}
}
}
export interface PrayerTimes {
Fajr: string
Dhuhr: string
Asr: string
Maghrib: string
Isha: string
}
export interface Coordinates {
latitude: number
longitude: number
}