├── 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 | 6 | -------------------------------------------------------------------------------- /.idea/jsLinters/eslint.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 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
Baca Hadits
; 8 | }; 9 | 10 | export default ReadHadithHeader; 11 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 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 |
7 |
8 |
9 |
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 | 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 | 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 | 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 | 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 | 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 | Quran illustration 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 | 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 |
    39 |
    40 | 41 | 42 |
    43 |
    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 | 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 | Logo 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 | // Baca Quran 43 | // 44 | // Baca Quran 45 | // 46 | // 47 | // 51 | // Baca Quran 57 | // Hadits 58 | // 59 | // 63 | // Baca Quran 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 | 70 |
    {currentFontSize}
    71 | 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 | 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 | 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 | 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 | --------------------------------------------------------------------------------