Skip to content

Commit

Permalink
feat: quran reader infinite scroll and sync with audio
Browse files Browse the repository at this point in the history
  • Loading branch information
aacmal committed Jun 25, 2023
1 parent 0b05e91 commit 3b3d0ad
Show file tree
Hide file tree
Showing 10 changed files with 231 additions and 61 deletions.
16 changes: 15 additions & 1 deletion src/app/quran/surah/[chapterId]/[ayahId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { getSpecificVerse } from '@utils/verse';
import { ArrowIcon } from '@components/icons';
import QuranReader from '@components/quranReader/QuranReader';
import VerseSkeleton from '@components/quranReader/VerseSkeleton';
import Verses from '@components/quranReader/Verses';

const SingleAyahPage = () => {
const params = useParams();
Expand Down Expand Up @@ -49,7 +50,20 @@ const SingleAyahPage = () => {
<span>Kembali ke surah</span>
</Link>
</div>
{!isLoading ? <QuranReader versesData={data} /> : <VerseSkeleton />}
{!isLoading ? (
<div className="mt-3 text-justify">
<Verses
key={data.id}
id={data.id}
verse_number={data.verse_number}
translations={data.translations}
text_uthmani={data.text_uthmani}
verse_key={data.verse_key}
/>
</div>
) : (
<VerseSkeleton />
)}
</Wrapper>
);
};
Expand Down
9 changes: 7 additions & 2 deletions src/app/quran/surah/[chapterId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react';

import { notFound } from 'next/navigation';
import { Metadata } from 'next';
import { getAllVerseByChapter } from '@utils/verse';
import { getVersesByChapter } from '@utils/verse';
import { getAllChaptersData, getChapter, getChapterInfo } from '@utils/chapter';
import Wrapper from '@components/Wrapper';
import ChapterBanner from '@components/Banner/ChapterBanner';
Expand Down Expand Up @@ -49,7 +49,10 @@ export async function generateMetadata({ params }): Promise<Metadata> {

export default async function SurahPage({ params }) {
const { chapterId: id } = params;
const chapterVerses = await getAllVerseByChapter({ chapterId: id });
const chapterVerses = await getVersesByChapter({
chapterId: id,
per_page: 20,
});
const chapterInfo = await getChapterInfo(id);
const chapterData = await getChapter(id);

Expand All @@ -67,6 +70,8 @@ export default async function SurahPage({ params }) {
<QuranReader
bismillahPre={chapterData.bismillah_pre}
versesData={chapterVerses.verses}
versesCount={chapterData.verses_count}
chapterId={chapterData.id}
/>
</Wrapper>
);
Expand Down
19 changes: 14 additions & 5 deletions src/components/AudioPlayer/AudioPlayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -220,21 +220,30 @@ const AudioPlayer = () => {
const highlightedElement = document.querySelector(
`[data-verse="${highlightedVerse}"]`
) as HTMLElement;
const verseYLocation = highlightedElement?.offsetTop;
window.scrollTo(0, verseYLocation - 200);

if (!highlightedElement) return;
highlightedElement.scrollIntoView({
behavior: 'smooth',
block: 'center',
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [highlightedVerse]);

useEffect(() => {
if (parseInt(params?.chapterId) !== audioId && autoScroll !== 'word')
return;
if (parseInt(params?.chapterId) === audioId && autoScroll === 'word') {
const highlightedElement = document.querySelector(
`[data-word="${highlightedWord}"]`
) as HTMLElement;
const wordYLocation = highlightedElement?.offsetTop;
window.scrollTo(0, wordYLocation - 200);
}

if (!highlightedElement) return;
highlightedElement.scrollIntoView({
behavior: 'smooth',
block: 'center',
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [highlightedWord]);

Expand Down
1 change: 0 additions & 1 deletion src/components/Hadith/HadithReader/DynamicHadithData.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ const DynamicHadithsData = ({ totalData, id }: Props) => {
(item) => item.number - LIMIT - 1 === index + 1
);

console.log('dataIndex', dataIndex, index);
if (!data[dataIndex]) {
return <VerseSkeleton />;
}
Expand Down
9 changes: 6 additions & 3 deletions src/components/TopBar/TopBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,12 @@ const TopBar = () => {
const pathname = usePathname();
console.log(pathname);

const { currentChapter } = useQuranReader((state) => ({
currentChapter: state.currentChapter,
}));
const { currentChapter } = useQuranReader(
(state) => ({
currentChapter: state.currentChapter,
}),
shallow
);

useEffect(() => {
if (chapterData.length > 0) return;
Expand Down
125 changes: 125 additions & 0 deletions src/components/quranReader/DynamicSurahVerse.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
'use client';

import { Verse, VersePagination } from '@utils/types/Verse';
import React, { useEffect, useRef, useState } from 'react';
import { ListItem, ListRange, Virtuoso, VirtuosoHandle } from 'react-virtuoso';
import VerseSkeleton from './VerseSkeleton';
import Verses from './Verses';
import { getVersesByChapter } from '@utils/verse';
import useQuranReader from '@stores/quranReaderStore';
import { shallow } from 'zustand/shallow';
import useSettings from '@stores/settingsStore';

type Props = {
totalData: number;
chapterId: number;
};

const START_AYAH = 21;
const LIMIT = 20;
const DynamicSurahVerse = ({ totalData, chapterId }: Props) => {
const [data, setData] = useState<Verse[]>([]);
const [paginationData, setPaginationData] = useState<VersePagination>();
const [itemsRendered, setItemsRendered] = useState<ListItem<any>[]>();

const ref = useRef<VirtuosoHandle>(null);
const { highlightedWord, highlightedVerse } = useQuranReader(
(state) => ({
highlightedWord: state.highlightedWord,
highlightedVerse: state.highlightedVerse,
}),
shallow
);
const autoScroll = useSettings((state) => state.autoScroll, shallow);

const loadMoreData = async (index: ListRange) => {
const page = Math.floor(index.endIndex / LIMIT) + 2; // 2 because page start from 2

if (!!data[index.endIndex] || page > paginationData?.total_pages) {
return;
}

const res = await getVersesByChapter({
chapterId,
page,
per_page: LIMIT,
});

setPaginationData(res.pagination);
setData((prev) => {
// remove duplicate data and sort by verse_number
const newData = res.verses.filter(
(item) => !prev.find((prevItem) => prevItem.id === item.id)
);
return [...prev, ...newData].sort(
(a, b) => a.verse_number - b.verse_number
);
});
};

useEffect(() => {
if (!highlightedVerse || !ref.current || !autoScroll) return;
const idAndVerse = highlightedVerse.split(':');
const verseNumber = parseInt(idAndVerse[1]);
const chapterNumber = parseInt(idAndVerse[0]);
if (chapterNumber !== chapterId) return;

const isVerseRendered = itemsRendered?.find(
(item) => item.index === verseNumber - LIMIT - 1
);

if (verseNumber <= LIMIT || !!isVerseRendered) return;
ref.current.scrollToIndex(verseNumber - LIMIT - 1);

// eslint-disable-next-line react-hooks/exhaustive-deps
}, [highlightedWord]);

useEffect(() => {
function getInitialData() {
getVersesByChapter({
chapterId,
page: 2,
per_page: LIMIT,
}).then((res) => {
setPaginationData(res.pagination);
setData(res.verses);
});
}

getInitialData();
}, []);

const renderRow = (index: number) => {
const dataIndex = data.findIndex(
(item) => item.verse_number - LIMIT - 1 === index
);

if (!data[dataIndex]) {
return <VerseSkeleton />;
}

return (
<Verses
id={data[dataIndex].id}
verse_number={data[dataIndex].verse_number}
translations={data[dataIndex].translations}
text_uthmani={data[dataIndex].text_uthmani}
verse_key={data[dataIndex].verse_key}
words={data[dataIndex].words}
/>
);
};

return (
<Virtuoso
totalCount={totalData - LIMIT}
useWindowScroll
itemContent={renderRow}
rangeChanged={loadMoreData}
itemsRendered={(items) => setItemsRendered(items)}
ref={ref}
/>
);
};

export default DynamicSurahVerse;
27 changes: 27 additions & 0 deletions src/components/quranReader/InitialSurahVerse.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Verse } from '@utils/types/Verse';
import React from 'react';
import Verses from './Verses';

type Props = {
versesData: Verse[];
};

const InitialSurahVerse = ({ versesData }: Props) => {
return (
<>
{versesData.map((verse) => (
<Verses
key={verse.id}
id={verse.id}
verse_number={verse.verse_number}
translations={verse.translations}
text_uthmani={verse.text_uthmani}
verse_key={verse.verse_key}
words={verse.words}
/>
))}
</>
);
};

export default InitialSurahVerse;
55 changes: 13 additions & 42 deletions src/components/quranReader/QuranReader.tsx
Original file line number Diff line number Diff line change
@@ -1,58 +1,29 @@
import Verses from './Verses';
import Bismillah from './Bismillah';
import { Verse } from '@utils/types/Verse';
import InitialSurahVerse from './InitialSurahVerse';
import DynamicSurahVerse from './DynamicSurahVerse';

type QuranReaderProps = {
versesData: Verse[] | Verse;
versesData: Verse[];
bismillahPre?: boolean;
versesCount: number;
chapterId: number;
};

const QuranReader = ({ versesData, bismillahPre }: QuranReaderProps) => {
// return versesData.map((e) => (
// <Verses
// key={e.id}
// id={e.id}
// verse_number={e.verse_number}
// translations={e.translations}
// text_uthmani={e.text_uthmani}
// verse_key={e.verse_key}
// // setTafsirData={setTafsirData}
// />
// ));

const QuranReader = ({
versesData,
bismillahPre,
versesCount,
chapterId,
}: QuranReaderProps) => {
return (
<div className="mt-3">
{/* <TafsirModal
isOpen={tafsirData.isOpen}
verseKey={tafsirData.verseKey}
verseId={tafsirData.verseId}
closeModal={() => setTafsirData({ ...tafsirData, isOpen: false })}
/> */}
<>
<Bismillah className={!bismillahPre && 'hidden'} />
<div className="text-justify mt-12">
{Array.isArray(versesData) ? (
versesData.map((e) => (
<Verses
key={e.id}
id={e.id}
verse_number={e.verse_number}
translations={e.translations}
text_uthmani={e.text_uthmani}
verse_key={e.verse_key}
words={e.words}
/>
))
) : (
<Verses
key={versesData.id}
id={versesData.id}
verse_number={versesData.verse_number}
translations={versesData.translations}
text_uthmani={versesData.text_uthmani}
verse_key={versesData.verse_key}
/>
)}
<InitialSurahVerse versesData={versesData} />
<DynamicSurahVerse totalData={versesCount} chapterId={chapterId} />
</div>
</>
</div>
Expand Down
13 changes: 13 additions & 0 deletions src/utils/types/Verse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,22 @@ export type Verse = {
words: VerseWord[];
};

export type VersePagination = {
per_page: number;
current_page: number;
total_pages: number;
total_count: number;
};

export type GetVerseParams = {
lang?: string;
words?: boolean;
per_page?: number;
chapterId?: number;
page?: number;
};

export type VersesByChapterResponse = {
verses: Verse[];
pagination: VersePagination;
};

0 comments on commit 3b3d0ad

Please sign in to comment.