Browse Source

feat: add mobile header components and update modal imports

fix : search duas completed
master
sina_sajjadi 4 weeks ago
parent
commit
3de35e03b2
  1. 15
      package-lock.json
  2. 1
      package.json
  3. 5
      public/assets/images/NoDataFound.svg
  4. 11
      src/components/layout/mobile-header.tsx
  5. 46
      src/components/layout/mobile-navigation.tsx
  6. 19
      src/components/mobile-header/hamburger.tsx
  7. 0
      src/components/mobile-header/mobile-search.tsx
  8. 0
      src/components/mobile-header/mobile-setting.tsx
  9. 2
      src/components/modals/modal-manager.tsx
  10. 3
      src/components/modals/modal.tsx
  11. 145
      src/components/modals/search-modal.tsx
  12. 4
      src/components/modals/setting.tsx
  13. 2
      src/components/ui/download-app.tsx
  14. 4
      src/components/ui/range-input.tsx
  15. 4
      src/pages/_app.tsx
  16. 2
      src/pages/duas/[slug].tsx
  17. 26
      src/utils/motion/fade-in-bottom.ts

15
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",

1
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",

5
public/assets/images/NoDataFound.svg

@ -0,0 +1,5 @@
<svg width="98" height="99" viewBox="0 0 98 99" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M45.0816 71.2798C30.9696 71.2798 19.6016 59.7958 19.6016 45.5398C19.6016 31.2838 30.9696 19.7998 45.0816 19.7998C59.1936 19.7998 70.5616 31.2838 70.5616 45.5398C70.5616 59.7958 59.1936 71.2798 45.0816 71.2798ZM45.0816 23.7598C33.1256 23.7598 23.5216 33.4618 23.5216 45.5398C23.5216 57.6178 33.1256 67.3198 45.0816 67.3198C57.0376 67.3198 66.6416 57.6178 66.6416 45.5398C66.6416 33.4618 57.0376 23.7598 45.0816 23.7598Z" fill="#8B8B8B" stroke="#8B8B8B" stroke-width="0.5"/>
<path d="M64.0605 61.9087L81.6613 79.6891L78.8899 82.4888L61.2891 64.7084L64.0605 61.9087Z" fill="#8B8B8B" stroke="#8B8B8B" stroke-width="0.5"/>
<path d="M77.7678 29.8828L79.5355 28.115L77.7678 26.3472L75.6528 24.2322L73.885 22.4645L72.1172 24.2322L68.5 27.8495L64.8828 24.2322L63.115 22.4645L61.3472 24.2322L59.2322 26.3472L57.4645 28.115L59.2322 29.8828L62.8495 33.5L59.2322 37.1172L57.4645 38.885L59.2322 40.6528L61.3472 42.7678L63.115 44.5355L64.8828 42.7678L68.5 39.1505L72.1172 42.7678L73.885 44.5355L75.6528 42.7678L77.7678 40.6528L79.5355 38.885L77.7678 37.1172L74.1505 33.5L77.7678 29.8828Z" fill="#8B8B8B" stroke="#F5F5F5" stroke-width="5"/>
</svg>

11
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<MobileHeaderProps> = () => {
const router = useRouter();
@ -23,9 +23,8 @@ const MobileHeader: React.FC<MobileHeaderProps> = () => {
</div>
<div className="p-4 flex justify-between items-center">
<div className="flex">
<button className="p-1 bg-white rounded-[15px]">
<Image width={30} src={Hamburegure} alt="Hamburger" />
</button>
<HamburgerButton/>
<div className="w-[38px]" />
</div>

46
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 (
<AnimatePresence>
{sidebarDisplay && (
<motion.div
className="fixed inset-0 z-50 flex justify-end"
initial="hidden"
animate="visible"
exit="exit"
variants={sidebarVariants}
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
>
<div className="w-64 bg-white h-full shadow-lg p-4">
<button onClick={closeSidebar} className="text-gray-600">
Close
</button>
{/* Add your sidebar content here */}
</div>
</motion.div>
)}
</AnimatePresence>
);
};
export default MobileNavigation;

19
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 (
<>
<button onClick={openSidebar} className="p-1 bg-white rounded-[15px] z-50">
<Image width={30} src={Hamburegure} alt="Hamburger" />
</button>
</>
);
};
export default HamburgerButton;

0
src/components/ui/mobile-search.tsx → src/components/mobile-header/mobile-search.tsx

0
src/components/ui/mobile-setting.tsx → src/components/mobile-header/mobile-setting.tsx

2
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(

3
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) {

145
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<string | null>(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 (
<div className="bg-white w-full h-full p-6">
<div className="bg-[#F5F5F5]">
<div className="flex justify-between">
<button className="p-3 rounded-2xl bg-white">
<div className="bg-[#F5F5F5] w-full h-full overflow-y-auto">
<div className="flex flex-col bg-gray-200 p-6 gap-6">
<div className="flex justify-between items-center">
<button
onClick={() => {
closeModal();
}}
className="p-3 rounded-2xl bg-white"
>
<IoMdClose size={18} />
</button>
<h3>Search</h3>
<div />
<h3 className="text-base font-bold">Search</h3>
<div className="w-[43px]" />
</div>
<div className="flex w-full h-12 p-2 bg-white rounded-2xl">
<label htmlFor="search-input" className="flex items-center w-full h-full">
<div className="flex w-full h-12 p-2 bg-white rounded-2xl focus-within:outline outline-[1px]">
<label
htmlFor="search-input"
className="flex items-center w-full h-full"
>
<Image width={24} height={24} src={search} alt="Search" />
<input id="search-input" className="w-full h-full ml-2" type="text" placeholder="Search..." />
<input
value={value}
onChange={(e) => {
setValue(e.target.value);
}}
id="search-input"
className="w-full h-full ml-2 text-xs font-normal focus:outline-none"
type="text"
placeholder="Type a title or keyword to search"
/>
</label>
</div>
</div>
<div className="p-4">
{!!counts && (
<p className="text-xs font-normal">{counts} Dua(s) found</p>
)}
{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,
`<span style="color: #F4846F;">$1</span>`
);
};
return (
<div
className="flex justify-between p-3 bg-white my-4 rounded-2xl cursor-pointer"
key={index}
onClick={() => openDua(item)}
>
<div className="flex items-center gap-2">
<p
dangerouslySetInnerHTML={{
__html: highlightText(item?.title || "", value),
}}
/>
</div>
{item?.not_synced && (
<div className="flex items-center p-3 bg-[#EBEBEB] rounded-lg">
<Image src={Audio} alt="audio available" />
</div>
)}
</div>
);
})}
{noData && <div className="flex flex-col text-[#4D4D4D] absolute top-1/2 left-[42%] items-center">
<Image src={NoData} alt="No data found"/>
<p className="text-sm font-semibold">Nothing Found!</p>
</div>}
</div>
</div>
);
};

4
src/components/ui/setting.tsx → 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 {

2
src/components/ui/download-app.tsx

@ -32,7 +32,7 @@ const DownloadApp: React.FC = () => {
<p className="text-white font-Calibri">Download</p>
</button>
</div>
<div className="hidden lg:block"/>
<div className="hidden sm:block"/>
</div>
);
};

4
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<RangeInputProps> = ({ 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}%)`,
}}
/>
<span className="text-[#7D8394] text-xs font-normal w-8">{value}%</span>

4
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) {
</main>
</div>
<DownloadApp />
<MobileNavigation/>
</div>
<ManagedModal />
</UIProvider>

2
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";

26
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,
},
},
};
}
Loading…
Cancel
Save