├── src ├── vite-env.d.ts ├── index.css ├── main.tsx ├── components │ ├── CountryClock.tsx │ ├── LocaleClock.tsx │ ├── Input.tsx │ └── PrayerTimes.tsx ├── assets │ └── react.svg └── App.tsx ├── public ├── preview1.gif └── vite.svg ├── postcss.config.cjs ├── vite.config.ts ├── tsconfig.node.json ├── .gitignore ├── index.html ├── tailwind.config.cjs ├── tsconfig.json ├── package.json └── README.md /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /public/preview1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MohanadOO/prayer-time-api/HEAD/public/preview1.gif -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()] 7 | }) 8 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | .inputRTL { 6 | text-align: right; 7 | unicode-bidi: bidi-override; 8 | direction: rtl; 9 | } 10 | 11 | .inputLTR { 12 | text-align: left; 13 | unicode-bidi: bidi-override; 14 | direction: ltr; 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Prayer Times 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import App from './App' 4 | import '@fontsource/cairo' 5 | import '@fontsource/cairo/600.css' 6 | import '@fontsource/cairo/700.css' 7 | import '@fontsource/cairo/900.css' 8 | import './index.css' 9 | import { Toaster } from 'react-hot-toast' 10 | 11 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( 12 | 13 | 14 | 15 | 16 | ) 17 | -------------------------------------------------------------------------------- /tailwind.config.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | 3 | const defaultTheme = require('tailwindcss/defaultTheme') 4 | module.exports = { 5 | content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], 6 | theme: { 7 | extend: { 8 | colors: { 9 | primary: { 10 | 800: '#22313F', 11 | 700: '#34495E', 12 | 400: '#8DC6FF', 13 | 200: '#E4F1FE', 14 | }, 15 | }, 16 | fontFamily: { 17 | cairo: ['Cairo', ...defaultTheme.fontFamily.sans], 18 | }, 19 | }, 20 | }, 21 | plugins: [require('@tailwindcss/forms')], 22 | } 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"], 20 | "references": [{ "path": "./tsconfig.node.json" }] 21 | } 22 | -------------------------------------------------------------------------------- /src/components/CountryClock.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react' 2 | 3 | const localeTimeString = new Date() 4 | export default function CountryClock({ timeZone }: { timeZone: string }) { 5 | const [currentTime, setCurrentTime] = useState( 6 | localeTimeString.toLocaleTimeString('en-US', { timeZone }) 7 | ) 8 | 9 | useEffect(() => { 10 | const interval = setInterval(() => { 11 | const timestamp = new Date().getTime() 12 | const updatedTime = new Date(timestamp) 13 | setCurrentTime(updatedTime.toLocaleTimeString('en-US', { timeZone })) 14 | }, 1000) 15 | return () => clearInterval(interval) 16 | }, []) 17 | 18 | return ( 19 |

{currentTime}

20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /src/components/LocaleClock.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react' 2 | 3 | const localeTimeString = new Date().toLocaleTimeString() 4 | export default function LocaleClock() { 5 | const [currentTime, setCurrentTime] = useState(localeTimeString) 6 | 7 | useEffect(() => { 8 | const interval = setInterval(() => { 9 | const timestamp = new Date().getTime() 10 | const updatedTime = new Date(timestamp) 11 | setCurrentTime(updatedTime.toLocaleTimeString()) 12 | }, 1000) 13 | return () => clearInterval(interval) 14 | }, []) 15 | 16 | return ( 17 |
18 | الوقت المحلي 19 | {currentTime} 20 |
21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prayer-time-api", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "@fontsource/cairo": "^4.5.10", 13 | "axios": "^0.30.2", 14 | "framer-motion": "^7.3.6", 15 | "react": "^18.2.0", 16 | "react-dom": "^18.2.0", 17 | "react-hot-toast": "^2.4.0", 18 | "react-icons": "^4.4.0", 19 | "react-spinners-kit": "^1.9.1" 20 | }, 21 | "devDependencies": { 22 | "@tailwindcss/forms": "^0.5.3", 23 | "@types/react": "^18.0.17", 24 | "@types/react-dom": "^18.0.6", 25 | "@vitejs/plugin-react": "^5.1.2", 26 | "autoprefixer": "^10.4.12", 27 | "postcss": "^8.5.6", 28 | "tailwindcss": "^3.1.8", 29 | "typescript": "^4.6.4", 30 | "vite": "^7.2.7" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # City prayer time API 🕌 2 | 3 | This project was done by using the open-source [Aladhan API](https://aladhan.com/) which has many Islamic API options to use in Islamic sites or sites visited by Muslims. 4 | 5 | ## What is Prayer Times API? 6 | 7 | Prayer Times API has many endpoints that return JSON including the Five Prayers times in a specific city, country, address, or an exact month of the year. 8 | 9 | ## Timings By City 10 | 11 | In this project I used the Timing By City endpoint to search for prayer times based on the city written. 12 | 13 | ## Preview 📺 14 | 15 | ![App Preview](./public/preview1.gif) 16 | 17 | ## Tools Used ⚙️ 18 | 19 | - [React](https://reactjs.org/) - JS library 20 | - [Tailwind CSS](https://tailwindcss.com/) - CSS framework 21 | - [Framer Motion](https://www.framer.com/motion/) - A production-ready motion library for React. 22 | 23 | ## Created By Mohanad ✍️ 24 | 25 | - Website - [Mohanad Portfolio](https://portfolio-mohanadoo.vercel.app/) 26 | - Frontend Mentor - [@MohanadOO\_](https://twitter.com/MohanadOO_) 27 | - Twitter - [@Mohanad_OOO](https://twitter.com/Mohanad_OOO) 28 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/Input.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import { BiSearchAlt } from 'react-icons/bi' 3 | 4 | type InputType = { 5 | inputRef: React.RefObject 6 | } 7 | 8 | export default function Input({ inputRef }: InputType) { 9 | const [searchInput, setSearchInput] = useState('') 10 | 11 | function handleInput(e: React.ChangeEvent) { 12 | setSearchInput(() => e.target.value) 13 | 14 | if (inputRef.current !== null) { 15 | const firstLetter = inputRef.current.value[0] 16 | const inputLength = inputRef.current.value.length 17 | if (inputLength === 1) { 18 | if ( 19 | 65 <= firstLetter.charCodeAt(0) && 20 | 122 >= firstLetter.charCodeAt(0) 21 | ) { 22 | inputRef.current.classList.add('inputLTR') 23 | } else { 24 | inputRef.current.classList.remove('inputLTR') 25 | } 26 | } 27 | } 28 | } 29 | 30 | return ( 31 |
32 | 33 | 42 |
43 | ) 44 | } 45 | -------------------------------------------------------------------------------- /src/components/PrayerTimes.tsx: -------------------------------------------------------------------------------- 1 | import { motion } from 'framer-motion' 2 | 3 | type PrayerTimesType = { 4 | prayerTimes: { 5 | Fajr: string 6 | Sunrise: string 7 | Dhuhr: string 8 | Asr: string 9 | Maghrib: string 10 | Isha: string 11 | } 12 | } 13 | 14 | export default function PrayerTimes({ prayerTimes }: PrayerTimesType) { 15 | function toArabic(prayerName: string) { 16 | switch (prayerName) { 17 | case 'Fajr': 18 | return 'الفجر' 19 | case 'Sunrise': 20 | return 'الشروق' 21 | case 'Dhuhr': 22 | return 'الظهر' 23 | case 'Asr': 24 | return 'العصر' 25 | case 'Maghrib': 26 | return 'المغرب' 27 | case 'Isha': 28 | return 'العشاء' 29 | } 30 | } 31 | 32 | function convertTime(time: string) { 33 | const split = time.split(':') 34 | const hours = Number(split[0]) 35 | const minutes = split[1] 36 | 37 | if (hours < 12) return time + ' AM' 38 | 39 | return `${ 40 | hours - 12 > 10 41 | ? `0${hours - 12}:${minutes} PM` 42 | : `${hours}:${minutes} PM` 43 | }` 44 | } 45 | 46 | return ( 47 | 53 | {Object.entries(prayerTimes).map((prayer) => { 54 | return ( 55 | 60 |

{convertTime(prayer[1])}

61 |

{toArabic(prayer[0])}

62 |
63 | ) 64 | })} 65 |
66 | ) 67 | } 68 | 69 | const prayerListContainerVariants = { 70 | initial: { opacity: 0 }, 71 | animate: { 72 | opacity: 1, 73 | transition: { 74 | staggerChildren: 0.2, 75 | }, 76 | }, 77 | } 78 | 79 | const prayerVariant = { 80 | initial: { x: '100%' }, 81 | animate: { x: '0' }, 82 | } 83 | -------------------------------------------------------------------------------- /src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useRef, useState } from 'react' 2 | 3 | import { toast } from 'react-hot-toast' 4 | import { motion } from 'framer-motion' 5 | 6 | import axios from 'axios' 7 | import Input from './components/Input' 8 | 9 | import PrayerTimes from './components/PrayerTimes' 10 | import LocaleClock from './components/LocaleClock' 11 | import CountryClock from './components/CountryClock' 12 | import { CircleSpinner } from 'react-spinners-kit' 13 | 14 | const ALADHAN_API_BASE_URL = 'https://api.aladhan.com/v1/timingsByCity' 15 | const BIGDATACLOUD_BASE_URL = 16 | 'https://api.bigdatacloud.net/data/reverse-geocode-client' 17 | 18 | type DateType = { 19 | gregorian: string 20 | hijri: { 21 | day: string 22 | month: { number: number } 23 | weekday: { ar: string } 24 | year: string 25 | } 26 | } 27 | 28 | type LocationNameType = { 29 | city: string 30 | country: string 31 | timezone: string 32 | } 33 | 34 | type PrayerTimesType = { 35 | Fajr: string 36 | Sunrise: string 37 | Dhuhr: string 38 | Asr: string 39 | Maghrib: string 40 | Isha: string 41 | } 42 | 43 | function App() { 44 | const inputRef = useRef(null) 45 | 46 | const [isLoading, setIsLoading] = useState(false) 47 | const [isError, setIsError] = useState(false) 48 | 49 | const [prayerTimes, setPrayerTimes] = useState(null) 50 | const [date, setDate] = useState(null) 51 | const [locationName, setLocationName] = useState( 52 | null 53 | ) 54 | 55 | function handleSubmit(e: React.FormEvent) { 56 | e.preventDefault() 57 | if (inputRef.current !== null) { 58 | if (inputRef.current.value === '') return 59 | setIsLoading(true) 60 | searchForLocation(inputRef.current.value) 61 | } 62 | } 63 | 64 | async function searchForLocation(city: string) { 65 | axios 66 | .get(ALADHAN_API_BASE_URL, { 67 | params: { 68 | city, 69 | country: city, 70 | }, 71 | }) 72 | .then((response) => { 73 | const data = response.data.data 74 | 75 | const { day, month, weekday, year } = data.date.hijri 76 | setDate({ 77 | gregorian: data.date.readable, 78 | hijri: { day, month: month.number, weekday, year }, 79 | }) 80 | 81 | const { Fajr, Sunrise, Dhuhr, Asr, Maghrib, Isha } = data.timings 82 | setPrayerTimes({ Fajr, Sunrise, Dhuhr, Asr, Maghrib, Isha }) 83 | 84 | const { latitude, longitude, timezone } = data.meta 85 | searchLocationName(latitude, longitude, timezone) 86 | }) 87 | .catch((err) => { 88 | console.error(err) 89 | setIsLoading(false) 90 | setIsError(true) 91 | toast.error('لا يوجد مكان بهذا الأسم') 92 | }) 93 | } 94 | 95 | async function searchLocationName( 96 | latitude: number, 97 | longitude: number, 98 | timezone: string 99 | ) { 100 | axios 101 | .get(BIGDATACLOUD_BASE_URL, { 102 | params: { 103 | latitude, 104 | longitude, 105 | localityLanguage: 'ar', 106 | }, 107 | }) 108 | .then((response) => { 109 | const { city, countryName } = response.data 110 | setLocationName({ city, country: countryName, timezone }) 111 | }) 112 | .catch((err) => { 113 | console.error(err) 114 | if (inputRef.current !== null) { 115 | setLocationName({ 116 | city: inputRef.current.value, 117 | country: '', 118 | timezone, 119 | }) 120 | } 121 | }) 122 | .finally(() => { 123 | setIsLoading(false) 124 | setIsError(false) 125 | }) 126 | } 127 | 128 | const hijriDate = `(${date?.hijri.year}/${date?.hijri.month}/${date?.hijri.day})` 129 | const gregorianDate = date?.gregorian 130 | 131 | return ( 132 | 133 |
134 | 146 |
147 | {isLoading ? ( 148 |
149 | 150 |
151 | ) : isError ? ( 152 |

153 | ) : ( 154 |
155 | {locationName === null ? ( 156 |

157 | اكتب اسم المدينة في خانة البحث 158 |

159 | ) : ( 160 | <> 161 |

162 | {locationName.city} {',' + locationName.country} 163 |

164 | 165 |
166 |

{date?.hijri.weekday.ar}

167 | 168 |
169 |

{gregorianDate}

170 |

{hijriDate}

171 |
172 |
173 | 174 | 175 | 176 | )} 177 |
178 | )} 179 |
180 | ) 181 | } 182 | 183 | export default App 184 | --------------------------------------------------------------------------------