├── src
├── utils
│ ├── types
│ │ ├── Api.ts
│ │ ├── Audio.ts
│ │ ├── Juz.ts
│ │ ├── Hadith.ts
│ │ ├── Chapter.ts
│ │ ├── Tafsir.ts
│ │ └── Verse.ts
│ ├── regex.ts
│ ├── fonts
│ │ ├── Lato-Regular.ttf
│ │ ├── al_qalam
│ │ │ └── alqalam.ttf
│ │ ├── me_quran
│ │ │ ├── me_quran-2.ttf
│ │ │ └── me_quran-2.woff2
│ │ ├── uthmanic_hafs
│ │ │ ├── UthmanicHafs1Ver18.ttf
│ │ │ └── UthmanicHafs1Ver18.woff2
│ │ ├── nastaleeq
│ │ │ └── indopak
│ │ │ │ ├── indopak-nastaleeq-waqf-lazim-v4.2.1.ttf
│ │ │ │ ├── indopak-nastaleeq-waqf-lazim-v4.2.1.woff
│ │ │ │ └── indopak-nastaleeq-waqf-lazim-v4.2.1.woff2
│ │ └── index.ts
│ ├── url.ts
│ ├── api.ts
│ ├── audio.ts
│ ├── juz.ts
│ ├── theme.ts
│ ├── tafsir.ts
│ ├── api
│ │ └── hadith.ts
│ ├── chapter.ts
│ ├── verse.ts
│ └── seo.ts
├── components
│ ├── icons
│ │ ├── type.ts
│ │ ├── XIcon.tsx
│ │ ├── ChevronIcon.tsx
│ │ ├── ListsIcon.tsx
│ │ ├── LoadingIcon
│ │ │ ├── AnimatedLoadingIcon.jsx
│ │ │ └── AnimatedLoadingIcon.module.css
│ │ ├── DownloadIcon.tsx
│ │ ├── BookmarkIcon.tsx
│ │ ├── RepeatIcon.tsx
│ │ ├── DotsIcon.tsx
│ │ ├── TrashIcon.tsx
│ │ ├── AdjustmentIcon.tsx
│ │ ├── ArrowIcon.tsx
│ │ ├── RewindIcon.tsx
│ │ ├── TafsirIcon.tsx
│ │ ├── PauseIcon.tsx
│ │ ├── InfoIcon.tsx
│ │ ├── StarIcon.tsx
│ │ ├── PlayIcon.tsx
│ │ ├── IconWrapper.tsx
│ │ ├── index.ts
│ │ ├── CopyIcon.tsx
│ │ ├── QuranIcon.tsx
│ │ └── HistoryIcon.tsx
│ ├── quranReader
│ │ ├── Arabic
│ │ │ ├── Word.module.css
│ │ │ └── Word.tsx
│ │ ├── InitialSurahVerse.tsx
│ │ ├── action
│ │ │ ├── CopyToClipboard.tsx
│ │ │ ├── HandleTafsir.tsx
│ │ │ └── HandleBookmark.tsx
│ │ ├── Switcher.tsx
│ │ ├── ScrollToAyah.tsx
│ │ ├── QuranReader.tsx
│ │ ├── VerseSkeleton.tsx
│ │ ├── Verses.tsx
│ │ ├── ArabicText.tsx
│ │ └── FetchInfiniteVerse.tsx
│ ├── Bookmark
│ │ ├── BookmarkHadithLists.tsx
│ │ ├── BookmarkedItem.tsx
│ │ ├── BookmarkedVerseLists.tsx
│ │ └── BookmarkWrapper.tsx
│ ├── Tafsir
│ │ ├── tafsirText.module.css
│ │ └── TafsirSkeleton.tsx
│ ├── Header
│ │ ├── ReadHadithHeader.tsx
│ │ ├── ReadQuranHeader.tsx
│ │ ├── ChapterHeader.tsx
│ │ └── index.tsx
│ ├── Container
│ │ └── index.tsx
│ ├── Wrapper
│ │ └── index.tsx
│ ├── TopBar
│ │ ├── DeveloperUtility
│ │ │ ├── AdjustmentWrapper.tsx
│ │ │ ├── OptionList.tsx
│ │ │ ├── TranslationOption.tsx
│ │ │ ├── AutoScroll.tsx
│ │ │ ├── Transliteration.tsx
│ │ │ ├── ThemeAdjustment.tsx
│ │ │ ├── LanguageAdjustment.tsx
│ │ │ ├── DeveloperUtility.tsx
│ │ │ └── FontAdjustment.tsx
│ │ ├── DropdownSurahLists
│ │ │ ├── VerseList.tsx
│ │ │ ├── ChapterLists.tsx
│ │ │ └── DropdownSurahLists.tsx
│ │ ├── TopBar.tsx
│ │ └── DropdownHadithLists
│ │ │ └── DropdownHadithLists.tsx
│ ├── chapters
│ │ ├── ChaptersView.tsx
│ │ ├── Card
│ │ │ ├── ChapterWrapper.tsx
│ │ │ ├── ChapterCardSkeleton.tsx
│ │ │ ├── JuzWrapper.tsx
│ │ │ └── ChapterCard.tsx
│ │ ├── index.tsx
│ │ └── JuzsView.tsx
│ ├── Banner
│ │ ├── HadithBanner.tsx
│ │ ├── ChapterBannerSkeleton.tsx
│ │ ├── HomeBanner.tsx
│ │ ├── BannerWrapper.tsx
│ │ ├── SurahInfo.tsx
│ │ └── ChapterBanner.tsx
│ ├── Hadith
│ │ ├── HadithReader
│ │ │ ├── InitialHadithData.tsx
│ │ │ ├── HadithReader.tsx
│ │ │ └── DynamicHadithData.tsx
│ │ ├── HadithCard.tsx
│ │ └── HadithVerse.tsx
│ ├── AudioPlayer
│ │ ├── PlayAudioButton.tsx
│ │ ├── PlaybackController
│ │ │ ├── PlaybackController.tsx
│ │ │ └── PlaybackOption.tsx
│ │ └── AudioPlayer.module.css
│ ├── Skeleton
│ │ ├── Skeleton.tsx
│ │ └── skeleton.module.css
│ ├── GoogleAnalytics
│ │ └── GoogleAnalytics.tsx
│ ├── Switch
│ │ └── index.tsx
│ ├── TransitionWrapper
│ │ └── TransitionWrapper.tsx
│ └── Search
│ │ └── index.tsx
├── app
│ ├── (main)
│ │ ├── surah
│ │ │ ├── page.tsx
│ │ │ ├── [chapterId]
│ │ │ │ ├── _hooks
│ │ │ │ │ └── scrollToTop.tsx
│ │ │ │ ├── [ayahId]
│ │ │ │ │ ├── loading.tsx
│ │ │ │ │ ├── layout.tsx
│ │ │ │ │ ├── page.tsx
│ │ │ │ │ ├── opengraph-image.tsx
│ │ │ │ │ └── twitter-image.tsx
│ │ │ │ ├── layout.tsx
│ │ │ │ ├── not-found.tsx
│ │ │ │ ├── info
│ │ │ │ │ └── page.tsx
│ │ │ │ ├── page.tsx
│ │ │ │ └── twitter-image.tsx
│ │ │ └── loading.tsx
│ │ ├── loading.tsx
│ │ ├── InitChapterData.tsx
│ │ ├── juz
│ │ │ ├── loading.tsx
│ │ │ ├── page.tsx
│ │ │ └── [juzId]
│ │ │ │ └── page.tsx
│ │ ├── layout.tsx
│ │ └── page.tsx
│ ├── robots.ts
│ ├── ThemeHandler.tsx
│ ├── ~offline
│ │ └── page.tsx
│ ├── sitemap.ts
│ ├── globals.css
│ └── layout.tsx
├── ~page-offline
│ ├── hadits
│ │ ├── [hadithId]
│ │ │ ├── layout.tsx
│ │ │ ├── handleClient.tsx
│ │ │ └── page.tsx
│ │ └── page.tsx
│ └── page.tsx
└── store
│ ├── surahStore.ts
│ ├── quranReaderStore.ts
│ ├── hadithStore.ts
│ └── settingsStore.ts
├── .npmrc
├── .eslintrc.json
├── public
├── googlea82d7709e5ffe685.html
├── favicon.ico
├── quranapp.jpg
├── icon-192x192.png
├── icon-256x256.png
├── icon-384x384.png
├── icon-512x512.png
├── images
│ └── read_quran.png
└── manifest.json
├── .vscode
└── settings.json
├── .prettierrc
├── postcss.config.js
├── .idea
├── .gitignore
├── vcs.xml
├── prettier.xml
├── jsLinters
│ └── eslint.xml
├── inspectionProfiles
│ └── Project_Default.xml
├── modules.xml
└── quran-app-nextjs.iml
├── data
├── chapter
│ └── type.ts
└── hadith
│ └── hadith.json
├── .dockerignore
├── next-env.d.ts
├── Dockerfile
├── .gitignore
├── package.json
├── tsconfig.json
├── next.config.js
└── README.md
/src/utils/types/Api.ts:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | auto-install-peers=true
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/public/googlea82d7709e5ffe685.html:
--------------------------------------------------------------------------------
1 | google-site-verification: googlea82d7709e5ffe685.html
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.tabSize": 2,
3 | "editor.formatOnSave": true,
4 | }
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aacmal/quran-app-nextjs/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/public/quranapp.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aacmal/quran-app-nextjs/HEAD/public/quranapp.jpg
--------------------------------------------------------------------------------
/src/utils/regex.ts:
--------------------------------------------------------------------------------
1 | export const removeHTMLTags = (str: string) => str.replace(/<[^>]*>?/gm, '');
2 |
--------------------------------------------------------------------------------
/public/icon-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aacmal/quran-app-nextjs/HEAD/public/icon-192x192.png
--------------------------------------------------------------------------------
/public/icon-256x256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aacmal/quran-app-nextjs/HEAD/public/icon-256x256.png
--------------------------------------------------------------------------------
/public/icon-384x384.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aacmal/quran-app-nextjs/HEAD/public/icon-384x384.png
--------------------------------------------------------------------------------
/public/icon-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aacmal/quran-app-nextjs/HEAD/public/icon-512x512.png
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "es5",
3 | "tabWidth": 2,
4 | "semi": true,
5 | "singleQuote": false
6 | }
--------------------------------------------------------------------------------
/public/images/read_quran.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aacmal/quran-app-nextjs/HEAD/public/images/read_quran.png
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/src/utils/fonts/Lato-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aacmal/quran-app-nextjs/HEAD/src/utils/fonts/Lato-Regular.ttf
--------------------------------------------------------------------------------
/src/utils/fonts/al_qalam/alqalam.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aacmal/quran-app-nextjs/HEAD/src/utils/fonts/al_qalam/alqalam.ttf
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Editor-based HTTP Client requests
5 | /httpRequests/
6 |
--------------------------------------------------------------------------------
/src/utils/fonts/me_quran/me_quran-2.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aacmal/quran-app-nextjs/HEAD/src/utils/fonts/me_quran/me_quran-2.ttf
--------------------------------------------------------------------------------
/src/components/icons/type.ts:
--------------------------------------------------------------------------------
1 | export type IconProps = {
2 | onClick?: () => void;
3 | className?: string;
4 | fill?: string;
5 | };
6 |
--------------------------------------------------------------------------------
/src/utils/fonts/me_quran/me_quran-2.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aacmal/quran-app-nextjs/HEAD/src/utils/fonts/me_quran/me_quran-2.woff2
--------------------------------------------------------------------------------
/src/utils/fonts/uthmanic_hafs/UthmanicHafs1Ver18.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aacmal/quran-app-nextjs/HEAD/src/utils/fonts/uthmanic_hafs/UthmanicHafs1Ver18.ttf
--------------------------------------------------------------------------------
/data/chapter/type.ts:
--------------------------------------------------------------------------------
1 | export type LocalChapter = {
2 | id: number;
3 | verses_count: number;
4 | name_simple: string;
5 | revelation_place: string;
6 | };
7 |
--------------------------------------------------------------------------------
/src/app/(main)/surah/page.tsx:
--------------------------------------------------------------------------------
1 | import { notFound, redirect } from "next/navigation";
2 |
3 | export default function SurahPage() {
4 | return redirect("/");
5 | }
6 |
--------------------------------------------------------------------------------
/src/utils/fonts/uthmanic_hafs/UthmanicHafs1Ver18.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aacmal/quran-app-nextjs/HEAD/src/utils/fonts/uthmanic_hafs/UthmanicHafs1Ver18.woff2
--------------------------------------------------------------------------------
/src/utils/url.ts:
--------------------------------------------------------------------------------
1 | export const getBasePath = () =>
2 | `${process.env.NODE_ENV === 'development' ? 'http' : 'https'}://${
3 | process.env.VERCEL_URL
4 | }`;
5 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | *.log
3 | Dockerfile*
4 | docker-compose*
5 | .dockerignore
6 | .git
7 | .gitignore
8 | README.md
9 | LICENSE
10 | .vscode
11 | .next
12 | *.swp
--------------------------------------------------------------------------------
/src/components/quranReader/Arabic/Word.module.css:
--------------------------------------------------------------------------------
1 | .highlighted {
2 | filter: drop-shadow(0 4px 5px rgba(35, 215, 155, 0.3))
3 | drop-shadow(0 2px 2px rgba(33, 218, 157, 0.3));
4 | }
5 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/utils/fonts/nastaleeq/indopak/indopak-nastaleeq-waqf-lazim-v4.2.1.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aacmal/quran-app-nextjs/HEAD/src/utils/fonts/nastaleeq/indopak/indopak-nastaleeq-waqf-lazim-v4.2.1.ttf
--------------------------------------------------------------------------------
/src/utils/fonts/nastaleeq/indopak/indopak-nastaleeq-waqf-lazim-v4.2.1.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aacmal/quran-app-nextjs/HEAD/src/utils/fonts/nastaleeq/indopak/indopak-nastaleeq-waqf-lazim-v4.2.1.woff
--------------------------------------------------------------------------------
/src/utils/fonts/nastaleeq/indopak/indopak-nastaleeq-waqf-lazim-v4.2.1.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aacmal/quran-app-nextjs/HEAD/src/utils/fonts/nastaleeq/indopak/indopak-nastaleeq-waqf-lazim-v4.2.1.woff2
--------------------------------------------------------------------------------
/.idea/prettier.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/jsLinters/eslint.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/utils/types/Audio.ts:
--------------------------------------------------------------------------------
1 | export type Recitation = {
2 | id: number;
3 | reciter_name: string;
4 | style: string;
5 | translated_name: {
6 | name: string;
7 | language_name: string;
8 | };
9 | };
10 |
--------------------------------------------------------------------------------
/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/basic-features/typescript for more information.
6 |
--------------------------------------------------------------------------------
/src/app/robots.ts:
--------------------------------------------------------------------------------
1 | import { MetadataRoute } from 'next';
2 |
3 | export default function robots(): MetadataRoute.Robots {
4 | return {
5 | rules: {
6 | userAgent: '*',
7 | allow: '/',
8 | },
9 | };
10 | }
11 |
--------------------------------------------------------------------------------
/src/utils/types/Juz.ts:
--------------------------------------------------------------------------------
1 | export type Juz = {
2 | id: number;
3 | juz_number: number;
4 | verse_mapping: {
5 | [key: string]: string;
6 | };
7 | first_verse_id: number;
8 | last_verse_id: number;
9 | verses_count: number;
10 | };
11 |
--------------------------------------------------------------------------------
/src/components/Bookmark/BookmarkHadithLists.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | type Props = {};
4 |
5 | const BookmarkHadithLists = (props: Props) => {
6 | return
BookmarkHadithLists
;
7 | };
8 |
9 | export default BookmarkHadithLists;
10 |
--------------------------------------------------------------------------------
/src/app/(main)/surah/[chapterId]/_hooks/scrollToTop.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useEffect } from "react";
4 |
5 | export default function UseScrollToTop() {
6 | useEffect(() => {
7 | window.scrollTo(0, 0);
8 | }, []);
9 |
10 | return null;
11 | }
12 |
--------------------------------------------------------------------------------
/src/components/Tafsir/tafsirText.module.css:
--------------------------------------------------------------------------------
1 |
2 |
3 | .tafsir_text h2{
4 | @apply text-2xl py-2
5 | }
6 |
7 | .tafsir_text p{
8 | @apply lg:text-lg
9 | }
10 |
11 | .tafsir_text div{
12 | @apply lg:text-4xl text-right lg:py-5 text-4xl py-3 lg:leading-relaxed
13 | }
--------------------------------------------------------------------------------
/src/components/Header/ReadHadithHeader.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Header from '.';
3 |
4 | type Props = {};
5 |
6 | const ReadHadithHeader = (props: Props) => {
7 | return ;
8 | };
9 |
10 | export default ReadHadithHeader;
11 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/app/(main)/surah/[chapterId]/[ayahId]/loading.tsx:
--------------------------------------------------------------------------------
1 | import VerseSkeleton from '@components/quranReader/VerseSkeleton';
2 | import React from 'react';
3 |
4 | type Props = {};
5 |
6 | const LoadingSpecificAyah = (props: Props) => {
7 | return ;
8 | };
9 |
10 | export default LoadingSpecificAyah;
11 |
--------------------------------------------------------------------------------
/src/components/Container/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | type ContainerProps = {
4 | children: React.ReactNode;
5 | };
6 |
7 | const Container = ({ children }: ContainerProps) => {
8 | return {children}
;
9 | };
10 |
11 | export default Container;
12 |
--------------------------------------------------------------------------------
/src/components/Header/ReadQuranHeader.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Header from '.';
3 | import Search from '@components/Search';
4 |
5 | type Props = {};
6 |
7 | const ReadQuranHeader = (props: Props) => {
8 | return }>Baca Quran;
9 | };
10 |
11 | export default ReadQuranHeader;
12 |
--------------------------------------------------------------------------------
/src/utils/types/Hadith.ts:
--------------------------------------------------------------------------------
1 | export type HadithBook = {
2 | name: string;
3 | id: string;
4 | available: number;
5 | };
6 |
7 | export type HadithContent = {
8 | number: number;
9 | arab: string;
10 | id: string;
11 | };
12 |
13 | export type HaditsDetail = HadithBook & {
14 | requested: number;
15 | hadiths: HadithContent[];
16 | };
17 |
--------------------------------------------------------------------------------
/src/utils/api.ts:
--------------------------------------------------------------------------------
1 | const API_HOST = 'https://api.quran.com';
2 |
3 | const API_ROOT_PATH = '/api/v4';
4 |
5 | export const makeUrl = (path, parameters?) => {
6 | if (!parameters) {
7 | return `${API_HOST}${API_ROOT_PATH}${path}`;
8 | }
9 |
10 | const queryParams = `?${parameters}`;
11 |
12 | return `${API_HOST}${API_ROOT_PATH}${path}${queryParams}`;
13 | };
14 |
--------------------------------------------------------------------------------
/src/~page-offline/hadits/[hadithId]/layout.tsx:
--------------------------------------------------------------------------------
1 | // import TopBar from '@components/TopBar/TopBar';
2 | // import React from 'react';
3 |
4 | // const HadithDetailLayout = ({ children }: { children: React.ReactNode }) => {
5 | // return (
6 | // <>
7 | //
8 | // {children}
9 | // >
10 | // );
11 | // };
12 |
13 | // export default HadithDetailLayout;
14 |
--------------------------------------------------------------------------------
/src/components/icons/XIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const XIcon = ({className}) => {
4 | return (
5 |
6 |
7 |
8 | )
9 | }
10 |
11 | export default XIcon
--------------------------------------------------------------------------------
/src/components/icons/ChevronIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const ChevronIcon = ({className}) => {
4 | return (
5 |
6 |
7 |
8 | )
9 | }
10 |
11 | export default ChevronIcon
--------------------------------------------------------------------------------
/src/components/icons/ListsIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const ListsIcon = ({className}) => {
4 | return (
5 |
6 |
7 |
8 | )
9 | }
10 |
11 | export default ListsIcon
--------------------------------------------------------------------------------
/src/components/icons/LoadingIcon/AnimatedLoadingIcon.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import style from './AnimatedLoadingIcon.module.css'
3 |
4 | const AnimatedLoadingIcon = () => {
5 | return (
6 |
10 | )
11 | }
12 |
13 | export default AnimatedLoadingIcon
--------------------------------------------------------------------------------
/src/app/(main)/loading.tsx:
--------------------------------------------------------------------------------
1 | import ChapterCardSkeleton from "@components/chapters/Card/ChapterCardSkeleton";
2 | import React from "react";
3 |
4 | const Loading = () => {
5 | return (
6 |
7 | {[...Array(3)].map((e, i) => (
8 |
9 | ))}
10 |
11 | );
12 | };
13 |
14 | export default Loading;
15 |
--------------------------------------------------------------------------------
/src/components/icons/DownloadIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const DownloadIcon = ({className}) => {
4 | return (
5 |
6 |
7 |
8 | )
9 | }
10 |
11 | export default DownloadIcon
--------------------------------------------------------------------------------
/src/components/icons/BookmarkIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const Bookmark = ({className, fill}) => {
4 | return (
5 |
6 |
7 |
8 | )
9 | }
10 |
11 | export default Bookmark
--------------------------------------------------------------------------------
/src/components/Wrapper/index.tsx:
--------------------------------------------------------------------------------
1 | import classNames from 'classnames';
2 |
3 | type WrapperProps = {
4 | children: React.ReactNode;
5 | className?: string;
6 | };
7 |
8 | const Wrapper = ({ children, className }: WrapperProps) => {
9 | return (
10 |
16 | {children}
17 |
18 | );
19 | };
20 |
21 | export default Wrapper;
22 |
--------------------------------------------------------------------------------
/src/components/icons/RepeatIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const RepeatIcon = ({className}) => {
4 | return (
5 |
6 |
7 |
8 | )
9 | }
10 |
11 | export default RepeatIcon
--------------------------------------------------------------------------------
/src/components/TopBar/DeveloperUtility/AdjustmentWrapper.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | type AdjustmentWrapperProps = {
4 | children: React.ReactNode;
5 | title: string;
6 | };
7 |
8 | const AdjustmentWrapper = ({ children, title }: AdjustmentWrapperProps) => (
9 |
10 |
{title}
11 | {children}
12 |
13 | );
14 |
15 | export default AdjustmentWrapper;
16 |
--------------------------------------------------------------------------------
/src/components/icons/DotsIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const DotsIcon = ({className}) => {
4 | return (
5 |
6 |
7 |
8 | )
9 | }
10 |
11 | export default DotsIcon
--------------------------------------------------------------------------------
/.idea/quran-app-nextjs.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/components/icons/TrashIcon.tsx:
--------------------------------------------------------------------------------
1 | const TrashIcon = ({className}) => {
2 | return (
3 |
4 |
5 |
6 | )
7 | }
8 |
9 | export default TrashIcon
--------------------------------------------------------------------------------
/src/utils/audio.ts:
--------------------------------------------------------------------------------
1 | import { makeUrl } from "./api"
2 |
3 | export const getAllRecitations = async (lang='en') => {
4 | const response = await fetch(makeUrl(`/resources/recitations`, `language=${lang}`))
5 | const data = await response.json()
6 | return data
7 | }
8 |
9 | export const getAudioFile = async (reciterId, chapterNumber) => {
10 | const response = await fetch(makeUrl(`/chapter_recitations/${reciterId}/${chapterNumber}`, 'segments=true'))
11 | const data = await response.json()
12 | return data
13 | }
--------------------------------------------------------------------------------
/src/components/icons/AdjustmentIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const AdjustmentIcon = ({className}) => {
4 | return (
5 |
6 |
7 |
8 | )
9 | }
10 |
11 | export default AdjustmentIcon
--------------------------------------------------------------------------------
/src/utils/juz.ts:
--------------------------------------------------------------------------------
1 | import { makeUrl } from './api';
2 | import { Juz } from './types/Juz';
3 |
4 | export const getJuzs = async (
5 | lang = 'id'
6 | ): Promise<{
7 | juzs: Juz[];
8 | }> => {
9 | const response = await fetch(makeUrl(`/juzs`, `language=${lang}`));
10 | const data = await response.json();
11 | return data;
12 | };
13 |
14 | export const getJuzData = async (id: number): Promise => {
15 | const response = await fetch(makeUrl(`/juzs/${id}`));
16 | const data = await response.json();
17 | return data.juz;
18 | };
19 |
--------------------------------------------------------------------------------
/src/utils/types/Chapter.ts:
--------------------------------------------------------------------------------
1 | export type Chapter = {
2 | id: number;
3 | revelation_place: string;
4 | revelation_order: number;
5 | bismillah_pre: boolean;
6 | name_complex: string;
7 | name_arabic: string;
8 | name_simple: string;
9 | verses_count: number;
10 | translated_name: {
11 | language_name: string;
12 | name: string;
13 | };
14 | pages: number[];
15 | };
16 |
17 | export type ChapterInfo = {
18 | id: number;
19 | chapter_id: number;
20 | short_text: string;
21 | text: string;
22 | source: string;
23 | };
24 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # Use an official Node.js runtime as a parent image
2 | FROM node:18-alpine
3 |
4 | # Set the working directory to /app
5 | WORKDIR /app
6 |
7 | # Copy package.json and package-lock.json to the working directory
8 | COPY package*.json ./
9 |
10 | # Install dependencies
11 | RUN npm install
12 |
13 | # Copy the rest of the application code to the working directory
14 | COPY . .
15 |
16 | # Build the Next.js app
17 | RUN npm run build
18 |
19 | # Expose port 3000 for the app to listen on
20 | EXPOSE 3000
21 |
22 | # Start the app
23 | CMD ["npm", "start"]
24 |
--------------------------------------------------------------------------------
/src/app/(main)/InitChapterData.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import useSurah from '@stores/surahStore';
4 | import { getLocalChapter } from '@utils/chapter';
5 | import React, { useEffect } from 'react';
6 |
7 | const InitChapterData = () => {
8 | const setChapterData = useSurah((state) => state.setChapterData);
9 | useEffect(() => {
10 | getLocalChapter().then((res) => {
11 | setChapterData(res);
12 | });
13 | // eslint-disable-next-line react-hooks/exhaustive-deps
14 | }, []);
15 |
16 | return <>>;
17 | };
18 |
19 | export default InitChapterData;
20 |
--------------------------------------------------------------------------------
/src/components/icons/ArrowIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import classNames from 'classnames'
3 |
4 | const ArrowIcon = ({className}) => {
5 |
6 | return (
7 |
8 |
9 |
10 | )
11 | }
12 |
13 | export default ArrowIcon
--------------------------------------------------------------------------------
/src/app/(main)/juz/loading.tsx:
--------------------------------------------------------------------------------
1 | import Wrapper from '@components/Wrapper';
2 | import VerseSkeleton from '@components/quranReader/VerseSkeleton';
3 | import React from 'react';
4 |
5 | const Loading = () => {
6 | return (
7 |
8 | {Array(4)
9 | .fill('')
10 | .map((_, i) => (
11 |
16 | ))}
17 |
18 | );
19 | };
20 |
21 | export default Loading;
22 |
--------------------------------------------------------------------------------
/src/components/icons/RewindIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { IconProps } from './type';
3 |
4 | const RewindIcon = ({ className, onClick }: IconProps) => {
5 | return (
6 |
13 |
14 |
15 | );
16 | };
17 |
18 | export default RewindIcon;
19 |
--------------------------------------------------------------------------------
/src/components/chapters/ChaptersView.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ChapterCard from './Card/ChapterCard';
3 | import { Chapter } from '@utils/types/Chapter';
4 |
5 | type ChaptersViewProps = {
6 | chapterData: Chapter[];
7 | };
8 |
9 | const ChaptersView = ({ chapterData }: ChaptersViewProps) => {
10 | return chapterData.map((e) => (
11 |
18 | ));
19 | };
20 |
21 | export default ChaptersView;
22 |
--------------------------------------------------------------------------------
/src/~page-offline/hadits/[hadithId]/handleClient.tsx:
--------------------------------------------------------------------------------
1 | // 'use client';
2 |
3 | // import useHadith from '@stores/hadithStore';
4 | // import React, { useEffect } from 'react';
5 |
6 | // type Props = {
7 | // hadithId: string;
8 | // };
9 |
10 | // const HadithHandleClient = ({ hadithId }: Props) => {
11 | // const setHadithActive = useHadith((state) => state.setHadithActive);
12 |
13 | // useEffect(() => {
14 | // setHadithActive(hadithId);
15 | // // eslint-disable-next-line react-hooks/exhaustive-deps
16 | // }, []);
17 | // return <>>;
18 | // };
19 |
20 | // export default HadithHandleClient;
21 |
--------------------------------------------------------------------------------
/src/components/icons/TafsirIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const TafsirIcon = ({className}) => {
4 | return (
5 |
6 |
7 |
8 | )
9 | }
10 |
11 | export default TafsirIcon
--------------------------------------------------------------------------------
/src/components/Banner/HadithBanner.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import BannerWrapper from './BannerWrapper';
3 |
4 | type Props = {
5 | name: string;
6 | available: number;
7 | };
8 |
9 | const HadithBanner = ({ name, available }: Props) => {
10 | return (
11 |
12 |
13 | {name}
14 |
15 |
16 | Total {available}
17 |
18 |
19 | );
20 | };
21 |
22 | export default HadithBanner;
23 |
--------------------------------------------------------------------------------
/src/components/Hadith/HadithReader/InitialHadithData.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import HadithVerse from '../HadithVerse';
3 |
4 | type Props = {
5 | hadiths: {
6 | id: string;
7 | number: number;
8 | arab: string;
9 | }[];
10 | };
11 |
12 | const InitialHadithData = ({ hadiths }: Props) => {
13 | return (
14 | <>
15 | {hadiths.map((item) => (
16 |
22 | ))}
23 | >
24 | );
25 | };
26 |
27 | export default InitialHadithData;
28 |
--------------------------------------------------------------------------------
/src/app/(main)/surah/[chapterId]/layout.tsx:
--------------------------------------------------------------------------------
1 | import dynamic from "next/dynamic";
2 | import React from "react";
3 | import UseScrollToTop from "./_hooks/scrollToTop";
4 |
5 | const SurahLayout = ({ children }: { children: React.ReactNode }) => {
6 | return (
7 | <>
8 |
9 | {children}
10 |
11 |
12 |
13 | Nyalakan JavaScript untuk men-load semua ayah
14 |
15 |
16 |
17 | >
18 | );
19 | };
20 |
21 | export default SurahLayout;
22 |
--------------------------------------------------------------------------------
/src/components/icons/PauseIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { IconProps } from './type';
3 |
4 | const PauseIcon = ({ className, onClick }: IconProps) => {
5 | return (
6 |
15 |
20 |
21 | );
22 | };
23 |
24 | export default PauseIcon;
25 |
--------------------------------------------------------------------------------
/src/utils/types/Tafsir.ts:
--------------------------------------------------------------------------------
1 | export type Tafsir = {
2 | id: number;
3 | surah_id: number;
4 | juz: number;
5 | arabic: string;
6 | latin: string;
7 | translation: string;
8 | ayah: number;
9 | surah: {
10 | id: number;
11 | arabic: string;
12 | latin: string;
13 | transliteration: string;
14 | translation: string;
15 | num_ayah: number;
16 | location: string;
17 | };
18 | tafsir: {
19 | tahlili: string;
20 | wajiz: string;
21 | info_surah: string;
22 | kosakata: string;
23 | munasabah_prev_surah: string;
24 | munasabah_prev_theme: string;
25 | theme_group: string;
26 | };
27 | };
28 |
--------------------------------------------------------------------------------
/src/components/chapters/Card/ChapterWrapper.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | type ChapterWrapperProps = {
4 | children: React.ReactNode;
5 | };
6 |
7 | const ChapterWrapper = ({ children }: ChapterWrapperProps) => {
8 | return (
9 |
10 | {children}
11 |
12 | );
13 | };
14 |
15 | export default ChapterWrapper;
16 |
--------------------------------------------------------------------------------
/src/components/Hadith/HadithReader/HadithReader.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import InitialHadithData from './InitialHadithData';
3 | import DynamicHadithsData from './DynamicHadithData';
4 |
5 | type Props = {
6 | id: string;
7 | available: number;
8 | hadiths: {
9 | id: string;
10 | number: number;
11 | arab: string;
12 | }[];
13 | };
14 |
15 | const HadithReader = ({ hadiths, available, id }: Props) => {
16 | return (
17 |
18 |
19 |
20 |
21 | );
22 | };
23 |
24 | export default HadithReader;
25 |
--------------------------------------------------------------------------------
/src/app/(main)/surah/loading.tsx:
--------------------------------------------------------------------------------
1 | import ChapterBannerSkeleton from "@components/Banner/ChapterBannerSkeleton";
2 | import Wrapper from "@components/Wrapper";
3 | import VerseSkeleton from "@components/quranReader/VerseSkeleton";
4 | import React from "react";
5 |
6 | const Loading = () => {
7 | return (
8 |
9 |
10 | {Array(4)
11 | .fill("")
12 | .map((_, i) => (
13 |
18 | ))}
19 |
20 | );
21 | };
22 |
23 | export default Loading;
24 |
--------------------------------------------------------------------------------
/src/app/ThemeHandler.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import useSettings from '@stores/settingsStore';
4 | import setTheme from '@utils/theme';
5 | import { usePathname } from 'next/navigation';
6 | import React, { useEffect } from 'react';
7 | import { shallow } from 'zustand/shallow';
8 |
9 | const ThemeHandler = () => {
10 | const pathname = usePathname();
11 | const { theme } = useSettings(
12 | (state) => ({
13 | theme: state.theme,
14 | }),
15 | shallow
16 | );
17 |
18 | useEffect(() => {
19 | if (theme) {
20 | setTheme(theme);
21 | }
22 | // update theme when pathname changes
23 | }, [theme, pathname]);
24 |
25 | return <>>;
26 | };
27 |
28 | export default ThemeHandler;
29 |
--------------------------------------------------------------------------------
/src/components/Banner/ChapterBannerSkeleton.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Skeleton from '../Skeleton/Skeleton'
3 |
4 | const ChapterBannerSkeleton = () => {
5 | return (
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | )
18 | }
19 |
20 | export default ChapterBannerSkeleton
--------------------------------------------------------------------------------
/src/components/icons/InfoIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | type InfoIconProps = {
4 | className?: string;
5 | onClick?: () => void;
6 | };
7 |
8 | const InfoIcon = ({ className, onClick }: InfoIconProps) => {
9 | return (
10 |
17 |
22 |
23 | );
24 | };
25 |
26 | export default InfoIcon;
27 |
--------------------------------------------------------------------------------
/src/components/AudioPlayer/PlayAudioButton.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React from 'react';
4 | import { PlayIcon } from '../icons';
5 | import useQuranReader from '@stores/quranReaderStore';
6 |
7 | const PlayAudioButton = ({ surahId }: { surahId: string }) => {
8 | const { setAudioId } = useQuranReader((state) => ({
9 | setAudioId: state.setAudioId,
10 | }));
11 |
12 | return (
13 | setAudioId(parseInt(surahId))}
16 | >
17 | Putar Audio
18 |
19 | );
20 | };
21 |
22 | export default PlayAudioButton;
23 |
--------------------------------------------------------------------------------
/src/components/chapters/Card/ChapterCardSkeleton.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Skeleton from '../../Skeleton/Skeleton';
3 |
4 | const ChapterCardSkeleton = () => {
5 | return (
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | );
19 | };
20 |
21 | export default ChapterCardSkeleton;
22 |
--------------------------------------------------------------------------------
/src/app/~offline/page.tsx:
--------------------------------------------------------------------------------
1 | import { Metadata } from 'next';
2 | import React from 'react';
3 |
4 | type Props = {};
5 |
6 | export const metadata: Metadata = {
7 | title: '~ Anda sedang offline',
8 | robots: 'noindex,nofollow',
9 | };
10 |
11 | const OfflineFallback = (props: Props) => {
12 | return (
13 |
14 |
15 | Anda sedang offline
16 | Silakan cek koneksi internet Anda.
17 |
18 |
19 | );
20 | };
21 |
22 | export default OfflineFallback;
23 |
--------------------------------------------------------------------------------
/src/components/chapters/index.tsx:
--------------------------------------------------------------------------------
1 | import { Chapter } from '@utils/types/Chapter';
2 | import ChaptersView from './ChaptersView';
3 | import ChapterCard from './Card/ChapterCard';
4 |
5 | type ChaptersProps = {
6 | chapterLists: Chapter[];
7 | };
8 |
9 | const Chapters = ({ chapterLists }: ChaptersProps) => {
10 | return (
11 |
12 | {chapterLists.map((e) => (
13 |
20 | ))}
21 |
22 | );
23 | };
24 |
25 | export default Chapters;
26 |
--------------------------------------------------------------------------------
/src/components/Bookmark/BookmarkedItem.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Link from "next/link";
3 |
4 | type BookmarkedItemProps = {
5 | name_simple: string;
6 | verse_key: string;
7 | };
8 |
9 | const BookmarkedItem = ({ name_simple, verse_key }: BookmarkedItemProps) => {
10 | const verseKey = verse_key.split(":");
11 |
12 | return (
13 |
17 | {name_simple}
18 | {verse_key}
19 |
20 | );
21 | };
22 |
23 | export default BookmarkedItem;
24 |
--------------------------------------------------------------------------------
/src/components/Header/ChapterHeader.tsx:
--------------------------------------------------------------------------------
1 | // import React from 'react';
2 | // // import DropdownSurahLists from '../TopBar/DropdownSurahLists/DropdownSurahLists';
3 | // import DeveloperUtility from '../TopBar/DeveloperUtility/DeveloperUtility';
4 | // import classNames from 'classnames';
5 |
6 | // const ChapterHeader = () => {
7 | // return (
8 | //
13 | // {/* */}
14 | //
15 | //
16 | // );
17 | // };
18 |
19 | // export default ChapterHeader;
20 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 | .pnpm-debug.log*
27 |
28 | # local env files
29 | .env*.local
30 |
31 | # vercel
32 | .vercel
33 |
34 | # PWA
35 | **/public/precache.*.*.js
36 | **/public/sw.js
37 | **/public/workbox-*.js
38 | **/public/worker-*.js
39 | **/public/fallback-*.js
40 | **/public/precache.*.*.js.map
41 | **/public/sw.js.map
42 | **/public/workbox-*.js.map
43 | **/public/worker-*.js.map
44 | **/public/fallback-*.js
45 |
46 |
47 | # .vscode
48 | .vscode
--------------------------------------------------------------------------------
/src/components/icons/StarIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const StarIcon = ({className}) => {
4 | return (
5 |
6 |
7 |
8 |
9 | )
10 | }
11 |
12 | export default StarIcon
--------------------------------------------------------------------------------
/src/components/quranReader/InitialSurahVerse.tsx:
--------------------------------------------------------------------------------
1 | import { Verse } from '@utils/types/Verse';
2 | import React from 'react';
3 | import Verses from './Verses';
4 | import ScrollToAyah from './ScrollToAyah';
5 |
6 | type Props = {
7 | versesData: Verse[];
8 | };
9 |
10 | const InitialSurahVerse = ({ versesData }: Props) => {
11 | return (
12 | <>
13 |
14 | {versesData.map((verse) => (
15 |
24 | ))}
25 | >
26 | );
27 | };
28 |
29 | export default InitialSurahVerse;
30 |
--------------------------------------------------------------------------------
/src/utils/theme.ts:
--------------------------------------------------------------------------------
1 | import { Theme } from '@stores/settingsStore';
2 |
3 | export default function setTheme(theme: Theme) {
4 | if (theme === 'dark') {
5 | document.documentElement.classList.add('dark');
6 | document
7 | .querySelector('meta[name="theme-color"]')
8 | ?.setAttribute('content', '#334155');
9 | } else if (
10 | theme === 'default' &&
11 | window.matchMedia('(prefers-color-scheme: dark)').matches
12 | ) {
13 | document.documentElement.classList.add('dark');
14 | document
15 | .querySelector('meta[name="theme-color"]')
16 | ?.setAttribute('content', '#334155');
17 | } else {
18 | document.documentElement.classList.remove('dark');
19 | document
20 | .querySelector('meta[name="theme-color"]')
21 | ?.setAttribute('content', '#f1f5f9');
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/components/icons/PlayIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { IconProps } from './type';
3 |
4 | const PlayIcon = ({ className, onClick }: IconProps) => {
5 | return (
6 |
15 |
20 |
25 |
26 | );
27 | };
28 |
29 | export default PlayIcon;
30 |
--------------------------------------------------------------------------------
/src/components/Skeleton/Skeleton.tsx:
--------------------------------------------------------------------------------
1 | import classNames from 'classnames';
2 | import React from 'react';
3 |
4 | type SkeletonProps = {
5 | className?: string;
6 | color?: 'gray' | 'emerald';
7 | };
8 |
9 | const Skeleton = ({ className, color }: SkeletonProps) => {
10 | let colors;
11 |
12 | switch (color) {
13 | case 'gray':
14 | colors = 'bg-gray-300 dark:bg-slate-700';
15 | break;
16 |
17 | case 'emerald':
18 | colors = 'bg-emerald-200 dark:bg-emerald-600';
19 | break;
20 |
21 | default:
22 | colors = 'bg-emerald-200 dark:bg-emerald-600';
23 | break;
24 | }
25 |
26 | return (
27 |
33 | );
34 | };
35 |
36 | export default Skeleton;
37 |
--------------------------------------------------------------------------------
/src/components/icons/IconWrapper.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import classNames from 'classnames';
3 |
4 | type IconWrapperProps = {
5 | children: React.ReactNode;
6 | className?: string;
7 | onClick?: () => void;
8 | onHover?: string;
9 | } & React.ButtonHTMLAttributes;
10 |
11 | const IconWrapper = ({
12 | children,
13 | className,
14 | onClick,
15 | onHover,
16 | ...props
17 | }: IconWrapperProps) => (
18 |
29 | {children}
30 |
31 | );
32 |
33 | export default IconWrapper;
34 |
--------------------------------------------------------------------------------
/src/components/GoogleAnalytics/GoogleAnalytics.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import Script from 'next/script';
4 | import React from 'react';
5 |
6 | const IS_PRODUCTION = process.env.NEXT_PUBLIC_VERCEL_ENV === 'production';
7 |
8 | const GoogleAnalytics = () => {
9 | if (!IS_PRODUCTION) {
10 | return <>>;
11 | }
12 |
13 | return (
14 | <>
15 |
20 |
28 | >
29 | );
30 | };
31 |
32 | export default GoogleAnalytics;
33 |
--------------------------------------------------------------------------------
/src/components/Banner/HomeBanner.tsx:
--------------------------------------------------------------------------------
1 | // import React from 'react';
2 | // import { History } from '../icons';
3 | // import BannerWrapper from './BannerWrapper';
4 |
5 | // const HomeBanner = () => {
6 | // return (
7 | //
8 | //
9 | //
10 | //
11 | // Terakhir dibaca
12 | //
13 | //
14 | //
Al - Fatiha
15 | //
Surah No. 1
16 | //
17 | //
Klik untuk melanjutkan
18 | //
19 | //
20 | // );
21 | // };
22 |
23 | // export default HomeBanner;
24 |
--------------------------------------------------------------------------------
/src/components/quranReader/action/CopyToClipboard.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React from 'react';
4 | import { CopyIcon } from '../../icons';
5 | import IconWrapper from '../../icons/IconWrapper';
6 |
7 | type CopyToClipboardProps = {
8 | text_uthmani: string;
9 | };
10 |
11 | const CopyToClipboard = ({ text_uthmani }: CopyToClipboardProps) => {
12 | function copyToClipboard(content: string) {
13 | navigator.clipboard.writeText(content);
14 | }
15 |
16 | return (
17 |
21 | copyToClipboard(text_uthmani)}
23 | className="md:h-6 h-5 group-active:text-emerald-500"
24 | />
25 |
26 | );
27 | };
28 |
29 | export default CopyToClipboard;
30 |
--------------------------------------------------------------------------------
/src/app/sitemap.ts:
--------------------------------------------------------------------------------
1 | import { getAllChaptersData } from "@utils/chapter";
2 | import { MetadataRoute } from "next";
3 |
4 | async function getChapterData() {
5 | const res = await getAllChaptersData();
6 | return res.chapters;
7 | }
8 |
9 | const currentDomain = "https://quran.acml.me";
10 | export default async function sitemap(): Promise {
11 | const data = await getChapterData();
12 |
13 | const surahMap = data.map((d) => ({
14 | url: `${currentDomain}/surah/${d.id}`,
15 | lastModified: new Date(),
16 | priority: 4,
17 | })) satisfies MetadataRoute.Sitemap;
18 | return [
19 | {
20 | url: `${currentDomain}/`,
21 | lastModified: new Date(),
22 | priority: 6,
23 | },
24 | {
25 | url: `${currentDomain}/juz`,
26 | lastModified: new Date(),
27 | priority: 6,
28 | },
29 | ...surahMap,
30 | ];
31 | }
32 |
--------------------------------------------------------------------------------
/src/components/quranReader/Switcher.tsx:
--------------------------------------------------------------------------------
1 | // import classNames from 'classnames'
2 | // import React, { useContext, useEffect } from 'react'
3 | // import { StyleContext } from '../../context/StyleContext'
4 |
5 | // const Switcher = () => {
6 | // const { setReadMode, readMode } = useContext(StyleContext)
7 |
8 | // return (
9 | //
10 | //
setReadMode('translated')} className={classNames("px-3 rounded z-20 cursor-pointer text-sm lg:text-base", {"bg-emerald-200/40": readMode == 'translated'})}>Terjemah
11 | //
setReadMode('read')} className={classNames("px-3 rounded z-20 cursor-pointer text-sm lg:text-base", {"bg-emerald-200/40": readMode === 'read'})}>Baca
12 | //
13 | // )
14 | // }
15 |
16 | // export default Switcher
17 |
--------------------------------------------------------------------------------
/src/utils/tafsir.ts:
--------------------------------------------------------------------------------
1 | // export const getSingleTafsir = async (verseKey, lang) => {
2 | // //for now only support english and indonesian
3 | // if(lang === 'id'){
4 | // const response = await fetch(`https://quran.kemenag.go.id/api/v1/tafsirbyayat/${verseKey}`)
5 | // const data = await response.json()
6 | // return data
7 | // }
8 | // const response = await fetch(`https://api.qurancdn.com/api/qdc/tafsirs/en-tafisr-ibn-kathir/by_ayah/${verseKey}?locale=en&mushaf=7`)
9 | // const data = await response.json()
10 | // return data
11 | // }
12 |
13 | import { Tafsir } from './types/Tafsir';
14 |
15 | export const getTafsirByVerseId = async (verseId: number): Promise => {
16 | const response = await fetch(
17 | `https://web-api.qurankemenag.net/quran-tafsir/${verseId}`
18 | );
19 | const data = await response.json();
20 | return data.data;
21 | };
22 |
--------------------------------------------------------------------------------
/src/app/(main)/surah/[chapterId]/not-found.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Link from "next/link";
3 | import { ArrowIcon } from "@components/icons";
4 | import { Metadata } from "next";
5 |
6 | export const metadata: Metadata = {
7 | title: "Data tidak ditemukan",
8 | description: "Data tidak ditemukan",
9 | robots: "noindex, nofollow",
10 | };
11 |
12 | const NotFound = () => {
13 | return (
14 |
15 |
16 | Data tidak ditemukan
17 |
18 |
19 |
20 |
21 |
Kembali ke Home
22 |
23 |
24 |
25 | );
26 | };
27 |
28 | export default NotFound;
29 |
--------------------------------------------------------------------------------
/src/components/quranReader/ScrollToAyah.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useParams, useSearchParams } from 'next/navigation';
4 | import React, { useEffect } from 'react';
5 |
6 | const ScrollToAyah = () => {
7 | const searchParams = useSearchParams();
8 | const params = useParams();
9 |
10 | useEffect(() => {
11 | if (!searchParams.has('ayah')) return;
12 | const ayah = searchParams.get('ayah');
13 | if (Number(ayah) > 20) return;
14 | const verseElement = document.querySelector(
15 | `[data-verse="${params.chapterId}:${ayah}"]`
16 | ) as HTMLElement;
17 |
18 | if (!verseElement) return;
19 |
20 | verseElement.scrollIntoView({
21 | behavior: 'smooth',
22 | block: 'center',
23 | inline: 'center',
24 | });
25 | // eslint-disable-next-line react-hooks/exhaustive-deps
26 | }, [searchParams]);
27 | return <>>;
28 | };
29 |
30 | export default ScrollToAyah;
31 |
--------------------------------------------------------------------------------
/src/components/chapters/Card/JuzWrapper.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import React from "react";
3 |
4 | type JuzWrapperProps = {
5 | children: React.ReactNode;
6 | juz_number: number;
7 | };
8 |
9 | const JuzWrapper = ({ children, juz_number }: JuzWrapperProps) => {
10 | return (
11 |
12 |
13 |
17 | Juz {juz_number}
18 |
19 | {children}
20 |
21 |
22 | );
23 | };
24 |
25 | export default JuzWrapper;
26 |
--------------------------------------------------------------------------------
/src/components/Hadith/HadithCard.tsx:
--------------------------------------------------------------------------------
1 | import ChapterWrapper from '@components/chapters/Card/ChapterWrapper';
2 | import { StarIcon } from '@components/icons';
3 | import Link from 'next/link';
4 | import React from 'react';
5 |
6 | type Props = {
7 | name: string;
8 | available: number;
9 | id: string;
10 | };
11 |
12 | const HadithCard = ({ name, id, available }: Props) => {
13 | return (
14 |
15 |
16 |
17 | {available}
18 |
19 |
20 |
21 |
22 | {name}
23 |
24 |
25 |
26 |
27 | );
28 | };
29 |
30 | export default HadithCard;
31 |
--------------------------------------------------------------------------------
/src/components/Header/index.tsx:
--------------------------------------------------------------------------------
1 | import classNames from 'classnames';
2 | import DeveloperUtility from '@components/TopBar/DeveloperUtility/DeveloperUtility';
3 | import Search from '@components/Search';
4 | import React from 'react';
5 |
6 | type HeaderProps = {
7 | className?: string;
8 | children: React.ReactNode;
9 | search?: React.ReactNode;
10 | };
11 |
12 | const Header = ({ className, children, search }: HeaderProps) => {
13 | return (
14 |
15 |
16 |
22 | {children}
23 |
24 |
25 |
26 | {search}
27 |
28 | );
29 | };
30 |
31 | export default Header;
32 |
--------------------------------------------------------------------------------
/src/app/(main)/surah/[chapterId]/[ayahId]/layout.tsx:
--------------------------------------------------------------------------------
1 | import Wrapper from "@components/Wrapper";
2 | import { ArrowIcon } from "@components/icons";
3 | import Link from "next/link";
4 | import React from "react";
5 |
6 | type Props = {
7 | children: React.ReactNode;
8 | params: {
9 | chapterId: string;
10 | ayahId: string;
11 | };
12 | };
13 |
14 | const SpecificAyahLayout = ({ children, params }: Props) => {
15 | return (
16 |
17 |
18 |
22 |
23 |
Kembali ke surah
24 |
25 |
26 | {children}
27 |
28 | );
29 | };
30 |
31 | export default SpecificAyahLayout;
32 |
--------------------------------------------------------------------------------
/src/components/quranReader/action/HandleTafsir.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React from 'react';
4 | import { TafsirIcon } from '../../icons';
5 | import IconWrapper from '../../icons/IconWrapper';
6 | import useQuranReader from '@stores/quranReaderStore';
7 | import { shallow } from 'zustand/shallow';
8 |
9 | type HandleTafsirProps = {
10 | id: number;
11 | };
12 |
13 | const HandleTafsir = ({ id }: HandleTafsirProps) => {
14 | const { setTafsirState } = useQuranReader(
15 | (state) => ({
16 | setTafsirState: state.setTafsirState,
17 | }),
18 | shallow
19 | );
20 |
21 | return (
22 |
25 | setTafsirState({
26 | verseId: id,
27 | })
28 | }
29 | className="text-gray-500 dark:hover:text-gray-50"
30 | >
31 |
32 |
33 | );
34 | };
35 |
36 | export default HandleTafsir;
37 |
--------------------------------------------------------------------------------
/src/components/Tafsir/TafsirSkeleton.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Skeleton from '../Skeleton/Skeleton';
3 |
4 | const TafsirSkeleton = () => {
5 | return (
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | );
21 | };
22 |
23 | export default TafsirSkeleton;
24 |
--------------------------------------------------------------------------------
/src/components/icons/LoadingIcon/AnimatedLoadingIcon.module.css:
--------------------------------------------------------------------------------
1 | @keyframes ldio-f74bbihe7ji {
2 | 0% { transform: rotate(0deg) }
3 | 50% { transform: rotate(180deg) }
4 | 100% { transform: rotate(360deg) }
5 | }
6 | .ldio_f74bbihe7ji div {
7 | position: absolute;
8 | animation: ldio-f74bbihe7ji 1.1500000000000001s linear infinite;
9 | width: 62px;
10 | height: 62px;
11 | top: 19px;
12 | left: 19px;
13 | border-radius: 50%;
14 | box-shadow: 0 2.8000000000000003px 0 0 #1d0e0b;
15 | transform-origin: 31px 32.4px;
16 | }
17 | .loadingio_spinner_eclipse_4juan662yhp {
18 | width: 35px;
19 | height: 35px;
20 | display: inline-block;
21 | overflow: hidden;
22 | background: transparent;
23 | }
24 | .ldio_f74bbihe7ji {
25 | width: 100%;
26 | height: 100%;
27 | position: relative;
28 | transform: translateZ(0) scale(0.36);
29 | backface-visibility: hidden;
30 | transform-origin: 0 0; /* see note above */
31 | }
32 | .ldio_f74bbihe7ji div { box-sizing: content-box; }
33 | /* generated by https://loading.io/ */
--------------------------------------------------------------------------------
/data/hadith/hadith.json:
--------------------------------------------------------------------------------
1 | {
2 | "data": [
3 | {
4 | "name": "HR. Abu Daud",
5 | "id": "abu-daud",
6 | "available": 4419
7 | },
8 | {
9 | "name": "HR. Ahmad",
10 | "id": "ahmad",
11 | "available": 4305
12 | },
13 | {
14 | "name": "HR. Bukhari",
15 | "id": "bukhari",
16 | "available": 6638
17 | },
18 | {
19 | "name": "HR. Darimi",
20 | "id": "darimi",
21 | "available": 2949
22 | },
23 | {
24 | "name": "HR. Ibnu Majah",
25 | "id": "ibnu-majah",
26 | "available": 4285
27 | },
28 | {
29 | "name": "HR. Malik",
30 | "id": "malik",
31 | "available": 1587
32 | },
33 | {
34 | "name": "HR. Muslim",
35 | "id": "muslim",
36 | "available": 4930
37 | },
38 | {
39 | "name": "HR. Nasai",
40 | "id": "nasai",
41 | "available": 5364
42 | },
43 | {
44 | "name": "HR. Tirmidzi",
45 | "id": "tirmidzi",
46 | "available": 3625
47 | }
48 | ]
49 | }
50 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "quran-app",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@ducanh2912/next-pwa": "^9.0.1",
13 | "@vercel/og": "^0.5.8",
14 | "classnames": "^2.3.2",
15 | "framer-motion": "^10.12.10",
16 | "next": "14.1.3",
17 | "next-seo": "^6.0.0",
18 | "prettier": "^2.8.8",
19 | "query-string": "^8.1.0",
20 | "react": "18.2.0",
21 | "react-dom": "18.2.0",
22 | "react-hot-toast": "^2.4.1",
23 | "react-virtuoso": "^4.6.2",
24 | "zustand": "^4.3.8"
25 | },
26 | "devDependencies": {
27 | "@types/node": "^20.3.0",
28 | "@types/react": "18.2.6",
29 | "@types/react-dom": "^18.2.22",
30 | "autoprefixer": "^10.4.14",
31 | "eslint": "8.40.0",
32 | "eslint-config-next": "14.1.3",
33 | "postcss": "^8.4.23",
34 | "tailwindcss": "^3.3.2",
35 | "typescript": "^5.1.3"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "strict": false,
12 | "forceConsistentCasingInFileNames": true,
13 | "noEmit": true,
14 | "incremental": true,
15 | "esModuleInterop": true,
16 | "module": "esnext",
17 | "moduleResolution": "node",
18 | "resolveJsonModule": true,
19 | "isolatedModules": true,
20 | "jsx": "preserve",
21 | "plugins": [
22 | {
23 | "name": "next"
24 | }
25 | ],
26 | "baseUrl": ".",
27 | "paths": {
28 | "@components/*": ["./src/components/*"],
29 | "@utils/*": ["./src/utils/*"],
30 | "@stores/*": ["./src/store/*"],
31 | "@hooks/*": ["./src/hooks/*"],
32 | }
33 | },
34 | "include": [
35 | "next-env.d.ts",
36 | ".next/types/**/*.ts",
37 | "**/*.ts",
38 | "**/*.tsx",
39 | ],
40 | "exclude": [
41 | "node_modules"
42 | ]
43 | }
44 |
--------------------------------------------------------------------------------
/src/components/Switch/index.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import React from "react";
3 |
4 | type QuranSwitchProps = {
5 | active: string;
6 | };
7 |
8 | const QuranSwitch = ({ active }: QuranSwitchProps) => {
9 | return (
10 |
11 |
18 | Chapters
19 |
20 |
27 | Juzs
28 |
29 |
34 |
35 | );
36 | };
37 |
38 | export default QuranSwitch;
39 |
--------------------------------------------------------------------------------
/src/components/icons/index.ts:
--------------------------------------------------------------------------------
1 | import HistoryIcon from './HistoryIcon'
2 | import CopyIcon from './CopyIcon'
3 | import InfoIcon from './InfoIcon'
4 | import StarIcon from './StarIcon'
5 | import BookmarkIcon from './BookmarkIcon'
6 | import AdjustmentIcon from './AdjustmentIcon'
7 | import ArrowIcon from './ArrowIcon'
8 | import ChevronIcon from './ChevronIcon'
9 | import RewindIcon from './RewindIcon'
10 | import RepeatIcon from './RepeatIcon'
11 | import TafsirIcon from './TafsirIcon'
12 | import DotsIcon from './DotsIcon'
13 | import DownloadIcon from './DownloadIcon'
14 | import XIcon from './XIcon'
15 | import ListsIcon from './ListsIcon'
16 | import PlayIcon from './PlayIcon'
17 | import PauseIcon from './PauseIcon'
18 |
19 |
20 | export {
21 | HistoryIcon,
22 | CopyIcon,
23 | InfoIcon,
24 | StarIcon,
25 | BookmarkIcon,
26 | AdjustmentIcon,
27 | ArrowIcon,
28 | ChevronIcon,
29 | RewindIcon,
30 | RepeatIcon,
31 | TafsirIcon,
32 | DotsIcon,
33 | DownloadIcon,
34 | XIcon,
35 | ListsIcon,
36 | PauseIcon,
37 | PlayIcon
38 | }
--------------------------------------------------------------------------------
/src/utils/fonts/index.ts:
--------------------------------------------------------------------------------
1 | import classNames from 'classnames/bind';
2 | import localFont from 'next/font/local';
3 |
4 | const alQalamFont = localFont({
5 | src: './al_qalam/alqalam.ttf',
6 | });
7 |
8 | const meQuranFont = localFont({
9 | src: './me_quran/me_quran-2.woff2',
10 | });
11 |
12 | const nastaleeqFont = localFont({
13 | src: './nastaleeq/indopak/indopak-nastaleeq-waqf-lazim-v4.2.1.woff',
14 | });
15 |
16 | const uthmanicFont = localFont({
17 | src: './uthmanic_hafs/UthmanicHafs1Ver18.woff2',
18 | });
19 |
20 | export const alQalamClassName = alQalamFont.className;
21 | export const meQuranClassName = meQuranFont.className;
22 | export const nastaleeqClassName = nastaleeqFont.className;
23 | export const uthmanicClassName = uthmanicFont.className;
24 |
25 | const ArabicFontsClassNames = {
26 | alQalam: alQalamFont.className,
27 | meQuran: meQuranFont.className,
28 | nastaleeq: nastaleeqFont.className,
29 | uthmanic: uthmanicFont.className,
30 | };
31 |
32 | const arabicFontStyle = classNames.bind(ArabicFontsClassNames);
33 |
34 | export default arabicFontStyle;
35 |
--------------------------------------------------------------------------------
/src/components/TopBar/DeveloperUtility/OptionList.tsx:
--------------------------------------------------------------------------------
1 | import classNames from 'classnames';
2 | import React from 'react';
3 |
4 | type OptionListProps = {
5 | onClick: () => void;
6 | children: React.ReactNode;
7 | label?: string;
8 | active?: boolean;
9 | };
10 |
11 | const OptionList = ({ onClick, children }: OptionListProps) => {
12 | return (
13 |
17 | {children}
18 |
19 | );
20 | };
21 |
22 | export const OptionButton = ({
23 | onClick,
24 | children,
25 | label,
26 | active,
27 | }: OptionListProps) => (
28 |
39 | {children}
40 |
41 | );
42 |
43 | export default OptionList;
44 |
--------------------------------------------------------------------------------
/src/utils/api/hadith.ts:
--------------------------------------------------------------------------------
1 | import { HadithBook, HaditsDetail } from '@utils/types/Hadith';
2 | import queryString from 'query-string';
3 |
4 | const API_HOST = 'https://api.hadith.gading.dev/';
5 |
6 | const makeHaditsUrl = (path: string, parameters?: string) => {
7 | if (!parameters) {
8 | return `${API_HOST}${path}`;
9 | }
10 |
11 | const queryParams = `?${parameters}`;
12 |
13 | return `${API_HOST}${path}${queryParams}`;
14 | };
15 |
16 | export const getHadithBooks = (): Promise => {
17 | return new Promise((resolve) => {
18 | import('../../../data/hadith/hadith.json').then((data) => {
19 | resolve(data.data);
20 | });
21 | });
22 | };
23 |
24 | type HadithContentParams = {
25 | id: string;
26 | range?: string; // 1-300 max 300
27 | };
28 | export const getHadithDetail = async ({
29 | id,
30 | range = '1-50',
31 | }: HadithContentParams): Promise => {
32 | const response = await fetch(
33 | makeHaditsUrl(`books/${id}/`, queryString.stringify({ range })),
34 | { cache: 'force-cache' }
35 | );
36 | const data = await response.json();
37 | return data.data;
38 | };
39 |
--------------------------------------------------------------------------------
/src/app/(main)/layout.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import BookmarkedVerseLists from '@components/Bookmark/BookmarkedVerseLists';
4 | import Header from '@components/Header';
5 | import ReadQuranHeader from '@components/Header/ReadQuranHeader';
6 | import QuranSwitch from '@components/Switch';
7 | import Wrapper from '@components/Wrapper';
8 | import classNames from 'classnames';
9 | import { useSelectedLayoutSegments } from 'next/navigation';
10 |
11 | export default function HomePage({ children }) {
12 | const layoutSegments = useSelectedLayoutSegments();
13 |
14 | // remove Header and orther components if the path is in surah/[id]
15 | if (layoutSegments.length >= 2) {
16 | return children;
17 | }
18 |
19 | return (
20 |
21 |
22 |
23 |
28 |
29 | {children}
30 |
31 |
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/src/app/(main)/page.tsx:
--------------------------------------------------------------------------------
1 | import Chapters from "@components/chapters";
2 | import { getAllChaptersData } from "@utils/chapter";
3 | import {
4 | defaultOpenGraph,
5 | defaultTwitter,
6 | staticDescription,
7 | staticTitle,
8 | } from "@utils/seo";
9 | import { Metadata } from "next";
10 |
11 | const IS_PRODUCTION = process.env.NODE_ENV === "production";
12 |
13 | async function getChapterData() {
14 | const res = await getAllChaptersData();
15 | return res.chapters;
16 | }
17 |
18 | export const metadata: Metadata = {
19 | title: "Baca Quran",
20 | description: staticDescription["/"],
21 | robots: IS_PRODUCTION ? "index, follow" : "noindex, nofollow",
22 | openGraph: {
23 | ...defaultOpenGraph,
24 | title: staticTitle["/"],
25 | description: staticDescription["/"],
26 | images: "/quranapp.jpg",
27 | },
28 | twitter: {
29 | ...defaultTwitter,
30 | title: staticTitle["/"],
31 | description: staticDescription["/"],
32 | images: "/quranapp.jpg",
33 | },
34 | };
35 |
36 | export default async function HomePage() {
37 | const allChapters = await getChapterData();
38 | return ;
39 | }
40 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "theme_color": "#10b981",
3 | "background_color": "#10b981",
4 | "display": "standalone",
5 | "scope": "/",
6 | "start_url": "/",
7 | "name": "Baca Quran",
8 | "short_name": "Baca Quran",
9 | "description": "Baca Quran adalah aplikasi web interaktif yang memungkinkan pengguna untuk membaca, mencari, dan menjelajahi teks suci Al-Quran secara digital. Aplikasi ini dilengkapi dengan berbagai fitur yang memudahkan pengguna dalam mempelajari dan meresapi ayat-ayat Al-Quran, seperti terjemahan, tafsir, dan audio.",
10 | "icons": [
11 | {
12 | "src": "/icon-192x192.png",
13 | "sizes": "192x192",
14 | "type": "image/png"
15 | },
16 | {
17 | "src": "/icon-256x256.png",
18 | "sizes": "256x256",
19 | "type": "image/png"
20 | },
21 | {
22 | "src": "/icon-384x384.png",
23 | "sizes": "384x384",
24 | "type": "image/png"
25 | },
26 | {
27 | "src": "/icon-512x512.png",
28 | "sizes": "512x512",
29 | "type": "image/png"
30 | }
31 | ]
32 | }
--------------------------------------------------------------------------------
/src/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | html {
6 | @apply scroll-smooth;
7 | -webkit-tap-highlight-color: transparent;
8 | }
9 |
10 | body {
11 | width: 100%;
12 | overflow-x: hidden !important;
13 | @apply bg-gray-100;
14 | }
15 |
16 | body.white {
17 | @apply bg-white;
18 | }
19 |
20 | html,
21 | * {
22 | padding: 0px;
23 | margin: 0px;
24 | }
25 |
26 | a {
27 | color: inherit;
28 | text-decoration: none;
29 | }
30 |
31 | * {
32 | box-sizing: border-box;
33 | }
34 |
35 | h2 {
36 | font-weight: bold;
37 | font-size: 1.1rem;
38 | margin-top: 10px;
39 | }
40 |
41 | p {
42 | font-size: 1rem;
43 | }
44 |
45 | .surah-info h2 {
46 | @apply text-2xl lg:text-3xl font-bold text-emerald-100 dark:text-emerald-500;
47 | }
48 |
49 | .surah-info p {
50 | @apply text-lg md:text-xl text-justify;
51 | }
52 |
53 | .surah-info ol {
54 | list-style: square;
55 | @apply text-lg md:text-xl text-justify pl-5;
56 | }
57 |
58 | .scrollbar-hide {
59 | -ms-overflow-style: none;
60 | scrollbar-width: none; /* Firefox */
61 | }
62 |
63 | .scrollbar-hide::-webkit-scrollbar {
64 | display: none;
65 | }
66 |
--------------------------------------------------------------------------------
/src/components/TopBar/DropdownSurahLists/VerseList.tsx:
--------------------------------------------------------------------------------
1 | import classNames from "classnames";
2 | import { LocalChapter } from "data/chapter/type";
3 | import { useRouter } from "next/navigation";
4 |
5 | type VerseListProps = {
6 | chapterLists: LocalChapter[];
7 | chapterId: number;
8 | };
9 |
10 | const VerseList = ({ chapterLists, chapterId }: VerseListProps) => {
11 | const router = useRouter();
12 |
13 | return (
14 |
19 | {new Array(chapterLists[chapterId].verses_count)
20 | .fill(0)
21 | .map((key, index) => (
22 |
24 | router.push(`/surah/${chapterId + 1}?ayah=${index + 1}`)
25 | }
26 | key={index}
27 | className="p-1 cursor-pointer hover:bg-emerald-100 dark:hover:bg-emerald-400 dark:hover:text-slate-100 hover:text-emerald-500 rounded flex items-center"
28 | >
29 | {index + 1}
30 |
31 | ))}
32 |
33 | );
34 | };
35 |
36 | export default VerseList;
37 |
--------------------------------------------------------------------------------
/src/components/Banner/BannerWrapper.tsx:
--------------------------------------------------------------------------------
1 | import classNames from 'classnames';
2 | import React from 'react';
3 | // import Image from 'next/image'
4 |
5 | export type BannerWrapperProps = {
6 | children: React.ReactNode;
7 | imageOpacity?: number;
8 | imageScale?: number;
9 | className?: string;
10 | };
11 |
12 | const BannerWrapper = ({
13 | children,
14 | imageOpacity,
15 | imageScale,
16 | className,
17 | }: BannerWrapperProps) => {
18 | return (
19 |
20 |
{children}
21 | {/*
22 |
28 |
*/}
29 |
30 | );
31 | };
32 |
33 | export default BannerWrapper;
34 |
--------------------------------------------------------------------------------
/src/app/(main)/juz/page.tsx:
--------------------------------------------------------------------------------
1 | import JuzsView from "@components/chapters/JuzsView";
2 | import { getAllChaptersData } from "@utils/chapter";
3 | import { getJuzs } from "@utils/juz";
4 | import { canonicalUrl, staticDescription } from "@utils/seo";
5 | import { Metadata } from "next";
6 | import React from "react";
7 |
8 | const IS_PRODUCTION = process.env.NODE_ENV === "production";
9 |
10 | export const metadata: Metadata = {
11 | title: "Baca Quran - Juz",
12 | description: staticDescription["/juz"],
13 | robots: IS_PRODUCTION ? "index, follow" : "noindex, nofollow",
14 | openGraph: {
15 | title: "Baca Quran - Juz",
16 | description: staticDescription["/juz"],
17 | url: `${canonicalUrl}quran/juz}`,
18 | },
19 | twitter: {
20 | title: "Baca Quran - Juz",
21 | description: staticDescription["/juz"],
22 | },
23 | };
24 |
25 | const JuzList = async () => {
26 | const juzsData = await getJuzs();
27 | const chapterData = await getAllChaptersData();
28 |
29 | return (
30 |
31 |
32 |
33 | );
34 | };
35 |
36 | export default JuzList;
37 |
--------------------------------------------------------------------------------
/src/utils/types/Verse.ts:
--------------------------------------------------------------------------------
1 | export type VerseWord = {
2 | id: string;
3 | text: string;
4 | location: string;
5 | position: number;
6 | translation: {
7 | text: string;
8 | };
9 | transliteration: {
10 | text: string;
11 | };
12 | };
13 |
14 | export type Verse = {
15 | id: number;
16 | verse_number: number;
17 | text_uthmani: string;
18 | verse_key: string;
19 | translations: {
20 | id: string;
21 | resource_id: number;
22 | text: string;
23 | };
24 | words: VerseWord[];
25 | };
26 |
27 | export type VersePagination = {
28 | per_page: number;
29 | current_page: number;
30 | total_pages: number;
31 | total_count: number;
32 | };
33 |
34 | export enum GetVerseBy {
35 | Chapter = 'by_chapter',
36 | Juz = 'by_juz',
37 | }
38 |
39 | export type GetVerseParams = {
40 | lang?: string;
41 | words?: boolean;
42 | per_page?: number;
43 | id: number;
44 | page?: number;
45 | getBy: GetVerseBy;
46 | };
47 |
48 | export type GetVerseByJuzParams = {
49 | lang?: string;
50 | words?: boolean;
51 | per_page?: number;
52 | id: number;
53 | page?: number;
54 | };
55 |
56 | export type VersesResponse = {
57 | verses: Verse[];
58 | pagination: VersePagination;
59 | };
60 |
--------------------------------------------------------------------------------
/src/components/quranReader/QuranReader.tsx:
--------------------------------------------------------------------------------
1 | import Bismillah from "./Bismillah";
2 | import { GetVerseBy, Verse } from "@utils/types/Verse";
3 | import InitialSurahVerse from "./InitialSurahVerse";
4 | import dynamic from "next/dynamic";
5 |
6 | const FetchInfiniteVerse = dynamic(() => import("./FetchInfiniteVerse"), {
7 | ssr: false,
8 | });
9 |
10 | type QuranReaderProps = {
11 | versesData: Verse[];
12 | bismillahPre?: boolean;
13 | versesCount: number;
14 | id: number;
15 | type: "chapter" | "juz";
16 | };
17 |
18 | const QuranReader = ({
19 | versesData,
20 | bismillahPre,
21 | versesCount,
22 | id,
23 | type,
24 | }: QuranReaderProps) => {
25 | return (
26 |
27 | <>
28 |
29 |
30 |
31 |
38 |
39 | >
40 |
41 | );
42 | };
43 |
44 | export default QuranReader;
45 |
--------------------------------------------------------------------------------
/src/components/Bookmark/BookmarkedVerseLists.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import BookmarkedItem from './BookmarkedItem';
4 | import useSurah from '../../store/surahStore';
5 | import { getLocalChapter } from '../../utils/chapter';
6 | import { useEffect } from 'react';
7 | import { shallow } from 'zustand/shallow';
8 | import BookmarkWrapper from './BookmarkWrapper';
9 |
10 | const BookmarkedVerseLists = () => {
11 | const { bookmarkData, chapterData, setBookmarked } = useSurah(
12 | (state) => ({
13 | bookmarkData: state.bookmarked,
14 | chapterData: state.chapterData,
15 | setBookmarked: state.setBookmarkData,
16 | }),
17 | shallow
18 | );
19 |
20 | if (chapterData.length === 0) return <>>;
21 |
22 | return (
23 | setBookmarked([])}
25 | isEmpty={bookmarkData.length < 1}
26 | >
27 | {bookmarkData.map((e, index) => {
28 | const chapterId = e.split(':');
29 | return (
30 |
35 | );
36 | })}
37 |
38 | );
39 | };
40 |
41 | export default BookmarkedVerseLists;
42 |
--------------------------------------------------------------------------------
/src/components/chapters/JuzsView.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ChapterCard from './Card/ChapterCard';
3 | import JuzWrapper from './Card/JuzWrapper';
4 | import { Chapter } from '@utils/types/Chapter';
5 | import { Juz } from '@utils/types/Juz';
6 |
7 | type JuzsViewProps = {
8 | chapterData: Chapter[];
9 | juzsData: Juz[];
10 | };
11 |
12 | const JuzsView = ({ chapterData, juzsData }: JuzsViewProps) => {
13 | return (
14 | <>
15 | {juzsData.map((e) => {
16 | return (
17 |
18 | {Object.keys(e.verse_mapping).map((key, index) => {
19 | let { id, translated_name, name_arabic, name_simple } =
20 | chapterData[parseInt(key) - 1];
21 | return (
22 |
30 | );
31 | })}
32 |
33 | );
34 | })}
35 | >
36 | );
37 | };
38 |
39 | export default JuzsView;
40 |
--------------------------------------------------------------------------------
/src/components/TransitionWrapper/TransitionWrapper.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { motion } from 'framer-motion';
3 |
4 | type TransitionWrapperProps = {
5 | children: React.ReactNode;
6 | type?: 'toLeft' | 'toRight';
7 | withOpactity?: boolean;
8 | };
9 |
10 | const TransitionWrapper = ({
11 | children,
12 | type = 'toLeft',
13 | withOpactity = true,
14 | }: TransitionWrapperProps) => {
15 | const variants = {
16 | hidden: {
17 | opacity: withOpactity ? 0 : 1,
18 | x: type === 'toLeft' ? -200 : 200,
19 | y: 0,
20 | },
21 | enter: {
22 | opacity: 1,
23 | x: 0,
24 | y: 0,
25 | },
26 | exit: {
27 | opacity: withOpactity ? 0 : 1,
28 | x: type === 'toLeft' ? -100 : 100,
29 | y: 0,
30 | },
31 | };
32 |
33 | return (
34 |
41 | {children}
42 |
43 | );
44 | };
45 |
46 | export default TransitionWrapper;
47 |
--------------------------------------------------------------------------------
/src/store/surahStore.ts:
--------------------------------------------------------------------------------
1 | import { LocalChapter } from 'data/chapter/type';
2 | import { create } from 'zustand';
3 | import { createJSONStorage, persist } from 'zustand/middleware';
4 |
5 | interface SurahStore {
6 | bookmarked: string[];
7 | chapterData: LocalChapter[];
8 |
9 | setBookmarkData: (bookmarked: string[]) => void;
10 | addBookmarkData: (verseKey: string) => void;
11 | deleteBookmarkData: (verseKey: string) => void;
12 | setChapterData: (chapterData: LocalChapter[]) => void;
13 | }
14 |
15 | const useSurah = create()(
16 | persist(
17 | (set) => ({
18 | bookmarked: [],
19 | chapterData: [],
20 |
21 | setBookmarkData: (bookmarked) => set({ bookmarked }),
22 | addBookmarkData: (verseKey) =>
23 | set((state) => ({ bookmarked: [...state.bookmarked, verseKey] })),
24 | deleteBookmarkData: (verseKey) =>
25 | set((state) => ({
26 | bookmarked: state.bookmarked.filter((verse) => verse !== verseKey),
27 | })),
28 | setChapterData: (chapterData) => set({ chapterData }),
29 | }),
30 | {
31 | name: 'surah',
32 | storage: createJSONStorage(() => localStorage),
33 | partialize: (state) => ({
34 | bookmarked: state.bookmarked,
35 | }),
36 | }
37 | )
38 | );
39 |
40 | export default useSurah;
41 |
--------------------------------------------------------------------------------
/src/app/(main)/surah/[chapterId]/[ayahId]/page.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { getSpecificVerse } from "@utils/verse";
3 | import Verses from "@components/quranReader/Verses";
4 | import { Metadata } from "next";
5 | import { getLocalChapter } from "@utils/chapter";
6 |
7 | type Props = {
8 | params: {
9 | chapterId: string;
10 | ayahId: string;
11 | };
12 | };
13 |
14 | export async function generateMetadata({ params }: Props): Promise {
15 | const chapterData = await getLocalChapter();
16 |
17 | return {
18 | title: `${chapterData[parseInt(params.chapterId) - 1].name_simple} : ${
19 | params.ayahId
20 | }`,
21 | };
22 | }
23 |
24 | const SingleAyahPage = async ({ params }: Props) => {
25 | const { chapterId, ayahId } = params;
26 | const responseData = await getSpecificVerse(`${chapterId}:${ayahId}`);
27 |
28 | return (
29 |
30 |
39 |
40 | );
41 | };
42 |
43 | export default SingleAyahPage;
44 |
--------------------------------------------------------------------------------
/src/components/TopBar/DeveloperUtility/TranslationOption.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import AdjustmentWrapper from './AdjustmentWrapper';
3 | import { OptionButton } from './OptionList';
4 | import useSettings from '@stores/settingsStore';
5 |
6 | type Props = {};
7 |
8 | const TranslationOption = (props: Props) => {
9 | const { translationMode, setTranslationMode } = useSettings((state) => ({
10 | translationMode: state.translationMode,
11 | setTranslationMode: state.setTranslationMode,
12 | }));
13 |
14 | return (
15 |
16 |
17 |
18 | setTranslationMode('word')}
20 | label="scroll otomatis per kata"
21 | active={translationMode === 'word'}
22 | >
23 | Kata
24 |
25 | setTranslationMode('verse')}
27 | label="scroll otomatis per ayah"
28 | active={translationMode === 'verse'}
29 | >
30 | Ayah
31 |
32 |
33 |
34 |
35 | );
36 | };
37 |
38 | export default TranslationOption;
39 |
--------------------------------------------------------------------------------
/src/components/TopBar/DropdownSurahLists/ChapterLists.tsx:
--------------------------------------------------------------------------------
1 | import classNames from "classnames";
2 | import React from "react";
3 | import { useRouter } from "next/navigation";
4 | import { Chapter } from "@utils/types/Chapter";
5 |
6 | type ChapterListsProps = {
7 | chapterLists: Chapter[];
8 | chapterActive: number;
9 | };
10 |
11 | const ChapterLists = ({ chapterLists, chapterActive }: ChapterListsProps) => {
12 | const router = useRouter();
13 |
14 | return (
15 |
20 | {chapterLists?.map((e) => (
21 | router.push(`/surah/${e.id}`)}
24 | className={classNames(
25 | "px-2 py-1 cursor-pointer hover:bg-emerald-100/50 dark:hover:bg-emerald-400/30 dark:hover:text-slate-100 hover:text-emerald-500 rounded flex items-center",
26 | { "dark:bg-emerald-400 bg-emerald-200": chapterActive == e.id }
27 | )}
28 | >
29 |
30 | {e.id}
31 |
32 | {e.name_simple}
33 |
34 | ))}
35 |
36 | );
37 | };
38 |
39 | export default ChapterLists;
40 |
--------------------------------------------------------------------------------
/src/components/Banner/SurahInfo.tsx:
--------------------------------------------------------------------------------
1 | import classNames from "classnames";
2 | import Link from "next/link";
3 |
4 | type SurahInfoProps = {
5 | verses_count: number;
6 | revelation_place: string;
7 | short_text: string;
8 | chapterId: number;
9 | className?: string;
10 | };
11 |
12 | const SurahInfo = ({
13 | verses_count,
14 | revelation_place,
15 | short_text,
16 | chapterId,
17 | className,
18 | }: SurahInfoProps) => {
19 | return (
20 |
26 |
27 |
28 | Jumlah Ayah :
29 | {verses_count}
30 |
31 |
32 |
33 | Tempat Wahyu :
34 | {revelation_place}
35 |
36 |
37 |
38 |
{short_text}
39 |
40 |
41 |
42 | Tampilkan lebih lengkap
43 |
44 |
45 |
46 | );
47 | };
48 |
49 | export default SurahInfo;
50 |
--------------------------------------------------------------------------------
/src/components/Skeleton/skeleton.module.css:
--------------------------------------------------------------------------------
1 | .skeleton{
2 | border-radius: 5px;
3 | }
4 |
5 | .skeleton{
6 | position: relative;
7 | overflow: hidden;
8 | }
9 |
10 | .skeleton::after{
11 | content: '';
12 | display: block;
13 | position: absolute;
14 | width: 0%;
15 | height: 100%;
16 | /* background-color: red; */
17 | background-color: rgba(255, 255, 255, 0.411);
18 | /* filter: blur(10px); */
19 | animation: wave;
20 | animation-duration: 1.2s;
21 | animation-timing-function: ease-in-out;
22 | animation-iteration-count: infinite;
23 |
24 | }
25 |
26 | .skeleton.delay::after{
27 | animation-delay: 200ms;
28 | }
29 |
30 | @keyframes wave{
31 | /* 0% {
32 | left: -10%;
33 | opacity: 1;
34 | }
35 | 70% {
36 | left: 100%;
37 | opacity: 1;
38 | }
39 | 71% {
40 | left: 100%;
41 | opacity: 0;
42 | }
43 | 72% {
44 | opacity: 1;
45 | left: -10%;
46 | }
47 |
48 | 100% {
49 | left: 100%;
50 | } */
51 | 0% {
52 | width: 0%;
53 | }
54 | 50% {
55 | width: 100%;
56 | }
57 | 51% {
58 | transform: translateX(0%);
59 | width: 100%;
60 | }
61 | 70% {
62 | transform: translateX(0%);
63 | width: 100%;
64 | }
65 | 100% {
66 | transform: translateX(100%);
67 | width: 100%;
68 | }
69 |
70 | }
--------------------------------------------------------------------------------
/src/components/quranReader/action/HandleBookmark.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React from 'react';
4 | import IconWrapper from '../../icons/IconWrapper';
5 | import BookmarkIcon from '../../icons/BookmarkIcon';
6 | import useSurah from '../../../store/surahStore';
7 | import { toast } from 'react-hot-toast';
8 |
9 | type HandleBookmarkProps = {
10 | verseKey: string;
11 | };
12 |
13 | const HandleBookmark = ({ verseKey }: HandleBookmarkProps) => {
14 | const { bookmarked, addBookmark, deleteBookmarked } = useSurah((state) => ({
15 | bookmarked: state.bookmarked,
16 | addBookmark: state.addBookmarkData,
17 | deleteBookmarked: state.deleteBookmarkData,
18 | }));
19 |
20 | const isBookmarked = bookmarked.includes(verseKey);
21 |
22 | function handleBookmarkClick(verseKey) {
23 | if (isBookmarked) {
24 | deleteBookmarked(verseKey);
25 | } else {
26 | addBookmark(verseKey);
27 | toast.success('Ayat berhasil ditandai');
28 | }
29 | }
30 |
31 | return (
32 | handleBookmarkClick(verseKey)}
35 | className="text-gray-500 dark:hover:text-gray-50 group cursor-pointer"
36 | >
37 |
41 |
42 | );
43 | };
44 |
45 | export default HandleBookmark;
46 |
--------------------------------------------------------------------------------
/src/components/icons/CopyIcon.tsx:
--------------------------------------------------------------------------------
1 | const CopyIcon = ({className, onClick}) => {
2 | return (
3 |
4 |
5 |
6 |
7 | )
8 | }
9 |
10 | export default CopyIcon
--------------------------------------------------------------------------------
/src/store/quranReaderStore.ts:
--------------------------------------------------------------------------------
1 | import { create } from 'zustand';
2 |
3 | interface QuranReaderState {
4 | currentChapter: number;
5 | currentVerse: number;
6 | audioId: null | number;
7 | tafsirState: {
8 | verseId: number;
9 | } | null;
10 | highlightedWord: string | null;
11 | highlightedVerse: string | null;
12 |
13 | setCurrentChapter: (chapter: number) => void;
14 | setCurrentVerse: (verse: number) => void;
15 | setTafsirState: (tafsirState: QuranReaderState['tafsirState']) => void;
16 | setHighlightedWord: (
17 | highlightedWord: QuranReaderState['highlightedWord']
18 | ) => void;
19 | setHighlightedVerse: (
20 | highlightedVerse: QuranReaderState['highlightedVerse']
21 | ) => void;
22 | setAudioId: (audioId: QuranReaderState['audioId']) => void;
23 | }
24 |
25 | const useQuranReader = create()((set) => ({
26 | currentChapter: 1,
27 | currentVerse: 1,
28 | audioId: null,
29 | tafsirState: null,
30 | highlightedWord: null,
31 | highlightedVerse: null,
32 |
33 | setCurrentChapter: (chapter) => set({ currentChapter: chapter }),
34 | setCurrentVerse: (verse) => set({ currentVerse: verse }),
35 | setTafsirState: (tafsirState) => set({ tafsirState }),
36 | setHighlightedWord: (highlightedWord) => set({ highlightedWord }),
37 | setHighlightedVerse: (highlightedVerse) => set({ highlightedVerse }),
38 | setAudioId: (audioId) => set({ audioId }),
39 | }));
40 |
41 | export default useQuranReader;
42 |
--------------------------------------------------------------------------------
/src/store/hadithStore.ts:
--------------------------------------------------------------------------------
1 | import { HadithBook } from '@utils/types/Hadith';
2 | import { create } from 'zustand';
3 | import { createJSONStorage, persist } from 'zustand/middleware';
4 |
5 | interface HaditsStore {
6 | bookmarked: string[];
7 | hadithData: HadithBook[];
8 | hadithActive: string;
9 |
10 | setHadithActive: (hadithActive: string) => void;
11 | setHadithData: (hadithData: HadithBook[]) => void;
12 | setBookmarkData: (bookmarked: string[]) => void;
13 | addBookmarkData: (verseKey: string) => void;
14 | deleteBookmarkData: (verseKey: string) => void;
15 | }
16 |
17 | const useHadith = create()(
18 | persist(
19 | (set) => ({
20 | bookmarked: [],
21 | hadithData: [],
22 | hadithActive: 'muslim',
23 | setHadithActive: (hadithActive) => set({ hadithActive }),
24 | setHadithData: (hadithData) => set({ hadithData }),
25 | setBookmarkData: (bookmarked) => set({ bookmarked }),
26 | addBookmarkData: (verseKey) =>
27 | set((state) => ({ bookmarked: [...state.bookmarked, verseKey] })),
28 | deleteBookmarkData: (verseKey) =>
29 | set((state) => ({
30 | bookmarked: state.bookmarked.filter((verse) => verse !== verseKey),
31 | })),
32 | }),
33 | {
34 | name: 'hadith',
35 | storage: createJSONStorage(() => localStorage),
36 | partialize: (state) => ({
37 | bookmarked: state.bookmarked,
38 | }),
39 | }
40 | )
41 | );
42 |
43 | export default useHadith;
44 |
--------------------------------------------------------------------------------
/src/components/TopBar/DeveloperUtility/AutoScroll.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React, { useEffect, useState } from 'react';
4 | import AdjustmentWrapper from './AdjustmentWrapper';
5 | import useSettings from '../../../store/settingsStore';
6 | import { OptionButton } from './OptionList';
7 |
8 | const AutoScroll = () => {
9 | const { autoScroll, setAutoScroll } = useSettings((state) => ({
10 | autoScroll: state.autoScroll,
11 | setAutoScroll: state.setAutoScroll,
12 | }));
13 |
14 | return (
15 |
16 |
17 |
18 | setAutoScroll(false)}
20 | label="Matikan scroll otomatis"
21 | active={autoScroll === false}
22 | >
23 | Mati
24 |
25 | setAutoScroll('word')}
27 | label="scroll otomatis per kata"
28 | active={autoScroll === 'word'}
29 | >
30 | Kata
31 |
32 | setAutoScroll('verse')}
34 | label="scroll otomatis per ayah"
35 | active={autoScroll === 'verse'}
36 | >
37 | Ayah
38 |
39 |
40 |
41 |
42 | );
43 | };
44 |
45 | export default AutoScroll;
46 |
--------------------------------------------------------------------------------
/src/utils/chapter.ts:
--------------------------------------------------------------------------------
1 | import { LocalChapter } from "data/chapter/type";
2 | import { makeUrl } from "./api";
3 | import { Chapter, ChapterInfo } from "./types/Chapter";
4 |
5 | export const getAllChaptersData = async (
6 | lang = "id"
7 | ): Promise<{
8 | chapters: Chapter[];
9 | }> => {
10 | const response = await fetch(makeUrl(`/chapters`, `language=${lang}`), {
11 | cache: "force-cache",
12 | });
13 | const data = await response.json();
14 | return data;
15 | };
16 |
17 | export const getChapterInfo = async (
18 | chapterId: number,
19 | lang = "id"
20 | ): Promise<{
21 | chapter_info: ChapterInfo;
22 | }> => {
23 | const response = await fetch(
24 | makeUrl(`/chapters/${chapterId}/info`, `language=${lang}`)
25 | );
26 | const data = await response.json();
27 | return data;
28 | };
29 |
30 | export const getChapter = async (
31 | chapterId: number,
32 | lang = "id"
33 | ): Promise => {
34 | const response = await fetch(
35 | makeUrl(`/chapters/${chapterId}`, `language=${lang}`)
36 | );
37 | const data = await response.json();
38 | return data.chapter;
39 | };
40 |
41 | export const getLocalChapter = (lang = "id"): Promise => {
42 | return new Promise((resolve) => {
43 | import(`../../data/chapter/${lang}.json`).then((data) => {
44 | const array = Object.keys(data.default).map((key) => ({
45 | id: parseInt(key),
46 | verses_count: data.default[key].versesCount,
47 | name_simple: data.default[key].transliteratedName,
48 | revelation_place: data.default[key].revelationPlace,
49 | }));
50 | resolve(array);
51 | });
52 | });
53 | };
54 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | const withPWA = require("@ducanh2912/next-pwa").default({
2 | dest: "public",
3 | fallbacks: {
4 | document: "/~offline",
5 | }
6 | });
7 |
8 | /** @type {import('next').NextConfig} */
9 | const config = {
10 | // async redirects() {
11 | // return [
12 | // {
13 | // source: '/juz',
14 | // destination: '/quran/juz',
15 | // permanent: false
16 | // },
17 | // {
18 | // source: '/surah',
19 | // destination: '/quran/surah',
20 | // permanent: false
21 | // },
22 | // {
23 | // source: '/surah/:id/:ayah',
24 | // destination: '/quran/surah/:id/:ayah',
25 | // permanent: false
26 | // },
27 | // {
28 | // source: '/juz/:id',
29 | // destination: '/quran/juz/:id',
30 | // permanent: false
31 | // }
32 | // ]
33 | // },
34 | // experimental: {
35 | // scrollRestoration: true,
36 | // },
37 | reactStrictMode: true,
38 | eslint: {
39 | // Warning: This allows production builds to successfully complete even if
40 | // your project has ESLint errors.
41 | ignoreDuringBuilds: true,
42 | },
43 | // i18n: {
44 | // localeDetection: false,
45 | // // These are all the locales you want to support in
46 | // // your application
47 | // locales: ['id', 'en'],
48 | // // This is the default locale you want to be used when visiting
49 | // // a non-locale prefixed path e.g. `/hello`
50 | // defaultLocale: 'id',
51 | // },
52 | }
53 |
54 |
55 | /** @type {import('next').NextConfig} */
56 | const nextConfig = withPWA(config)
57 |
58 |
59 | module.exports = nextConfig
60 |
--------------------------------------------------------------------------------
/src/components/TopBar/DeveloperUtility/Transliteration.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import AdjustmentWrapper from './AdjustmentWrapper';
3 | import { shallow } from 'zustand/shallow';
4 | import useSettings from '@stores/settingsStore';
5 |
6 | type Props = {};
7 |
8 | const Transliteration = (props: Props) => {
9 | const { transliteration, setTransliteration } = useSettings(
10 | (state) => ({
11 | transliteration: state.transliteration,
12 | setTransliteration: state.setTransliteration,
13 | }),
14 | shallow
15 | );
16 |
17 | return (
18 |
19 |
23 |
30 | setTransliteration(!transliteration)}
32 | className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-emerald-300 dark:peer-focus:ring-slate-500/50 rounded-full peer dark:bg-slate-500 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-emerald-500 after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-white dark:peer-checked:bg-emerald-100"
33 | >
34 |
35 |
36 | );
37 | };
38 |
39 | export default Transliteration;
40 |
--------------------------------------------------------------------------------
/src/components/Bookmark/BookmarkWrapper.tsx:
--------------------------------------------------------------------------------
1 | import { BookmarkIcon } from '@components/icons';
2 | import IconWrapper from '@components/icons/IconWrapper';
3 | import TrashIcon from '@components/icons/TrashIcon';
4 | import React from 'react';
5 |
6 | type Props = {
7 | children: React.ReactNode;
8 | isEmpty: boolean;
9 | onClickDelete: () => void;
10 | };
11 |
12 | const BookmarkWrapper = ({ isEmpty, children, onClickDelete }: Props) => {
13 | return (
14 |
15 |
16 |
17 |
18 |
19 | Bookmark
20 |
21 |
26 |
27 |
28 |
29 |
30 | {isEmpty ? (
31 |
32 | Klik
33 |
34 | untuk menambahkan
35 |
36 | ) : (
37 | children
38 | )}
39 |
40 |
41 |
42 | );
43 | };
44 |
45 | export default BookmarkWrapper;
46 |
--------------------------------------------------------------------------------
/src/components/quranReader/VerseSkeleton.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Skeleton from "../Skeleton/Skeleton";
3 | import classNames from "classnames";
4 |
5 | type VerseSkeletonProps = {
6 | className?: string;
7 | animateDelay?: number | string;
8 | };
9 |
10 | const VerseSkeleton = ({ className, animateDelay }: VerseSkeletonProps) => {
11 | return (
12 |
13 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | );
43 | };
44 |
45 | export default VerseSkeleton;
46 |
--------------------------------------------------------------------------------
/src/components/TopBar/DeveloperUtility/ThemeAdjustment.tsx:
--------------------------------------------------------------------------------
1 | import classNames from 'classnames';
2 | import React from 'react';
3 | import AdjustmentWrapper from './AdjustmentWrapper';
4 | import useSettings from '../../../store/settingsStore';
5 |
6 | const ThemeAdjustment = () => {
7 | const { setTheme, theme } = useSettings((state) => ({
8 | theme: state.theme,
9 | setTheme: state.setTheme,
10 | }));
11 |
12 | return (
13 |
14 |
15 |
16 |
23 |
setTheme('light')}
29 | >
30 | Terang
31 |
32 |
setTheme('dark')}
38 | >
39 | Gelap
40 |
41 |
42 |
43 |
44 | );
45 | };
46 |
47 | export default ThemeAdjustment;
48 |
--------------------------------------------------------------------------------
/src/components/TopBar/TopBar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import classNames from "classnames";
3 | import React, { useEffect } from "react";
4 |
5 | import DeveloperUtility from "./DeveloperUtility/DeveloperUtility";
6 | import DropdownSurahLists from "./DropdownSurahLists/DropdownSurahLists";
7 | import useSurah from "../../store/surahStore";
8 | import { shallow } from "zustand/shallow";
9 | import useQuranReader from "@stores/quranReaderStore";
10 | import { useParams, usePathname } from "next/navigation";
11 | import DropdownHadithLists from "./DropdownHadithLists/DropdownHadithLists";
12 |
13 | const TopBar = () => {
14 | const { chapterData } = useSurah(
15 | (state) => ({
16 | chapterData: state.chapterData,
17 | }),
18 | shallow
19 | );
20 | const pathname = usePathname();
21 | const params = useParams();
22 |
23 | const { currentChapter } = useQuranReader(
24 | (state) => ({
25 | currentChapter: state.currentChapter,
26 | }),
27 | shallow
28 | );
29 |
30 | const showTopBar =
31 | Object.hasOwn(params, "chapterId") || Object.hasOwn(params, "juzId");
32 |
33 | if (chapterData.length === 0 || !showTopBar) return <>>;
34 | return (
35 |
40 |
44 | {/* {pathname.includes("/hadits") && } */}
45 |
46 |
47 | );
48 | };
49 |
50 | export default TopBar;
51 |
--------------------------------------------------------------------------------
/src/components/chapters/Card/ChapterCard.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import React from "react";
3 | import { StarIcon } from "../../icons";
4 | import ChapterWrapper from "./ChapterWrapper";
5 | import classNames from "classnames";
6 | import { nastaleeqClassName } from "@utils/fonts";
7 |
8 | type ChapterCardProps = {
9 | chapterId: number;
10 | translated_name: string;
11 | name_arabic: string;
12 | name_simple: string;
13 | verse_mapping?: string;
14 | };
15 |
16 | const ChapterCard = ({
17 | chapterId,
18 | translated_name,
19 | name_arabic,
20 | name_simple,
21 | verse_mapping,
22 | }: ChapterCardProps) => {
23 | return (
24 |
25 |
26 |
27 |
28 | {chapterId}
29 |
30 |
31 |
32 | {name_simple}
33 |
34 | {translated_name}
35 |
36 |
37 |
38 |
39 |
45 | {name_arabic}
46 |
47 | {verse_mapping && {verse_mapping} }
48 |
49 |
50 |
51 | );
52 | };
53 |
54 | export default ChapterCard;
55 |
--------------------------------------------------------------------------------
/src/utils/verse.ts:
--------------------------------------------------------------------------------
1 | import { makeUrl } from './api';
2 | import queryString from 'query-string';
3 | import {
4 | GetVerseByJuzParams,
5 | GetVerseParams,
6 | Verse,
7 | VersesResponse,
8 | } from './types/Verse';
9 |
10 | const translations_lists = [
11 | {
12 | id: 131,
13 | language_name: 'english',
14 | },
15 | {
16 | id: 33,
17 | language_name: 'indonesian',
18 | },
19 | ];
20 |
21 | export const getSpecificVerse = async (
22 | verseKey: string,
23 | lang = 'id'
24 | ): Promise<{
25 | verse: Verse;
26 | }> => {
27 | const params = {
28 | language: lang,
29 | fields: 'text_uthmani',
30 | translation_fields: ['resource_name', 'language_id'],
31 | translations:
32 | lang === 'id' ? translations_lists[1].id : translations_lists[0].id,
33 | per_page: 1,
34 | words: 'true',
35 | word_fields: 'text_uthmani, location',
36 | };
37 |
38 | const response = await fetch(
39 | makeUrl(`/verses/by_key/${verseKey}`, queryString.stringify(params)),
40 | {
41 | cache: 'no-cache',
42 | }
43 | );
44 | const data = await response.json();
45 | return data;
46 | };
47 |
48 | export const getVerses = async ({
49 | id,
50 | lang = 'id',
51 | per_page = 20,
52 | page = 1,
53 | getBy,
54 | }: GetVerseParams): Promise => {
55 | // const params = 'language=id&fields=text_uthmani&translation_fields=resource_name,language_id&translations=33&per_page=220'
56 | const params = {
57 | words: 'true',
58 | language: lang,
59 | fields: 'text_uthmani',
60 | translation_fields: ['resource_name', 'language_id'],
61 | translations:
62 | lang === 'id' ? translations_lists[1].id : translations_lists[0].id,
63 | per_page,
64 | word_fields: 'text_uthmani, location',
65 | page,
66 | };
67 | const response = await fetch(
68 | makeUrl(`/verses/${getBy}/${id}`, queryString.stringify(params)),
69 | {
70 | cache: 'force-cache',
71 | }
72 | );
73 | const data = response.json();
74 | return data;
75 | };
76 |
--------------------------------------------------------------------------------
/src/components/icons/QuranIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const QuranIcon = ({className}) => {
4 | return (
5 |
6 |
7 |
8 |
9 | )
10 | }
11 |
12 | export default QuranIcon
--------------------------------------------------------------------------------
/src/components/TopBar/DeveloperUtility/LanguageAdjustment.tsx:
--------------------------------------------------------------------------------
1 | import classNames from 'classnames';
2 | import React, { useEffect, useState } from 'react';
3 | import { useRouter } from 'next/router';
4 | import { ChevronIcon } from '../../icons';
5 | import AdjustmentWrapper from './AdjustmentWrapper';
6 | import OptionList from './OptionList';
7 |
8 | const LanguageAdjustment = () => {
9 | const [isExpanded, setExpanded] = useState(false);
10 |
11 | const router = useRouter();
12 |
13 | const lang = {
14 | id: 'Indonesia',
15 | en: 'English',
16 | };
17 | const [langId, setLang] = useState(null);
18 |
19 | function setLocale(lang) {
20 | router.push(router.asPath, router.asPath, { locale: lang, scroll: false });
21 | setLang(lang);
22 | }
23 |
24 | useEffect(() => {
25 | setLang(router.locale);
26 | }, []);
27 |
28 | return (
29 |
30 | setExpanded(!isExpanded)}
32 | className="text-center py-2 px-2 group text-black dark:text-slate-100 bg-gray-100 dark:bg-slate-500 rounded w-32 flex justify-between relative cursor-pointer"
33 | >
34 |
{lang[langId]}
35 |
40 |
41 | {/* Lists Language */}
42 |
49 | setLocale('id')}>Indonesia
50 | setLocale('en')}>English
51 |
52 |
53 |
54 | );
55 | };
56 |
57 | export default LanguageAdjustment;
58 |
--------------------------------------------------------------------------------
/src/components/icons/HistoryIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const HistoryIcon = ({className}) => {
4 | return (
5 |
6 |
7 |
8 |
9 | )
10 | }
11 |
12 | export default HistoryIcon
--------------------------------------------------------------------------------
/src/~page-offline/hadits/[hadithId]/page.tsx:
--------------------------------------------------------------------------------
1 | // import HadithBanner from '@components/Banner/HadithBanner';
2 | // import HadithVerse from '@components/Hadith/HadithVerse';
3 | // import Wrapper from '@components/Wrapper';
4 | // import { getHadithBooks, getHadithDetail } from '@utils/api/hadith';
5 | // import React from 'react';
6 | // import HadithHandleClient from './handleClient';
7 | // import HadithReader from '@components/Hadith/HadithReader/HadithReader';
8 |
9 | // type Props = {};
10 |
11 | // const IS_PRODUCTION = process.env.NODE_ENV === 'production';
12 |
13 | // export const generateStaticParams = async () => {
14 | // const data = await getHadithBooks();
15 | // return data.map((item) => ({
16 | // hadithId: item.id,
17 | // }));
18 | // };
19 |
20 | // export const generateMetadata = async ({ params }) => {
21 | // const data = await getHadithDetail({ id: params.hadithId });
22 | // return {
23 | // title: `${data.name}`,
24 | // description: `Baca hadits ${data.name} dengan jumlah ${data.available} hadits.`,
25 | // robots: IS_PRODUCTION ? 'index, follow' : 'noindex, nofollow',
26 | // openGraph: {
27 | // title: `${data.name}`,
28 | // description: `Baca hadits ${data.name} dengan jumlah ${data.available} hadits.`,
29 | // },
30 | // twitter: {
31 | // title: `${data.name}`,
32 | // description: `Baca hadits ${data.name} dengan jumlah ${data.available} hadits.`,
33 | // },
34 | // };
35 | // };
36 |
37 | // const HadithDetailPage = async ({
38 | // params,
39 | // }: {
40 | // params: {
41 | // hadithId: string;
42 | // };
43 | // }) => {
44 | // const data = await getHadithDetail({ id: params.hadithId, range: '1-30' });
45 | // return (
46 | //
47 | //
48 | //
49 | //
54 | //
55 | // );
56 | // };
57 |
58 | // export default HadithDetailPage;
59 |
--------------------------------------------------------------------------------
/src/~page-offline/hadits/page.tsx:
--------------------------------------------------------------------------------
1 | // import BookmarkHadithLists from '@components/Bookmark/BookmarkHadithLists';
2 | // import BookmarkedVerseLists from '@components/Bookmark/BookmarkedVerseLists';
3 | // import HadithCard from '@components/Hadith/HadithCard';
4 | // import ReadHadithHeader from '@components/Header/ReadHadithHeader';
5 | // import Wrapper from '@components/Wrapper';
6 | // import { getHadithBooks } from '@utils/api/hadith';
7 | // import {
8 | // defaultOpenGraph,
9 | // defaultTwitter,
10 | // staticDescription,
11 | // staticTitle,
12 | // } from '@utils/seo';
13 | // import classNames from 'classnames';
14 | // import { Metadata } from 'next';
15 | // import React from 'react';
16 |
17 | // type Props = {};
18 |
19 | // const IS_PRODUCTION = process.env.NODE_ENV === 'production';
20 |
21 | // export const metadata: Metadata = {
22 | // title: 'Baca Hadits',
23 | // description: staticDescription['/hadits'],
24 | // robots: IS_PRODUCTION ? 'index, follow' : 'noindex, nofollow',
25 | // openGraph: {
26 | // ...defaultOpenGraph,
27 | // title: staticTitle['/hadits'],
28 | // description: staticDescription['/hadits'],
29 | // },
30 | // twitter: {
31 | // ...defaultTwitter,
32 | // title: staticTitle['/hadits'],
33 | // description: staticDescription['/hadits'],
34 | // },
35 | // };
36 |
37 | // const HadithPage = async (props: Props) => {
38 | // const data = await getHadithBooks();
39 | // console.log(data);
40 |
41 | // return (
42 | //
43 | //
44 | // {/* */}
45 | //
50 | //
51 | // {data.map((item) => (
52 | //
58 | // ))}
59 | //
60 | //
61 | //
62 | // );
63 | // };
64 |
65 | // export default HadithPage;
66 |
--------------------------------------------------------------------------------
/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import classNames from "classnames";
2 | import "./globals.css";
3 | import { Lato } from "next/font/google";
4 | import { Metadata, Viewport } from "next";
5 | import dynamic from "next/dynamic";
6 | import GoogleAnalytics from "@components/GoogleAnalytics/GoogleAnalytics";
7 | import { canonicalUrl } from "@utils/seo";
8 | import { Toaster } from "react-hot-toast";
9 | import ThemeHandler from "./ThemeHandler";
10 | import TafsirModal from "@components/Tafsir/Tafsir";
11 | import InitChapterData from "./(main)/InitChapterData";
12 | import TopBar from "@components/TopBar/TopBar";
13 | import {
14 | defaultOpenGraph,
15 | defaultTwitter,
16 | staticDescription,
17 | staticTitle,
18 | } from "@utils/seo";
19 |
20 | const IS_PRODUCTION = process.env.NODE_ENV === "production";
21 |
22 | const lato = Lato({
23 | subsets: ["latin"],
24 | weight: ["100", "300", "400", "700", "900"],
25 | });
26 |
27 | export const metadata: Metadata = {
28 | metadataBase: canonicalUrl,
29 | manifest: "/manifest.json",
30 | title: staticTitle["/"],
31 | description: staticDescription["/"],
32 | robots: IS_PRODUCTION ? "index, follow" : "noindex, nofollow",
33 | openGraph: {
34 | ...defaultOpenGraph,
35 | images: "/quranapp.jpg",
36 | },
37 | twitter: {
38 | ...defaultTwitter,
39 | images: "/quranapp.jpg",
40 | },
41 | };
42 |
43 | export const viewport: Viewport = {
44 | themeColor: "#f1f5f9",
45 | };
46 |
47 | const AudioPlayer = dynamic(
48 | () => import("@components/AudioPlayer/AudioPlayer"),
49 | {
50 | ssr: false,
51 | }
52 | );
53 |
54 | export default function RootLayout({ children }) {
55 | return (
56 |
57 |
63 |
69 |
70 |
71 |
72 | {children}
73 |
74 |
75 |
76 |
77 |
78 | );
79 | }
80 |
--------------------------------------------------------------------------------
/src/components/TopBar/DeveloperUtility/DeveloperUtility.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import classNames from 'classnames';
4 | import React, { useState } from 'react';
5 | import { AdjustmentIcon } from '../../icons';
6 | import IconWrapper from '../../icons/IconWrapper';
7 | import AutoScroll from './AutoScroll';
8 | import { FontAdjustment, FontSizeAdjustment } from './FontAdjustment';
9 | import Transliteration from './Transliteration';
10 | import TranslationOption from './TranslationOption';
11 | import dynamic from 'next/dynamic';
12 |
13 | const ThemeAdjustment = dynamic(() => import('./ThemeAdjustment'), {
14 | ssr: false,
15 | });
16 |
17 | type DeveloperUtilityProps = {
18 | isInSurah?: boolean;
19 | };
20 |
21 | const DeveloperUtility = ({ isInSurah }: DeveloperUtilityProps) => {
22 | const [isExpanded, setExpanded] = useState(false);
23 |
24 | return (
25 |
26 |
setExpanded(!isExpanded)}
29 | onHover='none'
30 | className='bg-emerald-400'
31 | >
32 |
33 |
34 |
47 |
48 | {/*
*/}
49 | {isInSurah && (
50 | <>
51 |
52 |
53 |
54 |
55 |
56 | >
57 | )}
58 |
59 |
60 | );
61 | };
62 |
63 | export default DeveloperUtility;
64 |
--------------------------------------------------------------------------------
/src/components/Hadith/HadithVerse.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { StarIcon } from '@components/icons';
4 | import ArabicText from '@components/quranReader/ArabicText';
5 | import CopyToClipboard from '@components/quranReader/action/CopyToClipboard';
6 | import HandleBookmark from '@components/quranReader/action/HandleBookmark';
7 | import React, { ForwardedRef, forwardRef, useEffect } from 'react';
8 |
9 | type Props = {
10 | number: number;
11 | arab: string;
12 | translation: string;
13 | style?: any;
14 | measure?: any;
15 | };
16 |
17 | const HadithVerse = forwardRef(function Verse(
18 | { number, arab, translation, style, measure }: Props,
19 | ref: ForwardedRef
20 | ) {
21 | useEffect(() => {
22 | if (measure) {
23 | measure();
24 | }
25 | }, []);
26 |
27 | if (!number || !arab || !translation) return Loading
;
28 | return (
29 |
30 |
31 |
32 |
33 |
34 | {number}
35 |
36 |
37 |
38 |
44 |
45 |
46 |
52 |
53 | {translation}
54 |
55 |
56 |
57 |
58 |
59 | );
60 | });
61 |
62 | export default HadithVerse;
63 |
--------------------------------------------------------------------------------
/src/components/Hadith/HadithReader/DynamicHadithData.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { HadithContent, HaditsDetail } from '@utils/types/Hadith';
4 | import React, { useEffect, useState } from 'react';
5 | import HadithVerse from '../HadithVerse';
6 | import { getHadithDetail } from '@utils/api/hadith';
7 | import { ListRange, Virtuoso } from 'react-virtuoso';
8 | import VerseSkeleton from '@components/quranReader/VerseSkeleton';
9 |
10 | type Props = {
11 | totalData: number;
12 | id: string;
13 | };
14 |
15 | const START_RANGE = 31;
16 | const LIMIT = 30;
17 |
18 | const DynamicHadithsData = ({ totalData, id }: Props) => {
19 | const [data, setData] = useState([]);
20 |
21 | const loadMoreData = async (index: ListRange) => {
22 | /*
23 | round startRange to nearest LIMIT
24 | example: if index.startRange 22 and LIMIT 30, then startRange will be 1 and endRange will be 30
25 | startRange will start fetch data from number 31 because 1-30 already fetched in server
26 | */
27 | const startRange = Math.floor(index.startIndex / LIMIT) * LIMIT + LIMIT + 1;
28 | const endRange =
29 | startRange + LIMIT >= totalData ? totalData : startRange + LIMIT - 1;
30 |
31 | if (!!data[startRange - LIMIT]) {
32 | return;
33 | }
34 |
35 | const res = await getHadithDetail({
36 | id,
37 | range: `${startRange}-${endRange}`, // 31-60 to get 30 data
38 | });
39 | setData((prev) => {
40 | // remove duplicate data
41 | const newData = res.hadiths.filter(
42 | (item) => !prev.find((prevItem) => prevItem.id === item.id)
43 | );
44 | return [...prev, ...newData].sort((a, b) => a.number - b.number);
45 | });
46 | };
47 |
48 | const renderVerse = (index: number) => {
49 | const dataIndex = data.findIndex(
50 | (item) => item.number - LIMIT - 1 === index + 1
51 | );
52 |
53 | if (!data[dataIndex]) {
54 | return ;
55 | }
56 |
57 | return (
58 |
64 | );
65 | };
66 |
67 | return (
68 |
76 | );
77 | };
78 |
79 | export default DynamicHadithsData;
80 |
--------------------------------------------------------------------------------
/src/components/quranReader/Arabic/Word.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import classNames from 'classnames/bind';
4 | import React from 'react';
5 | import { shallow } from 'zustand/shallow';
6 | import styles from './Word.module.css';
7 | import useQuranReader from '@stores/quranReaderStore';
8 | import useSettings from '@stores/settingsStore';
9 |
10 | const cx = classNames.bind(styles);
11 |
12 | type WordProps = {
13 | position?: number;
14 | verseKey?: string;
15 | transalation: string;
16 | transliteration: string;
17 | location: string;
18 | children: React.ReactNode;
19 | isHighlighted: boolean;
20 | isAyahNumber: boolean;
21 | };
22 |
23 | const Word = ({
24 | position,
25 | verseKey,
26 | transalation,
27 | transliteration,
28 | location,
29 | children,
30 | isHighlighted,
31 | isAyahNumber,
32 | }: WordProps) => {
33 | const { highlightedWord } = useQuranReader(
34 | (state) => ({
35 | highlightedWord: state.highlightedWord,
36 | }),
37 | shallow
38 | );
39 |
40 | const { transliteration: shouldShowTransliteration, translationMode } =
41 | useSettings(
42 | (state) => ({
43 | transliteration: state.transliteration,
44 | translationMode: state.translationMode,
45 | }),
46 | shallow
47 | );
48 |
49 | return (
50 |
51 |
70 | {children}
71 | {shouldShowTransliteration && (
72 |
73 | {transliteration}
74 |
75 | )}
76 |
77 | {/* dont show Ayah number */}
78 | {translationMode === 'word' && !isAyahNumber && (
79 |
80 | {transalation}
81 |
82 | )}
83 |
84 | );
85 | };
86 |
87 | export default Word;
88 |
--------------------------------------------------------------------------------
/src/app/(main)/juz/[juzId]/page.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { notFound } from 'next/navigation';
4 | import { Metadata } from 'next';
5 | import { getVerses } from '@utils/verse';
6 | import Wrapper from '@components/Wrapper';
7 | import QuranReader from '@components/quranReader/QuranReader';
8 | import { canonicalUrl, defaultOpenGraph, defaultTwitter } from '@utils/seo';
9 | import { getJuzData, getJuzs } from '@utils/juz';
10 | import { GetVerseBy } from '@utils/types/Verse';
11 |
12 | const IS_PRODUCTION = process.env.NODE_ENV === 'production';
13 |
14 | export async function generateStaticParams() {
15 | const res = await getJuzs();
16 | const paths = res.juzs.map((item) => ({
17 | juzId: item.id.toString(),
18 | }));
19 |
20 | return paths;
21 | }
22 |
23 | export async function generateMetadata({ params }): Promise {
24 | const juzData = await getJuzData(params.juzId);
25 |
26 | if (!juzData) {
27 | return;
28 | }
29 |
30 | const description = `Baca Quran Juz ${juzData.id} yang didalamnya terdapat ${juzData.verse_mapping.length} dengan jumlah ${juzData.verses_count} ayat. Baca dengan terjemahan dan tafsir untuk memahami arti dan maksud ayat yang terkandung didalamnya.`;
31 | return {
32 | title: `Baca Quran - Juz ${juzData.id} (${juzData.verses_count} ayat)`,
33 | description: description,
34 | robots: IS_PRODUCTION ? 'index, follow' : 'noindex, nofollow',
35 | openGraph: {
36 | ...defaultOpenGraph,
37 | title: `Baca Quran - Juz ${juzData.id} (${juzData.verses_count} ayat)`,
38 | description: description,
39 | url: `${canonicalUrl}quran/surah/${juzData.id}`,
40 | },
41 | twitter: {
42 | ...defaultTwitter,
43 | title: `Baca Quran - Juz ${juzData.id} (${juzData.verses_count} ayat)`,
44 | description: description,
45 | },
46 | };
47 | }
48 |
49 | export default async function JuzPage({ params }) {
50 | const juzVerses = await getVerses({
51 | id: params.juzId,
52 | getBy: GetVerseBy.Juz,
53 | });
54 | const juzData = await getJuzData(params.juzId);
55 |
56 | if (!juzData) {
57 | notFound();
58 | }
59 |
60 | return (
61 |
62 | {/*
66 | */}
67 |
74 |
75 | );
76 | }
77 |
--------------------------------------------------------------------------------
/src/components/quranReader/Verses.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import ArabicText from "./ArabicText";
4 | import { StarIcon } from "../icons";
5 | import HandleBookmark from "./action/HandleBookmark";
6 | import CopyToClipboard from "./action/CopyToClipboard";
7 | import HandleTafsir from "./action/HandleTafsir";
8 | import { VerseWord } from "@utils/types/Verse";
9 | import useSettings from "@stores/settingsStore";
10 | import { shallow } from "zustand/shallow";
11 |
12 | type VersesProps = {
13 | id: number;
14 | verse_number: number;
15 | translations: any;
16 | text_uthmani: string;
17 | verse_key: string;
18 | words?: VerseWord[];
19 | };
20 |
21 | const Verses = ({
22 | id,
23 | verse_number,
24 | translations,
25 | text_uthmani,
26 | verse_key,
27 | words,
28 | }: VersesProps) => {
29 | const { translationMode } = useSettings(
30 | (state) => ({
31 | translationMode: state.translationMode,
32 | }),
33 | shallow
34 | );
35 |
36 | return (
37 |
38 |
42 |
43 |
44 |
45 | {verse_number}
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
64 | {translationMode === "verse" && (
65 |
69 | )}
70 |
71 |
72 |
73 |
74 | );
75 | };
76 |
77 | export default Verses;
78 |
--------------------------------------------------------------------------------
/src/app/(main)/surah/[chapterId]/info/page.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Link from "next/link";
3 | import { notFound } from "next/navigation";
4 | import { Metadata } from "next";
5 | import { getAllChaptersData, getChapter, getChapterInfo } from "@utils/chapter";
6 | import { ArrowIcon } from "@components/icons";
7 |
8 | export async function generateStaticParams() {
9 | const res = await getAllChaptersData();
10 | const paths = res.chapters.map((item) => ({
11 | chapterId: item.id.toString(),
12 | }));
13 |
14 | return paths;
15 | }
16 |
17 | export async function generateMetadata({ params }): Promise {
18 | const chapterData = await getChapter(params.chapterId);
19 | const chapterInfo = await getChapterInfo(params.chapterId);
20 |
21 | if (!chapterData) {
22 | return;
23 | }
24 |
25 | return {
26 | title: `${chapterData.name_simple} ~ `,
27 | description: `Surah ${chapterData.name_simple} diturunkan di ${chapterData.revelation_place} dengan jumlah ayat ${chapterData.verses_count}. ${chapterInfo.chapter_info.short_text}`,
28 | };
29 | }
30 |
31 | const getChapterInfoData = async (chapterId) => {
32 | const res = await getChapterInfo(chapterId);
33 | return res.chapter_info;
34 | };
35 |
36 | const SurahInfoPage = async ({ params }) => {
37 | const { chapterId: id } = params;
38 | const chapterInfo = await getChapterInfoData(id);
39 | const chapterData = await getChapter(id);
40 |
41 | if (!chapterData) {
42 | notFound();
43 | }
44 |
45 | return (
46 |
47 |
48 |
52 |
53 |
Kembali ke surah
54 |
55 |
56 |
{chapterData.name_complex}
57 | {chapterData.verses_count} Ayah
58 |
59 |
60 | Diturunkan di{" "}
61 | {chapterData.revelation_place}
62 |
63 |
64 |
65 |
69 |
70 |
71 | );
72 | };
73 |
74 | export default SurahInfoPage;
75 |
--------------------------------------------------------------------------------
/src/components/AudioPlayer/PlaybackController/PlaybackController.tsx:
--------------------------------------------------------------------------------
1 | import classNames from 'classnames';
2 | import React, { useContext } from 'react';
3 | import PlaybackOption from './PlaybackOption';
4 | import { RewindIcon, PlayIcon, PauseIcon, RepeatIcon } from '../../icons';
5 | import useQuranReader from '@stores/quranReaderStore';
6 |
7 | type ButtonSmallProps = {
8 | className?: string;
9 | onClick?: () => void;
10 | children?: React.ReactNode;
11 | };
12 |
13 | export const ButtonSmall = ({
14 | className,
15 | onClick,
16 | children,
17 | }: ButtonSmallProps) => (
18 |
25 | {children}
26 |
27 | );
28 |
29 | const PlaybackController = ({ state, dispatch, reset }) => {
30 | const { audioId, setAudioId } = useQuranReader((state) => ({
31 | audioId: state.audioId,
32 | setAudioId: state.setAudioId,
33 | }));
34 |
35 | function goToPrevSurah() {
36 | if (audioId > 1) {
37 | setAudioId(audioId - 1);
38 | } else {
39 | setAudioId(114);
40 | }
41 | }
42 |
43 | function goToNextSurah() {
44 | if (audioId < 114) {
45 | setAudioId(audioId + 1);
46 | } else {
47 | setAudioId(1);
48 | }
49 | }
50 |
51 | return (
52 | <>
53 |
54 |
55 |
dispatch({ type: 'repeat' })}
57 | className=""
58 | >
59 |
64 |
65 |
66 |
67 |
68 | {state.isPlaying ? (
69 |
dispatch({ type: 'pause' })}>
70 |
71 |
72 | ) : (
73 |
74 | dispatch({ type: 'play' })}
76 | className="h-6"
77 | />
78 |
79 | )}
80 |
81 |
82 |
83 |
84 |
85 |
86 | >
87 | );
88 | };
89 |
90 | export default PlaybackController;
91 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | Visit QuranApp
10 |
11 |
12 |
13 |
14 | About QuranApp
15 | WEB-based reading application, the purpose of this application is so that Muslim people can read and listen to Murottal anywhere and anytime without having to download the application as long as the device remains connected to the internet.
16 |
17 |
18 |
19 | Features
20 | - Dark Theme
21 |
22 | - Auto Highlight Verse When Playing Audio
23 |
24 | - Mark Verse
25 |
26 | - PWA (Progressive Web Apps)
27 |
28 |
29 |
30 | Credits
31 | - API Data Source Quran.com
32 |
33 |
34 |
35 |
36 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) and using [`tailwind`](https://tailwindcss.com/) for styling
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | ## Getting Started
45 |
46 | First, run the development server:
47 |
48 | ```bash
49 | npm run dev
50 | # or
51 | yarn dev
52 | ```
53 |
54 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
55 |
56 | You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file.
57 |
58 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`.
59 |
60 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
61 |
62 | ## Learn More
63 |
64 | To learn more about Next.js, take a look at the following resources:
65 |
66 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
67 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
68 |
69 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
70 |
71 | ## Deploy on Vercel
72 |
73 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
74 |
75 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
76 |
--------------------------------------------------------------------------------
/src/components/Banner/ChapterBanner.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React, { useEffect } from 'react';
4 | import { useState } from 'react';
5 | import IconWrapper from '../icons/IconWrapper';
6 | import BannerWrapper from './BannerWrapper';
7 | import SurahInfo from './SurahInfo';
8 | import { InfoIcon } from '../icons';
9 | import { shallow } from 'zustand/shallow';
10 | import useQuranReader from '@stores/quranReaderStore';
11 | import { Chapter, ChapterInfo } from '@utils/types/Chapter';
12 |
13 | type ChapterBannerProps = {
14 | chapterData: Chapter;
15 | chapterInfo: ChapterInfo;
16 | };
17 |
18 | const ChapterBanner = ({ chapterData, chapterInfo }: ChapterBannerProps) => {
19 | const { setCurrentChapter } = useQuranReader(
20 | (state) => ({
21 | setCurrentChapter: state.setCurrentChapter,
22 | }),
23 | shallow
24 | );
25 | const [isInfoOpen, setInfoOpen] = useState(false);
26 |
27 | useEffect(() => {
28 | setCurrentChapter(chapterData.id);
29 | // eslint-disable-next-line react-hooks/exhaustive-deps
30 | }, []);
31 |
32 | return (
33 |
34 |
39 | setInfoOpen(!isInfoOpen)}
43 | onHover="none"
44 | >
45 |
46 |
47 |
48 | {chapterData.name_simple}
49 |
50 |
51 | {chapterData.translated_name.name}
52 |
53 |
58 |
69 |
74 | {chapterData.revelation_place} -{' '}
75 | {chapterData.verses_count} Ayah
76 |
77 |
78 |
79 | );
80 | };
81 |
82 | export default ChapterBanner;
83 |
--------------------------------------------------------------------------------
/src/components/Search/index.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React, { useCallback, useState } from "react";
4 | import useStore from "../../store/surahStore";
5 | import Link from "next/link";
6 | import { LocalChapter } from "data/chapter/type";
7 |
8 | type SearchProps = {
9 | className?: string;
10 | };
11 |
12 | const Search = ({ className }: SearchProps) => {
13 | const allChapters = useStore((state) => state.chapterData);
14 | const notFound = {
15 | id: null,
16 | name_simple: "Ketik untuk mencari",
17 | verses_count: 0,
18 | revelation_place: "",
19 | };
20 |
21 | const [filteredChapters, setFilteredChapters] = useState([
22 | {
23 | ...notFound,
24 | },
25 | ]);
26 | const [isExpanded, setExpanded] = useState(false);
27 |
28 | const handleChange = useCallback(
29 | (e) => {
30 | const keyword = e.target.value;
31 |
32 | if (keyword !== "") {
33 | const result = allChapters.filter((chapters) => {
34 | return chapters.name_simple
35 | .toLowerCase()
36 | .includes(keyword.toLowerCase());
37 | });
38 | setFilteredChapters(() => {
39 | if (result.length > 0) {
40 | return result;
41 | } else {
42 | return [{ ...notFound }];
43 | }
44 | });
45 | } else {
46 | setFilteredChapters([
47 | {
48 | ...notFound,
49 | },
50 | ]);
51 | }
52 | },
53 | // eslint-disable-next-line react-hooks/exhaustive-deps
54 | [allChapters]
55 | );
56 |
57 | function handleBlur() {
58 | setTimeout(() => {
59 | setExpanded(false);
60 | }, 200);
61 | }
62 |
63 | return (
64 |
65 |
setExpanded(true)}
67 | onBlur={handleBlur}
68 | onChange={(e) => handleChange(e)}
69 | type="text"
70 | className={
71 | "bg-gray-100 w-full dark:bg-slate-600 dark:text-slate-200 dark:ring-emerald-500 py-2 px-3 my-3 rounded-lg outline-none focus:ring-2 ring-emerald-300 transition-all " +
72 | className
73 | }
74 | placeholder="Cari Surah"
75 | />
76 | {isExpanded && (
77 |
78 | {filteredChapters.length > 0 &&
79 | filteredChapters.map((e) => (
80 |
85 | {e.name_simple}
86 |
87 | ))}
88 |
89 | )}
90 |
91 | );
92 | };
93 |
94 | export default Search;
95 |
--------------------------------------------------------------------------------
/src/app/(main)/surah/[chapterId]/page.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { notFound } from "next/navigation";
4 | import { Metadata } from "next";
5 | import { getVerses } from "@utils/verse";
6 | import { getAllChaptersData, getChapter, getChapterInfo } from "@utils/chapter";
7 | import Wrapper from "@components/Wrapper";
8 | import ChapterBanner from "@components/Banner/ChapterBanner";
9 | import QuranReader from "@components/quranReader/QuranReader";
10 | import PlayAudioButton from "@components/AudioPlayer/PlayAudioButton";
11 | import { canonicalUrl, defaultOpenGraph, defaultTwitter } from "@utils/seo";
12 | import { GetVerseBy } from "@utils/types/Verse";
13 |
14 | const IS_PRODUCTION = process.env.NODE_ENV === "production";
15 |
16 | export async function generateStaticParams() {
17 | const res = await getAllChaptersData();
18 | const paths = res.chapters.map((item) => ({
19 | chapterId: item.id.toString(),
20 | }));
21 |
22 | return paths;
23 | }
24 |
25 | export async function generateMetadata({ params }): Promise {
26 | const chapterData = await getChapter(params.chapterId);
27 |
28 | if (!chapterData) {
29 | return;
30 | }
31 |
32 | const description = `Baca Surah ${chapterData.name_simple} (${chapterData.translated_name.name}) dengan jumlah ${chapterData.verses_count} ayat, surah ini diturunkan ke ${chapterData.revelation_order} di ${chapterData.revelation_place}. Halaman ini berisi bacaan surah ${chapterData.name_simple} dengan terjemahan bahasa Indonesia, tafsir, dan audio dengan qori yang berbeda.`;
33 | return {
34 | title: `${chapterData.name_simple} (${chapterData.translated_name.name})`,
35 | description: description,
36 | robots: IS_PRODUCTION ? "index, follow" : "noindex, nofollow",
37 | openGraph: {
38 | ...defaultOpenGraph,
39 | title: `${chapterData.name_simple} (${chapterData.translated_name.name})`,
40 | description: description,
41 | url: `${canonicalUrl}quran/surah/${chapterData.id}`,
42 | },
43 | twitter: {
44 | ...defaultTwitter,
45 | title: `${chapterData.name_simple} (${chapterData.translated_name.name})`,
46 | description: description,
47 | },
48 | };
49 | }
50 |
51 | export default async function SurahPage({ params }) {
52 | const { chapterId: id } = params;
53 | const chapterVerses = await getVerses({
54 | id,
55 | getBy: GetVerseBy.Chapter,
56 | });
57 | const chapterInfo = await getChapterInfo(id);
58 | const chapterData = await getChapter(id);
59 |
60 | if (!chapterData) {
61 | notFound();
62 | }
63 |
64 | return (
65 |
66 |
70 |
71 |
78 |
79 | );
80 | }
81 |
--------------------------------------------------------------------------------
/src/store/settingsStore.ts:
--------------------------------------------------------------------------------
1 | import { create } from 'zustand';
2 | import { createJSONStorage, persist } from 'zustand/middleware';
3 | import setTheme from '../utils/theme';
4 |
5 | export type Theme = 'default' | 'light' | 'dark';
6 | export type AutoScroll = 'verse' | 'word' | false;
7 | export type translationMode = 'word' | 'verse';
8 |
9 | interface SettingsStore {
10 | fontSize: number;
11 | fontFace: number;
12 | theme: Theme;
13 | autoScroll: AutoScroll;
14 | transliteration: boolean;
15 | translationMode: translationMode;
16 |
17 | setTheme: (theme: Theme) => void;
18 | setFontFace: (fontFace: number) => void;
19 | increaseFontSize: () => void;
20 | decreaseFontSize: () => void;
21 | setAutoScroll: (value: AutoScroll) => void;
22 | setTransliteration: (value: boolean) => void;
23 | setTranslationMode: (value: translationMode) => void;
24 | }
25 |
26 | const useSettings = create()(
27 | persist(
28 | (set, get) => ({
29 | fontSize: 42,
30 | fontFace: 3,
31 | theme: 'default',
32 | autoScroll: 'word',
33 | transliteration: false,
34 | translationMode: 'verse',
35 |
36 | setTheme: (theme) => {
37 | set({ theme: theme });
38 | },
39 | setFontFace: (fontFace) => set({ fontFace: fontFace }),
40 | increaseFontSize: () => {
41 | if (get().fontSize < 60) {
42 | set({ fontSize: get().fontSize + 5 });
43 | }
44 | },
45 | decreaseFontSize: () => {
46 | if (get().fontSize > 28) {
47 | set({ fontSize: get().fontSize - 5 });
48 | }
49 | },
50 | setAutoScroll: (value) => set({ autoScroll: value }),
51 | setTransliteration: (value) => set({ transliteration: value }),
52 | setTranslationMode: (value) => set({ translationMode: value }),
53 | }),
54 | {
55 | name: 'settings', // name of the item in the storage (must be unique)
56 | partialize: (state) => ({
57 | theme: state.theme,
58 | }),
59 | getStorage: () => localStorage,
60 | }
61 | )
62 | );
63 |
64 | // const useSettings = create(
65 | // (set, get) => ({
66 | // fontSize: 42,
67 | // fontFace: 3,
68 | // theme: 'default',
69 | // readMode: 'translated',
70 | // autoScroll: true,
71 |
72 | // setTheme: (theme) => {
73 | // setTheme(theme);
74 | // set({ theme: theme });
75 | // },
76 | // setFontFace: (fontFace) => set({ fontFace: fontFace }),
77 | // increaseFontSize: () => {
78 | // if (get().fontSize < 60) {
79 | // set({ fontSize: get().fontSize + 5 });
80 | // }
81 | // },
82 | // decreaseFontSize: () => {
83 | // if (get().fontSize > 28) {
84 | // set({ fontSize: get().fontSize - 5 });
85 | // }
86 | // },
87 | // setAutoScroll: (value) => set({ autoScroll: value }),
88 | // }),
89 | // {
90 | // name: 'settings', // name of the item in the storage (must be unique)
91 | // storage: createJSONStorage(() => sessionStorage), // (optional) by default, 'localStorage' is used
92 | // }
93 | // );
94 |
95 | export default useSettings;
96 |
--------------------------------------------------------------------------------
/src/~page-offline/page.tsx:
--------------------------------------------------------------------------------
1 | // import Link from 'next/link';
2 | // import Image from 'next/image';
3 | // import Wrapper from '@components/Wrapper';
4 | // import { Metadata } from 'next';
5 | // import {
6 | // defaultOpenGraph,
7 | // defaultTwitter,
8 | // staticDescription,
9 | // staticTitle,
10 | // } from '@utils/seo';
11 |
12 | // const IS_PRODUCTION = process.env.NODE_ENV === 'production';
13 |
14 | // export const metadata: Metadata = {
15 | // title: staticTitle['/'],
16 | // description: staticDescription['/'],
17 | // robots: IS_PRODUCTION ? 'index, follow' : 'noindex, nofollow',
18 | // openGraph: {
19 | // ...defaultOpenGraph,
20 | // },
21 | // twitter: {
22 | // ...defaultTwitter,
23 | // },
24 | // };
25 |
26 | // export default function HomePage() {
27 | // return (
28 | //
29 | //
30 | // Assalamu'alakum
31 | //
32 | //
33 | //
37 | //
43 | //
44 | // Baca Quran
45 | //
46 | //
47 | //
51 | //
57 | // Hadits
58 | //
59 | //
63 | //
69 | // Doa
70 | //
71 | //
72 | //
76 | // Credits
77 | //
78 | //
79 | // );
80 | // }
81 |
--------------------------------------------------------------------------------
/src/components/TopBar/DeveloperUtility/FontAdjustment.tsx:
--------------------------------------------------------------------------------
1 | import classNames from 'classnames';
2 | import React, { useContext, useEffect, useState } from 'react';
3 | import AdjustmentWrapper from './AdjustmentWrapper';
4 | import OptionList from './OptionList';
5 | import { ChevronIcon } from '../../icons';
6 | import useSettings from '../../../store/settingsStore';
7 |
8 | const FontAdjustment = () => {
9 | const [isExpanded, setExpanded] = useState(false);
10 |
11 | const { setFontFace, fontFace } = useSettings((state) => ({
12 | setFontFace: state.setFontFace,
13 | fontFace: state.fontFace,
14 | }));
15 |
16 | const fontFaces = ['Al - Qalam', 'Me - Quran', 'Nastaleeq', 'Uthmanic Hafs'];
17 |
18 | return (
19 |
20 | setExpanded(!isExpanded)}
22 | className="text-center py-2 px-2 group text-black dark:text-slate-100 bg-gray-100 dark:bg-slate-500 rounded w-36 flex justify-between relative cursor-pointer"
23 | >
24 |
25 | {fontFace ? fontFaces[fontFace] : 'Al - Qalam'}
26 |
27 |
32 |
33 | {/* Lists Language */}
34 |
41 | {fontFaces.map((key, index) => (
42 | setFontFace(index)}>
43 | {key}
44 |
45 | ))}
46 |
47 |
48 |
49 | );
50 | };
51 |
52 | const FontSizeAdjustment = () => {
53 | const { increaseFontSize, decreaseFontSize, currentFontSize } = useSettings(
54 | (state) => ({
55 | increaseFontSize: state.increaseFontSize,
56 | decreaseFontSize: state.decreaseFontSize,
57 | currentFontSize: state.fontSize,
58 | })
59 | );
60 |
61 | return (
62 |
63 |
64 |
decreaseFontSize()}
66 | className="w-5 h-5 flex items-center justify-center font-bold rounded bg-white dark:bg-slate-400"
67 | >
68 | -
69 |
70 |
{currentFontSize}
71 |
increaseFontSize()}
73 | className="w-5 h-5 flex items-center justify-center font-bold rounded bg-white dark:bg-slate-400"
74 | >
75 | +
76 |
77 |
78 |
79 | );
80 | };
81 |
82 | export { FontAdjustment, FontSizeAdjustment };
83 |
--------------------------------------------------------------------------------
/src/components/AudioPlayer/AudioPlayer.module.css:
--------------------------------------------------------------------------------
1 |
2 | input.audioSlider[type="range"] {
3 | --border-color: rgb(0, 196, 127);
4 | -webkit-appearance: none;
5 | margin: 0;
6 | padding: 0;
7 | height: 4px;
8 | outline: none;
9 | cursor: pointer;
10 | border-radius: 20px;
11 | touch-action: none;
12 | @apply bg-emerald-200/50 relative
13 | }
14 |
15 | input.audioSlider[type="range"]::-webkit-slider-runnable-track {
16 | width: 100%;
17 | height: 3px;
18 | cursor: pointer;
19 | background: linear-gradient(to right, rgba(0, 125, 181, 0.6) var(--buffered-width), rgba(0, 125, 181, 0.2) var(--buffered-width));
20 | }
21 |
22 | input.audioSlider[type="range"]::before {
23 | background-color: rgba(0, 196, 127, 0.921);
24 | position: absolute;
25 | content: "";
26 | left: 0;
27 | width: var(--seek-before-width);
28 | height: 4px;
29 | cursor: pointer;
30 | border-radius: 20px;
31 | }
32 |
33 | input.audioSlider[type="range"]::-webkit-slider-thumb {
34 | position: relative;
35 | -webkit-appearance: none;
36 | box-sizing: content-box;
37 | border: 1px solid var(--border-color);
38 | height: 15px;
39 | width: 15px;
40 | border-radius: 50%;
41 | background-color: #fff;
42 | cursor: pointer;
43 | margin: -7px 0 0 0;
44 | }
45 |
46 | input.audioSlider[type="range"]:active::-webkit-slider-thumb {
47 | transform: scale(1.2);
48 | background: var(--border-color);
49 | }
50 |
51 | input.audioSlider[type="range"]::-moz-range-track {
52 | width: 100%;
53 | height: 3px;
54 | cursor: pointer;
55 | background: linear-gradient(to right, rgba(0, 125, 181, 0.6) var(--buffered-width), rgba(0, 125, 181, 0.2) var(--buffered-width));
56 | }
57 | input.audioSlider[type="range"]::-moz-range-progress {
58 | background-color: var(--border-color);
59 | }
60 |
61 | input.audioSlider[type="range"]::-moz-focus-outer {
62 | border: 0;
63 | }
64 |
65 | input.audioSlider[type="range"]::-moz-range-thumb {
66 | box-sizing: content-box;
67 | border: 1px solid var(--border-color);
68 | height: 15px;
69 | width: 15px;
70 | border-radius: 50%;
71 | background-color: #fff;
72 | cursor: pointer;
73 | }
74 |
75 | input.audioSlider[type="range"]:active::-moz-range-thumb {
76 | transform: scale(1.2);
77 | background: var(--border-color);
78 | }
79 |
80 | input.audioSlider[type="range"]::-ms-track {
81 | width: 100%;
82 | height: 3px;
83 | cursor: pointer;
84 | background: transparent;
85 | border: solid transparent;
86 | color: transparent;
87 | }
88 |
89 | input.audioSlider[type="range"]::-ms-fill-lower {
90 | background-color: var(--border-color);
91 | }
92 |
93 | input.audioSlider[type="range"]::-ms-fill-upper {
94 | background: linear-gradient(to right, rgba(0, 125, 181, 0.6) var(--buffered-width), rgba(0, 125, 181, 0.2) var(--buffered-width));
95 | }
96 |
97 | input.audioSlider[type="range"]::-ms-thumb {
98 | box-sizing: content-box;
99 | border: 1px solid var(--border-color);
100 | height: 15px;
101 | width: 15px;
102 | border-radius: 50%;
103 | background-color: #fff;
104 | cursor: pointer;
105 | }
106 |
107 | input.audioSlider[type="range"]:active::-ms-thumb {
108 | transform: scale(1.2);
109 | background: var(--border-color);
110 | }
--------------------------------------------------------------------------------
/src/components/TopBar/DropdownHadithLists/DropdownHadithLists.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import classNames from 'classnames';
4 | import React, { useState, useCallback, useEffect } from 'react';
5 | import Link from 'next/link';
6 | import { ChevronIcon, ArrowIcon } from '../../icons';
7 | import { LocalChapter } from 'data/chapter/type';
8 | import { useRouter } from 'next/navigation';
9 | import useHadith from '@stores/hadithStore';
10 | import { getHadithBooks } from '@utils/api/hadith';
11 |
12 | type DropdownHadithListsProps = {};
13 |
14 | const DropdownHadithLists = ({}: DropdownHadithListsProps) => {
15 | const [open, setOpen] = useState(false);
16 | const router = useRouter();
17 | const { hadithData, setHadithData, hadithActive } = useHadith((state) => ({
18 | hadithData: state.hadithData,
19 | setHadithData: state.setHadithData,
20 | hadithActive: state.hadithActive,
21 | }));
22 |
23 | useEffect(() => {
24 | if (hadithData.length > 0) return;
25 |
26 | getHadithBooks().then((res) => {
27 | setHadithData(res);
28 | });
29 |
30 | // eslint-disable-next-line react-hooks/exhaustive-deps
31 | }, []);
32 |
33 | if (hadithData.length < 1) return <>>;
34 | const index = hadithData.findIndex((e) => e.id == hadithActive);
35 | return (
36 |
37 |
38 |
43 |
44 |
45 | {/* Dropdown Toggle */}
46 |
setOpen(!open)}
48 | className={classNames(
49 | 'p-2 cursor-pointer border border-transparent bg-white dark:bg-slate-600 w-fit rounded-md flex items-center relative',
50 | {
51 | 'border-emerald-500 shadow-lg shadow-emerald-500/10': open,
52 | }
53 | )}
54 | >
55 |
56 | {hadithData[index].name}
57 |
58 |
64 |
65 |
66 | {/* Dropdown Menu */}
67 |
74 |
79 | {hadithData.map((e) => (
80 | router.push(`hadits/${e.id}`)}
83 | className={classNames(
84 | 'px-2 py-1 cursor-pointer hover:bg-emerald-100/50 dark:hover:bg-emerald-400/30 dark:hover:text-slate-100 hover:text-emerald-500 rounded flex items-center',
85 | {
86 | 'dark:bg-emerald-400 bg-emerald-200':
87 | hadithData[index].id == e.id,
88 | }
89 | )}
90 | >
91 | {e.name}
92 |
93 | ))}
94 |
95 |
96 |
97 | );
98 | };
99 |
100 | export default DropdownHadithLists;
101 |
--------------------------------------------------------------------------------
/src/components/quranReader/ArabicText.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import classNames from 'classnames';
4 | import React from 'react';
5 | import useSettings from '../../store/settingsStore';
6 | import useSurah from '../../store/surahStore';
7 | import { shallow } from 'zustand/shallow';
8 | import arabicFontStyle from '../../utils/fonts';
9 | import Word from './Arabic/Word';
10 | import { VerseWord } from '@utils/types/Verse';
11 | import useQuranReader from '@stores/quranReaderStore';
12 |
13 | type ArabicTextProps = {
14 | textUthmani: string;
15 | verseNumber: number;
16 | verseKey?: string;
17 | words?: VerseWord[];
18 | leading?: 'normal' | 'medium' | 'tight';
19 | };
20 |
21 | const ArabicText = ({
22 | textUthmani,
23 | verseNumber,
24 | verseKey,
25 | words,
26 | leading = 'medium',
27 | }: ArabicTextProps) => {
28 | const { fontFace, currentFontSize, autoScroll } = useSettings(
29 | (state) => ({
30 | fontFace: state.fontFace,
31 | currentFontSize: state.fontSize,
32 | autoScroll: state.autoScroll,
33 | }),
34 | shallow
35 | );
36 |
37 | const { highlightedVerse } = useQuranReader(
38 | (state) => ({
39 | highlightedVerse: state.highlightedVerse,
40 | }),
41 | shallow
42 | );
43 |
44 | const arabicNumber = (value) => {
45 | const arabicNumbers =
46 | '\u0660\u0661\u0662\u0663\u0664\u0665\u0666\u0667\u0668\u0669';
47 | return String(value).replace(/[0123456789]/g, (d) => {
48 | return arabicNumbers[d];
49 | });
50 | };
51 |
52 | return (
53 |
70 |
79 | {words
80 | ? words.map((word) => (
81 |
93 | {word.text}
94 |
95 | ))
96 | : textUthmani}
97 |
98 | {!(fontFace === 3) && (
99 |
112 | {arabicNumber(verseNumber)}
113 |
114 | )}
115 |
116 | );
117 | };
118 |
119 | export default ArabicText;
120 |
--------------------------------------------------------------------------------
/src/components/TopBar/DropdownSurahLists/DropdownSurahLists.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import classNames from "classnames";
4 | import React, { useState, useCallback } from "react";
5 | import ChapterLists from "./ChapterLists";
6 | import { ChevronIcon, ArrowIcon } from "../../icons";
7 | import { LocalChapter } from "data/chapter/type";
8 | import VerseList from "./VerseList";
9 | import { useRouter } from "next/navigation";
10 |
11 | type DropdownSurahListsProps = {
12 | chapterLists: LocalChapter[];
13 | chapterActive: number;
14 | };
15 |
16 | const DropdownSurahLists = ({
17 | chapterLists,
18 | chapterActive,
19 | }: DropdownSurahListsProps) => {
20 | const [open, setOpen] = useState(false);
21 | const [filteredChapterLists, setFilteredChapterLists] = useState(null);
22 | const router = useRouter();
23 |
24 | const handleChange = useCallback((e) => {
25 | const keyword = e.target.value;
26 |
27 | const result = chapterLists.filter((chapters) => {
28 | return chapters.name_simple.toLowerCase().includes(keyword.toLowerCase());
29 | });
30 | setFilteredChapterLists(() => {
31 | if (result.length > 0) {
32 | return result;
33 | } else {
34 | return [{ name_simple: "Tidak ditemukan" }];
35 | }
36 | });
37 | // eslint-disable-next-line react-hooks/exhaustive-deps
38 | }, []);
39 |
40 | if (chapterLists) {
41 | return (
42 |
43 |
44 |
router.back()}
48 | >
49 |
50 |
51 | {/* Dropdown Toggle */}
52 |
setOpen(!open)}
54 | className={classNames(
55 | "p-2 cursor-pointer border border-transparent bg-white dark:bg-slate-600 w-fit rounded-md flex items-center relative",
56 | {
57 | "border-emerald-500 shadow-lg shadow-emerald-500/10": open,
58 | }
59 | )}
60 | >
61 |
62 | {chapterLists[chapterActive - 1].name_simple}
63 |
64 |
70 |
71 |
72 | {/* Dropdown Menu */}
73 |
80 |
81 | handleChange(value)}
83 | placeholder="Cari surah"
84 | className="w-full my-1 py-1 placeholder:text-sm bg-white dark:bg-slate-600 z-50 outline-none border border-emerald-500 rounded-md px-3"
85 | type="text"
86 | />
87 |
88 |
94 |
98 |
99 |
100 | );
101 | }
102 | };
103 |
104 | export default DropdownSurahLists;
105 |
--------------------------------------------------------------------------------
/src/utils/seo.ts:
--------------------------------------------------------------------------------
1 | import { getBasePath } from "./url";
2 | import { Metadata } from "next";
3 |
4 | export const websiteDescription =
5 | "Baca Quran adalah aplikasi web interaktif yang memungkinkan pengguna untuk membaca, mencari, dan menjelajahi teks suci Al-Quran secara digital. Aplikasi ini dilengkapi dengan berbagai fitur yang memudahkan pengguna dalam mempelajari dan meresapi ayat-ayat Al-Quran, seperti terjemahan, tafsir, dan audio.";
6 |
7 | export const config = {
8 | siteName: "Quran App",
9 | websiteLogo: `${getBasePath()}/quranapp.jpg`,
10 | twitterHandle: "@quran_app",
11 | twitterCardType: "summary_large_image",
12 | facebookApp: "5277775832307938",
13 | };
14 |
15 | export const staticDescription = {
16 | "/": "Pustaka Islam adalah aplikasi untuk membaca Quran, Hadits, dan Doa",
17 | "/surah":
18 | "Baca Quran secara online yang memungkinkan pengguna untuk membaca, mencari, dan menjelajahi teks suci Al-Quran secara digital. Aplikasi ini dilengkapi dengan berbagai fitur yang memudahkan pengguna dalam mempelajari dan meresapi ayat-ayat Al-Quran, seperti terjemahan, tafsir, dan audio dengan berbagai pilihan qori.",
19 | "/juz": "Baca Quran berdasarkan juz",
20 | "/hadits":
21 | "Baca Hadits secara online dengan berbagai pilihan kitab hadits dan di lengkapi dengan terjemahan",
22 | };
23 |
24 | export const staticTitle = {
25 | "/surah": "Baca Quran - Surah",
26 | "/juz": "Baca Quran - Juz",
27 | };
28 |
29 | export const canonicalUrl = new URL(getBasePath());
30 |
31 | export const defaultOpenGraph: Metadata["openGraph"] = {
32 | title: staticTitle["/"],
33 | type: "website",
34 | locale: "id",
35 | description: staticDescription["/"],
36 | siteName: "Quran App",
37 | url: canonicalUrl,
38 | };
39 |
40 | export const defaultTwitter: Metadata["twitter"] = {
41 | title: staticTitle["/"],
42 | creator: "@acmaul",
43 | card: "summary_large_image",
44 | description: staticDescription["/"],
45 | };
46 |
47 | // export function createSEOConfig({ title, description, canonicalUrl, locale }) {
48 | // const seoTitle = title || '';
49 |
50 | // return {
51 | // title: seoTitle,
52 | // description,
53 | // titleTemplate: '%s - Quran.com',
54 | // defaultTitle: config.siteName,
55 | // dangerouslySetAllPagesToNoFollow: !isProduction, // @see https://github.com/garmeeh/next-seo#dangerouslySetAllPagesToNoFollow
56 | // dangerouslySetAllPagesToNoIndex: !isProduction, // @see https://github.com/garmeeh/next-seo#dangerouslySetAllPagesToNoIndex
57 | // canonical: canonicalUrl,
58 | // openGraph: {
59 | // type: 'website',
60 | // locale: 'id',
61 | // url: canonicalUrl,
62 | // title: seoTitle,
63 | // description,
64 | // images: [
65 | // {
66 | // url: config.websiteLogo,
67 | // width: 2026,
68 | // height: 875,
69 | // alt: config.siteName,
70 | // },
71 | // ],
72 | // site_name: config.siteName,
73 | // },
74 | // facebook: {
75 | // appId: config.facebookApp,
76 | // },
77 | // twitter: {
78 | // handle: config.twitterHandle,
79 | // site: config.twitterHandle,
80 | // cardType: config.twitterCardType,
81 | // },
82 | // additionalMetaTags: [
83 | // {
84 | // name: 'Charset',
85 | // content: 'UTF-8',
86 | // },
87 | // {
88 | // name: 'Distribution',
89 | // content: 'Global', // indicates that your webpage is intended for everyone
90 | // },
91 | // {
92 | // name: 'Rating',
93 | // content: 'General', // lets the younger web-surfers know the content is appropriate
94 | // },
95 | // {
96 | // name: 'theme-color',
97 | // content: '#fff', // placeholder
98 | // },
99 | // {
100 | // name: 'viewport',
101 | // content: 'width=device-width, initial-scale=1, shrink-to-fit=no',
102 | // },
103 | // ],
104 | // };
105 | // }
106 |
--------------------------------------------------------------------------------
/src/app/(main)/surah/[chapterId]/[ayahId]/opengraph-image.tsx:
--------------------------------------------------------------------------------
1 | import { getChapter } from "@utils/chapter";
2 | import { ImageResponse } from "next/og";
3 | import { Lato } from "next/font/google";
4 |
5 | export const runtime = "edge";
6 |
7 | const lato = Lato({
8 | subsets: ["latin"],
9 | weight: ["100", "300", "400", "700", "900"],
10 | });
11 |
12 | export const size = {
13 | width: 1200,
14 | height: 630,
15 | };
16 | export const contentType = "image/png";
17 |
18 | export default async function Image({
19 | params,
20 | }: {
21 | params: { chapterId: string; ayahId: string };
22 | }) {
23 | const chapterData = await getChapter(parseInt(params.chapterId));
24 | const NastaleeqFont = await fetch(
25 | new URL(
26 | "../../../../../utils/fonts/nastaleeq/indopak/indopak-nastaleeq-waqf-lazim-v4.2.1.ttf",
27 | import.meta.url
28 | )
29 | ).then((res) => res.arrayBuffer());
30 |
31 | const LatoFont = await fetch(
32 | new URL("../../../../../utils/fonts/Lato-Regular.ttf", import.meta.url)
33 | ).then((res) => res.arrayBuffer());
34 |
35 | return new ImageResponse(
36 | (
37 |
52 |
60 |
68 |
75 | https://quran.acml.me
76 |
77 |
78 |
96 |
103 |
109 | {chapterData.name_complex}
110 |
111 |
117 | ({chapterData.translated_name.name})
118 |
119 |
124 |
129 | Ayah {params.ayahId}
130 |
131 |
132 |
138 | {chapterData.name_arabic}
139 |
140 |
141 |
142 | ),
143 | {
144 | ...size,
145 | fonts: [
146 | {
147 | name: "Nastaleeq",
148 | data: NastaleeqFont,
149 | style: "normal",
150 | },
151 | {
152 | name: "Lato",
153 | data: LatoFont,
154 | style: "normal",
155 | },
156 | ],
157 | }
158 | );
159 | }
160 |
--------------------------------------------------------------------------------
/src/app/(main)/surah/[chapterId]/[ayahId]/twitter-image.tsx:
--------------------------------------------------------------------------------
1 | import { getChapter } from "@utils/chapter";
2 | import { ImageResponse } from "next/og";
3 | import { Lato } from "next/font/google";
4 |
5 | export const runtime = "edge";
6 |
7 | const lato = Lato({
8 | subsets: ["latin"],
9 | weight: ["100", "300", "400", "700", "900"],
10 | });
11 |
12 | export const size = {
13 | width: 1200,
14 | height: 630,
15 | };
16 | export const contentType = "image/png";
17 |
18 | export default async function Image({
19 | params,
20 | }: {
21 | params: { chapterId: string; ayahId: string };
22 | }) {
23 | const chapterData = await getChapter(parseInt(params.chapterId));
24 | const NastaleeqFont = await fetch(
25 | new URL(
26 | "../../../../../utils/fonts/nastaleeq/indopak/indopak-nastaleeq-waqf-lazim-v4.2.1.ttf",
27 | import.meta.url
28 | )
29 | ).then((res) => res.arrayBuffer());
30 |
31 | const LatoFont = await fetch(
32 | new URL("../../../../../utils/fonts/Lato-Regular.ttf", import.meta.url)
33 | ).then((res) => res.arrayBuffer());
34 |
35 | return new ImageResponse(
36 | (
37 |
52 |
60 |
68 |
75 | https://quran.acml.me
76 |
77 |
78 |
96 |
103 |
109 | {chapterData.name_complex}
110 |
111 |
117 | ({chapterData.translated_name.name})
118 |
119 |
124 |
129 | Ayah {params.ayahId}
130 |
131 |
132 |
138 | {chapterData.name_arabic}
139 |
140 |
141 |
142 | ),
143 | {
144 | ...size,
145 | fonts: [
146 | {
147 | name: "Nastaleeq",
148 | data: NastaleeqFont,
149 | style: "normal",
150 | },
151 | {
152 | name: "Lato",
153 | data: LatoFont,
154 | style: "normal",
155 | },
156 | ],
157 | }
158 | );
159 | }
160 |
--------------------------------------------------------------------------------
/src/components/AudioPlayer/PlaybackController/PlaybackOption.tsx:
--------------------------------------------------------------------------------
1 | import classNames from 'classnames';
2 | import Link from 'next/link';
3 | import React, { useContext, useEffect, useState } from 'react';
4 | import { ButtonSmall } from './PlaybackController';
5 | import {
6 | XIcon,
7 | ListsIcon,
8 | DownloadIcon,
9 | DotsIcon,
10 | ChevronIcon,
11 | } from '../../icons';
12 | import { getAllRecitations } from '../../../utils/audio';
13 | import { AudioContext } from '../AudioPlayer';
14 | import { shallow } from 'zustand/shallow';
15 | import useQuranReader from '@stores/quranReaderStore';
16 |
17 | const PlaybackOption = ({ onClickReset }) => {
18 | const { audioId, setAudioId } = useQuranReader(
19 | (state) => ({
20 | audioId: state.audioId,
21 | setAudioId: state.setAudioId,
22 | }),
23 | shallow
24 | );
25 | const { audioState, dispatch } = useContext(AudioContext);
26 |
27 | const [isHidden, setHidden] = useState(true);
28 | const [location, setLocation] = useState('main');
29 | const [recitations, setRecitations] = useState([]);
30 |
31 | useEffect(() => {
32 | getAllRecitations().then((res) => {
33 | setRecitations(res.recitations);
34 | });
35 | }, []);
36 |
37 | return (
38 |
39 |
setHidden(!isHidden)} className="relative">
40 |
41 |
42 |
51 | {location === 'main' ? (
52 | <>
53 |
setLocation('recitations')}
56 | >
57 |
58 |
59 | Pilih Qari
60 |
61 |
62 |
63 |
68 |
69 |
70 | Unduh file Audio
71 |
72 |
73 |
77 |
78 | Tutup Pemutar Audio
79 |
80 | >
81 | ) : (
82 | <>
83 |
setLocation('main')}
86 | >
87 |
88 | Kembali
89 |
90 | {recitations.map((e) => (
91 |
92 |
98 | dispatch({ type: 'changeReciterId', payload: e.id })
99 | }
100 | >
101 | {e.reciter_name}
102 | {' '}
103 | {e.style && (
104 | ({e.style})
105 | )}
106 |
107 |
108 |
109 | ))}
110 | >
111 | )}
112 |
113 |
114 | );
115 | };
116 |
117 | export default PlaybackOption;
118 |
--------------------------------------------------------------------------------
/src/components/quranReader/FetchInfiniteVerse.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { GetVerseBy, Verse, VersePagination } from "@utils/types/Verse";
4 | import React, { useEffect, useRef, useState } from "react";
5 | import { ListItem, ListRange, Virtuoso, VirtuosoHandle } from "react-virtuoso";
6 | import VerseSkeleton from "./VerseSkeleton";
7 | import Verses from "./Verses";
8 | import { getVerses } from "@utils/verse";
9 | import useQuranReader from "@stores/quranReaderStore";
10 | import { shallow } from "zustand/shallow";
11 | import useSettings from "@stores/settingsStore";
12 | import { useSearchParams } from "next/navigation";
13 |
14 | type Props = {
15 | totalData: number;
16 | id: number;
17 | getVerseBy: GetVerseBy;
18 | };
19 |
20 | const LIMIT = 20;
21 | const FetchInfiniteVerse = ({ totalData, id, getVerseBy }: Props) => {
22 | const [data, setData] = useState([]);
23 | const [paginationData, setPaginationData] = useState();
24 | const [currentPage, setCurrentPage] = useState();
25 | const [itemsRendered, setItemsRendered] = useState[]>();
26 | const searchParams = useSearchParams();
27 |
28 | const ref = useRef(null);
29 | const { highlightedWord, highlightedVerse, currentChapter } = useQuranReader(
30 | (state) => ({
31 | highlightedWord: state.highlightedWord,
32 | highlightedVerse: state.highlightedVerse,
33 | currentChapter: state.currentChapter,
34 | }),
35 | shallow
36 | );
37 | const autoScroll = useSettings((state) => state.autoScroll, shallow);
38 |
39 | const loadMoreData = async (index: ListRange) => {
40 | const page = Math.floor(index.endIndex / LIMIT) + 2; // 2 because page start from 2
41 |
42 | if (!!data[index.endIndex] || page > paginationData?.total_pages) {
43 | return;
44 | }
45 |
46 | setCurrentPage(page);
47 | };
48 |
49 | const initData = () => {
50 | getVerses({
51 | id,
52 | page: 2,
53 | per_page: LIMIT,
54 | getBy: getVerseBy,
55 | }).then((res) => {
56 | setPaginationData(res.pagination);
57 | setData(res.verses);
58 | });
59 | };
60 |
61 | // fetch verses by page
62 | useEffect(() => {
63 | if (!currentPage) return;
64 | getVerses({
65 | id,
66 | page: currentPage,
67 | per_page: LIMIT,
68 | getBy: getVerseBy,
69 | }).then((res) => {
70 | setPaginationData(res.pagination);
71 | setData((prev) => {
72 | // remove duplicate data and sort by id
73 | const newData = res.verses.filter(
74 | (item) => !prev.find((prevItem) => prevItem.id === item.id)
75 | );
76 | return [...prev, ...newData].sort((a, b) => a.id - b.id);
77 | });
78 | });
79 |
80 | // eslint-disable-next-line react-hooks/exhaustive-deps
81 | }, [currentPage]);
82 |
83 | useEffect(() => {
84 | if (!highlightedVerse || !ref.current || !autoScroll) return;
85 | const idAndVerse = highlightedVerse.split(":");
86 | const verseNumber = parseInt(idAndVerse[1]);
87 | const chapterNumber = parseInt(idAndVerse[0]);
88 | if (chapterNumber !== currentChapter) return;
89 |
90 | const isVerseRendered = itemsRendered?.find(
91 | (item) => item.index === verseNumber - LIMIT - 1
92 | );
93 |
94 | if (verseNumber <= LIMIT || !!isVerseRendered) return;
95 | ref.current.scrollToIndex(verseNumber - LIMIT - 1);
96 |
97 | // eslint-disable-next-line react-hooks/exhaustive-deps
98 | }, [highlightedWord]);
99 |
100 | useEffect(() => {
101 | if (!searchParams.get("ayah")) return;
102 | const verseNumber = Number(searchParams.get("ayah"));
103 | if (verseNumber <= LIMIT) return; // because the first verse is already rendered on the server
104 | ref.current.scrollToIndex({
105 | index: verseNumber - LIMIT - 1,
106 | align: "center",
107 | });
108 | }, [searchParams]);
109 |
110 | const renderRow = (index: number) => {
111 | const dataIndex = index;
112 | if (!data[dataIndex]) {
113 | return ;
114 | }
115 |
116 | return (
117 |
125 | );
126 | };
127 |
128 | return (
129 | 20 ? totalData - LIMIT : 0}
131 | useWindowScroll
132 | increaseViewportBy={{ top: 10000, bottom: 1000 }}
133 | itemContent={renderRow}
134 | rangeChanged={loadMoreData}
135 | itemsRendered={(items) => setItemsRendered(items)}
136 | ref={ref}
137 | startReached={initData}
138 | />
139 | );
140 | };
141 |
142 | export default FetchInfiniteVerse;
143 |
--------------------------------------------------------------------------------
/src/app/(main)/surah/[chapterId]/twitter-image.tsx:
--------------------------------------------------------------------------------
1 | import { getChapter } from "@utils/chapter";
2 | import { ImageResponse } from "next/og";
3 | import { Lato } from "next/font/google";
4 |
5 | export const runtime = "edge";
6 |
7 | const lato = Lato({
8 | subsets: ["latin"],
9 | weight: ["100", "300", "400", "700", "900"],
10 | });
11 |
12 | export const size = {
13 | width: 1200,
14 | height: 630,
15 | };
16 | export const contentType = "image/png";
17 |
18 | export default async function Image({
19 | params,
20 | }: {
21 | params: { chapterId: string };
22 | }) {
23 | const chapterData = await getChapter(parseInt(params.chapterId));
24 | const NastaleeqFont = await fetch(
25 | new URL(
26 | "../../../../utils/fonts/nastaleeq/indopak/indopak-nastaleeq-waqf-lazim-v4.2.1.ttf",
27 | import.meta.url
28 | )
29 | ).then((res) => res.arrayBuffer());
30 |
31 | const LatoFont = await fetch(
32 | new URL("../../../../utils/fonts/Lato-Regular.ttf", import.meta.url)
33 | ).then((res) => res.arrayBuffer());
34 |
35 | return new ImageResponse(
36 | (
37 |
52 |
60 |
68 |
75 | https://quran.acml.me
76 |
77 |
78 |
96 |
103 |
109 | {chapterData.name_complex}
110 |
111 |
118 | ({chapterData.translated_name.name})
119 |
120 |
125 |
130 | {chapterData.verses_count} Ayah
131 |
132 |
137 | Diturunkan di
138 |
144 | {chapterData.revelation_place}
145 |
146 |
147 |
148 |
154 | {chapterData.name_arabic}
155 |
156 |
157 |
158 | ),
159 | {
160 | ...size,
161 | fonts: [
162 | {
163 | name: "Nastaleeq",
164 | data: NastaleeqFont,
165 | style: "normal",
166 | },
167 | {
168 | name: "Lato",
169 | data: LatoFont,
170 | style: "normal",
171 | },
172 | ],
173 | }
174 | );
175 | }
176 |
--------------------------------------------------------------------------------