diff --git a/package-lock.json b/package-lock.json index 3eff8af..66c18b1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "axios": "^1.7.9", "body-scroll-lock": "^4.0.0-beta.0", "classnames": "^2.5.1", + "dompurify": "^3.2.3", "framer-motion": "^11.15.0", "moment": "^2.30.1", "moment-hijri": "^3.0.0", @@ -950,6 +951,12 @@ "@types/react-icon-base": "*" } }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "optional": true + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.18.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.18.0.tgz", @@ -1938,6 +1945,14 @@ "node": ">=0.10.0" } }, + "node_modules/dompurify": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.3.tgz", + "integrity": "sha512-U1U5Hzc2MO0oW3DF+G9qYN0aT7atAou4AgI0XjWz061nyBPbdxkfdhfy5uMgGn6+oLFCfn44ZGbdDqCzVmlOWA==", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/dunder-proto": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.0.tgz", diff --git a/package.json b/package.json index bbe2f4b..bd82dd2 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "axios": "^1.7.9", "body-scroll-lock": "^4.0.0-beta.0", "classnames": "^2.5.1", + "dompurify": "^3.2.3", "framer-motion": "^11.15.0", "moment": "^2.30.1", "moment-hijri": "^3.0.0", diff --git a/public/assets/images/NoDataFound.svg b/public/assets/images/NoDataFound.svg new file mode 100644 index 0000000..3682b99 --- /dev/null +++ b/public/assets/images/NoDataFound.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/components/layout/mobile-header.tsx b/src/components/layout/mobile-header.tsx index 9ccc1b1..0a1bf83 100644 --- a/src/components/layout/mobile-header.tsx +++ b/src/components/layout/mobile-header.tsx @@ -2,11 +2,11 @@ import Image from "next/image"; import React from "react"; import Logo from "../../../public/assets/images/Hosseiniye.svg"; import HeaderImg from "../../../public/assets/images/islamic-pattern3.svg"; -import Hamburegure from "../../../public/assets/images/hamburgure.svg"; import Link from "next/link"; -import MobileSetting from "../ui/mobile-setting"; +import MobileSetting from "../mobile-header/mobile-setting"; import { useRouter } from "next/router"; -import MobileSearch from "../ui/mobile-search"; +import MobileSearch from "../mobile-header/mobile-search"; +import HamburgerButton from "../mobile-header/hamburger"; const MobileHeader: React.FC = () => { const router = useRouter(); @@ -23,9 +23,8 @@ const MobileHeader: React.FC = () => {
- + +
diff --git a/src/components/layout/mobile-navigation.tsx b/src/components/layout/mobile-navigation.tsx new file mode 100644 index 0000000..ea4c308 --- /dev/null +++ b/src/components/layout/mobile-navigation.tsx @@ -0,0 +1,46 @@ +import { useEffect } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { useUI } from '../context/ui.context'; + +const sidebarVariants = { + hidden: { x: '100%' }, + visible: { x: 0 }, + exit: { x: '100%' }, +}; + +const MobileNavigation = () => { + const { sidebarDisplay, closeSidebar } = useUI(); + + useEffect(() => { + if (!sidebarDisplay) { + document.body.style.overflow = 'hidden'; + } else { + document.body.style.overflow = 'auto'; + } + }, [sidebarDisplay]); +console.log("ssssssssssss"); + + return ( + + {sidebarDisplay && ( + +
+ + {/* Add your sidebar content here */} +
+
+ )} +
+ ); +}; + +export default MobileNavigation; \ No newline at end of file diff --git a/src/components/mobile-header/hamburger.tsx b/src/components/mobile-header/hamburger.tsx new file mode 100644 index 0000000..9c6609f --- /dev/null +++ b/src/components/mobile-header/hamburger.tsx @@ -0,0 +1,19 @@ +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'; + + +const HamburgerButton: React.FC = () => { + const {openSidebar} = useUI() + return ( + <> + + + ); +}; + +export default HamburgerButton; \ No newline at end of file diff --git a/src/components/ui/mobile-search.tsx b/src/components/mobile-header/mobile-search.tsx similarity index 100% rename from src/components/ui/mobile-search.tsx rename to src/components/mobile-header/mobile-search.tsx diff --git a/src/components/ui/mobile-setting.tsx b/src/components/mobile-header/mobile-setting.tsx similarity index 100% rename from src/components/ui/mobile-setting.tsx rename to src/components/mobile-header/mobile-setting.tsx diff --git a/src/components/modals/modal-manager.tsx b/src/components/modals/modal-manager.tsx index 4dd4d54..e087773 100644 --- a/src/components/modals/modal-manager.tsx +++ b/src/components/modals/modal-manager.tsx @@ -2,7 +2,7 @@ import { useUI } from "../context/ui.context"; import Modal from "./modal"; import dynamic from "next/dynamic"; // import Newsletter from "../newsletter"; -const SettingModal = dynamic(() => import("@/components/ui/setting")); +const SettingModal = dynamic(() => import("@/components/modals/setting")); const SearchModal = dynamic(() => import("@/components/modals/search-modal")); // const SignUpForm = dynamic(() => import("@components/auth/sign-up-form")); // const ForgetPasswordForm = dynamic( diff --git a/src/components/modals/modal.tsx b/src/components/modals/modal.tsx index 766bfc3..f87bcc9 100644 --- a/src/components/modals/modal.tsx +++ b/src/components/modals/modal.tsx @@ -12,7 +12,7 @@ import { fadeInOut } from '@/utils/motion/fade-in-out'; import { zoomOutIn } from '@/utils/motion/zoom-out-in'; import { useUI } from '../context/ui.context'; import useOnClickOutside from '@/utils/use-click-outside'; -import SettingModal from '../ui/setting'; +import SettingModal from './setting'; type ModalProps = { open?: boolean; @@ -52,7 +52,6 @@ export default function Modal({ const { closeModal } = useUI(); const modalRootRef = useRef() as DivElementRef; const modalInnerRef = useRef() as DivElementRef; - useOnClickOutside(modalInnerRef, () => closeModal()); useEffect(() => { if (modalInnerRef.current) { if (open) { diff --git a/src/components/modals/search-modal.tsx b/src/components/modals/search-modal.tsx index 0c306c6..dc967fe 100644 --- a/src/components/modals/search-modal.tsx +++ b/src/components/modals/search-modal.tsx @@ -1,27 +1,148 @@ import Image from "next/image"; -import React from "react"; -import search from "../../../public/assets/images/Search.svg" +import React, { useEffect, useState } from "react"; +import search from "../../../public/assets/images/Search.svg"; import { IoMdClose } from "react-icons/io"; +import { useUI } from "../context/ui.context"; +import http from "@/api/http"; // Assuming you have an http utility for API calls +import Audio from "../../../public/assets/images/Icon ionic-md-musical-notes.svg"; +import NoData from "../../../public/assets/images/NoDataFound.svg"; +import { useRouter } from "next/router"; +import DOMPurify from "dompurify"; // Install with `npm install dompurify` + const SearchModal: React.FC = () => { - console.log("fdasdfasfa"); + const { closeModal } = useUI(); + const [value, setValue] = useState(""); + const [results, setResults] = useState([]); + const [counts, setCounts] = useState(null); + const [noData, setNoData] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const router = useRouter(); + + const openDua = (dua: Dua) => { + const lastReadDuas = JSON.parse(localStorage.getItem("last-read") || "[]"); + const updatedDuas = [...lastReadDuas, dua]; + localStorage.setItem("last-read", JSON.stringify(updatedDuas)); + const slug = dua.title.toLowerCase().replaceAll(" ", "-"); + router.push(`/duas/${slug}-${dua.id}`); + closeModal(); + }; + + useEffect(() => { + setCounts(null); + setResults([]); + setNoData(false); + // If the input is empty, reset results and do not send a request + if (value.trim() === "") { + setResults([]); + setError(null); + return; + } + + // Set loading state + setIsLoading(true); + setError(null); + // Implement debounce: wait for 500ms after the user stops typing + const debounceTimer = setTimeout(() => { + // Perform the search API call with the actual user input + http + .get(`web/mafatih-duas/?search=${encodeURIComponent(value)}`) + .then((res) => { + setResults(res.data.results); + setIsLoading(false); + setCounts(res.data.count); + if (res.data.count === 0) { + setNoData(true); + } + }) + .catch((err) => { + console.error("Error fetching search results:", err); + setError("Failed to fetch search results."); + setIsLoading(false); + }); + }, 500); // 500ms debounce duration + + // Cleanup function to cancel the timeout if the value changes before 500ms + return () => clearTimeout(debounceTimer); + }, [value]); return ( -
-
-
- -

Search

-
+

Search

+
-
-
+
+ {!!counts && ( +

{counts} Dua(s) found

+ )} + {results.map((item, index) => { + // Function to highlight matching words + const highlightText = (text: string, query: string) => { + if (!query.trim()) return text; + + const sanitizedQuery = DOMPurify.sanitize(query); + const regex = new RegExp(`(${sanitizedQuery})`, "gi"); + return text.replace( + regex, + `$1` + ); + }; + + return ( +
openDua(item)} + > +
+

+

+ {item?.not_synced && ( +
+ audio available +
+ )} +
+ ); + })} + {noData &&
+ No data found +

Nothing Found!

+
} +
); }; diff --git a/src/components/ui/setting.tsx b/src/components/modals/setting.tsx similarity index 98% rename from src/components/ui/setting.tsx rename to src/components/modals/setting.tsx index 515e17d..13eb09e 100644 --- a/src/components/ui/setting.tsx +++ b/src/components/modals/setting.tsx @@ -3,8 +3,8 @@ import React, { useEffect, useRef } from "react"; import { useUI } from "../context/ui.context"; import Close from "../../../public/assets/images/Group 1000005170.svg"; import Image from "next/image"; -import CheckBox from "./check-box"; -import RangeInput from "./range-input"; +import CheckBox from "../ui/check-box"; +import RangeInput from "../ui/range-input"; import { useFontSettingsContext } from "../context/font-setting-context"; interface ModalProps { diff --git a/src/components/ui/download-app.tsx b/src/components/ui/download-app.tsx index 6b1d8de..30df48f 100644 --- a/src/components/ui/download-app.tsx +++ b/src/components/ui/download-app.tsx @@ -6,7 +6,7 @@ import { useUI } from "../context/ui.context"; const DownloadApp: React.FC = () => { const { closeDownload, displayDownload } = useUI(); - + console.log(displayDownload); return ( @@ -32,7 +32,7 @@ const DownloadApp: React.FC = () => {

Download

-
+
); }; diff --git a/src/components/ui/range-input.tsx b/src/components/ui/range-input.tsx index cbdc67b..d5dd447 100644 --- a/src/components/ui/range-input.tsx +++ b/src/components/ui/range-input.tsx @@ -1,4 +1,3 @@ -// components/ui/range-input.tsx import React from 'react'; interface RangeInputProps { @@ -22,10 +21,11 @@ const RangeInput: React.FC = ({ value, onChange, disabled = fal value={value} onChange={handleChange} disabled={disabled} - className={`w-28 h-[2px] ${disabled ? 'bg-gray-400' : 'bg-[#F4846F]'} outline-none transition-opacity duration-150 ease-in-out mr-3`} + className={`w-28 h-[2px] outline-none transition-opacity duration-150 ease-in-out mr-3`} style={{ appearance: 'none', opacity: disabled ? 0.5 : 1, + background: `linear-gradient(to right, #F4846F ${(value - 80) / 1.2}%, #EBEBEB ${(value - 80) / 1.2}%)`, }} /> {value}% diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 4afe064..c1aaaa9 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -2,10 +2,10 @@ import { FontSettingsProvider } from "@/components/context/font-setting-context" import { UIProvider } from "@/components/context/ui.context"; import Header from "@/components/layout/header"; 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 FooterSticky from "@/components/ui/footer-sticky"; import "@/styles/globals.css"; import type { AppProps } from "next/app"; @@ -24,6 +24,8 @@ export default function App({ Component, pageProps }: AppProps) {
+ +
diff --git a/src/pages/duas/[slug].tsx b/src/pages/duas/[slug].tsx index cc5f16e..d6178fa 100644 --- a/src/pages/duas/[slug].tsx +++ b/src/pages/duas/[slug].tsx @@ -10,7 +10,7 @@ import colorizeVowels from "@/components/utils/colorize-vowels"; import { FaArrowLeft } from "react-icons/fa6"; import { useRouter } from "next/router"; import { DuaComponentProps } from "@/components/utils/types"; -import SettingModal from "@/components/ui/setting"; +import SettingModal from "@/components/modals/setting"; import { useUI } from "@/components/context/ui.context"; import { useFontSettingsContext } from "@/components/context/font-setting-context"; diff --git a/src/utils/motion/fade-in-bottom.ts b/src/utils/motion/fade-in-bottom.ts new file mode 100644 index 0000000..8388a5b --- /dev/null +++ b/src/utils/motion/fade-in-bottom.ts @@ -0,0 +1,26 @@ +export function fadeInBottom(duration: number = 0.5) { + return { + hidden: { + y: -50, // Start above the viewport + transition: { + type: "easeInOut", + duration: duration, + }, + }, + visible: { + y: 0, // Center position + transition: { + type: "easeInOut", + duration: duration, + }, + }, + exit: { + y: 50, // Exit below the viewport + transition: { + type: "easeInOut", + duration: duration, + }, + }, + }; + } + \ No newline at end of file