Browse Source

feat:implement shimmer loading component, and enhance SEO with default settings

master
sina_sajjadi 2 weeks ago
parent
commit
cadf898f64
  1. 76
      package-lock.json
  2. 5
      package.json
  3. 9
      public/assets/images/noduas.svg
  4. 59
      src/components/common/default-seo.tsx
  5. 144
      src/components/sidebar/categories.tsx
  6. 83
      src/components/sidebar/famous.tsx
  7. 137
      src/components/sidebar/nearby.tsx
  8. 21
      src/components/sidebar/tabs.tsx
  9. 146
      src/components/sidebar/today.tsx
  10. 30
      src/components/ui/list-loading.tsx
  11. 13
      src/components/utils/ga.js
  12. 10
      src/components/utils/types.ts
  13. 13
      src/pages/_app.tsx
  14. 2
      src/pages/_document.tsx
  15. 304
      src/pages/duas/[slug].tsx
  16. 11
      tailwind.config.ts

76
package-lock.json

@ -19,10 +19,13 @@
"moment-hijri": "^3.0.0",
"next": "15.1.0",
"next-i18next": "^15.4.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"next-seo": "^6.6.0",
"react": "18.2.0",
"react-content-loader": "^7.0.2",
"react-dom": "18.2.0",
"react-ga4": "^2.1.0",
"react-i18next": "^15.4.0",
"react-icons": "^5.4.0"
"react-icons": "4.11.0"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
@ -856,7 +859,7 @@
"react-dom": "^16.8.0 || 17.x"
}
},
"node_modules/@reach/utils": {
"node_modules/@reach/portal/node_modules/@reach/utils": {
"version": "0.18.0",
"resolved": "https://registry.npmjs.org/@reach/utils/-/utils-0.18.0.tgz",
"integrity": "sha512-KdVMdpTgDyK8FzdKO9SCpiibuy/kbv3pwgfXshTI6tEcQT1OOwj7BAksnzGC0rPz0UholwC+AgkqEl3EJX3M1A==",
@ -3651,8 +3654,7 @@
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
},
"node_modules/js-yaml": {
"version": "4.1.0",
@ -3794,7 +3796,6 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"dev": true,
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
},
@ -4041,6 +4042,16 @@
"react-i18next": ">= 13.5.0"
}
},
"node_modules/next-seo": {
"version": "6.6.0",
"resolved": "https://registry.npmjs.org/next-seo/-/next-seo-6.6.0.tgz",
"integrity": "sha512-0VSted/W6XNtgAtH3D+BZrMLLudqfm0D5DYNJRXHcDgan/1ZF1tDFIsWrmvQlYngALyphPfZ3ZdOqlKpKdvG6w==",
"peerDependencies": {
"next": "^8.1.1-canary.54 || >=9.0.0",
"react": ">=16.0.0",
"react-dom": ">=16.0.0"
}
},
"node_modules/next/node_modules/postcss": {
"version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
@ -4544,24 +4555,44 @@
]
},
"node_modules/react": {
"version": "19.0.0",
"resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz",
"integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==",
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
"integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==",
"dependencies": {
"loose-envify": "^1.1.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-content-loader": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/react-content-loader/-/react-content-loader-7.0.2.tgz",
"integrity": "sha512-773S98JTyC8VB2nu7LXUhpHx8tZMieGxMcx3qTe7IkohT6Br7d9AXnIXs/wQ6IhlUdKQcw6JLKk1QKigYCWDRA==",
"engines": {
"node": ">=10"
},
"peerDependencies": {
"react": ">=16.0.0"
}
},
"node_modules/react-dom": {
"version": "19.0.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz",
"integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==",
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
"integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==",
"dependencies": {
"scheduler": "^0.25.0"
"loose-envify": "^1.1.0",
"scheduler": "^0.23.0"
},
"peerDependencies": {
"react": "^19.0.0"
"react": "^18.2.0"
}
},
"node_modules/react-ga4": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/react-ga4/-/react-ga4-2.1.0.tgz",
"integrity": "sha512-ZKS7PGNFqqMd3PJ6+C2Jtz/o1iU9ggiy8Y8nUeksgVuvNISbmrQtJiZNvC/TjDsqD0QlU5Wkgs7i+w9+OjHhhQ=="
},
"node_modules/react-i18next": {
"version": "15.4.0",
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.4.0.tgz",
@ -4584,9 +4615,9 @@
}
},
"node_modules/react-icons": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.4.0.tgz",
"integrity": "sha512-7eltJxgVt7X64oHh6wSWNwwbKTCtMfK35hcjvJS0yxEAhPM8oUKdS3+kqaW1vicIltw+kR2unHaa12S9pPALoQ==",
"version": "4.11.0",
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.11.0.tgz",
"integrity": "sha512-V+4khzYcE5EBk/BvcuYRq6V/osf11ODUM2J8hg2FDSswRrGvqiYUYPRy4OdrWaQOBj4NcpJfmHZLNaD+VH0TyA==",
"peerDependencies": {
"react": "*"
}
@ -4767,9 +4798,12 @@
}
},
"node_modules/scheduler": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz",
"integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA=="
"version": "0.23.2",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
"dependencies": {
"loose-envify": "^1.1.0"
}
},
"node_modules/semver": {
"version": "7.6.3",

5
package.json

@ -20,10 +20,13 @@
"moment-hijri": "^3.0.0",
"next": "15.1.0",
"next-i18next": "^15.4.1",
"next-seo": "^6.6.0",
"react": "18.2.0",
"react-content-loader": "^7.0.2",
"react-dom": "18.2.0",
"react-ga4": "^2.1.0",
"react-i18next": "^15.4.0",
"react-icons": "4.11.0"
"react-icons": "4.11.0"
},
"devDependencies": {
"@eslint/eslintrc": "^3",

9
public/assets/images/noduas.svg

@ -0,0 +1,9 @@
<svg width="167" height="136" viewBox="0 0 167 136" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M126.895 23.3844C126.431 23.0074 125.884 22.7457 125.3 22.6209C124.716 22.4961 124.11 22.5118 123.533 22.6667L87.2281 34L50.8481 15.5267C50.3508 15.2965 49.8094 15.1773 49.2614 15.1773C48.7134 15.1773 48.172 15.2965 47.6747 15.5267L9.89696 30.9778C9.20111 31.2636 8.60645 31.7507 8.18916 32.3766C7.77188 33.0026 7.55097 33.7388 7.55473 34.4911V111.86C7.55782 112.479 7.71283 113.087 8.00615 113.632C8.29946 114.177 8.72211 114.641 9.23693 114.984C9.75176 115.328 10.343 115.539 10.9587 115.6C11.5744 115.662 12.1957 115.571 12.7681 115.336L49.1103 100.413L85.3392 119C85.8769 119.275 86.4728 119.418 87.077 119.416C87.4526 119.473 87.8347 119.473 88.2103 119.416L125.988 107.629C126.784 107.379 127.475 106.874 127.954 106.19C128.432 105.507 128.671 104.684 128.632 103.851V26.4444C128.613 25.8317 128.444 25.2329 128.142 24.6997C127.839 24.1666 127.411 23.7152 126.895 23.3844ZM120.888 101.056L89.457 110.878V102.227H83.4125V109.782L51.6792 93.5V87.1155H45.6347V93.6133L15.1103 106.193V36.9844L45.6347 24.4044V33.2822H51.6792V24.4422L83.4125 40.6867V48.62H89.457V41.5555L120.888 31.5822V101.056Z" fill="#BCBCBC"/>
<path d="M83.4141 56.8933H89.4585V71.2867H83.4141V56.8933Z" fill="#BCBCBC"/>
<path d="M83.4141 79.56H89.4585V93.9533H83.4141V79.56Z" fill="#BCBCBC"/>
<path d="M45.6367 41.7822H51.6812V56.1756H45.6367V41.7822Z" fill="#BCBCBC"/>
<path d="M45.6367 64.7133H51.6812V78.88H45.6367V64.7133Z" fill="#BCBCBC"/>
<path d="M150 29.75C150 41.486 140.486 51 128.75 51C117.014 51 107.5 41.486 107.5 29.75C107.5 18.0139 117.014 8.5 128.75 8.5C140.486 8.5 150 18.0139 150 29.75Z" fill="#F5F5F5"/>
<path d="M155.667 56.6667L143.776 44.776M150 29.75C150 18.0139 140.486 8.5 128.75 8.5C117.014 8.5 107.5 18.0139 107.5 29.75C107.5 41.486 117.014 51 128.75 51C140.486 51 150 41.486 150 29.75Z" stroke="#BCBCBC" stroke-width="7.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

59
src/components/common/default-seo.tsx

@ -0,0 +1,59 @@
import { DefaultSeo as NextDefaultSeo } from "next-seo";
interface DefaultSeoProps {
title?: string;
description?: string;
keywords?: string;
}
const DefaultSeo: React.FC<DefaultSeoProps> = ({
title = "Dua Site",
description = "A comprehensive collection of Duas",
keywords = "dua, islam, prayer, supplication",
}) => {
return (
<NextDefaultSeo
title={title}
titleTemplate={`%s | ${title}`}
description={description}
canonical="https://duasapp.com"
openGraph={{
type: "website",
locale: "en_US",
site_name: title,
description: description,
images: [
{
url: "/assets/images/Hosseiniye.svg",
width: 1200,
height: 630,
alt: "Dua Site",
},
],
}}
additionalMetaTags={[
{
name: "keywords",
content: keywords,
},
{
name: "viewport",
content: "width=device-width, initial-scale=1",
},
{
charSet: "utf-8",
},
{
name: "apple-mobile-web-app-capable",
content: "yes",
},
{
name: "theme-color",
content: "#ffffff",
},
]}
/>
);
};
export default DefaultSeo;

144
src/components/sidebar/list.tsx → src/components/sidebar/categories.tsx

@ -4,59 +4,30 @@
import http from "@/api/http";
import Image from "next/image";
import categoryImage from "../../../public/assets/images/Group 27009.svg";
import DayContainer from "../../../public/assets/images/Vector.svg";
import Folder from "../../../public/assets/images/Inner Plugin Iframe.svg";
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 { useRouter } from "next/navigation";
import colorizeVowels from "../utils/colorize-vowels";
import { formatHijriDate } from "../utils/date-formaters";
import { Category, Dua, ListProps } from "../utils/types";
import { Category, Dua, CategoriesProps } from "../utils/types";
import { useTranslation } from "next-i18next"; // Importing the translation hook
const List: React.FC<ListProps> = ({ tab, path, setPath, data, setData }) => {
import ShimmerLoader from "../ui/list-loading";
const Categories: React.FC<CategoriesProps> = ({
path,
setPath,
data,
setData,
}) => {
const { t } = useTranslation("common"); // Initialize translation hook for "common" namespace
const [loading, setLoading] = useState<boolean>(false);
const [currentDhikr, setCurrentDhikr] = useState<string>("");
const [currentTranslation, setCurrentTranslation] = useState<string>("");
const [error, setError] = useState<string | null>(null); // State to hold error messages
const router = useRouter();
const today = new Date();
const dayOfWeek = new Intl.DateTimeFormat("en-US", {
weekday: "long",
}).format(today);
let locale: string;
useEffect(() => {
locale = localStorage.getItem("locale") || "en";
if (tab === "Today") {
setLoading(true);
setError(null); // Reset error state before starting the request
http
.get("web/mafatih-duas/dhikrs/?today=true")
.then((res) => {
const dhikrForToday = res.data.find(
(item: any) => item.day === dayOfWeek
);
if (dhikrForToday) {
setCurrentDhikr(dhikrForToday.text);
setCurrentTranslation(
dhikrForToday.translation[locale] || dhikrForToday.translation.en
);
}
setLoading(false);
})
.catch((error) => {
setLoading(false); // Ensure loading state is false if request fails
setError(t("error.fetchDhikrData")); // Display a translated error message
console.error("Error fetching Dhikr data:", error);
});
}
}, [tab, dayOfWeek, locale, t]);
const openCategory = (category: Category) => {
if (category.children.length === 0) {
return;
}
setData({ type: "children", data: category.children });
setPath((prev) => [
...prev,
@ -65,44 +36,32 @@ const List: React.FC<ListProps> = ({ tab, path, setPath, data, setData }) => {
};
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}`);
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(() => {
setData({ type: null, data: [] });
setError(null); // Reset error state before starting the request
if (tab === "Categories") {
setLoading(true);
http
.get("web/mafatih-categories/")
.then((res) => {
setData({ type: "Categories", data: res.data });
setPath([{ name: "", type: "Categories", data: res.data }]);
setLoading(false);
})
.catch((error) => {
setLoading(false);
setError(t("error.fetchCategories")); // Display a translated error message
console.error("Error fetching categories:", error);
});
}
if (tab === "Today") {
http
.get("web/mafatih-duas/?today=true")
.then((res) => {
setData({ type: null, data: res.data.results });
})
.catch((error) => {
setError(t("error.fetchTodayDuas"));
console.error("Error fetching today's duas:", error);
});
}
}, [setData, setPath, tab, t]);
setLoading(true);
http
.get("web/mafatih-categories/")
.then((res) => {
setData({ type: "Categories", data: res.data });
setPath([{ name: "", type: "Categories", data: res.data }]);
setLoading(false);
})
.catch((error) => {
setLoading(false);
setError(t("error.fetchCategories")); // Display a translated error message
console.error("Error fetching categories:", error);
});
}, [setData, setPath, t]);
useEffect(() => {
if (data.data.length) {
@ -111,6 +70,7 @@ const List: React.FC<ListProps> = ({ tab, path, setPath, data, setData }) => {
filteredData.forEach((category: Category) => {
const hasDua = category.children.filter((item: any) => !item.title);
http
.get(`web/mafatih-duas/?category=${category.id}`)
.then((res) => {
@ -142,10 +102,12 @@ const List: React.FC<ListProps> = ({ tab, path, setPath, data, setData }) => {
});
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [path, t]);
if (loading) {
return <p>{t("loading")}</p>; // Translating the loading message
return <ShimmerLoader />; // Translating the loading message
}
if (error) {
@ -154,36 +116,6 @@ const List: React.FC<ListProps> = ({ tab, path, setPath, data, setData }) => {
return (
<div>
{tab === "Today" && (
<div className="flex items-center flex-col">
<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="absolute inset-0 rounded-xl bg-gradient-to-r from-[#FE7F781B] to-[#EA6D564D] opacity-50"></div>
<div className="relative w-full h-full bg-white rounded-xl flex items-center flex-col p-4">
<div className="absolute top-[-13px]">
<div className="relative w-40 flex justify-center items-center mb-6">
<Image
src={DayContainer}
alt="Day Container"
className="rounded-full w-full h-full object-cover"
/>
<p className="absolute text-center text-[#EE755F] font-medium text-sm">
{t("todayDhikr")}
</p>{" "}
{/* Translating the title */}
</div>
</div>
<div className="text-2xl mt-1 text-[#292524] mb-2 leading-relaxed">
{colorizeVowels(currentDhikr)}
</div>
<p className="text-sm text-[#8B8B8B]">{currentTranslation}</p>
</div>
</div>
</div>
)}
{data?.data?.length ? (
data.data.map((item: Category | Dua) => {
if (data.type === "Categories") {
@ -253,4 +185,4 @@ const List: React.FC<ListProps> = ({ tab, path, setPath, data, setData }) => {
);
};
export default List;
export default Categories;

83
src/components/sidebar/famous.tsx

@ -0,0 +1,83 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
"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 { useRouter } from "next/navigation";
import {Dua } from "../utils/types";
import { useTranslation } from "next-i18next"; // Importing the translation hook
import ShimmerLoader from "../ui/list-loading";
const Famous: React.FC = () => {
const { t } = useTranslation("common"); // Initialize translation hook for "common" namespace
const [data, setData] = useState<{ type: string; data: Dua[] } | null>(null);
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null); // State to hold error messages
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}`);
};
useEffect(() => {
setLoading(true);
setError(null); // Reset error state before starting the request
http
.get("web/mafatih-duas/?famous=true")
.then((res) => {
setLoading(false);
setData({ type: "Famous", data: res.data.results });
})
.catch((error) => {
setError(t("error.fetchTodayDuas"));
console.error("Error fetching today's duas:", error);
});
}, [setData, t]);
if (loading) {
return <ShimmerLoader/>; // Translating the loading message
}
if (error) {
return <p>{error}</p>; // Displaying the error message
}
return (
<div>
{data?.data?.length ? (
data.data.map((dua: Dua) => {
return (
<div
className="flex justify-between p-3 bg-white my-4 rounded-2xl cursor-pointer"
key={dua.id}
onClick={() => openDua(dua)}
>
<div className="flex items-center gap-2">
<p>{dua.title}</p>
</div>
{dua.not_synced && (
<div className="flex items-center p-3 bg-[#EBEBEB] rounded-lg">
<Image src={Audio} alt="audio available" />
</div>
)}
</div>
);
})
) : (
<div className="flex my-[40%] items-center justify-center">
<Image src={NoData} alt="no data" />
</div>
)}
</div>
);
};
export default Famous;

137
src/components/sidebar/nearby.tsx

@ -0,0 +1,137 @@
import http from "@/api/http";
import { FaLocationCrosshairs } from "react-icons/fa6";
import { useState } from "react";
import { Dua } from "../utils/types";
import { useRouter } from "next/router";
import Image from "next/image";
import NoData from "../../../public/assets/images/noduas.svg";
import Audio from "../../../public/assets/images/Icon ionic-md-musical-notes.svg";
import ShimmerLoader from "../ui/list-loading";
const NearBy = () => {
const [data, setData] = useState<{ type: string; data: Dua[] } | null>(null);
const [loading, setLoading] = useState(false); // For managing loading state
const [error, setError] = useState<string | null>(null); // For error handling
const [fetched, setFetched] = useState(false);
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}`);
};
const onClick = () => {
// Check if geolocation is available
if (navigator.geolocation) {
setLoading(true);
setError(null); // Reset error state before attempting to get location
navigator.geolocation.getCurrentPosition(
(position) => {
const { latitude, longitude } = position.coords;
// Make the API request with the user's latitude and longitude
http
.get(`web/mafatih-duas/?lat=${latitude}&lon=${longitude}`)
.then((res) => {
console.log("Nearby Duas:", res.data); // Handle the response here
setData({ type: "Famous", data: res.data.results });
setFetched(true);
})
.catch((err) => {
setError("Failed to fetch Duas. Please try again later.");
console.error("Error fetching Duas:", err);
})
.finally(() => {
setLoading(false);
});
},
(error) => {
// Handle errors related to geolocation
setLoading(false);
setError(
"Failed to get your location. Please allow location access."
);
console.error("Geolocation Error:", error);
}
);
} else {
setError("Geolocation is not supported by your browser.");
}
};
return (
<div className="h-[calc(100%-70px)]">
{data?.data?.length ? (
<>
<div className="flex flex-col items-center justify-center mt-6">
<p className="text-sm font-normal">
Discover Location-Based Du&apos;as
</p>
<button
className="flex items-center justify-center gap-2 bg-[#F4846F] w-48 h-10 rounded-lg my-4 text-white"
onClick={onClick}
>
<FaLocationCrosshairs size={24} />
<p>Find Du&apos;as Near Me</p>
</button>
</div>
{data.data.map((dua: Dua) => {
return (
<div
className="flex justify-between p-3 bg-white my-4 rounded-2xl cursor-pointer"
key={dua.id}
onClick={() => openDua(dua)}
>
<div className="flex items-center gap-2">
<p>{dua.title}</p>
</div>
{dua.not_synced && (
<div className="flex items-center p-3 bg-[#EBEBEB] rounded-lg">
<Image src={Audio} alt="audio available" />
</div>
)}
</div>
);
})}
</>
) : loading ? <ShimmerLoader/> : fetched ? (
<>
<div className="flex flex-col items-center justify-center mt-6">
<p className="text-sm font-normal">
Discover Location-Based Du&apos;as
</p>
<button
className="flex items-center justify-center gap-2 bg-[#F4846F] w-48 h-10 rounded-lg my-4 text-white"
onClick={onClick}
>
<FaLocationCrosshairs size={24} />
<p>Find Du&apos;as Near Me</p>
</button>
</div>
<div className="flex flex-col my-[40%] items-center justify-center">
<Image src={NoData} alt="no data" />
<p className="text-[#4D4D4D] text-sm font-normal">No Du&apos;as available for this location</p>
</div>
</>
) : (
<div className="flex flex-col items-center h-full justify-center mt-6">
<p className="text-sm font-normal">
Discover Location-Based Du&apos;as
</p>
<button
className="flex items-center justify-center gap-2 bg-[#F4846F] w-48 h-10 rounded-lg my-4 text-white"
onClick={onClick}
>
<FaLocationCrosshairs size={24} />
<p>Find Du&apos;as Near Me</p>
</button>
</div>
)}
</div>
);
};
export default NearBy;

21
src/components/sidebar/tabs.tsx

@ -3,6 +3,11 @@ import List from "./list";
import { FaArrowLeft } from "react-icons/fa6";
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";
const tabs = [
{ name: "Categories" },
@ -100,13 +105,25 @@ const Tabs = () => {
</ul>
</div>
)}
<List
{
selected.name === "Categories" && <Categories path={path} data={data} setData={setData} setPath={setPath} />
}
{
selected.name === "Famous" && <Famous/>
}
{
selected.name === "near_by" && <NearBy/>
}
{
selected.name === "Today" && <Today/>
}
{/* <List
tab={selected.name}
data={data}
setData={setData}
path={path}
setPath={setPath}
/>
/> */}
</>
);
};

146
src/components/sidebar/today.tsx

@ -0,0 +1,146 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
"use client";
import http from "@/api/http";
import Image from "next/image";
import DayContainer from "../../../public/assets/images/Vector.svg";
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 { useRouter } from "next/navigation";
import colorizeVowels from "../utils/colorize-vowels";
import { formatHijriDate } from "../utils/date-formaters";
import { Dua } from "../utils/types";
import { useTranslation } from "next-i18next"; // Importing the translation hook
import ShimmerLoader from "../ui/list-loading";
const Today: React.FC = () => {
const { t } = useTranslation("common"); // Initialize translation hook for "common" namespace
const [data, setData] = useState<{ type: string; data: Dua[] } | null>(null);
const [loading, setLoading] = useState<boolean>(false);
const [currentDhikr, setCurrentDhikr] = useState<string>("");
const [currentTranslation, setCurrentTranslation] = useState<string>("");
const [error, setError] = useState<string | null>(null); // State to hold error messages
const router = useRouter();
const today = new Date();
const dayOfWeek = new Intl.DateTimeFormat("en-US", {
weekday: "long",
}).format(today);
let locale: string;
useEffect(() => {
locale = localStorage.getItem("locale") || "en";
setLoading(true);
setError(null); // Reset error state before starting the request
http
.get("web/mafatih-duas/dhikrs/?today=true")
.then((res) => {
const dhikrForToday = res.data.find(
(item: any) => item.day === dayOfWeek
);
if (dhikrForToday) {
setCurrentDhikr(dhikrForToday.text);
setCurrentTranslation(
dhikrForToday.translation[locale] || dhikrForToday.translation.en
);
}
setLoading(false);
})
.catch((error) => {
setLoading(false); // Ensure loading state is false if request fails
setError(t("error.fetchDhikrData")); // Display a translated error message
console.error("Error fetching Dhikr data:", error);
});
}, [dayOfWeek, locale, t]);
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(() => {
setData({ type: null, data: [] });
setError(null); // Reset error state before starting the request
http
.get("web/mafatih-duas/?today=true")
.then((res) => {
setData({ type: "Today", data: res.data.results });
})
.catch((error) => {
setError(t("error.fetchTodayDuas"));
console.error("Error fetching today's duas:", error);
});
}, []);
if (loading) {
return <ShimmerLoader/>; // Translating the loading message
}
if (error) {
return <p>{error}</p>; // Displaying the error message
}
return (
<div>
<div className="flex items-center flex-col">
<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="absolute inset-0 rounded-xl bg-gradient-to-r from-[#FE7F781B] to-[#EA6D564D] opacity-50"></div>
<div className="relative w-full h-full bg-white rounded-xl flex items-center flex-col p-4">
<div className="absolute top-[-13px]">
<div className="relative w-40 flex justify-center items-center mb-6">
<Image
src={DayContainer}
alt="Day Container"
className="rounded-full w-full h-full object-cover"
/>
<p className="absolute text-center text-[#EE755F] font-medium text-sm">
{t("todayDhikr")}
</p>{" "}
{/* Translating the title */}
</div>
</div>
<div className="text-2xl mt-1 text-[#292524] mb-2 leading-relaxed">
{colorizeVowels(currentDhikr)}
</div>
<p className="text-sm text-[#8B8B8B]">{currentTranslation}</p>
</div>
</div>
</div>
{data?.data?.length ? (
data.data.map((dua: Dua) => {
return (
<div
className="flex justify-between p-3 bg-white my-4 rounded-2xl cursor-pointer"
key={dua.id}
onClick={() => openDua(dua)}
>
<div className="flex items-center gap-2">
<p>{dua.title}</p>
</div>
{dua.not_synced && (
<div className="flex items-center p-3 bg-[#EBEBEB] rounded-lg">
<Image src={Audio} alt="audio available " />
</div>
)}
</div>
);
})
) : (
<div className="flex my-[40%] items-center justify-center">
<Image src={NoData} alt="no data" />
</div>
)}
</div>
);
};
export default Today;

30
src/components/ui/list-loading.tsx

@ -0,0 +1,30 @@
import React from 'react';
import ContentLoader from 'react-content-loader';
const ShimmerLoader = () => {
return (
<ContentLoader
className='mt-3'
speed={2}
width={365}
height={10 * 48 + 12 * 9} // 10 rectangles with 12px margin
viewBox="0 0 365 10*48 + 9*12"
backgroundColor="#f0f0f0"
foregroundColor="#ecebeb"
>
{Array.from({ length: 10 }).map((_, index) => (
<rect
key={index}
x="0"
y={index * (48 + 12)} // Adjust Y position for each rectangle with 12px margin
rx="16" // Set border-radius to 16px
ry="16" // Set border-radius to 16px
width="365"
height="48"
/>
))}
</ContentLoader>
);
};
export default ShimmerLoader;

13
src/components/utils/ga.js

@ -0,0 +1,13 @@
// utils/ga.js
import ReactGA from "react-ga4";
// Initialize Google Analytics with the provided Measurement ID
export const initGA = (measurementId) => {
ReactGA.initialize(measurementId);
};
// Log page view in Google Analytics
export const logPageView = () => {
ReactGA.send("pageview");
};

10
src/components/utils/types.ts

@ -75,3 +75,13 @@ export interface ListProps {
data: any; // Adjust this type as needed
setData: React.Dispatch<React.SetStateAction<any>>; // Adjust this type as needed
}
export interface CategoriesProps {
path: Path[]; // Expecting an array of Path objects
setPath: React.Dispatch<React.SetStateAction<Path[]>>; // A setter for the Path[] array
data: any; // Adjust this type as needed
setData: React.Dispatch<React.SetStateAction<any>>; // Adjust this type as needed
}
export interface FamousProps {
data: any; // Adjust this type as needed
setData: React.Dispatch<React.SetStateAction<any>>; // Adjust this type as needed
}

13
src/pages/_app.tsx

@ -1,3 +1,4 @@
import DefaultSeo from "@/components/common/default-seo";
import { AudioProvider } from "@/components/context/audio-conext";
import { FontSettingsProvider } from "@/components/context/font-setting-context";
import { UIProvider } from "@/components/context/ui.context";
@ -15,21 +16,23 @@ import type { AppProps } from "next/app";
const rtlLanguages = ["ar", "ur", "fa", "ks", "tg", "bn"];
function App({ Component, pageProps }: AppProps) {
const isRtl = rtlLanguages.includes(pageProps?._nextI18Next?.initialLocale) || false;
const isRtl =
rtlLanguages.includes(pageProps?._nextI18Next?.initialLocale) || false;
return (
<FontSettingsProvider>
<UIProvider>
<AudioProvider>
<div dir={isRtl ? "rtl" : "ltr"}> {/* Apply rtl class if the language is RTL */}
<DefaultSeo />
<div dir={isRtl ? "rtl" : "ltr"}>
{" "}
{/* Apply rtl class if the language is RTL */}
<Header />
<MobileHeader />
<div className="m-auto lg:p-6">
<div className="flex m-auto max-w-[1440px] flex-col lg:flex-row lg:gap-6 relative">
<SideBar />
<main
className={`w-full`}>
<main className={`w-full`}>
<Component {...pageProps} />
</main>
</div>

2
src/pages/_document.tsx

@ -2,7 +2,7 @@ import { Html, Head, Main, NextScript } from "next/document";
export default function Document() {
return (
<Html lang="en">
<Html>
<Head />
<body className="antialiased text-black bg-[#F5F5F5] lg:bg-[#EAEAEA]">
<Main />

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

@ -14,6 +14,7 @@ import { useUI } from "@/components/context/ui.context";
import { useFontSettingsContext } from "@/components/context/font-setting-context";
import { useAudio } from "@/components/context/audio-conext";
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
import { DefaultSeo } from "next-seo";
// Define the Dua interface
interface Dua {
@ -51,53 +52,53 @@ const DuaComponent: React.FC<DuaComponentProps> = ({
// Use the shared context for font settings
const { fontSettings } = useFontSettingsContext();
// State hooks
const [duaParts, setDuaParts] = useState<Dua[]>([]);
const [recitingPart, setRecitingPart] = useState<Dua | null>(null);
const [loading, setLoading] = useState(false);
// Fetch Dua parts and audio
// Use useCallback to memoize fetchData so it doesn’t change between renders
const fetchData = useCallback(async (nextPage) => {
if (!slug || fetching) return; // Prevent fetching if data is already being fetched
const id = slug.split("-").pop();
if (!id) return;
setFetching(true); // Set fetching to true when data starts fetching
// Reset the duaParts state when the slug changes
const offset = nextPage ? duaParts.length : 0;
try {
setLoading(true);
// Fetch the data
const duaResponse = await http.get<DuaPartsResponse>(
`web/mafatih-duas/${id}/parts/?offset=${offset}`
);
// Append the new results to the existing duaParts
setDuaParts((prev) => [...prev, ...duaResponse.data.results]);
} catch (error) {
console.error("Error fetching Dua parts:", error);
} finally {
setLoading(false);
setFetching(false); // Reset fetching state after fetching is done
}
getAudio(id);
}, [slug, fetching, duaParts, getAudio]); // Dependencies for fetchData
// Use useCallback to memoize fetchData so it doesn’t change between renders
const fetchData = useCallback(
async (nextPage) => {
if (!slug || fetching) return; // Prevent fetching if data is already being fetched
const id = slug.split("-").pop();
if (!id) return;
setFetching(true); // Set fetching to true when data starts fetching
// Reset the duaParts state when the slug changes
const offset = nextPage ? duaParts.length : 0;
try {
setLoading(true);
// Fetch the data
const duaResponse = await http.get<DuaPartsResponse>(
`web/mafatih-duas/${id}/parts/?offset=${offset}`
);
// Append the new results to the existing duaParts
setDuaParts((prev) => [...prev, ...duaResponse.data.results]);
} catch (error) {
console.error("Error fetching Dua parts:", error);
} finally {
setLoading(false);
setFetching(false); // Reset fetching state after fetching is done
}
// Use the memoized fetchData in the effect hook
useEffect(() => {
setDuaParts([]); // Reset Dua parts when slug changes
fetchData(false);
}, [slug]); // Only slug changes should trigger this effect
getAudio(id);
},
[slug, fetching, duaParts, getAudio]
); // Dependencies for fetchData
// Use the memoized fetchData in the effect hook
useEffect(() => {
setDuaParts([]); // Reset Dua parts when slug changes
fetchData(false);
}, [slug]); // Only slug changes should trigger this effect
// Play audio from a specific part
const playAudio = useCallback(
@ -218,123 +219,144 @@ useEffect(() => {
}
}, [recitingPart, duaParts]);
const handleScroll = useCallback((e: React.UIEvent<HTMLDivElement>) => {
const target = e.currentTarget;
const isBottom = target.scrollHeight - target.scrollTop === target.clientHeight;
const handleScroll = useCallback(
(e: React.UIEvent<HTMLDivElement>) => {
const target = e.currentTarget;
const isBottom =
target.scrollHeight - target.scrollTop === target.clientHeight;
if (isBottom && !fetching) { // Only fetch when not already fetching
fetchData(true);
}
if (isBottom && !fetching) {
// Only fetch when not already fetching
fetchData(true);
}
setScrollPosition(target.scrollTop); // Update scroll position
}, [fetching]);
setScrollPosition(target.scrollTop); // Update scroll position
},
[fetching]
);
if (!slug) {
return null; // Handling the case where slug is not available
}
const title = processSlug(slug); // Title derived from slug
const description = "This is a detailed description for the specific Dua page."; // You can customize this further
const keywords = "dua, islam, prayer, supplication, dua parts"; // Keywords for SEO
return (
<div
onScroll={handleScroll} // Add the onScroll event listener here
className={`rounded-3xl overflow-y-auto flex-grow h-[calc(100vh-130px)] lg:bg-[#F5F5F5] lg:p-6 lg:rounded-3xl ${
!slug && "hidden lg:flex"
}`}
>
<div className="hidden justify-between items-center bg-gradient-to-r from-[#F79B59] to-[#EB6E57] p-6 rounded-3xl text-white lg:flex">
<button
onClick={handleBack}
className="bg-white/20 rounded-2xl p-[11px] lg:hidden"
>
<FaArrowLeft size={18} />
</button>
<p className="text-sm font-semibold">{processSlug(slug)}</p>
<div className="relative">
<>
<DefaultSeo title={title} description={description} keywords={keywords} />
<div
onScroll={handleScroll} // Add the onScroll event listener here
className={`rounded-3xl overflow-y-auto flex-grow h-[calc(100vh-130px)] lg:bg-[#F5F5F5] lg:p-6 lg:rounded-3xl ${
!slug && "hidden lg:flex"
}`}
>
<div className="hidden justify-between items-center bg-gradient-to-r from-[#F79B59] to-[#EB6E57] p-6 rounded-3xl text-white lg:flex">
<button
className="p-2 bg-white/20 rounded-2xl"
aria-label="Settings"
onClick={openSetting}
onClick={handleBack}
className="bg-white/20 rounded-2xl p-[11px] lg:hidden"
>
<Image width={24} height={24} src={SettingIcon} alt="Settings" />
<FaArrowLeft size={18} />
</button>
<SettingModal className="w-96 absolute right-0 rtl:right-auto rtl:left-0" />
<p className="text-sm font-semibold">{processSlug(slug)}</p>
<div className="relative">
<button
className="p-2 bg-white/20 rounded-2xl"
aria-label="Settings"
onClick={openSetting}
>
<Image width={24} height={24} src={SettingIcon} alt="Settings" />
</button>
<SettingModal className="w-96 absolute right-0 rtl:right-auto rtl:left-0" />
</div>
</div>
</div>
<div className="p-6">
{duaParts.map((item, index) => (
<div
key={item.id}
ref={(el) => {
partRefs.current[index] = el;
}}
className="p-1 rounded-3xl my-4"
style={{
background:
"linear-gradient(to right, rgba(254, 127, 120, 0.11), rgba(234, 109, 86, 0.3))",
borderRadius: "1.5rem",
padding: "1px",
}}
>
<div className="p-3 bg-white rounded-3xl">
{item.text && (
<div
className={`mb-4 text-right ${!fontSettings.arabic && "hidden"}`}
style={{
fontSize: `${25 * (fontSettings.arabicRange / 100)}px`,
}}
>
{colorizeVowels(item.text)}
</div>
)}
{item.local_alpha && (
<p
className={`text-sm font-normal mb-4 ${!fontSettings.transliteration && "hidden"}`}
style={{
fontSize: `${14 * (fontSettings.transliterationRange / 100)}px`,
}}
>
{item.local_alpha}
</p>
)}
{item.translation && (
<p
className={`text-sm font-normal border-t pt-4 ${!fontSettings.translation && "hidden"}`}
style={{
fontSize: `${14 * (fontSettings.translationRange / 100)}px`,
}}
>
{item.translation}
</p>
)}
{item.description && (
<p className="text-[#36363C] italic text-xs font-normal">
{item.description}
</p>
)}
{audio && !!Object.keys(audio)?.length && (
<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 className="p-6">
{duaParts.map((item, index) => (
<div
key={item.id}
ref={(el) => {
partRefs.current[index] = el;
}}
className="p-1 rounded-3xl my-4"
style={{
background:
"linear-gradient(to right, rgba(254, 127, 120, 0.11), rgba(234, 109, 86, 0.3))",
borderRadius: "1.5rem",
padding: "1px",
}}
>
<div className="p-3 bg-white rounded-3xl">
{item.text && (
<div
className={`mb-4 text-right ${
!fontSettings.arabic && "hidden"
}`}
style={{
fontSize: `${25 * (fontSettings.arabicRange / 100)}px`,
}}
>
{colorizeVowels(item.text)}
</div>
)}
{item.local_alpha && (
<p
className={`text-sm font-normal mb-4 ${
!fontSettings.transliteration && "hidden"
}`}
style={{
fontSize: `${
14 * (fontSettings.transliterationRange / 100)
}px`,
}}
>
{item.local_alpha}
</p>
)}
{item.translation && (
<p
className={`text-sm font-normal border-t pt-4 ${
!fontSettings.translation && "hidden"
}`}
style={{
fontSize: `${
14 * (fontSettings.translationRange / 100)
}px`,
}}
>
{item.translation}
</p>
)}
{item.description && (
<p className="text-[#36363C] italic text-xs font-normal">
{item.description}
</p>
)}
{audio && !!Object.keys(audio)?.length && (
<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>
{audio && Object.keys(audio).length > 0 && (
<audio ref={audioRef} src={audio.audio} preload="auto" />
)}
</div>
{audio && Object.keys(audio).length > 0 && (
<audio ref={audioRef} src={audio.audio} preload="auto" />
)}
</div>
</>
);
};

11
tailwind.config.ts

@ -8,6 +8,15 @@ export default {
],
theme: {
extend: {
keyframes: {
shimmer: {
'0%': { backgroundPosition: '-200% 0' },
'100%': { backgroundPosition: '200% 0' },
},
},
animation: {
shimmer: 'shimmer 1.5s infinite linear', // Add shimmer animation here
},
boxShadow: {
'header': '2px -10px 129px 35px #F4846F',
'inner-header': '2px -20px 15px 30px rgba(244,132,111,1);',
@ -26,7 +35,7 @@ export default {
addUtilities(
{
'.text-rendering-optimize-speed': {
'text-rendering': 'geometricPrecision',
'text-rendering': 'optimizeSpeed',
},
'.no-ligatures': {
'font-variant-ligatures': 'none', // Add this utility

Loading…
Cancel
Save