Browse Source

feat : path and audio added

master
unknown 1 month ago
parent
commit
d9dd7e0047
  1. 22
      package-lock.json
  2. 1
      package.json
  3. 11
      public/assets/images/Frame 26952.svg
  4. 3
      public/assets/images/HomeIcon.svg
  5. 6
      public/assets/images/dots-vertical-svgrepo-com.svg
  6. BIN
      public/assets/images/jamkaran.png
  7. 5
      public/assets/images/search-alt-svgrepo-com.svg
  8. 4
      public/assets/images/🦆 icon _play_.svg
  9. 2
      src/api/http.tsx
  10. 18
      src/components/layout/header.tsx
  11. 18
      src/components/layout/sidebar.tsx
  12. 84
      src/components/sidebar/list.tsx
  13. 137
      src/components/sidebar/tabs.tsx
  14. 39
      src/components/ui/footer-sticky.tsx
  15. 127
      src/components/ui/search-duas.tsx
  16. 2
      src/components/utils/colorize-vowels.tsx
  17. 48
      src/components/utils/hooks/local-storage.tsx
  18. 9
      src/pages/_app.tsx
  19. 38
      src/pages/about.tsx
  20. 215
      src/pages/duas/[slug].tsx
  21. 3
      src/pages/index.tsx

22
package-lock.json

@ -22,6 +22,7 @@
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^19", "@types/react": "^19",
"@types/react-dom": "^19", "@types/react-dom": "^19",
"@types/react-icons": "^2.2.7",
"eslint": "^9", "eslint": "^9",
"eslint-config-next": "15.1.0", "eslint-config-next": "15.1.0",
"postcss": "^8", "postcss": "^8",
@ -903,6 +904,27 @@
"@types/react": "^19.0.0" "@types/react": "^19.0.0"
} }
}, },
"node_modules/@types/react-icon-base": {
"version": "2.1.6",
"resolved": "https://registry.npmjs.org/@types/react-icon-base/-/react-icon-base-2.1.6.tgz",
"integrity": "sha512-ebbN1JjCm6RxBd3HdI1+8VCdiOI4qMjnl9DIHWJFrB/eYLF4mzIgdL34PIqCJBLY3vlwil9v6IHQvzsa8vgMsg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/react": "*"
}
},
"node_modules/@types/react-icons": {
"version": "2.2.7",
"resolved": "https://registry.npmjs.org/@types/react-icons/-/react-icons-2.2.7.tgz",
"integrity": "sha512-qxc8xtwgDG5Ub/WILU9tZa7zxz2UZqOU4yXbBa+Xg+0LbP031NB9gvf1d/ALvHLGCsCf3WEVttNoW/wc30jn1w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/react": "*",
"@types/react-icon-base": "*"
}
},
"node_modules/@typescript-eslint/eslint-plugin": { "node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.18.0", "version": "8.18.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.18.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.18.0.tgz",

1
package.json

@ -23,6 +23,7 @@
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^19", "@types/react": "^19",
"@types/react-dom": "^19", "@types/react-dom": "^19",
"@types/react-icons": "^2.2.7",
"eslint": "^9", "eslint": "^9",
"eslint-config-next": "15.1.0", "eslint-config-next": "15.1.0",
"postcss": "^8", "postcss": "^8",

11
public/assets/images/Frame 26952.svg

@ -0,0 +1,11 @@
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_5011_5546)">
<path d="M14 24.5C11.2152 24.5 8.54451 23.3938 6.57538 21.4246C4.60625 19.4555 3.5 16.7848 3.5 14C3.5 11.2152 4.60625 8.54451 6.57538 6.57538C8.54451 4.60625 11.2152 3.5 14 3.5C16.7848 3.5 19.4555 4.60625 21.4246 6.57538C23.3938 8.54451 24.5 11.2152 24.5 14C24.5 16.7848 23.3938 19.4555 21.4246 21.4246C19.4555 23.3938 16.7848 24.5 14 24.5ZM14 26C17.1826 26 20.2348 24.7357 22.4853 22.4853C24.7357 20.2348 26 17.1826 26 14C26 10.8174 24.7357 7.76516 22.4853 5.51472C20.2348 3.26428 17.1826 2 14 2C10.8174 2 7.76516 3.26428 5.51472 5.51472C3.26428 7.76516 2 10.8174 2 14C2 17.1826 3.26428 20.2348 5.51472 22.4853C7.76516 24.7357 10.8174 26 14 26Z" fill="#4D4D4D"/>
<path d="M15.3949 11.882L11.9599 12.3125L11.8369 12.8825L12.5119 13.007C12.9529 13.112 13.0399 13.271 12.9439 13.7105L11.8369 18.9125C11.5459 20.258 11.9944 20.891 13.0489 20.891C13.8664 20.891 14.8159 20.513 15.2464 19.994L15.3784 19.37C15.0784 19.634 14.6404 19.739 14.3494 19.739C13.9369 19.739 13.7869 19.4495 13.8934 18.9395L15.3949 11.882ZM15.4999 8.75C15.4999 9.14782 15.3419 9.52936 15.0606 9.81066C14.7793 10.092 14.3978 10.25 13.9999 10.25C13.6021 10.25 13.2206 10.092 12.9393 9.81066C12.658 9.52936 12.4999 9.14782 12.4999 8.75C12.4999 8.35218 12.658 7.97064 12.9393 7.68934C13.2206 7.40804 13.6021 7.25 13.9999 7.25C14.3978 7.25 14.7793 7.40804 15.0606 7.68934C15.3419 7.97064 15.4999 8.35218 15.4999 8.75Z" fill="#4D4D4D"/>
</g>
<defs>
<clipPath id="clip0_5011_5546">
<rect width="24" height="24" fill="white" transform="translate(2 2)"/>
</clipPath>
</defs>
</svg>

3
public/assets/images/HomeIcon.svg

@ -0,0 +1,3 @@
<svg width="20" height="22" viewBox="0 0 20 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.38889 0.59753C8.83974 0.211641 9.41038 0 10 0C10.5896 0 11.1603 0.211641 11.6111 0.59753L19.1111 7.0238C19.6756 7.50563 20 8.2154 20 8.96466V20.0253C20 20.549 19.7951 21.0513 19.4305 21.4216C19.0658 21.792 18.5713 22 18.0556 22H14.7222C14.2065 22 13.7119 21.792 13.3473 21.4216C12.9826 21.0513 12.7778 20.549 12.7778 20.0253V14.3833C12.7778 14.1588 12.69 13.9435 12.5337 13.7848C12.3774 13.6261 12.1655 13.537 11.9444 13.537H8.05556C7.83454 13.537 7.62258 13.6261 7.4663 13.7848C7.31002 13.9435 7.22222 14.1588 7.22222 14.3833V20.0253C7.22222 20.549 7.01736 21.0513 6.65271 21.4216C6.28805 21.792 5.79348 22 5.27778 22H1.94444C1.6891 22 1.43625 21.9489 1.20034 21.8497C0.964427 21.7504 0.750073 21.605 0.569515 21.4216C0.388956 21.2383 0.24573 21.0206 0.148012 20.781C0.0502946 20.5414 0 20.2846 0 20.0253V8.96353C0 8.2154 0.324444 7.50563 0.888889 7.02267L8.38889 0.59753Z" fill="#8B8B8B"/>
</svg>

6
public/assets/images/dots-vertical-svgrepo-com.svg

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="28px" height="28px" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 12C9.10457 12 10 12.8954 10 14C10 15.1046 9.10457 16 8 16C6.89543 16 6 15.1046 6 14C6 12.8954 6.89543 12 8 12Z" fill="#4D4D4D"/>
<path d="M8 6C9.10457 6 10 6.89543 10 8C10 9.10457 9.10457 10 8 10C6.89543 10 6 9.10457 6 8C6 6.89543 6.89543 6 8 6Z" fill="#4D4D4D"/>
<path d="M10 2C10 0.89543 9.10457 -4.82823e-08 8 0C6.89543 4.82823e-08 6 0.895431 6 2C6 3.10457 6.89543 4 8 4C9.10457 4 10 3.10457 10 2Z" fill="#4D4D4D"/>
</svg>

BIN
public/assets/images/jamkaran.png

After

Width: 661  |  Height: 371  |  Size: 370 KiB

5
public/assets/images/search-alt-svgrepo-com.svg

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17 17L21 21" stroke="#323232" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M19 11C19 15.4183 15.4183 19 11 19C6.58172 19 3 15.4183 3 11C3 6.58172 6.58172 3 11 3C15.4183 3 19 6.58172 19 11Z" stroke="#4D4D4D" stroke-width="2"/>
</svg>

4
public/assets/images/🦆 icon _play_.svg

@ -0,0 +1,4 @@
<svg width="21" height="21" viewBox="0 0 21 21" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.273 10.4996L9.39478 13.1758V7.82411L13.273 10.4996Z" fill="#F4846F" stroke="#F4846F" stroke-width="1.58643"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.5 2.29545C8.32402 2.29545 6.23716 3.15986 4.69851 4.69851C3.15986 6.23716 2.29545 8.32402 2.29545 10.5C2.29545 12.676 3.15986 14.7628 4.69851 16.3015C6.23716 17.8401 8.32402 18.7045 10.5 18.7045C12.676 18.7045 14.7628 17.8401 16.3015 16.3015C17.8401 14.7628 18.7045 12.676 18.7045 10.5C18.7045 8.32402 17.8401 6.23716 16.3015 4.69851C14.7628 3.15986 12.676 2.29545 10.5 2.29545ZM1 10.5C1 5.25341 5.25341 1 10.5 1C15.7466 1 20 5.25341 20 10.5C20 15.7466 15.7466 20 10.5 20C5.25341 20 1 15.7466 1 10.5Z" fill="#F4846F" stroke="#F4846F" stroke-width="0.38"/>
</svg>

2
src/api/http.tsx

@ -1,7 +1,7 @@
import axios from 'axios'; import axios from 'axios';
const http = axios.create({ const http = axios.create({
baseURL: 'http://88.99.212.243:7051',
baseURL: 'https://habibapp.com',
// headers: { // headers: {
// 'Authorization': 'Token 36d8be53bed5cab8027b66952b3f2c334cdca664', // 'Authorization': 'Token 36d8be53bed5cab8027b66952b3f2c334cdca664',
// }, // },

18
src/components/layout/header.tsx

@ -2,10 +2,10 @@ import Logo from "../../../public/assets/images/Hosseiniye.svg";
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import LanguageSwitcher from "../language-switcher"; import LanguageSwitcher from "../language-switcher";
import { HiMiniMagnifyingGlass } from "react-icons/hi2";
import SearchDuas from "../ui/search-duas";
const Header = () => { const Header = () => {
return ( return (
<header className="w-full shadow-sm sticky top-0 bg-white">
<header className="w-full shadow-sm sticky top-0 bg-white hidden lg:flex z-10">
<div className="max-w-[1440px] h-20 m-auto flex justify-between w-full px-11 items-center"> <div className="max-w-[1440px] h-20 m-auto flex justify-between w-full px-11 items-center">
<div className="flex gap-11 h-full items-center"> <div className="flex gap-11 h-full items-center">
<div> <div>
@ -17,7 +17,7 @@ const Header = () => {
<Link href={"#"}>Home</Link> <Link href={"#"}>Home</Link>
</li> </li>
<li className="h-full flex items-center border-[#F4846F] hover:border-b hover:text-[#EB6E57] "> <li className="h-full flex items-center border-[#F4846F] hover:border-b hover:text-[#EB6E57] ">
<Link href={"#"}>About us</Link>
<Link href={"/about"}>About us</Link>
</li> </li>
<li className="h-full flex items-center border-[#F4846F] hover:border-b hover:text-[#EB6E57] "> <li className="h-full flex items-center border-[#F4846F] hover:border-b hover:text-[#EB6E57] ">
<Link href={"#"}>Last Read</Link> <Link href={"#"}>Last Read</Link>
@ -26,16 +26,8 @@ const Header = () => {
</div> </div>
</div> </div>
<div className="flex gap-6 h-11"> <div className="flex gap-6 h-11">
<div className="flex items-center w-64 h-11 px-4 rounded-2xl gap-3 bg-[#EBEBEB]">
<label htmlFor="input">
<HiMiniMagnifyingGlass size={24} />
</label>
<input
id="input"
placeholder="Type a title or keyword to search"
className="text-xs bg-[#EBEBEB] w-full"
/>
</div>
<SearchDuas/>
<div> <div>
<LanguageSwitcher /> <LanguageSwitcher />
</div> </div>

18
src/components/layout/sidebar.tsx

@ -1,9 +1,23 @@
import { useParams } from "next/navigation";
import Tabs from "../sidebar/tabs"; import Tabs from "../sidebar/tabs";
import { useRouter } from "next/router";
function SideBar() { function SideBar() {
const router = useRouter();
const params = useParams();
const slug = params?.slug as string;
console.log(router);
if (router.pathname.includes("/about") || router.pathname.includes("/last-read")) {
return null;
}
return ( return (
<aside className="w-min h-[80vh] self-start overflow-auto bg-[#F5F5F5] rounded-3xl shadow-lg p-6">
<Tabs/>
<aside
className={`w-full h-[calc(100vh-55px)] self-start overflow-auto rounded-3xl p-6 lg:max-w-[430px] lg:bg-[#F5F5F5] ${
slug && "hidden lg:block"
}`}
>
<Tabs />
</aside> </aside>
); );
} }

84
src/components/sidebar/list.tsx

@ -1,7 +1,7 @@
"use client"; "use client";
import http from "@/api/http"; import http from "@/api/http";
import Image from "next/image"; import Image from "next/image";
import categoryImage from "../../../public/assets/images/Group 27009.svg";
import categoryImage from "../../../public/assets/images/Group 27009.svg";
import DayContainer from "../../../public/assets/images/Vector.svg"; import DayContainer from "../../../public/assets/images/Vector.svg";
import Folder from "../../../public/assets/images/Inner Plugin Iframe.svg"; import Folder from "../../../public/assets/images/Inner Plugin Iframe.svg";
import NoData from "../../../public/assets/images/Frame 1000005074.svg"; import NoData from "../../../public/assets/images/Frame 1000005074.svg";
@ -9,8 +9,8 @@ import { useEffect, useState } from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import colorizeVowels from "../utils/colorize-vowels"; import colorizeVowels from "../utils/colorize-vowels";
import { formatHijriDate } from "../utils/date-formaters"; import { formatHijriDate } from "../utils/date-formaters";
import useLocalStorage from "../utils/hooks/local-storage";
// Define types for the data being handled
interface Category { interface Category {
id: number; id: number;
name: string; name: string;
@ -20,27 +20,26 @@ interface Category {
interface Dua { interface Dua {
id: number; id: number;
name: string; name: string;
title: string;
slug: string; slug: string;
} }
interface ListProps { interface ListProps {
tab: string; // You can specify other tabs if required
tab: string;
path: { name: string; type: string; data?: Category[] }[];
setPath: React.Dispatch<React.SetStateAction<{ name: string; type: string; data?: Category[] }[]>>;
} }
type DataState = {
type: "Categories" | "children" | "Today" | null;
data: Category[] | Dua[] | [];
};
const List: React.FC<ListProps> = ({ tab }) => {
const [data, setData] = useState<DataState>({ type: null, data: [] });
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [path, setPath] = useState<{ name: string; type: string; data?: Category[] }[]>([]);
const List: React.FC<ListProps> = ({ tab, path, setPath , data, setData }) => {
const [loading, setLoading] = useState<boolean>(false); const [loading, setLoading] = useState<boolean>(false);
const router = useRouter(); const router = useRouter();
const [lastReads, setLastReads] = useLocalStorage("last-read", []);
const today = new Date(); const today = new Date();
const dayOfWeek = new Intl.DateTimeFormat("en-US", { weekday: "long" }).format(today);
const dayOfWeek = new Intl.DateTimeFormat("en-US", {
weekday: "long",
}).format(today);
const openCategory = (category: Category) => { const openCategory = (category: Category) => {
setData({ type: "children", data: category.children }); setData({ type: "children", data: category.children });
@ -51,8 +50,13 @@ const List: React.FC<ListProps> = ({ tab }) => {
}; };
const openDua = (dua: Dua) => { const openDua = (dua: Dua) => {
setPath((prev) => [...prev, { name: dua.name, type: "dua" }]);
router.push(`/duas/${dua.slug}-${dua.id}`);
setPath((prev) => [
...prev,
{ name: dua.name, type: "dua" },
]);
setLastReads((prev)=>[...prev , dua])
const slug = dua.title.toLowerCase().replaceAll(" ", "-");
router.push(`/duas/${slug}-${dua.id}`);
}; };
useEffect(() => { useEffect(() => {
@ -61,21 +65,42 @@ const List: React.FC<ListProps> = ({ tab }) => {
setLoading(true); setLoading(true);
http.get("web/mafatih-categories/").then((res) => { http.get("web/mafatih-categories/").then((res) => {
setData({ type: "Categories", data: res.data }); setData({ type: "Categories", data: res.data });
setPath((prev) => [...prev, { name: "", type: "Categories", data: res.data }]);
setPath([{ name: "", type: "Categories", data: res.data }]);
setLoading(false); setLoading(false);
}); });
} }
if (tab === "Today") { if (tab === "Today") {
// For "Today" tab, you can uncomment and make the API call to get today's data
// http.get("https://www.habibapp.com/web/mafatih-duas/?today=true").then((res) => {
// console.log(res);
// })
http.get("web/mafatih-duas/?today=true").then((res) => {
setData({ type: null, data: res.data.results });
});
} }
}, [tab]); }, [tab]);
useEffect(() => {
if (data.data.length) {
const filteredData = data.data.filter((item: any) => !item.title); // Exclude objects with a "title"
filteredData.forEach((category: Category) => {
const hasDua = category.children.filter((item: any) => !item.title)
http.get(`web/mafatih-duas/?category=${category.id}`).then((res) => {
const duas = res.data.results;
if (duas.length) {
category.children = [...(hasDua || []), ...duas];
setData((prevState) => ({
...prevState,
data: filteredData, // Update state with filtered data
}));
}
});
});
}
}, [path]);
if (loading) { if (loading) {
return <p>Loading ...</p>; return <p>Loading ...</p>;
} }
console.log(data);
return ( return (
<div> <div>
@ -83,33 +108,25 @@ const List: React.FC<ListProps> = ({ tab }) => {
<div className="flex items-center flex-col"> <div className="flex items-center flex-col">
<p className="text-xs text-[#8B8B8B] m-6">{formatHijriDate(today)}</p> <p className="text-xs text-[#8B8B8B] m-6">{formatHijriDate(today)}</p>
<div className="flex w-full flex-col items-center bg-[#FDF6F6] rounded-xl p-[1px] relative"> <div className="flex w-full flex-col items-center bg-[#FDF6F6] rounded-xl p-[1px] relative">
{/* Gradient Border */}
<div className="absolute inset-0 rounded-xl bg-gradient-to-r from-[#FE7F781B] to-[#EA6D564D] opacity-50"></div> <div className="absolute inset-0 rounded-xl bg-gradient-to-r from-[#FE7F781B] to-[#EA6D564D] opacity-50"></div>
{/* Inner Content */}
<div className="relative w-full h-full bg-white rounded-xl flex items-center flex-col p-4"> <div className="relative w-full h-full bg-white rounded-xl flex items-center flex-col p-4">
{/* Date */}
<div className="absolute top-[-13px]"> <div className="absolute top-[-13px]">
<div className="relative w-40 flex justify-center items-center mb-6"> <div className="relative w-40 flex justify-center items-center mb-6">
{/* Background Image */}
<Image <Image
src={DayContainer} src={DayContainer}
alt="Day Container" alt="Day Container"
className="rounded-full w-full h-full object-cover" className="rounded-full w-full h-full object-cover"
/> />
{/* Centered Text */}
<p className="absolute text-center text-[#EE755F] font-medium text-sm"> <p className="absolute text-center text-[#EE755F] font-medium text-sm">
{dayOfWeek}&apos;s Dhikr {dayOfWeek}&apos;s Dhikr
</p> </p>
</div> </div>
</div> </div>
{/* Arabic Dhikr */}
<div className="text-2xl mt-1 text-[#292524] font-[UthmanTaha] mb-2 leading-relaxed"> <div className="text-2xl mt-1 text-[#292524] font-[UthmanTaha] mb-2 leading-relaxed">
{colorizeVowels("یا أَرْحَمَ الرَّاحِمِین")} {colorizeVowels("یا أَرْحَمَ الرَّاحِمِین")}
</div> </div>
{/* English Translation */}
<p className="text-sm text-[#8B8B8B]"> <p className="text-sm text-[#8B8B8B]">
O Most Merciful of all givers O Most Merciful of all givers
</p> </p>
@ -117,10 +134,12 @@ const List: React.FC<ListProps> = ({ tab }) => {
</div> </div>
</div> </div>
)} )}
{data.data.length ? (
{data?.data?.length ? (
data.data.map((item: Category | Dua) => { data.data.map((item: Category | Dua) => {
if (data.type === "Categories" && (item as Category).children?.length) {
if (data.type === "Categories") {
const category = item as Category; const category = item as Category;
return ( return (
<div <div
className="flex justify-between p-3 bg-white my-4 rounded-2xl" className="flex justify-between p-3 bg-white my-4 rounded-2xl"
@ -128,7 +147,7 @@ const List: React.FC<ListProps> = ({ tab }) => {
onClick={() => openCategory(category)} onClick={() => openCategory(category)}
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Image src={categoryImage } width={24} height={24} alt="category" />
<Image src={categoryImage} width={24} height={24} alt="category" />
<p>{category.name}</p> <p>{category.name}</p>
</div> </div>
<p>{category.children.length}</p> <p>{category.children.length}</p>
@ -136,6 +155,7 @@ const List: React.FC<ListProps> = ({ tab }) => {
); );
} else if ((item as Category).children?.length) { } else if ((item as Category).children?.length) {
const category = item as Category; const category = item as Category;
return ( return (
<div <div
className="flex justify-between p-3 bg-white my-4 rounded-2xl" className="flex justify-between p-3 bg-white my-4 rounded-2xl"
@ -158,7 +178,7 @@ const List: React.FC<ListProps> = ({ tab }) => {
onClick={() => openDua(dua)} onClick={() => openDua(dua)}
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<p>{dua.name}</p>
<p>{dua.title}</p>
</div> </div>
</div> </div>
); );

137
src/components/sidebar/tabs.tsx

@ -1,8 +1,9 @@
"use client"
"use client";
import { useState } from "react"; import { useState } from "react";
import List from "./list"; import List from "./list";
import Image from "next/image";
import { FaArrowLeft } from "react-icons/fa6";
const tabs = [ const tabs = [
{ name: "Categories" }, { name: "Categories" },
{ name: "Famous" }, { name: "Famous" },
@ -10,29 +11,123 @@ const tabs = [
{ name: "Today" }, { name: "Today" },
]; ];
interface Category {
id: number;
name: string;
children: Category[];
}
interface Dua {
id: number;
name: string;
title: string;
slug: string;
}
type DataState = {
type: "Categories" | "children" | "Today" | null;
data: Category[] | Dua[] | [];
};
const Tabs = () => { const Tabs = () => {
const [selected , setSelected ]= useState({name : "Today"})
const [selected, setSelected] = useState({ name: "Today" });
const [data, setData] = useState<DataState>({ type: null, data: [] });
const [path, setPath] = useState<
{ name: string; type: string; data?: Category[] }[]
>([]);
const back = () => {
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
});
setData(path[path.length - 2]);
};
console.log(path);
const handlePathClick = (index: number) => {
setPath((prev) => {
const newPath = [...prev]; // Create a shallow copy of the array
newPath.splice(index + 1); // Remove elements starting from index + 1
console.log(newPath);
return newPath; // Return the updated array
});
// Optionally, you can update the data here as well:
setData(path[index]);
};
return ( return (
<> <>
<div className=" rounded-2xl bg-[#EBEBEB]">
<ul className="p-1 flex gap-6 font-semibold text-xs whitespace-nowrap text-[#8B8B8B]">
{tabs.map((tab) => (
<li
key={tab.name}
className=" hover:bg-white hover:shadow-lg w-20 h-9 rounded-xl flex items-center justify-center cursor-pointer"
onClick={()=>{setSelected(tab);
}}
>
<p>{tab.name}</p>
</li>
))}
</ul>
</div>
<List tab={selected.name}/>
</>
{" "}
{path.length > 1 && (
<div>
<div className="flex items-center justify-between">
<div onClick={back} className="p-2 bg-white rounded-xl">
<FaArrowLeft size="23" />
</div>
<p className="text-base font-bold">{path[1]?.name}</p>
<div className="w-10" />
</div>
{path.length > 2 &&
path.map((item, index) => {
if (item.name && index + 1 < path.length && index > 0) {
return (
<>
<span
className="text-xs text-[#8B8B8B] font-semibold"
key={index}
onClick={()=>handlePathClick(index)}
>
{item.name}
</span>
<span className="mx-2"></span>
</>
);
} else {
return (
<span className="text-xs font-semibold" key={index} onClick={()=>handlePathClick(index)}>
{item.name}
</span>
);
}
})}
</div>
)}
{path.length < 2 && (
<div className="rounded-2xl bg-[#EBEBEB]">
{/* Render the path names */}
<ul className="p-1 flex gap-6 font-semibold text-xs whitespace-nowrap text-[#8B8B8B]">
{tabs.map((tab) => (
<li
key={tab.name}
className="hover:bg-white hover:shadow-lg w-20 h-9 rounded-xl flex items-center justify-center cursor-pointer"
onClick={() => {
setSelected(tab);
}}
>
<p>{tab.name}</p>
</li>
))}
</ul>
</div>
)}
<List
tab={selected.name}
data={data}
setData={setData}
path={path}
setPath={setPath}
/>
</>
); );
}; };

39
src/components/ui/footer-sticky.tsx

@ -0,0 +1,39 @@
import Home from "../../../public/assets/images/HomeIcon.svg";
import About from "../../../public/assets/images/Frame 26952.svg";
import Search from "../../../public/assets/images/search-alt-svgrepo-com.svg";
import Menue from "../../../public/assets/images/dots-vertical-svgrepo-com.svg";
import Image from "next/image";
const FooterSticky = () => {
const Navigations = [
{
name: "Home",
icon: Home,
},
{
name: "Search",
icon: Search,
},
{
name: "About Us",
icon: About,
},
{
name: "Menue",
icon: Menue,
},
];
return (
<div className="sticky bottom-0 flex justify-between items-center lg:hidden">
{Navigations.map((item) => (
<div key={item.name} className="flex items-center flex-col">
<Image width={28} height={28} alt={item.name} src={item.icon} />
<p>{item.name}</p>
</div>
))}
</div>
);
};
export default FooterSticky;

127
src/components/ui/search-duas.tsx

@ -0,0 +1,127 @@
import React, { useEffect, useState, ChangeEvent, useRef } from "react";
import http from "@/api/http";
import { HiMiniMagnifyingGlass } from "react-icons/hi2";
interface Dua {
id: number;
title: string;
// Add other relevant fields based on your API response
}
const SearchDuas: React.FC = () => {
const [value, setValue] = useState<string>("");
const [results, setResults] = useState<Dua[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null);
const [show, setShow] = useState<boolean>(false);
// Ref for the main container
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
// If the input is empty, reset results and do not send a request
if (value.trim() === "") {
setResults([]);
setError(null);
setShow(false)
return;
}
// 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
// console.log(res);
})
.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]);
// Handle input changes
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
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)
}
};
// Bind the event listener
document.addEventListener("mousedown", handleClickOutside);
return () => {
// Unbind the event listener on cleanup
document.removeEventListener("mousedown", handleClickOutside);
};
}, []);
return (
<div className="relative flex flex-col items-center" ref={containerRef}>
<div className="flex items-center w-64 h-11 px-4 rounded-2xl gap-3 bg-[#EBEBEB]">
<label htmlFor="search-input" className="cursor-pointer">
<HiMiniMagnifyingGlass size={24} />
</label>
<input
onChange={handleChange}
id="search-input"
type="text"
value={value}
placeholder="Type a title or keyword to search"
className="text-xs bg-[#EBEBEB] w-full focus:outline-none"
aria-label="Search Duas"
onClick={()=>{setShow(true)}}
/>
</div>
{/* Loading Indicator */}
{isLoading && show &&(
<div className="mt-2 text-sm text-gray-600">Loading...</div>
)}
{/* Error Message */}
{error && show && (
<div className="mt-2 text-sm text-red-500">{error}</div>
)}
{/* Search Results */}
{results.length > 0 && show && (
<ul className="absolute top-14 mt-2 w-64 z-20 bg-white shadow-lg rounded-lg h-60 overflow-y-auto">
{results.map((dua) => (
<li
key={dua.id}
className="px-4 py-2 hover:bg-gray-100 cursor-pointer"
// Add onClick handler or link if needed
>
{dua.title}
</li>
))}
</ul>
)}
</div>
);
};
export default SearchDuas;

2
src/components/utils/colorize-vowels.tsx

@ -9,7 +9,7 @@ const colorizeVowels = (text: string) => {
return ( return (
<div className="relative"> <div className="relative">
{/* Bottom layer: Full text with vowels in orange */} {/* Bottom layer: Full text with vowels in orange */}
<div className="absolute top-0 left-0 text-2xl text-[#EB6E57] font-[UthmanTaha]">
<div className="absolute top-0 right-0 text-2xl text-[#EB6E57] font-[UthmanTaha]">
{normalizedText} {normalizedText}
</div> </div>
{/* Top layer: Text without vowels in black */} {/* Top layer: Text without vowels in black */}

48
src/components/utils/hooks/local-storage.tsx

@ -0,0 +1,48 @@
import { useState, useEffect } from "react";
/**
* useLocalStorage Hook
*
* @param key - The key in localStorage
* @param initialValue - The initial value to use if key is not found
* @returns [storedValue, setValue] - The current value and a setter function
*/
function useLocalStorage<T>(key: string, initialValue: T): [T, (value: T | ((val: T) => T)) => void] {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key);
return item ? (JSON.parse(item) as T) : initialValue;
} catch (error) {
console.error("Error reading localStorage key “" + key + "”:", error);
return initialValue;
}
});
const setValue = (value: T | ((val: T) => T)) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error("Error setting localStorage key “" + key + "”:", error);
}
};
useEffect(() => {
const handleStorageChange = () => {
try {
const item = window.localStorage.getItem(key);
setStoredValue(item ? (JSON.parse(item) as T) : initialValue);
} catch (error) {
console.error("Error reading localStorage key “" + key + "”:", error);
}
};
window.addEventListener("storage", handleStorageChange);
return () => window.removeEventListener("storage", handleStorageChange);
}, [key, initialValue]);
return [storedValue, setValue];
}
export default useLocalStorage;

9
src/pages/_app.tsx

@ -1,22 +1,23 @@
import Header from "@/components/layout/header"; import Header from "@/components/layout/header";
import SideBar from "@/components/layout/sidebar"; import SideBar from "@/components/layout/sidebar";
import FooterSticky from "@/components/ui/footer-sticky";
import "@/styles/globals.css"; import "@/styles/globals.css";
import type { AppProps } from "next/app"; import type { AppProps } from "next/app";
export default function App({ Component, pageProps }: AppProps) { export default function App({ Component, pageProps }: AppProps) {
console.log(pageProps); console.log(pageProps);
return ( return (
<> <>
<Header /> <Header />
<div className="max-w-[1440px] m-auto">
<div className="p-11 flex gap-11">
<div className=" m-auto bg-[#EAEAEA] lg:p-6">
<div className="max-w-[1440px] flex flex-col lg:flex-row gap-6 relative">
<SideBar /> <SideBar />
<main className="flex-grow">
<main className={`w-full`}>
<Component {...pageProps} /> <Component {...pageProps} />
</main> </main>
</div> </div>
</div> </div>
<FooterSticky/>
</> </>
); );
} }

38
src/pages/about.tsx

@ -0,0 +1,38 @@
import Image from "next/image";
import img from "../../public/assets/images/jamkaran.png";
import { useParams, useRouter } from "next/router";
const About = () => {
return (
<div className="w-full flex items-center justify-center">
<div className="w-[660px]">
<p>
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam
nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat,
sed diam voluptua. At vero eos et accusam et justo duo dolores et ea
rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem
ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur
sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et
dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam
et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea
takimata sanctus est Lorem ipsum dolor sit amet. gubergren, no sea
takimata sanctus est Lorem ipsum dolor sit amet Lorem ipsum dolor sit
amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor
invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.
At vero eos et accusam et justo duo dolores et ea rebum. Stet clita
kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit
amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed
diam nonumy.
</p>
<Image src={img} alt="photo" />
<p>
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam
nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat,
sed diam voluptua. At vero eos et accusam et justo duo dolores et ea
rebum. Stet clita kasd gubergren, no sea takimata{" "}
</p>
</div>
</div>
);
};
export default About;

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

@ -1,6 +1,7 @@
import Image from "next/image"; import Image from "next/image";
import Setting from "../../../public/assets/images/Setting.svg";
import { useEffect, useState } from "react";
import SettingIcon from "../../../public/assets/images/Setting.svg";
import PlayIcon from "../../../public/assets/images/🦆 icon _play_.svg";
import { useEffect, useRef, useState, useCallback } from "react";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
import http from "@/api/http"; import http from "@/api/http";
import colorizeVowels from "@/components/utils/colorize-vowels"; import colorizeVowels from "@/components/utils/colorize-vowels";
@ -14,45 +15,181 @@ interface Dua {
translation: string; translation: string;
} }
type Data = {
// Define the structure for audio synchronization data
interface AudioSyncData {
id: number;
duration: [number, number][];
}
// Define the Audio interface
interface Audio {
audio: string;
audio_sync_data: AudioSyncData[];
}
// Define the API response structure
interface DuaPartsResponse {
results: Dua[]; results: Dua[];
};
}
const Dua: React.FC = () => {
interface AudiosResponse {
results: Audio[];
}
const DuaComponent: React.FC = () => {
const params = useParams(); const params = useParams();
const slug = params?.slug as string; const slug = params?.slug as string;
// Explicitly define the type of duaParts as Dua[]
const [duaParts, setDuaParts] = useState<Dua[]>([]); // <-- specify the type here
// 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
useEffect(() => { useEffect(() => {
const id: string | undefined = slug?.split("-").pop();
if (id) {
http.get(`web/mafatih-duas/${id}/parts/`).then((res: { data: Data }) => {
setDuaParts(res.data.results); // This is now valid because duaParts is typed as Dua[]
if (!slug) return;
const fetchData = async () => {
const id = slug.split("-").pop();
if (!id) return;
try {
// Fetching dua parts
const duaResponse = await http.get<DuaPartsResponse>(
`web/mafatih-duas/${id}/parts/`
);
setDuaParts(duaResponse.data.results);
} catch (error) {
console.error("Error fetching Dua parts:", error);
// 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
}
};
fetchData();
}, [slug]);
// Play audio from a specific part
const playAudio = useCallback(
(part: Dua) => {
if (audios.length === 0) return;
const selectedAudio = audios[0].audio_sync_data.find(
(item) => item.id === part.id
);
if (selectedAudio && audioRef.current) {
console.log("fdafads")
const startTime = selectedAudio.duration[0][0]; // Assuming in seconds
audioRef.current.currentTime = startTime;
audioRef.current.play().catch((error) => {
console.error("Error playing audio:", error);
});
}
},
[audios]
);
// Handle audio end to scroll to the next part
const handleAudioEnd = useCallback(() => {
if (!recitingPart) return;
const currentIndex = duaParts.findIndex(
(item) => item.id === recitingPart.id
);
const nextPart = partRefs.current[currentIndex + 1];
if (nextPart) {
nextPart.scrollIntoView({
behavior: "smooth",
block: "start",
}); });
} }
}, [slug]);
}, [recitingPart, duaParts]);
// Handle audio progress for updating reciting part
const handleAudioProgress = useCallback(() => {
if (!audioRef.current || audios.length === 0) return;
const currentTime = audioRef.current.currentTime;
const currentAudioSync = audios[0].audio_sync_data.find(
(item) =>
item.duration[0][0] < currentTime && currentTime < item.duration[0][1]
);
const currentRecitingPart =
duaParts.find((item) => item.id === currentAudioSync?.id) || null;
setRecitingPart(currentRecitingPart);
}, [audios, duaParts]);
// Setup audio event listeners and progress monitoring
useEffect(() => {
const audioEl = audioRef.current;
if (!audioEl) return;
console.log(duaParts[1]);
const progressInterval = setInterval(handleAudioProgress, 500);
audioEl.addEventListener("ended", handleAudioEnd);
return () => {
clearInterval(progressInterval);
audioEl.removeEventListener("ended", handleAudioEnd);
};
}, [handleAudioProgress, handleAudioEnd]);
// Scroll to the reciting part whenever it changes
useEffect(() => {
if (!recitingPart) return;
const index = duaParts.findIndex((item) => item.id === recitingPart.id);
const partElement = partRefs.current[index];
if (partElement) {
partElement.scrollIntoView({
behavior: "smooth",
block: "center",
});
}
}, [recitingPart, duaParts]);
if (!slug) { if (!slug) {
return null; // Handling the case where slug is not available return null; // Handling the case where slug is not available
} }
return ( return (
<div className="bg-[#F5F5F5] rounded-3xl h-[80vh] max-w-[887px] overflow-y-auto">
<div className="flex justify-between items-center bg-gradient-to-r from-[#F79B59] to-[#EB6E57] p-6 rounded-3xl text-white">
<p className="text-sm font-semibold">Dua of the month of Rajab from </p>
<div className="p-2 bg-white/20 rounded-2xl">
<Image sizes="24" src={Setting} alt="setting" />
</div>
</div>
<div className={` rounded-3xl max-w-[887px] overflow-y-auto flex-grow h-[calc(100vh-55px)] bg-[#F5F5F5] lg:p-6 lg:rounded-3xl ${!slug && "hidden lg:flex"}`} >
<header className="flex justify-between items-center bg-gradient-to-r from-[#F79B59] to-[#EB6E57] p-6 rounded-3xl text-white">
<p className="text-sm font-semibold">Dua of the month of Rajab</p>
<button
className="p-2 bg-white/20 rounded-2xl"
aria-label="Settings"
// Add onClick handler if settings functionality is needed
>
<Image width={24} height={24} src={SettingIcon} alt="Settings" />
</button>
</header>
<div className="p-6"> <div className="p-6">
{duaParts.map((item: Dua) => (
{duaParts.map((item, index) => (
<div <div
key={item.id} key={item.id}
ref={(el) => (partRefs.current[index] = el)}
className="p-1 rounded-3xl my-4" className="p-1 rounded-3xl my-4"
style={{ style={{
background: background:
@ -62,28 +199,46 @@ const Dua: React.FC = () => {
}} }}
> >
<div className="p-3 bg-white rounded-3xl"> <div className="p-3 bg-white rounded-3xl">
{item?.text && (
<div className="mb-4 text-right">{colorizeVowels(item?.text)}</div>
{item.text && (
<div className="mb-4 text-right">
{colorizeVowels(item.text)}
</div>
)} )}
{item?.local_alpha && (
{item.local_alpha && (
<p className="text-sm font-normal border-b mb-4 pb-4"> <p className="text-sm font-normal border-b mb-4 pb-4">
{item?.local_alpha}
{item.local_alpha}
</p> </p>
)} )}
{item?.translation && (
<p className="text-sm font-normal">{item?.translation}</p>
{item.translation && (
<p className="text-sm font-normal">{item.translation}</p>
)} )}
{item?.description && (
{item.description && (
<p className="text-[#36363C] italic text-xs font-normal"> <p className="text-[#36363C] italic text-xs font-normal">
{item?.description}
{item.description}
</p> </p>
)} )}
{/* Play button to start audio from specific time */}
<button
onClick={() => playAudio(item)}
className="mt-5 cursor-pointer"
aria-label={`Play audio for part ${index + 1}`}
>
<Image width={24} height={24} src={PlayIcon} alt="Play Audio" />
</button>
</div> </div>
</div> </div>
))} ))}
</div> </div>
{audios.length > 0 && (
<audio
ref={audioRef}
src={audios[0].audio}
preload="auto"
/>
)}
</div> </div>
); );
}; };
export default Dua;
export default DuaComponent;

3
src/pages/index.tsx

@ -3,8 +3,9 @@ import NoData from "../../public/assets/images/Untitled-1-02.svg";
export default function Home() { export default function Home() {
return ( return (
<div className="bg-[#F5F5F5] rounded-3xl h-[80vh] max-w-[887px] flex items-center justify-center">
<div className={`flex-grow w-full items-center justify-center h-[calc(100vh-55px)] bg-[#F5F5F5] lg:p-6 lg:rounded-3xl hidden lg:flex`}>
<Image src={NoData} alt="no data"/> <Image src={NoData} alt="no data"/>
</div> </div>
); );

Loading…
Cancel
Save