Browse Source

feat: implement RTL support, adjust layout for RTL languages, and remove unused Last Reads page

master
sina_sajjadi 2 weeks ago
parent
commit
4f601aa1c4
  1. 111
      src/components/modals/languages-modal.tsx
  2. 65
      src/components/sidebar/list.tsx
  3. 11
      src/components/sticky-components/audio-controls.tsx
  4. 39
      src/components/ui/range-input.tsx
  5. 33
      src/pages/_app.tsx
  6. 2
      src/pages/duas/[slug].tsx
  7. 69
      src/pages/last-reads.tsx

111
src/components/modals/languages-modal.tsx

@ -3,12 +3,98 @@ import { IoMdClose } from "react-icons/io";
import { useUI } from "../context/ui.context";
import { FaCheckCircle } from "react-icons/fa";
import { useTranslation } from "next-i18next"; // Import useTranslation
import { useRouter } from "next/router";
interface Language {
code: string; // Assuming each language has a unique ID
name: string;
}
const data = [
{
"name": "English",
"code": "en"
},
{
"name": "Spanish",
"code": "es"
},
{
"name": "German",
"code": "de"
},
{
"name": "Uzbek",
"code": "uz"
},
{
"name": "Portuguese",
"code": "pt"
},
{
"name": "Bengali",
"code": "bn"
},
{
"name": "Chinese",
"code": "zh"
},
{
"name": "Azerbaijani",
"code": "az"
},
{
"name": "Urdu",
"code": "ur"
},
{
"name": "French",
"code": "fr"
},
{
"name": "Turkish",
"code": "tr"
},
{
"name": "Indonesian",
"code": "id"
},
{
"name": "Swahili",
"code": "sw"
},
{
"name": "Russian",
"code": "ru"
},
{
"name": "Arabic",
"code": "ar"
},
{
"name": "Tajik",
"code": "tg"
},
{
"name": "Persian",
"code": "fa"
},
{
"name": "Gujarati",
"code": "gu"
},
{
"name": "Kashmiri",
"code": "ks"
},
{
"name": "Hausa",
"code": "ha"
}
]
const LanguageModal = () => {
const [languages, setLanguages] = useState<Language[]>([]);
const [selectedLanguage, setSelectedLanguage] = useState<string>("");
@ -16,18 +102,19 @@ const LanguageModal = () => {
const [error, setError] = useState<string | null>(null);
const { closeModal } = useUI();
const { t } = useTranslation("common"); // Initialize useTranslation hook for common.json
const router = useRouter();
useEffect(() => {
const fetchLanguages = async () => {
try {
setIsLoading(true);
const response = await http.get("v1/languages/");
setLanguages(response.data);
} catch (err) {
setError(t("failed_to_load_languages")); // Use translation for error message
} finally {
// try {
// setIsLoading(true);
// const response = await http.get("v1/languages/");
setLanguages(data);
// } catch (err) {
// setError(t("failed_to_load_languages")); // Use translation for error message
// } finally {
setIsLoading(false);
}
// }
};
fetchLanguages();
@ -39,8 +126,12 @@ const LanguageModal = () => {
}, [t]); // Add t as dependency to ensure it updates with language change
const handleLanguageSelect = (code: string) => {
setSelectedLanguage(code);
localStorage.setItem("locale", code); // Persist selection in localStorage
router.push(router.asPath, undefined, { locale: code }).then(() => {
// Optionally you can trigger the language change effect after the page is pushed
setSelectedLanguage(code);
localStorage.setItem("locale", code); // Save the selected language in localStorage
window.location.reload();
});
};
if (isLoading) {

65
src/components/sidebar/list.tsx

@ -13,13 +13,14 @@ 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 { useTranslation } from "next-i18next"; // Importing the translation hook
import { useTranslation } from "next-i18next"; // Importing the translation hook
const List: React.FC<ListProps> = ({ tab, 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", {
@ -32,6 +33,7 @@ const List: React.FC<ListProps> = ({ tab, path, setPath, data, setData }) => {
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) => {
@ -47,11 +49,12 @@ const List: React.FC<ListProps> = ({ tab, path, setPath, data, setData }) => {
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);
setLoading(false);
});
}
}, [tab, dayOfWeek, locale]);
}, [tab, dayOfWeek, locale, t]);
const openCategory = (category: Category) => {
setData({ type: "children", data: category.children });
@ -71,39 +74,49 @@ const List: React.FC<ListProps> = ({ tab, path, setPath, data, setData }) => {
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);
});
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 });
});
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]);
}, [setData, setPath, tab, t]);
useEffect(() => {
if (data.data.length) {
const filteredData = data.data.filter((item: any) => !item.title); // Exclude objects with a "title"
filteredData.forEach((category: Category) => {
// Filter out children that do not have a title
const hasDua = category.children.filter((item: any) => !item.title);
// Make an HTTP GET request to fetch duas for the current category
http
.get(`web/mafatih-duas/?category=${category.id}`)
.then((res) => {
console.log(res);
let duas = res.data.results;
if (duas.length) {
// Deduplicate duas based on their 'id'
const uniqueDuasMap = new Map();
duas.forEach((dua: any) => {
if (!uniqueDuasMap.has(dua.id)) {
@ -111,20 +124,17 @@ const List: React.FC<ListProps> = ({ tab, path, setPath, data, setData }) => {
}
});
// Convert the Map back to an array of unique duas
duas = Array.from(uniqueDuasMap.values());
// Assign the combined array to category.children
category.children = [...hasDua, ...duas];
// Update the state with the modified filteredData
setData((prevState: Category) => ({
...prevState,
data: filteredData, // Update state with the updated filtered data
data: filteredData,
}));
}
})
.catch((error) => {
setError(t("error.fetchDuasForCategory"));
console.error(
`Error fetching duas for category ${category.id}:`,
error
@ -132,10 +142,14 @@ const List: React.FC<ListProps> = ({ tab, path, setPath, data, setData }) => {
});
});
}
}, [path]);
}, [path, t]);
if (loading) {
return <p>{t("loading")}</p>; {/* Translating the loading message */}
return <p>{t("loading")}</p>; // Translating the loading message
}
if (error) {
return <p>{error}</p>; // Displaying the error message
}
return (
@ -155,7 +169,8 @@ const List: React.FC<ListProps> = ({ tab, path, setPath, data, setData }) => {
/>
<p className="absolute text-center text-[#EE755F] font-medium text-sm">
{t("todayDhikr")}
</p> {/* Translating the title */}
</p>{" "}
{/* Translating the title */}
</div>
</div>

11
src/components/sticky-components/audio-controls.tsx

@ -16,12 +16,17 @@ const AudioControls = () => {
const [isPlaying, setIsPlaying] = useState(false);
const [currentTime, setCurrentTime] = useState(0);
const [duration, setDuration] = useState(1); // Default duration to 1 to prevent division by zero
const [isRtl, setIsRtl] = useState(false);
const params = useParams();
const slug = params?.slug as string;
// Initialize the translation hook
const { t } = useTranslation("common");
useEffect(() => {
const storedLocale = localStorage.getItem('locale');
const rtlLanguages = ['ar', 'ur', 'fa', 'ks', 'tg', 'bn'];
setIsRtl(rtlLanguages.includes(storedLocale ?? ""));
}, []);
const play = () => {
if (audioRef.current && audio) {
if (lastPart && audio.audio_sync_data.length > 0) {
@ -72,6 +77,10 @@ const AudioControls = () => {
function convertTo180(numerator, denominator) {
if (!numerator || !denominator) return -20; // Safeguard for invalid inputs
const newNumerator = (numerator * 200) / denominator;
if (isRtl) {
return -newNumerator; // Adjust to start from -20 degrees
}
return newNumerator; // Adjust to start from -20 degrees
}
function processSlug(slug: string): string {

39
src/components/ui/range-input.tsx

@ -1,4 +1,4 @@
import React from 'react';
import React, { useEffect, useState } from "react";
interface RangeInputProps {
value: number;
@ -6,12 +6,33 @@ interface RangeInputProps {
disabled?: boolean;
}
const RangeInput: React.FC<RangeInputProps> = ({ value, onChange, disabled = false }) => {
const RangeInput: React.FC<RangeInputProps> = ({
value,
onChange,
disabled = false,
}) => {
// State for detecting RTL
const [isRtl, setIsRtl] = useState(false);
useEffect(() => {
// Check language from localStorage
const storedLocale = localStorage.getItem("locale");
// Define RTL languages
const rtlLanguages = ["ar", "ur", "fa", "ks", "tg", "bn"];
// Set RTL state if the stored locale is in the RTL languages
if (storedLocale && rtlLanguages.includes(storedLocale)) {
setIsRtl(true);
} else {
setIsRtl(false);
}
}, []);
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
onChange(Number(event.target.value));
};
// Calculate the normalized value for RTL languages
return (
<div className="flex items-center">
<input
@ -23,25 +44,27 @@ const RangeInput: React.FC<RangeInputProps> = ({ value, onChange, disabled = fal
disabled={disabled}
className={`w-28 h-[2px] outline-none transition-opacity duration-150 ease-in-out mr-3`}
style={{
appearance: 'none',
appearance: "none",
opacity: disabled ? 0.5 : 1,
background: `linear-gradient(to right, #F4846F ${(value - 80) / 1.2}%, #EBEBEB ${(value - 80) / 1.2}%)`,
background: isRtl
? `linear-gradient(to left, #F4846F ${ (value - 80) / 1.2 }%, #EBEBEB ${(value - 80) / 1.2}%)`
: `linear-gradient(to right, #F4846F ${(value - 80) / 1.2}%, #EBEBEB ${(value - 80) / 1.2}%)`,
}}
/>
<span className="text-[#7D8394] text-xs font-normal w-8">{value}%</span>
<style jsx>{`
input[type='range']::-webkit-slider-thumb {
input[type="range"]::-webkit-slider-thumb {
appearance: none;
width: 14px;
height: 14px;
background: #F4846F;
background: #f4846f;
border-radius: 50%;
cursor: pointer;
}
input[type='range']::-moz-range-thumb {
input[type="range"]::-moz-range-thumb {
width: 20px;
height: 20px;
background: #F4846F;
background: #f4846f;
border-radius: 50%;
cursor: pointer;
}

33
src/pages/_app.tsx

@ -11,30 +11,37 @@ import "@/styles/globals.css";
import { appWithTranslation } from "next-i18next";
import type { AppProps } from "next/app";
// List of RTL languages
const rtlLanguages = ["ar", "ur", "fa", "ks", "tg", "bn"];
function App({ Component, pageProps }: AppProps) {
console.log(pageProps);
const isRtl = rtlLanguages.includes(pageProps?._nextI18Next?.initialLocale) || false;
console.log(isRtl);
return (
<FontSettingsProvider>
<UIProvider>
<AudioProvider>
<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`}>
<Component {...pageProps} />
</main>
<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`}>
<Component {...pageProps} />
</main>
</div>
<StickyComponents />
<MobileNavigation />
</div>
<StickyComponents />
<MobileNavigation />
<ManagedModal />
</div>
<ManagedModal />
</AudioProvider>
</UIProvider>
</FontSettingsProvider>
);
}
export default appWithTranslation(App);

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

@ -233,7 +233,7 @@ const DuaComponent: React.FC<DuaComponentProps> = ({
>
<Image width={24} height={24} src={SettingIcon} alt="Settings" />
</button>
<SettingModal className="w-96 absolute right-0" />
<SettingModal className="w-96 absolute right-0 rtl:right-auto rtl:left-0" />
</div>
</div>

69
src/pages/last-reads.tsx

@ -1,69 +0,0 @@
import Image from "next/image";
import { useEffect, useState } from "react";
import Audio from "../../public/assets/images/Icon ionic-md-musical-notes.svg";
import NoData from "../../public/assets/images/Untitled-1-02.svg";
import DuaComponent from "./duas/[slug]";
import { Dua } from "@/components/utils/types";
const LastReads = () => {
const [lastDuas, setLastDuas] = useState([]);
const [SelectedDua, setSelectedDua] = useState("");
const openDua = (dua: Dua) => {
const slug = dua.title.toLowerCase().replaceAll(" ", "-");
setSelectedDua(`${slug}-${dua.id}`);
};
useEffect(() => {
const local = localStorage.getItem("last-read");
if (local) {
setLastDuas(JSON.parse(local));
} else {
setLastDuas([]); // Set default value if the item does not exist
}
}, []);
return (
<div className="flex flex-col lg:flex-row gap-6 ">
<aside
className={`w-full h-[calc(100vh-130px)] self-start overflow-auto rounded-3xl p-6 lg:max-w-[430px] lg:bg-[#F5F5F5]
${SelectedDua && "hidden lg:block"}`}
>
{lastDuas?.map((dua: Dua) => (
<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>
))}
</aside>
{SelectedDua ? (
<DuaComponent
setSelectedDua={setSelectedDua}
SelectedDua={SelectedDua}
/>
) : (
<div
className={`flex-grow w-full items-center justify-center h-[calc(100vh-130px)] bg-[#F5F5F5] lg:p-6 lg:rounded-3xl hidden lg:flex`}
>
<Image src={NoData} alt="no data" />
</div>
)}
</div>
);
};
export default LastReads;
// ${
// slug && "hidden lg:block"
Loading…
Cancel
Save