Browse Source
feat:implement shimmer loading component, and enhance SEO with default settings
master
feat:implement shimmer loading component, and enhance SEO with default settings
master
sina_sajjadi
2 weeks ago
16 changed files with 785 additions and 278 deletions
-
76package-lock.json
-
5package.json
-
9public/assets/images/noduas.svg
-
59src/components/common/default-seo.tsx
-
144src/components/sidebar/categories.tsx
-
83src/components/sidebar/famous.tsx
-
137src/components/sidebar/nearby.tsx
-
21src/components/sidebar/tabs.tsx
-
146src/components/sidebar/today.tsx
-
30src/components/ui/list-loading.tsx
-
13src/components/utils/ga.js
-
10src/components/utils/types.ts
-
13src/pages/_app.tsx
-
2src/pages/_document.tsx
-
302src/pages/duas/[slug].tsx
-
11tailwind.config.ts
@ -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> |
@ -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; |
@ -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; |
@ -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'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'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'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'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'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'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'as Near Me</p> |
||||
|
</button> |
||||
|
</div> |
||||
|
)} |
||||
|
</div> |
||||
|
); |
||||
|
}; |
||||
|
|
||||
|
export default NearBy; |
@ -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; |
@ -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; |
@ -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"); |
||||
|
}; |
Write
Preview
Loading…
Cancel
Save
Reference in new issue