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 (
- {isOpen && ( -
    - {languages.map(lang => ( + {isOpen && !isLoading && !error && ( + )} + + {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
+ +
+ + +
+ ); +}; + +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} -

- {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} +

{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 = () => {
.
-

{audio?.reciter?.name}

+

{processSlug(slug)}

{audio?.reciter?.avatar?.sm ? ( {
-