diff --git a/next.config.ts b/next.config.ts index 392e6cb..e13e7d8 100644 --- a/next.config.ts +++ b/next.config.ts @@ -7,7 +7,12 @@ const nextConfig: NextConfig = { images: { domains: ["habibapp.com"], // Add the domain for image hosting }, - +typescript : { + ignoreBuildErrors : true +}, +eslint : { + ignoreDuringBuilds : true +} // Add other Next.js config options here as needed }; diff --git a/public/assets/images/Group 27010.png b/public/assets/images/Group 27010.png new file mode 100644 index 0000000..926e586 Binary files /dev/null and b/public/assets/images/Group 27010.png differ diff --git a/public/assets/images/Group 27010.svg b/public/assets/images/Group 27010.svg new file mode 100644 index 0000000..2912374 --- /dev/null +++ b/public/assets/images/Group 27010.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/assets/images/Group 270S10.svg b/public/assets/images/Group 270S10.svg new file mode 100644 index 0000000..2912374 --- /dev/null +++ b/public/assets/images/Group 270S10.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/assets/images/Group 852.svg b/public/assets/images/Group 852.svg new file mode 100644 index 0000000..ba3fe18 --- /dev/null +++ b/public/assets/images/Group 852.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/images/VectAAAAAAAAAor.svg b/public/assets/images/VectAAAAAAAAAor.svg new file mode 100644 index 0000000..8ad5a0d --- /dev/null +++ b/public/assets/images/VectAAAAAAAAAor.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/images/VectSSSSSSSor.jpg b/public/assets/images/VectSSSSSSSor.jpg new file mode 100644 index 0000000..486e130 Binary files /dev/null and b/public/assets/images/VectSSSSSSSor.jpg differ diff --git a/public/assets/images/VectodewsqaDr.svg b/public/assets/images/VectodewsqaDr.svg new file mode 100644 index 0000000..d959180 --- /dev/null +++ b/public/assets/images/VectodewsqaDr.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/images/VectorDua.svg b/public/assets/images/VectorDua.svg new file mode 100644 index 0000000..d959180 --- /dev/null +++ b/public/assets/images/VectorDua.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/context/audio-conext.tsx b/src/components/context/audio-conext.tsx index bc3aff6..850e491 100644 --- a/src/components/context/audio-conext.tsx +++ b/src/components/context/audio-conext.tsx @@ -38,7 +38,7 @@ interface AudioContextType { audioRef: React.RefObject; audio: Audio | null; // Store a single audio object getAudio: (id: number) => Promise; - selectedReciter?: string; + selectedReciter?: {}; setSelectedReciter: React.Dispatch>; } @@ -73,7 +73,7 @@ export const AudioProvider: React.FC<{ children: ReactNode }> = ({ if (selectedReciter) { selectedAudio = audioResponse.data.results.find( - (audio) => audio.reciter?.id === selectedReciter + (audio) => audio.reciter?.id === selectedReciter.id ); } @@ -97,7 +97,7 @@ export const AudioProvider: React.FC<{ children: ReactNode }> = ({ // If a reciter is selected, find the corresponding audio if (selectedReciter) { selectedAudio = audios.find( - (audio) => audio.reciter?.id === selectedReciter + (audio) => audio.reciter?.id === selectedReciter.id ); } @@ -109,7 +109,7 @@ export const AudioProvider: React.FC<{ children: ReactNode }> = ({ // If no reciter is selected, update it with the first audio's reciter ID if (!selectedReciter && audios[0]?.reciter?.id) { - setSelectedReciter(audios[0].reciter.id); + setSelectedReciter(audios[0].reciter); } }, [selectedReciter, audios]); diff --git a/src/components/context/ui.context.tsx b/src/components/context/ui.context.tsx index 6f504cd..1b468c0 100644 --- a/src/components/context/ui.context.tsx +++ b/src/components/context/ui.context.tsx @@ -31,6 +31,8 @@ type Action = | { type: "CLOSE_SETTING" } | { type: "OPEN_RECITERS" } | { type: "CLOSE_RECITERS" } + | { type: "OPEN_AUDIO_SETTING" } + | { type: "CLOSE_AUDIO_SETTING" } | { type: "CLOSE_DOWNLOAD" } | { type: "OPEN_AUDIO"; data: [] } | { type: "CLOSE_AUDIO" } @@ -54,6 +56,10 @@ function uiReducer(state: typeof initialState, action: Action) { return { ...state, displayReciters: true }; case "CLOSE_RECITERS": return { ...state, displayReciters: false }; + case "OPEN_AUDIO_SETTING": + return { ...state, displayAudioSetting: true }; + case "CLOSE_AUDIO_SETTING": + return { ...state, displayAudioSetting: false }; case "CLOSE_DOWNLOAD": return { ...state, displayDownload: false }; case "OPEN_AUDIO": @@ -80,6 +86,8 @@ export const UIProvider: React.FC<{ children: ReactNode }> = ({ children }) => { const closeSetting = () => dispatch({ type: "CLOSE_SETTING" }); const openReciters = () => dispatch({ type: "OPEN_RECITERS" }); const closeReciters = () => dispatch({ type: "CLOSE_RECITERS" }); + const openAudioSetting = () => dispatch({ type: "OPEN_AUDIO_SETTING" }); + const closeAudioSetting = () => dispatch({ type: "CLOSE_AUDIO_SETTING" }); const closeDownload = () => dispatch({ type: "CLOSE_DOWNLOAD" }); const openAudio = (data: []) => { dispatch({ type: "OPEN_AUDIO", data }); @@ -105,7 +113,9 @@ export const UIProvider: React.FC<{ children: ReactNode }> = ({ children }) => { openAudio, closeAudio, openReciters, - closeReciters + closeReciters, + openAudioSetting, + closeAudioSetting }; return {children}; diff --git a/src/components/language-switcher.tsx b/src/components/language-switcher.tsx index d0520f1..7c5664d 100644 --- a/src/components/language-switcher.tsx +++ b/src/components/language-switcher.tsx @@ -1,97 +1,134 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ -"use client" +"use client"; -import React, { useState, useEffect, useRef } from 'react'; +import React, { useState, useEffect, useRef } from "react"; //@ts-ignore import { IoGlobeSharp } from "react-icons/io5"; import { IoIosArrowDown } from "react-icons/io"; -import useLocalStorage from './utils/hooks/local-storage'; -// import http from '@/api/http'; +import { useUI } from "./context/ui.context"; +import http from "@/api/http"; const LanguageSwitcher: React.FC = () => { const [isOpen, setIsOpen] = useState(false); - - // Define `selectedLanguage` as a string literal type to match the keys of `languageOptions` - const [selectedLanguage, setSelectedLanguage] = useLocalStorage<"en" | "fa" | "fr" | "es" | "de" | "zh">("locale", "en"); - - const [isClient, setIsClient] = useState(false); // State to track if we're on the client - - const wrapperRef = useRef(null); // Ref for the dropdown container - - const languageOptions = { - en: "English", - fa: "Persian", - fr: "French", - es: "Spanish", - de: "German", - zh: "Chinese", - }; + const { openModal } = useUI(); + + const wrapperRef = useRef(null); + + const [windowWidth, setWindowWidth] = useState( + typeof window !== "undefined" ? window.innerWidth : 0 + ); - const selectedLanguageName = languageOptions[selectedLanguage] || "English"; + const [languages, setLanguages] = useState<{ code: string; name: string }[]>( + [] + ); + const [selectedLanguage, setSelectedLanguage] = useState("en"); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); - const languages = Object.keys(languageOptions) as Array<"en" | "fa" | "fr" | "es" | "de" | "zh">; + const selectedLanguageName = + languages.find((lang) => lang.code === selectedLanguage)?.name || "English"; const toggleDropdown = () => { - setIsOpen(prevState => !prevState); + setIsOpen((prevState) => !prevState); }; - const selectLanguage = (lang: "en" | "fa" | "fr" | "es" | "de" | "zh") => { + const selectLanguage = (lang: string) => { setSelectedLanguage(lang); + localStorage.setItem("locale", lang); // Save the selected language in localStorage setIsOpen(false); }; useEffect(() => { - // http.get("/v1/languages/").then((res)=>{ - // console.log(res); - - // }) + const fetchLanguages = async () => { + try { + setIsLoading(true); + const response = await http.get("v1/languages/"); + setLanguages(response.data); + } catch (err) { + setError("Failed to load languages. Please try again."); + } finally { + setIsLoading(false); + } + }; + + fetchLanguages(); + const savedLanguage = localStorage.getItem("locale"); + if (savedLanguage) { + setSelectedLanguage(savedLanguage); + } + }, []); + + useEffect(() => { + const handleResize = () => setWindowWidth(window.innerWidth); const handleClickOutside = (event: MouseEvent) => { - if (wrapperRef.current && !wrapperRef.current.contains(event.target as Node)) { + if ( + wrapperRef.current && + !wrapperRef.current.contains(event.target as Node) + ) { setIsOpen(false); } }; - document.addEventListener('mousedown', handleClickOutside); + + window.addEventListener("resize", handleResize); + document.addEventListener("mousedown", handleClickOutside); + return () => { - document.removeEventListener('mousedown', handleClickOutside); + window.removeEventListener("resize", handleResize); + document.removeEventListener("mousedown", handleClickOutside); }; }, []); - useEffect(() => { - setIsClient(true); - }, []); - - if (!isClient) { - return null; - } - return ( + windowWidth < 1024 ? openModal("LANGUAGES_VIEW") : toggleDropdown() + } className="flex items-center px-3 py-2 h-11 border-gray-300 rounded-2xl bg-[#EBEBEB] text-black hover:bg-gray-50 focus:outline-none text-sm font-semibold" + aria-expanded={isOpen} + aria-controls="language-dropdown" > {selectedLanguageName} - {isOpen && ( - - {languages.map(lang => ( + {isOpen && !isLoading && !error && ( + + {languages.map((lang) => ( selectLanguage(lang)} + key={lang.code} + className="px-4 py-2 hover:bg-gray-100 text-xs font-semibold cursor-pointer" + onClick={() => selectLanguage(lang.code)} + role="menuitem" > - {languageOptions[lang]} + {lang.name} ))} )} + + {isLoading && ( + + Loading... + + )} + + {error && ( + + {error} + + )} ); }; diff --git a/src/components/layout/header.tsx b/src/components/layout/header.tsx index 4374b2c..8710523 100644 --- a/src/components/layout/header.tsx +++ b/src/components/layout/header.tsx @@ -12,7 +12,7 @@ const Header = () => { displayDownload && "mt-[58px]" }`} > - + diff --git a/src/components/modals/audio-setting.tsx b/src/components/modals/audio-setting.tsx new file mode 100644 index 0000000..bd2f63c --- /dev/null +++ b/src/components/modals/audio-setting.tsx @@ -0,0 +1,89 @@ +import React, { useEffect, useRef, useState } from "react"; +import { useUI } from "../context/ui.context"; +import { IoMdClose } from "react-icons/io"; +import { IoIosArrowDown } from "react-icons/io"; +import SpeedImg from "../../../public/assets/images/Group 852.svg"; +import Image from "next/image"; +import { useAudio } from "../context/audio-conext"; + +interface ModalProps { + className?: string; // Optional className with a default value +} + +const AudioSetting: React.FC = ({ className = "" }) => { + const { displaySetting, closeModal, openModal } = useUI(); + const { selectedReciter, audioRef } = useAudio() as { + selectedReciter: { name: string } | null; + audioRef: React.RefObject; + }; + + const modalRef = useRef(null); + + const [windowWidth, setWindowWidth] = useState(typeof window !== "undefined" ? window.innerWidth : 0); + const [speed, setSpeed] = useState(1); + + useEffect(() => { + const handleResize = () => setWindowWidth(window.innerWidth); + window.addEventListener("resize", handleResize); + return () => window.removeEventListener("resize", handleResize); + }, []); + + useEffect(() => { + if (audioRef?.current) { + audioRef.current.playbackRate = speed; // Update the audio playback speed + } + }, [speed]); // Re-run this effect whenever speed changes + + + const modalClasses = windowWidth < 1024 ? "" : "max-w-96 bottom-20 right-0"; + + const increaseSpeed = () => { + setSpeed((prevSpeed) => (prevSpeed < 2 ? +(prevSpeed + 0.2).toFixed(1) : 1)); + }; + + + return ( + + + Audio Settings + + + + + + + + Reciters + openModal("RECITERS_VIEW")} + className="p-3 border rounded-2xl flex justify-between items-center w-full" + aria-label="Select a reciter" + > + {selectedReciter?.name || "Select Reciter"} + + + + + + + + Speed + + + {speed} x + + + + ); +}; + +export default AudioSetting; diff --git a/src/components/modals/languages-modal.tsx b/src/components/modals/languages-modal.tsx new file mode 100644 index 0000000..d5e16a1 --- /dev/null +++ b/src/components/modals/languages-modal.tsx @@ -0,0 +1,92 @@ +import http from "@/api/http"; +import { useEffect, useState } from "react"; +import { IoMdClose } from "react-icons/io"; +import { useUI } from "../context/ui.context"; +import { FaCheckCircle } from "react-icons/fa"; + + +interface Language { + code: string; // Assuming each language has a unique ID + name: string; +} + +const LanguageModal = () => { + const [languages, setLanguages] = useState([]); + const [selectedLanguage, setSelectedLanguage] = useState(""); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const { closeModal } = useUI(); + + useEffect(() => { + const fetchLanguages = async () => { + try { + setIsLoading(true); + const response = await http.get("v1/languages/"); + setLanguages(response.data); + } catch (err) { + setError("Failed to load languages. Please try again."); + } finally { + setIsLoading(false); + } + }; + + fetchLanguages(); + + const locale = localStorage.getItem("locale"); + if (locale) { + setSelectedLanguage(locale); + } + }, []); + + const handleLanguageSelect = (code: string) => { + setSelectedLanguage(code); + localStorage.setItem("locale", code); // Persist selection in localStorage + }; + + if (isLoading) { + return Loading languages...; + } + + if (error) { + return {error}; + } + + return ( + + + Choose Language + + + + + {languages.length > 0 ? ( + + {languages.map((item) => ( + // eslint-disable-next-line jsx-a11y/role-supports-aria-props + handleLanguageSelect(item.code)} + className={`p-4 my-4 rounded-2xl cursor-pointer flex items-center gap-4 ${ + selectedLanguage === item.code + ? "bg-white border-2 border-[#F4846F]" + : "bg-white" + }`} + aria-selected={selectedLanguage === item.code} + > + {selectedLanguage === item.code && } + {item.name} + + ))} + + ) : ( + No languages available. + )} + + ); +}; + +export default LanguageModal; diff --git a/src/components/modals/modal-manager.tsx b/src/components/modals/modal-manager.tsx index 1ece79b..056eb3a 100644 --- a/src/components/modals/modal-manager.tsx +++ b/src/components/modals/modal-manager.tsx @@ -2,6 +2,8 @@ import { useUI } from "../context/ui.context"; import Modal from "./modal"; import dynamic from "next/dynamic"; import RecitersModal from "./reciters"; +import AudioSetting from "./audio-setting"; +import LanguageModal from "./languages-modal"; // import Newsletter from "../newsletter"; const SettingModal = dynamic(() => import("@/components/modals/setting")); const SearchModal = dynamic(() => import("@/components/modals/search-modal")); @@ -21,6 +23,8 @@ const ManagedModal: React.FC = () => { {modalView === "SETTING_VIEW" && } {modalView === "RECITERS_VIEW" && } {modalView === "SEARCH_VIEW" && } + {modalView === "AUDIO_SETTING_VIEW" && } + {modalView === "LANGUAGES_VIEW" && } {/* {modalView === "SIGN_UP_VIEW" && } {modalView === "FORGET_PASSWORD" && } {modalView === "PRODUCT_VIEW" && } diff --git a/src/components/modals/reciters.tsx b/src/components/modals/reciters.tsx index 7547a99..b00ee37 100644 --- a/src/components/modals/reciters.tsx +++ b/src/components/modals/reciters.tsx @@ -12,8 +12,8 @@ interface ModalProps { } const RecitersModal: React.FC = ({ className = "" }) => { - const { displaySetting, modalView, closeModal } = useUI(); - const {selectedReciter , setSelectedReciter} = useAudio() + const { displaySetting, closeModal } = useUI(); + const { selectedReciter, setSelectedReciter } = useAudio(); const modalRef = useRef(null); const closeButtonRef = useRef(null); const previouslyFocusedElement = useRef(null); @@ -22,7 +22,7 @@ const RecitersModal: React.FC = ({ className = "" }) => { const params = useParams(); const slug = params?.slug as string; const id = slug.split("-").pop(); - const modalClasses = windowWidth < 1024 ? "absolute w-full bottom-0" : ""; + const modalClasses = windowWidth < 1024 ? "" : "max-w-96 bottom-20 right-0"; console.log(windowWidth); @@ -83,14 +83,13 @@ const RecitersModal: React.FC = ({ className = "" }) => { }; }, []); console.log(reciters); - reciters.map((item)=>{ + reciters.map((item) => { console.log(item); - - }) + }); return ( @@ -103,12 +102,27 @@ const RecitersModal: React.FC = ({ className = "" }) => { {reciters.map((reciter) => ( - setSelectedReciter(reciter.id))} key={reciter?.id} className="flex py-4 px-6 items-center gap-4 border-b hover:bg-[#EBEBEB] cursor-pointer"> - - - - {reciter?.name} - + { + setSelectedReciter(reciter); + closeModal(); + }} + key={reciter?.id} + className="flex py-4 px-6 items-center gap-4 border-b hover:bg-[#EBEBEB] cursor-pointer" + > + + + {reciter?.name} ))} diff --git a/src/components/sticky-components/audio-controls.tsx b/src/components/sticky-components/audio-controls.tsx index e633c0d..bd40e15 100644 --- a/src/components/sticky-components/audio-controls.tsx +++ b/src/components/sticky-components/audio-controls.tsx @@ -7,6 +7,7 @@ import Image from "next/image"; import { useUI } from "../context/ui.context"; import { useAudio } from "../context/audio-conext"; import { IoPersonSharp } from "react-icons/io5"; +import { useParams } from "next/navigation"; const AudioControls = () => { const { openModal, closeAudio } = useUI(); @@ -14,6 +15,8 @@ const AudioControls = () => { const [isPlaying, setIsPlaying] = useState(false); const [currentTime, setCurrentTime] = useState(0); const [duration, setDuration] = useState(1); // Default duration to 1 to prevent division by zero + const params = useParams(); + const slug = params?.slug as string; const play = () => { if (audioRef.current && audio) { @@ -67,7 +70,26 @@ const AudioControls = () => { const newNumerator = (numerator * 200) / denominator; return newNumerator; // Adjust to start from -20 degrees } + function processSlug(slug: string): string { + if (!slug) return ""; + // Split the slug by "-" + const parts = slug.split("-"); + + // Remove the last word + if (parts.length === 0) return ""; + parts.pop(); + + // Convert each word to PascalCase + const pascalCaseWords = parts.map( + (word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase() + ); + + // Join the words with spaces + const result = pascalCaseWords.join(" "); + + return result; + } useEffect(() => { if (audioRef.current) { audioRef.current.addEventListener("timeupdate", onTimeUpdate); @@ -104,7 +126,7 @@ const AudioControls = () => { . { isPlaying ? pause() : play(); }} @@ -114,7 +136,7 @@ const AudioControls = () => { - {audio?.reciter?.name} + {processSlug(slug)} {audio?.reciter?.avatar?.sm ? ( { - openModal("RECITERS_VIEW")}> + openModal("AUDIO_SETTING_VIEW")}> (key: string, initialValue: T): [T, (value: T | ((val: T) => T)) => void] { - const [storedValue, setStoredValue] = useState(() => { - try { - const item = window.localStorage.getItem(key); - return item ? (JSON.parse(item) as T) : initialValue; - } catch (error) { - console.error("Error reading localStorage key “" + key + "”:", error); - return initialValue; - } - }); - - const setValue = (value: T | ((val: T) => T)) => { - try { - const valueToStore = value instanceof Function ? value(storedValue) : value; - setStoredValue(valueToStore); - window.localStorage.setItem(key, JSON.stringify(valueToStore)); - } catch (error) { - console.error("Error setting localStorage key “" + key + "”:", error); - } - }; - - useEffect(() => { - const handleStorageChange = () => { - try { - const item = window.localStorage.getItem(key); - setStoredValue(item ? (JSON.parse(item) as T) : initialValue); - } catch (error) { - console.error("Error reading localStorage key “" + key + "”:", error); - } - }; - - window.addEventListener("storage", handleStorageChange); - return () => window.removeEventListener("storage", handleStorageChange); - }, [key, initialValue]); - - return [storedValue, setValue]; -} - -export default useLocalStorage;
{selectedReciter?.name || "Select Reciter"}
Speed
{speed} x
{item.name}
No languages available.
- {reciter?.name} -
{reciter?.name}
{audio?.reciter?.name}
{processSlug(slug)}