diff --git a/next-sitemap.config.js b/next-sitemap.config.js new file mode 100644 index 0000000..5f2b3ce --- /dev/null +++ b/next-sitemap.config.js @@ -0,0 +1,12 @@ +const config = { + siteUrl: 'https://www.example.com', + generateRobotsTxt: true, + robotsTxtOptions: { + policies: [ + { userAgent: '*', allow: '/', disallow: ['/api/', '/admin/', '/private/'] }, + ], + }, + }; + + module.exports = config; + \ No newline at end of file diff --git a/next.config.ts b/next.config.ts index 030322f..393a2fb 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,7 +1,7 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { - reactStrictMode: true, + reactStrictMode: false, images: { domains: ["habibapp.com"], // Add the domain for image hosting }, diff --git a/package-lock.json b/package-lock.json index 9434862..f610776 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "next": "15.1.0", "next-i18next": "^15.4.1", "next-seo": "^6.6.0", + "next-sitemap": "^4.2.3", "react": "18.2.0", "react-content-loader": "^7.0.2", "react-dom": "18.2.0", @@ -64,6 +65,11 @@ "node": ">=6.9.0" } }, + "node_modules/@corex/deepmerge": { + "version": "4.0.43", + "resolved": "https://registry.npmjs.org/@corex/deepmerge/-/deepmerge-4.0.43.tgz", + "integrity": "sha512-N8uEMrMPL0cu/bdboEWpQYb/0i2K5Qn8eCsxzOmxSggJbbQte7ljMRoXm917AbntqTGOzdTu+vP3KOOzoC70HQ==" + }, "node_modules/@emnapi/runtime": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.3.1.tgz", @@ -797,7 +803,6 @@ "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" @@ -810,7 +815,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, "engines": { "node": ">= 8" } @@ -819,7 +823,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" @@ -1543,7 +1546,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, "dependencies": { "fill-range": "^7.1.1" }, @@ -2629,7 +2631,6 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", - "dev": true, "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -2645,7 +2646,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, "dependencies": { "is-glob": "^4.0.1" }, @@ -2669,7 +2669,6 @@ "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", - "dev": true, "dependencies": { "reusify": "^1.0.4" } @@ -2690,7 +2689,6 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, "dependencies": { "to-regex-range": "^5.0.1" }, @@ -3360,7 +3358,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -3408,7 +3405,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, "dependencies": { "is-extglob": "^2.1.1" }, @@ -3444,7 +3440,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, "engines": { "node": ">=0.12.0" } @@ -3822,7 +3817,6 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, "engines": { "node": ">= 8" } @@ -3831,7 +3825,6 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" @@ -3875,7 +3868,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -4052,6 +4044,37 @@ "react-dom": ">=16.0.0" } }, + "node_modules/next-sitemap": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/next-sitemap/-/next-sitemap-4.2.3.tgz", + "integrity": "sha512-vjdCxeDuWDzldhCnyFCQipw5bfpl4HmZA7uoo3GAaYGjGgfL4Cxb1CiztPuWGmS+auYs7/8OekRS8C2cjdAsjQ==", + "funding": [ + { + "url": "https://github.com/iamvishnusankar/next-sitemap.git" + } + ], + "dependencies": { + "@corex/deepmerge": "^4.0.43", + "@next/env": "^13.4.3", + "fast-glob": "^3.2.12", + "minimist": "^1.2.8" + }, + "bin": { + "next-sitemap": "bin/next-sitemap.mjs", + "next-sitemap-cjs": "bin/next-sitemap.cjs" + }, + "engines": { + "node": ">=14.18" + }, + "peerDependencies": { + "next": "*" + } + }, + "node_modules/next-sitemap/node_modules/@next/env": { + "version": "13.5.8", + "resolved": "https://registry.npmjs.org/@next/env/-/env-13.5.8.tgz", + "integrity": "sha512-YmiG58BqyZ2FjrF2+5uZExL2BrLr8RTQzLXNDJ8pJr0O+rPlOeDPXp1p1/4OrR3avDidzZo3D8QO2cuDv1KCkw==" + }, "node_modules/next/node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -4322,7 +4345,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, "engines": { "node": ">=8.6" }, @@ -4538,7 +4560,6 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, "funding": [ { "type": "github", @@ -4732,7 +4753,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" @@ -4742,7 +4762,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, "funding": [ { "type": "github", @@ -5414,7 +5433,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, "dependencies": { "is-number": "^7.0.0" }, diff --git a/package.json b/package.json index 5d348dd..0366eb2 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "next": "15.1.0", "next-i18next": "^15.4.1", "next-seo": "^6.6.0", + "next-sitemap": "^4.2.3", "react": "18.2.0", "react-content-loader": "^7.0.2", "react-dom": "18.2.0", diff --git a/public/assets/images/Frame 1116606661.svg b/public/assets/images/Frame 1116606661.svg new file mode 100644 index 0000000..d89b331 --- /dev/null +++ b/public/assets/images/Frame 1116606661.svg @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 282162f..0609db6 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -1,38 +1,44 @@ { - "home": "Home", - "about": "About us", - "donate": "Donate", - "search_placeholder": "Type a title or keyword to search", - "loading": "Loading...", - "error_message": "Failed to fetch search results.", - "close": "Close", - "habib_app": "Habib App", - "better_experience": "For Better Experience", - "download": "Download", - "timeline": "Time Line", - "settings": "Settings", - "categories": "Categories", - "famous": "Famous", - "near_by": "Near by", - "today": "Today", - "todayDhikr": "Today’s Dhikr", - "audio_settings": "Audio Settings", - "reciters": "Reciters", - "select_reciter": "Select Reciter", - "increase_speed": "Increase speed", - "speed": "Speed", - "choose_language": "Choose Language", - "loading_languages": "Loading languages...", - "failed_to_load_languages": "Failed to load languages. Please try again.", - "no_languages_available": "No languages available.", - "failed_to_load_reciters": "Failed to load reciters. Please try again.", - "search": "Search", - "nothing_found": "Nothing Found!", - "dua_s_found": "Dua(s) found", - "audio_available": "Audio Available", - "no_data_found": "No Data Found", - "arabic": "Arabic", - "translation": "Translation", - "transliteration": "Transliteration" - } - \ No newline at end of file + "home": "Home", + "about": "About us", + "donate": "Donate", + "search_placeholder": "Type a title or keyword to search", + "loading": "Loading...", + "error_message": "Failed to fetch search results.", + "close": "Close", + "habib_app": "Habib App", + "better_experience": "For Better Experience", + "download": "Download", + "timeline": "Time Line", + "settings": "Settings", + "categories": "Categories", + "famous": "Famous", + "near_by": "Near by", + "today": "Today", + "todayDhikr": "Today’s Dhikr", + "audio_settings": "Audio Settings", + "reciters": "Reciters", + "select_reciter": "Select Reciter", + "increase_speed": "Increase speed", + "speed": "Speed", + "choose_language": "Choose Language", + "loading_languages": "Loading languages...", + "failed_to_load_languages": "Failed to load languages. Please try again.", + "no_languages_available": "No languages available.", + "failed_to_load_reciters": "Failed to load reciters. Please try again.", + "search": "Search", + "nothing_found": "Nothing Found!", + "dua_s_found": "Dua(s) found", + "audio_available": "Audio Available", + "no_data_found": "No Data Found", + "arabic": "Arabic", + "translation": "Translation", + "transliteration": "Transliteration", + "support_us_to_grow": "Support us to Grow!", + "donation_message": "With God’s grace and your support, we’ve launched this app to promote faith. To grow further and reach more people, we need your financial help. Every contribution, no matter how small, will make a difference.", + "one_time": "One time", + "monthly": "Monthly", + "enter_desired_amount": "Enter the desired amount...", + "donate_now": "Donate Now", + "credits": "Credits" +} diff --git a/src/components/common/default-seo.tsx b/src/components/common/default-seo.tsx index 0e19397..721c561 100644 --- a/src/components/common/default-seo.tsx +++ b/src/components/common/default-seo.tsx @@ -1,33 +1,81 @@ import { DefaultSeo as NextDefaultSeo } from "next-seo"; interface DefaultSeoProps { + type?: "site" | "dua"; // Determines the type of SEO title?: string; description?: string; keywords?: string; + url?: string; // For specific pages like a Dua page + text?: string; // For the Arabic text of the Dua + translation?: string; // For the English translation of the Dua } const DefaultSeo: React.FC = ({ + type = "site", title = "Dua Site", description = "A comprehensive collection of Duas", keywords = "dua, islam, prayer, supplication", + url = "https://duasapp.com", + text, + translation, }) => { + // Define structured data dynamically based on the type + const structuredData = + type === "site" + ? { + "@context": "https://schema.org/", + "@type": "WebSite", + "name": title, + "url": url, + "description": description, + "publisher": { + "@type": "Organization", + "name": "DuasApp", + "url": url, + "logo": { + "@type": "ImageObject", + "url": "/assets/images/Hosseiniye.svg", + }, + }, + "potentialAction": { + "@type": "SearchAction", + "target": `${url}/?search={search_term_string}`, + "query-input": "required name=search_term_string", + }, + "inLanguage": "en", + } + : { + "@context": "https://schema.org/", + "@type": "CreativeWork", + "name": title, + "url": url, + "description": description, + "text": text, + "translation": { + "@type": "CreativeWork", + "name": "Translation", + "text": translation, + }, + "keywords": keywords, + }; + return ( = ({ content: "width=device-width, initial-scale=1", }, { - charSet: "utf-8", + charset: "utf-8", }, { name: "apple-mobile-web-app-capable", @@ -52,6 +100,12 @@ const DefaultSeo: React.FC = ({ content: "#ffffff", }, ]} + additionalScripts={[ + { + type: "application/ld+json", + innerHTML: JSON.stringify(structuredData), + }, + ]} /> ); }; diff --git a/src/components/context/search-context.tsx b/src/components/context/search-context.tsx new file mode 100644 index 0000000..3632bb7 --- /dev/null +++ b/src/components/context/search-context.tsx @@ -0,0 +1,26 @@ +import React, { createContext, useContext, useState, ReactNode } from 'react'; + +interface SearchContextProps { + searchTerm: string; + setSearchTerm: (term: string) => void; +} + +const SearchContext = createContext(undefined); + +export const SearchProvider: React.FC<{ children: ReactNode }> = ({ children }) => { + const [searchTerm, setSearchTerm] = useState(''); + + return ( + + {children} + + ); +}; + +export const useSearch = (): SearchContextProps => { + const context = useContext(SearchContext); + if (!context) { + throw new Error('useSearch must be used within a SearchProvider'); + } + return context; +}; \ No newline at end of file diff --git a/src/components/layout/header.tsx b/src/components/layout/header.tsx index 51ef139..243817e 100644 --- a/src/components/layout/header.tsx +++ b/src/components/layout/header.tsx @@ -6,42 +6,55 @@ import Logo from "../ui/logo"; import { useTranslation } from "next-i18next"; const Header = () => { - const { displayDownload } = useUI(); - const {t} = useTranslation("common") + const { displayDownload, openModal } = useUI(); + const { t } = useTranslation("common"); + return ( ); }; + export default Header; diff --git a/src/components/modals/donate-modal.tsx b/src/components/modals/donate-modal.tsx new file mode 100644 index 0000000..3cfbad9 --- /dev/null +++ b/src/components/modals/donate-modal.tsx @@ -0,0 +1,123 @@ +import React, { useState, MouseEvent } from "react"; +import { IoClose } from "react-icons/io5"; +import credits from "../.././../public/assets/images/Frame 1116606661.svg"; +import Image from "next/image"; +import { useUI } from "../context/ui.context"; +import { useTranslation } from "next-i18next"; // Importing translation hook + +const DonateModal: React.FC = () => { + const { t } = useTranslation("common"); // Initialize translation hook + const { closeModal } = useUI(); + const [selected, setSelected] = useState("one-time"); // 'one-time' or 'monthly' + const [selectedPrice, setSelectedPrice] = useState(null); + + // Define the prices for each type + const oneTimePrices = [5, 15, 30, 50]; + const monthlyPrices = [5, 10, 20, 30]; + + // Determine which price list to display based on the selected type + const prices = selected === "monthly" ? monthlyPrices : oneTimePrices; + + // Closes modal if backdrop is clicked + const handleBackdropClick = (event: MouseEvent) => { + // Ensure we're clicking directly on the backdrop (not a child element) + if (event.target === event.currentTarget) { + closeModal(); + } + }; + + return ( +
+
e.stopPropagation()} + > +
+ +
+ {t("support_us_to_grow")} +
+
+
+

+ {t("donation_message")} +

+
+
+
+ + +
+
+ +
+
+ {prices.map((price) => ( + + ))} +
+ + +
+ + +
+
+ ); +}; + +export default DonateModal; diff --git a/src/components/modals/modal-manager.tsx b/src/components/modals/modal-manager.tsx index ed352c9..f3a3fc7 100644 --- a/src/components/modals/modal-manager.tsx +++ b/src/components/modals/modal-manager.tsx @@ -4,6 +4,7 @@ import dynamic from "next/dynamic"; import RecitersModal from "./reciters"; import AudioSetting from "./audio-setting"; import LanguageModal from "./languages-modal"; +import DonateModal from "./donate-modal"; // import Newsletter from "../newsletter"; const SettingModal = dynamic(() => import("@/components/modals/setting")); const SearchModal = dynamic(() => import("@/components/modals/search-modal")); @@ -24,11 +25,8 @@ const ManagedModal: React.FC = () => { {modalView === "SEARCH_VIEW" && } {modalView === "AUDIO_SETTING_VIEW" && } {modalView === "LANGUAGES_VIEW" && } - {/* {modalView === "SIGN_UP_VIEW" && } - {modalView === "FORGET_PASSWORD" && } - {modalView === "PRODUCT_VIEW" && } - {modalView === "NEWSLETTER_VIEW" && } - {modalView === "CHAT_VIEW" && } */} + {modalView === "DONATE_VIEW" && } + ); }; diff --git a/src/components/sidebar/search.tsx b/src/components/sidebar/search.tsx new file mode 100644 index 0000000..d5c2e15 --- /dev/null +++ b/src/components/sidebar/search.tsx @@ -0,0 +1,114 @@ +"use client"; + +import http from "@/api/http"; +import Image from "next/image"; +import NoData from "../../../public/assets/images/Frame 1000005074.webp"; +import Audio from "../../../public/assets/images/Icon ionic-md-musical-notes.svg"; +import { useEffect, useState } from "react"; +import { useSearchParams, useRouter } from "next/navigation"; +import { Dua } from "../utils/types"; +import { useTranslation } from "next-i18next"; // Importing the translation hook +import ShimmerLoader from "../ui/list-loading"; +import DOMPurify from "dompurify"; // Sanitize input to avoid XSS attacks + +const Search: React.FC = () => { + const { t } = useTranslation("common"); // Initialize translation hook for "common" namespace + const [data, setData] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); // State to hold error messages + const router = useRouter(); + const searchParams = useSearchParams(); // Use to retrieve query parameters + + 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}`); + }; + + useEffect(() => { + const searchParam = searchParams?.get("search") || ""; // Extract search query from URL + if (!searchParam) return; + + setLoading(true); + setError(null); // Reset error state before starting the request + + const debounceTimer = setTimeout(() => { + // Perform the search API call with the actual user input + http + .get(`web/mafatih-duas/?search=${encodeURIComponent(searchParam)}`) + .then((res) => { + setData(res.data.results); + }) + .catch((err) => { + console.error("Error fetching search results:", err); + setError(t("error_message")); // Use translation for error message + }) + .finally(() => { + setLoading(false); // Always stop loading here + }); + }, 500); + + return () => clearTimeout(debounceTimer); + }, [searchParams?.get("search") , t]); + + // 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` + ); + }; + + if (loading) { + return ; // Translating the loading message + } + + if (error) { + return

{error}

; // Displaying the error message + } + + return ( +
+

+ {t("dua_s_found", { count: data.length })} +

+ {data.length ? ( + data.map((dua: Dua) => { + const searchQuery = searchParams?.get("search") || ""; // Get current search query + return ( +
openDua(dua)} + > +
+

+

+ {dua.not_synced && ( +
+ {t("audio_available")} +
+ )} +
+ ); + }) + ) : ( +
+ {t("no_data_found")} +
+ )} +
+ ); +}; + +export default Search; diff --git a/src/components/sidebar/tabs.tsx b/src/components/sidebar/tabs.tsx index 2079d4b..1193236 100644 --- a/src/components/sidebar/tabs.tsx +++ b/src/components/sidebar/tabs.tsx @@ -1,13 +1,14 @@ import { useState } from "react"; -import List from "./list"; import { FaArrowLeft } from "react-icons/fa6"; -import { DataState, Path } from "@/components/utils/types"; // Import types from types.ts +import { DataState, Path } from "@/components/utils/types"; // Import types from types.ts import { useTranslation } from "next-i18next"; // Importing useTranslation -import { tap } from "node:test/reporters"; import Categories from "./categories"; import Famous from "./famous"; import Today from "./today"; import NearBy from "./nearby"; +import Search from "./search"; +import { useSearchParams } from "next/navigation"; +import Link from "next/link"; const tabs = [ { name: "Categories" }, @@ -21,109 +22,116 @@ const Tabs = () => { const [selected, setSelected] = useState({ name: "Today" }); const [data, setData] = useState({ type: null, data: [] }); const [path, setPath] = useState([]); + const searchParams = useSearchParams(); // Use to retrieve query parameters + // const router = useRouter(); + + const searchQuery = searchParams?.get("search") || ""; // Extract `search` query param const back = () => { + if (path.length <= 1) return; // Avoid out-of-bounds access setPath((prevPath) => { - const newPath = [...prevPath]; // Create a shallow copy of the array - newPath.pop(); // Remove the last item from the copy - return newPath; // Return the updated array to setPath + const newPath = [...prevPath]; + newPath.pop(); + return newPath; }); - - setData(path[path.length - 2]); + setData(path[path.length - 2] || { type: null, data: [] }); // Safe access }; const handlePathClick = (index: number) => { setPath((prev) => { - const newPath = [...prev]; // Create a shallow copy of the array + const newPath = [...prev]; newPath.splice(index + 1); // Remove elements starting from index + 1 - - return newPath; // Return the updated array + return newPath; }); - // Optionally, you can update the data here as well: setData({ type: path[index].type, data: path[index].data }); }; return ( <> - {path.length > 1 && ( + {path.length > 1 && !searchQuery.trim() && (
-
+
-

{path[1]?.name}

+ +

{path[1]?.name}

{/* Heading for current tab */}
{path.length > 2 && path.map((item, index) => { if (item.name && index + 1 < path.length && index > 0) { return ( - <> - handlePathClick(index)} - > - {item.name} - + + handlePathClick(index)}> + + {item.name} + + - + ); } else { return ( - handlePathClick(index)} - > - {item.name} - + handlePathClick(index)}> + {item.name} + ); } })}
)} - {path.length < 2 && ( + {path.length < 2 && !searchQuery.trim() && (
- {/* Render the path names */} - +

Navigation Tabs

{/* Hidden heading for screen readers */}
    {tabs.map((tab) => (
  • { - setSelected(tab); - }} + onClick={() => setSelected(tab)} > -

    {t(tab.name.toLowerCase())}

    {/* Translate tab name */} +

    {t(tab.name.toLowerCase())}

  • ))}
)} - { - selected.name === "Categories" && - } - { - selected.name === "Famous" && - } - { - selected.name === "near_by" && - } - { - selected.name === "Today" && - } - {/* */} + {selected.name === "Categories" && !searchQuery.trim() && ( + <> +

{t("categories")}

+ + + )} + {selected.name === "Famous" && !searchQuery.trim() && ( + <> +

{t("famous")}

+ + + )} + {selected.name === "near_by" && !searchQuery.trim() && ( + <> +

{t("near_by")}

+ + + )} + {selected.name === "Today" && !searchQuery.trim() && ( + <> +

{t("today")}

+ + + )} + {searchQuery.trim() && ( + <> + + + )} ); }; diff --git a/src/components/ui/search-duas.tsx b/src/components/ui/search-duas.tsx index 4cb4bd7..221c13f 100644 --- a/src/components/ui/search-duas.tsx +++ b/src/components/ui/search-duas.tsx @@ -1,141 +1,76 @@ -import React, { useEffect, useState, ChangeEvent, useRef } from "react"; -import http from "@/api/http"; -import { HiMiniMagnifyingGlass } from "react-icons/hi2"; +import React, { useState, ChangeEvent, useEffect, KeyboardEvent } from "react"; import { useRouter } from "next/router"; -import { useTranslation } from "next-i18next"; // Import useTranslation hook - -interface Dua { - id: number; - title: string; - // Add other relevant fields based on your API response -} +import { HiMiniMagnifyingGlass } from "react-icons/hi2"; +import { useTranslation } from "next-i18next"; const SearchDuas: React.FC = () => { - const { t } = useTranslation('common'); // Initialize translation hook with the 'common' namespace - const router = useRouter(); + const router = useRouter(); // Initialize the Next.js router + const { t } = useTranslation("common"); // Translation hook const [value, setValue] = useState(""); - const [results, setResults] = useState([]); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - const [show, setShow] = useState(false); - - // Ref for the main container - const containerRef = useRef(null); - - const openDua = (dua: Dua) => { - setShow(false); - 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}`); - }; + // On mount (or whenever router.query.search changes), set the input value if there's a 'search' query param useEffect(() => { - // If the input is empty, reset results and do not send a request - if (value.trim() === "") { - setResults([]); - setError(null); - setShow(false); - return; + const param = router.query.search; + if (typeof param === "string") { + setValue(param); } - - // Set loading state - setIsLoading(true); - setError(null); - setShow(true); - // 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); - // Optionally, you can handle the response here - }) - .catch((err) => { - console.error("Error fetching search results:", err); - setError(t("error_message")); // Use translation for error message - setIsLoading(false); + }, [router.query.search]); + + + useEffect(()=>{ + // If value is empty, remove any existing search query param. + // Otherwise, set the search query param to the current value. + if (!value.trim()) { + // Remove search param + router.push(router.pathname); + } else { + // Append/update search param + router.push({ + pathname: router.pathname, + query: { ...router.query, search: value.trim() }, }); - }, 500); // 500ms debounce duration - - // Cleanup function to cancel the timeout if the value changes before 500ms - return () => clearTimeout(debounceTimer); - }, [value, t]); // Add t to the dependency array - - // Handle input changes + } + } , [router, value]) + // Update the input value when the user types const handleChange = (e: ChangeEvent) => { setValue(e.target.value); }; - useEffect(() => { - // Function to handle clicks outside the component - const handleClickOutside = (event: MouseEvent) => { - if ( - containerRef.current && - !containerRef.current.contains(event.target as Node) - ) { - setShow(false); + // Handle Enter key press + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === "Enter") { + // If value is empty, remove any existing search query param. + // Otherwise, set the search query param to the current value. + if (!value.trim()) { + // Remove search param + router.push(router.pathname); + } else { + // Append/update search param + router.push({ + pathname: router.pathname, + query: { ...router.query, search: value.trim() }, + }); } - }; - - // Bind the event listener - document.addEventListener("mousedown", handleClickOutside); - - return () => { - // Unbind the event listener on cleanup - document.removeEventListener("mousedown", handleClickOutside); - }; - }, []); + } + }; return ( -
+
{ - setShow(true); - }} + aria-label="Search Term" />
- - {/* Loading Indicator */} - {isLoading && show && ( -
- {t("loading")} {/* Use translation for loading text */} -
- )} - - {/* Error Message */} - {error && show && ( -
{error}
- )} - - {/* Search Results */} - {results.length > 0 && show && ( -
    - {results.map((dua) => ( -
  • openDua(dua)} - key={dua.id} - className="px-4 py-2 hover:bg-gray-100 cursor-pointer" - > - {dua.title} -
  • - ))} -
- )}
); }; diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 2f8233d..7f2d42d 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -1,6 +1,7 @@ import DefaultSeo from "@/components/common/default-seo"; import { AudioProvider } from "@/components/context/audio-conext"; import { FontSettingsProvider } from "@/components/context/font-setting-context"; +import { SearchProvider } from "@/components/context/search-context"; import { UIProvider } from "@/components/context/ui.context"; import Header from "@/components/layout/header"; import MobileHeader from "@/components/layout/mobile-header"; @@ -23,24 +24,26 @@ function App({ Component, pageProps }: AppProps) { - -
- {" "} - {/* Apply rtl class if the language is RTL */} -
- -
-
- -
- -
+ + +
+ {" "} + {/* Apply rtl class if the language is RTL */} +
+ +
+
+ +
+ +
+
+ +
- - +
- -
+ diff --git a/src/pages/duas/[slug].tsx b/src/pages/duas/[slug].tsx index b27d615..becb6b8 100644 --- a/src/pages/duas/[slug].tsx +++ b/src/pages/duas/[slug].tsx @@ -96,8 +96,9 @@ const DuaComponent: React.FC = ({ // Use the memoized fetchData in the effect hook useEffect(() => { - setDuaParts([]); // Reset Dua parts when slug changes - fetchData(false); + setDuaParts([]); + + fetchData(false); // Fetch data for the first page }, [slug]); // Only slug changes should trigger this effect // Play audio from a specific part @@ -219,6 +220,8 @@ const DuaComponent: React.FC = ({ } }, [recitingPart, duaParts]); + + const handleScroll = useCallback( (e: React.UIEvent) => { const target = e.currentTarget; @@ -286,7 +289,7 @@ const DuaComponent: React.FC = ({ padding: "1px", }} > -
+
{item.text && (