Added app to repo.
138
.gitignore
vendored
Normal 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
@@ -0,0 +1 @@
|
||||
{ "recommendations": ["expo.vscode-expo-tools"] }
|
||||
7
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll": "explicit",
|
||||
"source.organizeImports": "explicit",
|
||||
"source.sortMembers": "explicit"
|
||||
}
|
||||
}
|
||||
40
actions/getCity.ts
Normal 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
@@ -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,
|
||||
};
|
||||
},
|
||||
});
|
||||
};
|
||||
30
actions/getQiblaBearing.ts
Normal 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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,
|
||||
},
|
||||
});
|
||||
BIN
assets/fonts/SpaceMono-Regular.ttf
Executable file
BIN
assets/images/adaptive-icon.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
assets/images/favicon.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
assets/images/icon.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
assets/images/splash-icon.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
77
components/EditScreenInfo.tsx
Normal 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',
|
||||
},
|
||||
});
|
||||
25
components/ExternalLink.tsx
Normal 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
@@ -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,
|
||||
},
|
||||
});
|
||||
11
components/QueryProvider.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
5
components/StyledText.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Text, TextProps } from './Themed';
|
||||
|
||||
export function MonoText(props: TextProps) {
|
||||
return <Text {...props} style={[props.style, { fontFamily: 'SpaceMono' }]} />;
|
||||
}
|
||||
67
components/ThemeProvider.tsx
Normal 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
@@ -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} />;
|
||||
}
|
||||
10
components/__tests__/StyledText-test.js
Normal 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();
|
||||
});
|
||||
4
components/useClientOnlyValue.ts
Normal 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;
|
||||
}
|
||||
12
components/useClientOnlyValue.web.ts
Normal 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;
|
||||
}
|
||||
12
components/useColorScheme.ts
Normal 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';
|
||||
}
|
||||
}
|
||||
15
components/useColorScheme.web.ts
Normal 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
544
ios/Miqat.xcodeproj/project.pbxproj
Normal 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 */;
|
||||
}
|
||||
88
ios/Miqat.xcodeproj/xcshareddata/xcschemes/Miqat.xcscheme
Normal 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>
|
||||
10
ios/Miqat.xcworkspace/contents.xcworkspacedata
generated
Normal 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>
|
||||
70
ios/Miqat/AppDelegate.swift
Normal 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
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 58 KiB |
14
ios/Miqat/Images.xcassets/AppIcon.appiconset/Contents.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"images": [
|
||||
{
|
||||
"filename": "App-Icon-1024x1024@1x.png",
|
||||
"idiom": "universal",
|
||||
"platform": "ios",
|
||||
"size": "1024x1024"
|
||||
}
|
||||
],
|
||||
"info": {
|
||||
"version": 1,
|
||||
"author": "expo"
|
||||
}
|
||||
}
|
||||
6
ios/Miqat/Images.xcassets/Contents.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "expo"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
23
ios/Miqat/Images.xcassets/SplashScreenLegacy.imageset/Contents.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
||||
BIN
ios/Miqat/Images.xcassets/SplashScreenLegacy.imageset/image.png
vendored
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
ios/Miqat/Images.xcassets/SplashScreenLegacy.imageset/image@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
ios/Miqat/Images.xcassets/SplashScreenLegacy.imageset/image@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 59 KiB |
89
ios/Miqat/Info.plist
Normal 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>
|
||||
3
ios/Miqat/Miqat-Bridging-Header.h
Normal file
@@ -0,0 +1,3 @@
|
||||
//
|
||||
// Use this file to import your target's public headers that you would like to expose to Swift.
|
||||
//
|
||||
5
ios/Miqat/Miqat.entitlements
Normal 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>
|
||||
48
ios/Miqat/PrivacyInfo.xcprivacy
Normal 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>
|
||||
48
ios/Miqat/SplashScreen.storyboard
Normal 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>
|
||||
12
ios/Miqat/Supporting/Expo.plist
Normal 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
@@ -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
5
ios/Podfile.properties.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"expo.jsEngine": "hermes",
|
||||
"EX_DEV_CLIENT_NETWORK_INSPECTOR": "true",
|
||||
"newArchEnabled": "true"
|
||||
}
|
||||
45
package.json
Normal 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
21
tsconfig.json
Normal 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
@@ -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
|
||||
}
|
||||