Browse Source

feat: add logo component, update mobile header, implement sticky components and audio controls

master
sina_sajjadi 4 weeks ago
parent
commit
18f3da5536
  1. 3
      next.config.ts
  2. 3
      public/assets/images/Ellipse 441.svg
  3. 4
      public/assets/images/GraySetting.svg
  4. 139
      src/components/context/audio-conext.tsx
  5. 34
      src/components/context/ui.context.tsx
  6. 14
      src/components/layout/header.tsx
  7. 9
      src/components/layout/mobile-header.tsx
  8. 74
      src/components/layout/mobile-navigation.tsx
  9. 5
      src/components/mobile-header/hamburger.tsx
  10. 17
      src/components/mobile-header/mobile-setting.tsx
  11. 3
      src/components/modals/modal-manager.tsx
  12. 119
      src/components/modals/reciters.tsx
  13. 38
      src/components/modals/setting.tsx
  14. 151
      src/components/sticky-components/audio-controls.tsx
  15. 6
      src/components/sticky-components/download-app.tsx
  16. 14
      src/components/sticky-components/sticky-components.tsx
  17. 12
      src/components/ui/logo.tsx
  18. 9
      src/pages/_app.tsx
  19. 67
      src/pages/duas/[slug].tsx

3
next.config.ts

@ -4,6 +4,9 @@ import type { NextConfig } from "next";
const nextConfig: NextConfig = {
reactStrictMode: true,
images: {
domains: ["habibapp.com"], // Add the domain for image hosting
},
// Add other Next.js config options here as needed
};

3
public/assets/images/Ellipse 441.svg

@ -0,0 +1,3 @@
<svg width="62" height="41" viewBox="0 0 62 41" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.49459 40.5095C-0.000395198 35.871 -0.380694 30.9453 0.384556 26.1323C1.14981 21.3192 3.03905 16.7544 5.89895 12.8083C8.75885 8.8622 12.5088 5.64595 16.8446 3.42058C21.1803 1.19522 25.9797 0.0234406 30.8531 0.000346046C35.7265 -0.0227447 40.5368 1.10349 44.8934 3.28767C49.25 5.47184 53.0303 8.65241 55.9275 12.5713C58.8247 16.4901 60.7571 21.0368 61.5679 25.8424C62.3788 30.6479 62.0452 35.577 60.5942 40.2295L58.9371 39.7126C60.3068 35.3207 60.6217 30.6676 59.8563 26.1312C59.0908 21.5947 57.2666 17.3026 54.5317 13.6032C51.7967 9.90378 48.2281 6.90131 44.1154 4.83944C40.0027 2.77757 35.4619 1.7144 30.8613 1.7362C26.2608 1.758 21.7302 2.86416 17.6372 4.96491C13.5443 7.06567 10.0043 10.1018 7.3045 13.827C4.60474 17.5521 2.82129 21.8613 2.0989 26.4049C1.3765 30.9484 1.7355 35.5982 3.14677 39.977L1.49459 40.5095Z" fill="#BCBCBC"/>
</svg>

4
public/assets/images/GraySetting.svg

@ -0,0 +1,4 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.03339 11.2216C10.2621 11.2216 11.2582 10.2277 11.2582 9.00165C11.2582 7.77563 10.2621 6.78174 9.03339 6.78174C7.80467 6.78174 6.80859 7.77563 6.80859 9.00165C6.80859 10.2277 7.80467 11.2216 9.03339 11.2216Z" stroke="#8B8B8B" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.4076 5.77977L15.5159 4.24225C15.1437 3.60305 14.33 3.38711 13.6894 3.75853L13.2479 4.00903C12.3562 4.51866 11.2482 3.87946 11.2482 2.85156V2.34194C11.2482 1.60773 10.6508 1.01172 9.91501 1.01172H8.13172C7.3959 1.01172 6.7986 1.60773 6.7986 2.34194V2.85156C6.7986 3.87946 5.69051 4.51866 4.79886 4.00903L4.35739 3.75853C3.71679 3.38711 2.90304 3.61169 2.5308 4.24225L1.63915 5.77977C1.26691 6.41897 1.49198 7.23092 2.12393 7.60234L2.56543 7.85284C3.45708 8.36247 3.45708 9.64949 2.56543 10.1591L2.12393 10.4096C1.48333 10.781 1.26691 11.593 1.63915 12.2322L2.5308 13.7697C2.90304 14.4089 3.71679 14.6249 4.35739 14.2534L4.79886 14.0029C5.69051 13.4933 6.7986 14.1325 6.7986 15.1604V15.67C6.7986 16.4042 7.3959 17.0003 8.13172 17.0003H9.91501C10.6508 17.0003 11.2482 16.4042 11.2482 15.67V15.1604C11.2482 14.1325 12.3562 13.4933 13.2479 14.0029L13.6894 14.2534C14.33 14.6249 15.1437 14.4003 15.5159 13.7697L16.4076 12.2322C16.7798 11.593 16.5548 10.781 15.9228 10.4096L15.4813 10.1591C14.5897 9.64949 14.5897 8.36247 15.4813 7.85284L15.9228 7.60234C16.5634 7.23092 16.7798 6.41033 16.4076 5.77977Z" stroke="#8B8B8B" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

139
src/components/context/audio-conext.tsx

@ -0,0 +1,139 @@
import http from "@/api/http";
import React, {
createContext,
useContext,
useState,
ReactNode,
useRef,
useEffect,
} from "react";
interface Dua {
id: number;
text: string;
description: string;
local_alpha: string;
translation: string;
}
interface AudioSyncData {
id: number;
duration: [number, number][];
}
interface Audio {
audio: string;
audio_sync_data: AudioSyncData[];
reciter?: {
id: string;
name: string;
};
}
interface AudiosResponse {
results: Audio[];
}
interface AudioContextType {
audioRef: React.RefObject<HTMLAudioElement>;
audio: Audio | null; // Store a single audio object
getAudio: (id: number) => Promise<void>;
selectedReciter?: string;
setSelectedReciter: React.Dispatch<React.SetStateAction<string | undefined>>;
}
const AudioContext = createContext<AudioContextType | undefined>(undefined);
export const AudioProvider: React.FC<{ children: ReactNode }> = ({
children,
}) => {
const audioRef = useRef<HTMLAudioElement | null>(null);
const partRefs = useRef<(HTMLDivElement | null)[]>([]);
const [audios, setAudios] = useState<Audio | null>({});
const [audio, setAudio] = useState<Audio | null>({});
const [selectedReciter, setSelectedReciter] = useState<string | undefined>();
const [lastPart , setLastPart] = useState({})
const getAudio = async (id: number) => {
try {
// Fetching audio files
const audioResponse = await http.get<AudiosResponse>(
`web/mafatih/${id}/audios/`
);
console.log(
"Audio Response:",
audioResponse.data.results,
"selectedReciter:",
selectedReciter
);
setAudios(audioResponse.data.results);
let selectedAudio: Audio | undefined;
if (selectedReciter) {
selectedAudio = audioResponse.data.results.find(
(audio) => audio.reciter?.id === selectedReciter
);
}
if (!selectedAudio) {
selectedAudio = audioResponse.data.results.find(
(audio) => audio.audio_sync_data.length > 0
);
}
setAudio(selectedAudio || audioResponse.data.results[0] || null);
} catch (error) {
console.error("Error fetching audios:", error);
}
};
useEffect(() => {
if (!audios) return; // Ensure `audios` is not null or undefined
let selectedAudio: Audio | undefined;
// If a reciter is selected, find the corresponding audio
if (selectedReciter) {
selectedAudio = audios.find(
(audio) => audio.reciter?.id === selectedReciter
);
}
// If no reciter is selected, pick the first audio with sync data or the first audio
selectedAudio = selectedAudio || audios[0];
// Update the current audio
setAudio(selectedAudio || null);
// 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);
}
}, [selectedReciter, audios]);
const value: AudioContextType = {
audioRef,
partRefs,
audio,
getAudio,
selectedReciter,
setSelectedReciter,
lastPart , setLastPart
};
console.log(audios);
return (
<AudioContext.Provider value={value}>{children}</AudioContext.Provider>
);
};
export const useAudio = (): AudioContextType => {
const context = useContext(AudioContext);
if (context === undefined) {
throw new Error("useAudio must be used within an AudioProvider");
}
return context;
};

34
src/components/context/ui.context.tsx

@ -1,9 +1,12 @@
import React, { createContext, useContext, ReactNode } from "react";
import React, { createContext, useContext, ReactNode, useRef } from "react";
export interface UIContextProps {
displaySidebar: boolean;
displayDownload: boolean;
displayAudio: boolean;
audioData: [];
displaySetting: boolean;
displayReciters: boolean;
displayModal: boolean;
modalView: string | null;
toastMessage: string | null;
@ -12,7 +15,10 @@ export interface UIContextProps {
const initialState = {
displaySidebar: false,
displaySetting: false,
displayReciters: false,
displayDownload: true,
displayAudio: false,
audioData: [],
displayModal: false,
modalView: null,
toastMessage: null,
@ -23,7 +29,11 @@ type Action =
| { type: "CLOSE_SIDEBAR" }
| { type: "OPEN_SETTING" }
| { type: "CLOSE_SETTING" }
| { type: "OPEN_RECITERS" }
| { type: "CLOSE_RECITERS" }
| { type: "CLOSE_DOWNLOAD" }
| { type: "OPEN_AUDIO"; data: [] }
| { type: "CLOSE_AUDIO" }
| { type: "OPEN_MODAL"; view: string }
| { type: "CLOSE_MODAL" }
| { type: "SET_TOAST_MESSAGE"; message: string };
@ -40,8 +50,16 @@ function uiReducer(state: typeof initialState, action: Action) {
return { ...state, displaySetting: true };
case "CLOSE_SETTING":
return { ...state, displaySetting: false };
case "OPEN_RECITERS":
return { ...state, displayReciters: true };
case "CLOSE_RECITERS":
return { ...state, displayReciters: false };
case "CLOSE_DOWNLOAD":
return { ...state, displayDownload: false };
case "OPEN_AUDIO":
return { ...state, displayAudio: true, audioData: action.data };
case "CLOSE_AUDIO":
return { ...state, displayAudio: false, audioData: [] };
case "OPEN_MODAL":
return { ...state, displayModal: true, modalView: action.view };
case "CLOSE_MODAL":
@ -60,8 +78,16 @@ export const UIProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const closeSidebar = () => dispatch({ type: "CLOSE_SIDEBAR" });
const openSetting = () => dispatch({ type: "OPEN_SETTING" });
const closeSetting = () => dispatch({ type: "CLOSE_SETTING" });
const openReciters = () => dispatch({ type: "OPEN_RECITERS" });
const closeReciters = () => dispatch({ type: "CLOSE_RECITERS" });
const closeDownload = () => dispatch({ type: "CLOSE_DOWNLOAD" });
const openModal = (view: string) => {dispatch({ type: "OPEN_MODAL", view })};
const openAudio = (data: []) => {
dispatch({ type: "OPEN_AUDIO", data });
};
const closeAudio = () => dispatch({ type: "CLOSE_AUDIO" });
const openModal = (view: string) => {
dispatch({ type: "OPEN_MODAL", view });
};
const closeModal = () => dispatch({ type: "CLOSE_MODAL" });
const setToastMessage = (message: string) =>
dispatch({ type: "SET_TOAST_MESSAGE", message });
@ -76,6 +102,10 @@ export const UIProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
closeDownload,
openSetting,
closeSetting,
openAudio,
closeAudio,
openReciters,
closeReciters
};
return <UIContext.Provider value={value}>{children}</UIContext.Provider>;

14
src/components/layout/header.tsx

@ -1,18 +1,20 @@
import Logo from "../../../public/assets/images/Hosseiniye.svg";
import Image from "next/image";
import Link from "next/link";
import LanguageSwitcher from "../language-switcher";
import SearchDuas from "../ui/search-duas";
import { useUI } from "../context/ui.context";
import Logo from "../ui/logo";
const Header = () => {
const {displayDownload} = useUI()
const { displayDownload } = useUI();
return (
<header className={`w-full shadow-sm sticky top-0 bg-white hidden lg:flex z-10 ${displayDownload && "mt-[58px]"}`}>
<header
className={`w-full shadow-sm sticky top-0 bg-white hidden lg:flex z-10 ${
displayDownload && "mt-[58px]"
}`}
>
<div className="max-w-[1440px] h-20 m-auto flex justify-between w-full items-center">
<div className="flex gap-11 h-full items-center">
<div>
<Image src={Logo} alt="Logo" />
</div>
<Logo />
<div className="h-full">
<ul className="flex gap-11 font-semibold text-[#4D4D4D] h-full items-center">
<li className="h-full flex items-center border-[#c79389] hover:border-b hover:text-[#EB6E57] ">

9
src/components/layout/mobile-header.tsx

@ -4,14 +4,11 @@ import Logo from "../../../public/assets/images/Hosseiniye.svg";
import HeaderImg from "../../../public/assets/images/islamic-pattern3.svg";
import Link from "next/link";
import MobileSetting from "../mobile-header/mobile-setting";
import { useRouter } from "next/router";
import MobileSearch from "../mobile-header/mobile-search";
import HamburgerButton from "../mobile-header/hamburger";
const MobileHeader: React.FC<MobileHeaderProps> = () => {
const router = useRouter();
const MobileHeader: React.FC = () => {
console.log(router);
return (
<header className="lg:hidden">
@ -25,14 +22,14 @@ const MobileHeader: React.FC<MobileHeaderProps> = () => {
<div className="flex">
<HamburgerButton/>
<div className="w-[38px]" />
{/* <div className="w-[38px]" /> */}
</div>
<Link className="z-50" href={"/"}>
<Image src={Logo} alt="Du'as Logo" />
</Link>
<div className="flex gap-3">
{!!router.pathname.includes("duas") && <MobileSetting />}
<MobileSetting />
<MobileSearch />
</div>
</div>

74
src/components/layout/mobile-navigation.tsx

@ -1,43 +1,77 @@
import { useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { useUI } from '../context/ui.context';
import { useEffect } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { useUI } from "../context/ui.context";
import Logo from "../ui/logo";
import { IoMdClose } from "react-icons/io";
import Link from "next/link";
import LanguageSwitcher from "../language-switcher";
const sidebarVariants = {
hidden: { x: '100%' },
visible: { x: 0 },
exit: { x: '100%' },
hidden: { x: "-100%" }, // Sidebar starts off-screen to the left
visible: { x: 0 }, // Sidebar is fully visible on the screen
exit: { x: "-100%" }, // Sidebar exits to the left
};
const MobileNavigation = () => {
const { sidebarDisplay, closeSidebar } = useUI();
const { displaySidebar, closeSidebar } = useUI();
useEffect(() => {
if (!sidebarDisplay) {
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = 'auto';
}
}, [sidebarDisplay]);
console.log("ssssssssssss");
document.body.style.overflow = displaySidebar ? "hidden" : "auto";
}, [displaySidebar]);
return (
<AnimatePresence>
{sidebarDisplay && (
{displaySidebar && (
<>
{/* Overlay: Closes sidebar when clicked */}
<motion.div
className="fixed inset-0 z-50 flex justify-end"
className="fixed inset-0 z-40 bg-black bg-opacity-50"
onClick={closeSidebar} // Close the sidebar on clicking outside
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
></motion.div>
{/* Sidebar */}
<motion.div
className="fixed inset-y-0 left-0 z-50 flex justify-start"
initial="hidden"
animate="visible"
exit="exit"
variants={sidebarVariants}
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
transition={{ type: "spring", stiffness: 250, damping: 30 }}
>
<div className="w-64 bg-white h-full shadow-lg p-4">
<div className="flex flex-col justify-between w-64 bg-white h-full shadow-lg p-8">
<div>
<div className="flex justify-between">
<Logo />
<button onClick={closeSidebar} className="text-gray-600">
Close
<IoMdClose size={18} color="black" />
</button>
{/* Add your sidebar content here */}
</div>
<div className="mt-14">
<ul className="flex flex-col gap-8 font-semibold text-black h-full items-start">
<li className="h-full flex items-center border-[#c79389] hover:border-b hover:text-[#EB6E57] ">
<Link href={"/"}>Home</Link>
</li>
<li className="h-full flex items-center border-[#F4846F] hover:border-b hover:text-[#EB6E57] ">
<Link href={"/about"}>About us</Link>
</li>
<li className="">
<Link
href={"/"}
className="bg-gradient-to-r from-[#F79B59] to-[#EB6E57] text-transparent bg-clip-text font-semibold"
>
Donate
</Link>
</li>
</ul>
</div>
</div>
<LanguageSwitcher />
</div>
</motion.div>
</>
)}
</AnimatePresence>
);

5
src/components/mobile-header/hamburger.tsx

@ -2,14 +2,15 @@ import React from 'react';
import Image from 'next/image';
import Hamburegure from "../../../public/assets/images/hamburgure.svg";
import { useUI } from '../context/ui.context';
import MobileNavigation from '../layout/mobile-navigation';
import { useRouter } from 'next/router';
const HamburgerButton: React.FC = () => {
const {openSidebar} = useUI()
const router = useRouter()
return (
<>
<button onClick={openSidebar} className="p-1 bg-white rounded-[15px] z-50">
<button onClick={openSidebar} className={`p-1 bg-white rounded-[15px] z-50 ${router.pathname.includes("duas") && "mr-[50px]"}`}>
<Image width={30} src={Hamburegure} alt="Hamburger" />
</button>
</>

17
src/components/mobile-header/mobile-setting.tsx

@ -1,16 +1,27 @@
import React from 'react';
import Image from 'next/image';
import { useRouter } from 'next/router'; // Import useRouter
import Setting from "../../../public/assets/images/Setting.svg";
import { useUI } from '../context/ui.context';
const MobileSetting: React.FC = () => {
const {openModal} = useUI()
const { openModal } = useUI();
const router = useRouter(); // Initialize the router
// Conditional rendering logic
if (router.pathname.includes("duas")) {
return (
<button onClick={()=>{openModal("SETTING_VIEW")}} className="p-1 bg-white rounded-[15px] z-50">
<button
onClick={() => openModal("SETTING_VIEW")}
className="p-1 bg-white rounded-[15px] z-50"
>
<Image width={30} src={Setting} alt="Setting" />
</button>
);
}
// If the condition is not met, return null
return null;
};
export default MobileSetting;

3
src/components/modals/modal-manager.tsx

@ -1,6 +1,7 @@
import { useUI } from "../context/ui.context";
import Modal from "./modal";
import dynamic from "next/dynamic";
import RecitersModal from "./reciters";
// import Newsletter from "../newsletter";
const SettingModal = dynamic(() => import("@/components/modals/setting"));
const SearchModal = dynamic(() => import("@/components/modals/search-modal"));
@ -13,10 +14,12 @@ const SearchModal = dynamic(() => import("@/components/modals/search-modal"));
const ManagedModal: React.FC = () => {
const { displayModal, closeModal, modalView } = useUI();
console.log(modalView);
return (
<Modal rootClassName="bottom" variant="bottom" open={displayModal} onClose={closeModal}>
{modalView === "SETTING_VIEW" && <SettingModal />}
{modalView === "RECITERS_VIEW" && <RecitersModal />}
{modalView === "SEARCH_VIEW" && <SearchModal />}
{/* {modalView === "SIGN_UP_VIEW" && <SignUpForm />}
{modalView === "FORGET_PASSWORD" && <ForgetPasswordForm />}

119
src/components/modals/reciters.tsx

@ -0,0 +1,119 @@
// components/ui/setting.tsx
import React, { useEffect, useRef, useState } from "react";
import { useUI } from "../context/ui.context";
import { IoMdClose } from "react-icons/io";
import http from "@/api/http";
import { useParams } from "next/navigation";
import Image from "next/image";
import { useAudio } from "../context/audio-conext";
interface ModalProps {
className?: string; // Made optional with default value
}
const RecitersModal: React.FC<ModalProps> = ({ className = "" }) => {
const { displaySetting, modalView, closeModal } = useUI();
const {selectedReciter , setSelectedReciter} = useAudio()
const modalRef = useRef<HTMLDivElement>(null);
const closeButtonRef = useRef<HTMLButtonElement>(null);
const previouslyFocusedElement = useRef<HTMLElement | null>(null);
const [windowWidth, setWindowWidth] = useState(window.innerWidth);
const [reciters, setReciters] = useState([]);
const params = useParams();
const slug = params?.slug as string;
const id = slug.split("-").pop();
const modalClasses = windowWidth < 1024 ? "absolute w-full bottom-0" : "";
console.log(windowWidth);
useEffect(() => {
http.get(`web/mafatih/${id}/reciters/`).then((res) => {
console.log("fdasfdasfads", res);
setReciters(res.data.results);
});
const handleResize = () => {
setWindowWidth(window.innerWidth);
};
window.addEventListener("resize", handleResize);
// Cleanup the event listener on component unmount
return () => {
window.removeEventListener("resize", handleResize);
};
}, [id]);
// Handle modal visibility and accessibility
useEffect(() => {
const handleOutsideClick = (event: MouseEvent) => {
if (
modalRef.current &&
!modalRef.current.contains(event.target as Node)
) {
closeModal();
}
};
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") {
closeModal();
}
};
if (displaySetting) {
previouslyFocusedElement.current = document.activeElement as HTMLElement;
document.addEventListener("mousedown", handleOutsideClick);
document.addEventListener("keydown", handleKeyDown);
// Prevent background scrolling
document.body.style.overflow = "hidden";
// Shift focus to close button
closeButtonRef.current?.focus();
} else {
document.removeEventListener("mousedown", handleOutsideClick);
document.removeEventListener("keydown", handleKeyDown);
document.body.style.overflow = "auto";
// Return focus to previously focused element
previouslyFocusedElement.current?.focus();
}
return () => {
document.removeEventListener("mousedown", handleOutsideClick);
document.removeEventListener("keydown", handleKeyDown);
document.body.style.overflow = "auto";
};
}, []);
console.log(reciters);
reciters.map((item)=>{
console.log(item);
})
return (
<div
ref={modalRef}
className={`flex flex-col z-50 bg-white text-black min-h-60 max-h-[739px] rounded-3xl ${modalClasses} ${className}`}
role="dialog"
aria-modal="true"
>
<div className="flex justify-between items-center p-6 ">
<div className="text-[#8B8B8B] text-sm font-bold">Reciter</div>
<div></div>
<button onClick={closeModal}>
<IoMdClose size={18} />
</button>
</div>
<div>
{reciters.map((reciter) => (
<div onClick={(()=>setSelectedReciter(reciter.id))} key={reciter?.id} className="flex py-4 px-6 items-center gap-4 border-b hover:bg-[#EBEBEB] cursor-pointer">
<div className={`w-4 h-4 rounded-full border border-black ${selectedReciter === reciter?.id && "bg-[#F4846F] border-0"}`}></div>
<Image className="rounded-full w-12 h-12" alt={reciter?.name} width={50} height={50} src={reciter?.avatar?.sm}/>
<p>
{reciter?.name}
</p>
</div>
))}
</div>
</div>
);
};
export default RecitersModal;

38
src/components/modals/setting.tsx

@ -12,29 +12,38 @@ interface ModalProps {
}
const SettingModal: React.FC<ModalProps> = ({ className = "" }) => {
const { displaySetting, closeSetting, modalView, closeModal } = useUI();
const modalRef = useRef<HTMLDivElement>(null);
const closeButtonRef = useRef<HTMLButtonElement>(null);
const previouslyFocusedElement = useRef<HTMLElement | null>(null);
const { fontSettings, setFontSettings } = useFontSettingsContext();
const modalClasses = (modalView === "SETTING_VIEW") ? "absolute w-full bottom-0" : ""
const modalClasses =
modalView === "SETTING_VIEW" ? "absolute w-full bottom-0" : "";
// Handle modal visibility and accessibility
useEffect(() => {
const handleOutsideClick = (event: MouseEvent) => {
if (modalRef.current && !modalRef.current.contains(event.target as Node)) {
if (
modalRef.current &&
!modalRef.current.contains(event.target as Node)
) {
if (modalView === "SETTING_VIEW") {
closeModal();
} else {
closeSetting();
}
}
};
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") {
if (modalView === "SETTING_VIEW") {
closeModal();
} else {
closeSetting();
}
}
};
if (displaySetting) {
@ -61,7 +70,9 @@ const SettingModal: React.FC<ModalProps> = ({ className = "" }) => {
}, [displaySetting, closeSetting]);
console.log(
(!displaySetting || !(modalView === "SETTING_VIEW")) , displaySetting , modalView
!displaySetting || !(modalView === "SETTING_VIEW"),
displaySetting,
modalView
);
if (!displaySetting && !(modalView === "SETTING_VIEW")) {
return null;
@ -127,7 +138,10 @@ const SettingModal: React.FC<ModalProps> = ({ className = "" }) => {
<p className="text-[#8B8B8B] text-sm font-bold">Settings</p>
<button
ref={closeButtonRef}
onClick={()=>{closeSetting(); closeModal()}}
onClick={() => {
closeSetting();
closeModal();
}}
className="focus:outline-none"
aria-label="Close Settings"
>
@ -156,7 +170,10 @@ const SettingModal: React.FC<ModalProps> = ({ className = "" }) => {
{/* Translation Toggle */}
<div className="flex items-center justify-between">
<label htmlFor="translation-toggle" className="text-gray-700 font-medium">
<label
htmlFor="translation-toggle"
className="text-gray-700 font-medium"
>
Translation
</label>
<div className="flex gap-5 items-center">
@ -175,7 +192,10 @@ const SettingModal: React.FC<ModalProps> = ({ className = "" }) => {
{/* Transliteration Toggle */}
<div className="flex items-center justify-between">
<label htmlFor="transliteration-toggle" className="text-gray-700 font-medium">
<label
htmlFor="transliteration-toggle"
className="text-gray-700 font-medium"
>
Transliteration
</label>
<div className="flex gap-5 items-center">

151
src/components/sticky-components/audio-controls.tsx

@ -0,0 +1,151 @@
import { useEffect, useState } from "react";
import { FaPlay, FaPause } from "react-icons/fa6"; // Fixed import for FaPause
import Setting from "../../../public/assets/images/GraySetting.svg";
import Close from "../../../public/assets/images/Group 1000005170.svg";
import timeLine from "../../../public/assets/images/Ellipse 441.svg";
import Image from "next/image";
import { useUI } from "../context/ui.context";
import { useAudio } from "../context/audio-conext";
import { IoPersonSharp } from "react-icons/io5";
const AudioControls = () => {
const { openModal, closeAudio } = useUI();
const { audioRef, partRefs, audio, lastPart } = useAudio();
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 play = () => {
if (audioRef.current && audio) {
if (lastPart && audio.audio_sync_data.length > 0) {
const selectedPart = audio.audio_sync_data.find(
(item) => item.id === lastPart.id
);
if (selectedPart) {
// Set the audio current time to the start time of the lastPart
audioRef.current.currentTime = selectedPart.duration[0][0];
}
}
audioRef.current
.play()
.then(() => {
setIsPlaying(true);
})
.catch((error) => {
console.error("Error playing audio:", error);
});
}
};
const pause = () => {
if (audioRef.current) {
audioRef.current.pause();
setIsPlaying(false);
}
};
const checkPlayingState = () => {
if (audioRef.current) {
if (!audioRef.current.paused) {
setIsPlaying(true);
} else {
setIsPlaying(false);
}
}
};
const onTimeUpdate = () => {
if (audioRef.current) {
setCurrentTime(audioRef.current.currentTime);
setDuration(audioRef.current.duration);
}
};
function convertTo180(numerator, denominator) {
if (!numerator || !denominator) return -20; // Safeguard for invalid inputs
const newNumerator = (numerator * 200) / denominator;
return newNumerator; // Adjust to start from -20 degrees
}
useEffect(() => {
if (audioRef.current) {
audioRef.current.addEventListener("timeupdate", onTimeUpdate);
// Cleanup event listener on component unmount
return () => {
audioRef.current.removeEventListener("timeupdate", onTimeUpdate);
};
}
}, [audioRef]);
useEffect(() => {
checkPlayingState();
}, [audio, lastPart]);
return (
<div className="flex fixed bottom-0 left-0 right-0 p-5 justify-between items-center bg-white rounded-t-[45px] z-50">
<div className="flex gap-4 items-center">
<div>
<div className="relative flex justify-center rounded-full w-16 h-15 items-center">
<Image
width={58}
height={58}
className="absolute right bottom-4"
src={timeLine}
alt="Time Line"
/>
<div
className="absolute w-full h-full"
style={{
rotate: `${convertTo180(currentTime, duration)}deg`,
}}
>
<div className="text-[#F4846F] text-3xl font-bold">.</div>
</div>
<button
className="p-4 rounded-full w-12 h-12 flex items-center justify-center bg-[#F4846F]"
onClick={() => {
isPlaying ? pause() : play();
}}
>
{isPlaying ? <FaPause color="white" /> : <FaPlay color="white" />}
</button>
</div>
</div>
<div>
<p>{audio?.reciter?.name}</p>
<div className="flex items-center">
{audio?.reciter?.avatar?.sm ? (
<Image
className="rounded-full"
width={14}
height={14}
src={audio?.reciter?.avatar?.sm}
alt={audio?.reciter?.name}
/>
) : (
<IoPersonSharp className="rounded-full" />
)}
<p>{audio?.reciter?.name}</p>
</div>
</div>
</div>
<div className="flex gap-4 items-center">
<button onClick={() => openModal("RECITERS_VIEW")}>
<Image src={Setting} alt="Setting" />
</button>
<button
onClick={() => {
closeAudio();
pause();
}}
>
<Image src={Close} alt="Setting" />
</button>
</div>
</div>
);
};
export default AudioControls;

6
src/components/ui/download-app.tsx → src/components/sticky-components/download-app.tsx

@ -5,15 +5,13 @@ import Image from "next/image";
import { useUI } from "../context/ui.context";
const DownloadApp: React.FC = () => {
const { closeDownload, displayDownload } = useUI();
const { closeDownload, displayDownload , displayAudio } = useUI();
console.log(displayDownload);
return (
<div
className={`fixed bottom-3 left-0 right-0 mx-3 p-2 justify-between items-center bg-white rounded-2xl shadow-lg z-50 lg:flex-row-reverse lg:px-11 lg:top-0 lg:bottom-auto lg:mx-0 lg:rounded-none ${
displayDownload ? "flex" : "hidden"
}`}
className={`flex fixed bottom-3 left-0 right-0 mx-3 p-2 justify-between items-center bg-white rounded-2xl shadow-lg z-50 lg:flex-row-reverse lg:px-11 lg:top-0 lg:bottom-auto lg:mx-0 lg:rounded-none ${displayAudio && "mb-[88px]"}`}
>
<button className="" onClick={closeDownload}>
<Image src={Close} alt="Close" />

14
src/components/sticky-components/sticky-components.tsx

@ -0,0 +1,14 @@
import { useUI } from "../context/ui.context";
import AudioControls from "./audio-controls";
import DownloadApp from "./download-app";
const StickyComponents = () => {
const { displayDownload , displayAudio } = useUI();
return (
<div className="sticky-components">
{displayDownload && <DownloadApp />}
{displayAudio && <AudioControls />}
</div>
);
};
export default StickyComponents;

12
src/components/ui/logo.tsx

@ -0,0 +1,12 @@
import Image from "next/image";
import logo from "../../../public/assets/images/Hosseiniye.svg";
const Logo = () => {
return (
<div>
<Image src={logo} alt="Logo" />
</div>
);
};
export default Logo;

9
src/pages/_app.tsx

@ -1,3 +1,4 @@
import { AudioProvider } from "@/components/context/audio-conext";
import { FontSettingsProvider } from "@/components/context/font-setting-context";
import { UIProvider } from "@/components/context/ui.context";
import Header from "@/components/layout/header";
@ -5,7 +6,8 @@ import MobileHeader from "@/components/layout/mobile-header";
import MobileNavigation from "@/components/layout/mobile-navigation";
import SideBar from "@/components/layout/sidebar";
import ManagedModal from "@/components/modals/modal-manager";
import DownloadApp from "@/components/ui/download-app";
import DownloadApp from "@/components/sticky-components/download-app";
import StickyComponents from "@/components/sticky-components/sticky-components";
import "@/styles/globals.css";
import type { AppProps } from "next/app";
@ -14,6 +16,7 @@ export default function App({ Component, pageProps }: AppProps) {
return (
<FontSettingsProvider>
<UIProvider>
<AudioProvider>
<Header />
<MobileHeader />
<div className="m-auto lg:p-6">
@ -23,11 +26,11 @@ export default function App({ Component, pageProps }: AppProps) {
<Component {...pageProps} />
</main>
</div>
<DownloadApp />
<StickyComponents />
<MobileNavigation />
</div>
<ManagedModal />
</AudioProvider>
</UIProvider>
</FontSettingsProvider>
);

67
src/pages/duas/[slug].tsx

@ -13,6 +13,7 @@ import { DuaComponentProps } from "@/components/utils/types";
import SettingModal from "@/components/modals/setting";
import { useUI } from "@/components/context/ui.context";
import { useFontSettingsContext } from "@/components/context/font-setting-context";
import { useAudio } from "@/components/context/audio-conext";
// Define the Dua interface
interface Dua {
@ -30,20 +31,12 @@ interface AudioSyncData {
}
// Define the Audio interface
interface Audio {
audio: string;
audio_sync_data: AudioSyncData[];
}
// Define the API response structure
interface DuaPartsResponse {
results: Dua[];
}
interface AudiosResponse {
results: Audio[];
}
const DuaComponent: React.FC<DuaComponentProps> = ({
SelectedDua,
setSelectedDua,
@ -52,23 +45,22 @@ const DuaComponent: React.FC<DuaComponentProps> = ({
const slug = (params?.slug as string) ?? SelectedDua;
console.log(SelectedDua);
const router = useRouter();
const { openSetting } = useUI();
const { openSetting, openAudio } = useUI();
const { audioRef, partRefs, getAudio, audio, setLastPart } = useAudio();
console.log("Audio Ref:", audioRef.current);
// Use the shared context for font settings
const { fontSettings } = useFontSettingsContext();
// State hooks
const [duaParts, setDuaParts] = useState<Dua[]>([]);
const [audios, setAudios] = useState<Audio[]>([]);
const [recitingPart, setRecitingPart] = useState<Dua | null>(null);
// Audio reference
const audioRef = useRef<HTMLAudioElement | null>(null);
// Refs to track each part
const partRefs = useRef<(HTMLDivElement | null)[]>([]);
// Fetch Dua parts and audios
// Fetch Dua parts and audio
useEffect(() => {
if (!slug) return;
@ -87,16 +79,7 @@ const DuaComponent: React.FC<DuaComponentProps> = ({
// Optionally, set an error state here
}
try {
// Fetching audio files
const audioResponse = await http.get<AudiosResponse>(
`web/mafatih/${id}/audios/`
);
setAudios(audioResponse.data.results);
} catch (error) {
console.error("Error fetching audios:", error);
// Optionally, set an error state here
}
getAudio(id);
};
fetchData();
@ -105,22 +88,25 @@ const DuaComponent: React.FC<DuaComponentProps> = ({
// Play audio from a specific part
const playAudio = useCallback(
(part: Dua) => {
if (audios.length === 0) return;
if (!Object.keys(audio).length) return;
console.log(part);
const selectedAudio = audios[0].audio_sync_data.find(
const selectedAudio = audio.audio_sync_data.find(
(item) => item.id === part.id
);
if (selectedAudio && audioRef.current) {
const startTime = selectedAudio.duration[0][0]; // Assuming in seconds
audioRef.current.currentTime = startTime;
openAudio();
audioRef.current.play().catch((error) => {
console.error("Error playing audio:", error);
});
}
},
[audios]
[audio]
);
console.log(audio);
console.log(fontSettings.arabicRange);
@ -143,10 +129,10 @@ const DuaComponent: React.FC<DuaComponentProps> = ({
// Handle audio progress for updating reciting part
const handleAudioProgress = useCallback(() => {
if (!audioRef.current || audios.length === 0) return;
if (!audioRef.current || audio.length === 0) return;
const currentTime = audioRef.current.currentTime;
const currentAudioSync = audios[0].audio_sync_data.find(
const currentAudioSync = audio.audio_sync_data.find(
(item) =>
item.duration[0][0] < currentTime && currentTime < item.duration[0][1]
);
@ -154,7 +140,7 @@ const DuaComponent: React.FC<DuaComponentProps> = ({
const currentRecitingPart =
duaParts.find((item) => item.id === currentAudioSync?.id) || null;
setRecitingPart(currentRecitingPart);
}, [audios, duaParts]);
}, [audio, duaParts]);
function processSlug(slug: string): string {
if (!slug) return "";
@ -207,6 +193,8 @@ const DuaComponent: React.FC<DuaComponentProps> = ({
const index = duaParts.findIndex((item) => item.id === recitingPart.id);
const partElement = partRefs.current[index];
if (partElement) {
setLastPart(recitingPart);
partElement.scrollIntoView({
behavior: "smooth",
block: "center",
@ -218,7 +206,7 @@ const DuaComponent: React.FC<DuaComponentProps> = ({
return null; // Handling the case where slug is not available
}
console.log(audios, duaParts);
console.log(audioRef.current);
return (
<div
@ -264,7 +252,9 @@ const DuaComponent: React.FC<DuaComponentProps> = ({
<div className="p-3 bg-white rounded-3xl">
{item.text && (
<div
className={`mb-4 text-right ${!fontSettings.arabic && "hidden"}`}
className={`mb-4 text-right ${
!fontSettings.arabic && "hidden"
}`}
style={{
fontSize: `${25 * (fontSettings.arabicRange / 100)}px`,
}}
@ -274,7 +264,9 @@ const DuaComponent: React.FC<DuaComponentProps> = ({
)}
{item.local_alpha && (
<p
className={`text-sm font-normal mb-4 ${!fontSettings.transliteration && "hidden"}`}
className={`text-sm font-normal mb-4 ${
!fontSettings.transliteration && "hidden"
}`}
style={{
fontSize: `${
14 * (fontSettings.transliterationRange / 100)
@ -286,7 +278,9 @@ const DuaComponent: React.FC<DuaComponentProps> = ({
)}
{item.translation && (
<p
className={`text-sm font-normal border-t pt-4 ${!fontSettings.translation && "hidden"}`}
className={`text-sm font-normal border-t pt-4 ${
!fontSettings.translation && "hidden"
}`}
style={{
fontSize: `${14 * (fontSettings.translationRange / 100)}px`,
}}
@ -301,7 +295,7 @@ const DuaComponent: React.FC<DuaComponentProps> = ({
)}
{/* Play button to start audio from specific time */}
{!!audios.length && (
{audio && !!Object.keys(audio)?.length && (
<button
onClick={() => playAudio(item)}
className="mt-5 cursor-pointer"
@ -319,8 +313,9 @@ const DuaComponent: React.FC<DuaComponentProps> = ({
</div>
))}
</div>
{audios.length > 0 && (
<audio ref={audioRef} src={audios[0].audio} preload="auto" />
{audio && Object.keys(audio).length > 0 && (
<audio ref={audioRef} src={audio.audio} preload="auto" />
)}
</div>
);

Loading…
Cancel
Save