Skip to content

Commit c023107

Browse files
authored
feat: added Juz Table of Contents (PR #3)
Adding Juz (Part) Table of Contents (TOC) in Sidebar
2 parents 728a0ef + a1ff317 commit c023107

File tree

10 files changed

+426
-96
lines changed

10 files changed

+426
-96
lines changed

package-lock.json

Lines changed: 3 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/App.tsx

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,25 +7,28 @@ import { LanguageProvider } from './contexts/LanguageContext';
77
import Index from './pages/Index';
88
import NotFound from './pages/NotFound';
99
import { ThemeProvider } from 'next-themes';
10+
import { BrowserProvider } from './contexts/BrowserContext/BrowserProvider';
1011

1112
const queryClient = new QueryClient();
1213

1314
const App = () => (
1415
<QueryClientProvider client={queryClient}>
1516
<LanguageProvider>
16-
<ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
17-
<TooltipProvider>
18-
<Toaster />
19-
<Sonner />
20-
<BrowserRouter>
21-
<Routes>
22-
<Route path="/" element={<Index />} />
23-
{/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
24-
<Route path="*" element={<NotFound />} />
25-
</Routes>
26-
</BrowserRouter>
27-
</TooltipProvider>
28-
</ThemeProvider>
17+
<BrowserProvider>
18+
<ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
19+
<TooltipProvider>
20+
<Toaster />
21+
<Sonner />
22+
<BrowserRouter>
23+
<Routes>
24+
<Route path="/" element={<Index />} />
25+
{/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
26+
<Route path="*" element={<NotFound />} />
27+
</Routes>
28+
</BrowserRouter>
29+
</TooltipProvider>
30+
</ThemeProvider>
31+
</BrowserProvider>
2932
</LanguageProvider>
3033
</QueryClientProvider>
3134
);

src/components/DocumentViewer.tsx

Lines changed: 38 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,77 +1,31 @@
1-
import { useEffect, useRef, useState } from 'react';
2-
import { useIsMobile } from '@/hooks/use-mobile';
3-
import { Button } from '@/components/ui/button';
4-
import { ChevronLeft, ChevronRight, SkipBack, SkipForward } from 'lucide-react';
51
import PageViewer from './PageViewer';
2+
import { Button } from '@/components/ui/button';
63
import { useLanguage } from '@/contexts/LanguageContext';
4+
import { useBrowseQuran } from '@/lib/viewmodels/browse-quran.viewmodel';
5+
import { ChevronLeft, ChevronRight, SkipBack, SkipForward } from 'lucide-react';
76

87
interface DocumentViewerProps {
98
className?: string;
109
}
1110

1211
export default function DocumentViewer({ className }: DocumentViewerProps) {
13-
const { t, isRTL, currentLanguage } = useLanguage();
14-
const isMobile = useIsMobile();
1512
const totalPages = 604; // Total number of pages in the document
16-
const [currentPage, setCurrentPage] = useState<number>(1);
17-
18-
// Navigation state
19-
const [gotoPage, setGotoPage] = useState<string>('1');
20-
const [gotoError, setGotoError] = useState<string | null>(null);
21-
const inputRef = useRef<HTMLInputElement>(null);
22-
23-
// Calculate page increment based on view and RTL
24-
const pageIncrement = isMobile ? 1 : 2;
25-
26-
// Adjust page number when switching between mobile/desktop
27-
useEffect(() => {
28-
if (!isMobile && currentPage % 2 === 0) {
29-
setCurrentPage((prev) => prev - 1);
30-
}
31-
}, [isMobile, currentPage]);
32-
33-
// Navigation handlers with RTL consideration
34-
const goToNextPages = () => {
35-
setCurrentPage((prev) => Math.min(totalPages, prev + pageIncrement));
36-
};
37-
38-
const goToPreviousPages = () => {
39-
setCurrentPage((prev) => Math.max(1, prev - pageIncrement));
40-
};
41-
42-
const goToFirstPage = () => {
43-
setCurrentPage(1);
44-
};
45-
46-
const goToLastPage = () => {
47-
const lastPage = isMobile ? totalPages : totalPages % 2 === 0 ? totalPages - 1 : totalPages;
48-
setCurrentPage(lastPage);
49-
};
50-
51-
const handleGoto = () => {
52-
const page = parseInt(gotoPage, 10);
53-
if (isNaN(page) || page < 1 || page > totalPages) {
54-
setGotoError(t('pageNumberError', { min: 1, max: totalPages }));
55-
inputRef.current?.focus();
56-
} else {
57-
setCurrentPage(page);
58-
setGotoError(null);
59-
setGotoPage(String(page));
60-
inputRef.current?.blur();
61-
}
62-
};
63-
64-
const handleGotoInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
65-
if (e.key === 'Enter') {
66-
e.preventDefault();
67-
handleGoto();
68-
}
69-
};
13+
const { t, isRTL, currentLanguage } = useLanguage();
7014

71-
useEffect(() => {
72-
setGotoPage(String(currentPage));
73-
setGotoError(null);
74-
}, [currentPage]);
15+
const {
16+
isMobile,
17+
currentPage,
18+
gotoPage,
19+
gotoError,
20+
inputRef,
21+
setGotoPage,
22+
handleGoto,
23+
handleGotoInputKeyDown,
24+
goToNextPages,
25+
goToPreviousPages,
26+
goToFirstPage,
27+
goToLastPage,
28+
} = useBrowseQuran(totalPages);
7529

7630
const getPageDisplay = () => {
7731
if (isMobile) {
@@ -81,7 +35,7 @@ export default function DocumentViewer({ className }: DocumentViewerProps) {
8135
pageNumber={currentPage}
8236
totalPages={totalPages}
8337
className="animate-fadeIn aspect-[3/4] w-full max-w-md"
84-
/>
38+
/>,
8539
];
8640
}
8741

@@ -97,22 +51,24 @@ export default function DocumentViewer({ className }: DocumentViewerProps) {
9751
pageNumber={currentPage}
9852
totalPages={totalPages}
9953
className="animate-slideInLeft aspect-[3/4] w-1/2 max-w-md"
100-
/>
54+
/>,
10155
];
10256
};
10357

10458
const getNavigationIcons = () => {
105-
return isRTL ? {
106-
first: <SkipForward className="h-4 w-4" />,
107-
previous: <ChevronRight className="h-4 w-4" />,
108-
next: <ChevronLeft className="h-4 w-4" />,
109-
last: <SkipBack className="h-4 w-4" />
110-
} : {
111-
first: <SkipBack className="h-4 w-4" />,
112-
previous: <ChevronLeft className="h-4 w-4" />,
113-
next: <ChevronRight className="h-4 w-4" />,
114-
last: <SkipForward className="h-4 w-4" />
115-
};
59+
return isRTL
60+
? {
61+
first: <SkipForward className="h-4 w-4" />,
62+
previous: <ChevronRight className="h-4 w-4" />,
63+
next: <ChevronLeft className="h-4 w-4" />,
64+
last: <SkipBack className="h-4 w-4" />,
65+
}
66+
: {
67+
first: <SkipBack className="h-4 w-4" />,
68+
previous: <ChevronLeft className="h-4 w-4" />,
69+
next: <ChevronRight className="h-4 w-4" />,
70+
last: <SkipForward className="h-4 w-4" />,
71+
};
11672
};
11773

11874
const icons = getNavigationIcons();
@@ -121,7 +77,11 @@ export default function DocumentViewer({ className }: DocumentViewerProps) {
12177
<div className={className} dir={isRTL ? 'rtl' : 'ltr'}>
12278
<div className="flex flex-col items-center space-y-8">
12379
{/* Pages container */}
124-
<div className={`flex w-full items-center justify-center gap-1 md:gap-2 lg:gap-3 ${isRTL ? 'flex-row-reverse' : 'flex-row'}`}>
80+
<div
81+
className={`flex w-full items-center justify-center gap-1 md:gap-2 lg:gap-3 ${
82+
isRTL ? 'flex-row-reverse' : 'flex-row'
83+
}`}
84+
>
12585
{getPageDisplay()}
12686
</div>
12787

src/components/Sidebar.tsx

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { BrowserContext } from '@/contexts/BrowserContext/BrowserContext';
2+
import { useLanguage } from '@/contexts/LanguageContext';
3+
import { jozList } from '@/data/joz';
4+
import { useIsMobile } from '@/hooks/use-mobile';
5+
import { X } from 'lucide-react';
6+
import React, { useContext } from 'react';
7+
8+
const Sidebar = ({ setIsSidebarOpen }: { setIsSidebarOpen: React.Dispatch<React.SetStateAction<boolean>> }) => {
9+
const { isRTL } = useLanguage();
10+
const isMobile = useIsMobile();
11+
12+
const { setCurrentPage } = useContext(BrowserContext);
13+
14+
return (
15+
<aside
16+
className={` ${
17+
isMobile
18+
? 'fixed inset-0 z-50 flex flex-col bg-gray-100/95 px-4 pt-16 dark:bg-gray-800/95'
19+
: `sticky top-20 max-h-[80vh] min-w-[220px] transform overflow-y-auto rounded-xl p-4 shadow-md ${
20+
isRTL ? '' : 'md:order-first'
21+
}`
22+
} bg-gray-100 text-black transition-all duration-300 ease-in-out dark:bg-gray-800 dark:text-white`}
23+
>
24+
{isMobile && (
25+
<button
26+
onClick={() => setIsSidebarOpen(false)}
27+
className={`absolute top-4 ${isRTL ? 'left-4' : 'right-4'} rounded-full bg-gray-200 p-2 dark:bg-gray-700`}
28+
>
29+
<X size={20} />
30+
</button>
31+
)}
32+
33+
<h2 className="mb-4 text-lg font-semibold tracking-wide">{isRTL ? 'قائمة الأجزاء' : 'Joz List'}</h2>
34+
<ul className={`space-y-2 ${isMobile ? 'flex-1 overflow-y-auto' : ''}`}>
35+
{jozList.map((element, index) => (
36+
<li
37+
key={index}
38+
onClick={(e) => {
39+
e.preventDefault();
40+
setCurrentPage(element.page);
41+
if (isMobile) setIsSidebarOpen(false);
42+
}}
43+
className="cursor-pointer rounded-md px-3 py-2 text-sm font-medium transition-all hover:scale-105 hover:bg-green-200 dark:hover:bg-green-700"
44+
>
45+
{element.name[isRTL ? 'ar' : 'en']}
46+
</li>
47+
))}
48+
</ul>
49+
</aside>
50+
);
51+
};
52+
53+
export default Sidebar;
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { createContext } from 'react';
2+
3+
interface BrowserContextType {
4+
gotoError: string | null;
5+
setGotoError: React.Dispatch<React.SetStateAction<string>>;
6+
gotoPage: string;
7+
setGotoPage: React.Dispatch<React.SetStateAction<string>>;
8+
currentPage: number;
9+
setCurrentPage: React.Dispatch<React.SetStateAction<number>>;
10+
}
11+
12+
export const BrowserContext = createContext<BrowserContextType | undefined>(undefined);
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import React, { ReactNode, useState } from 'react';
2+
import { BrowserContext } from './BrowserContext';
3+
4+
interface BrowserProviderProps {
5+
children: ReactNode;
6+
}
7+
8+
export const BrowserProvider: React.FC<BrowserProviderProps> = ({ children }) => {
9+
const [currentPage, setCurrentPage] = useState<number>(1);
10+
const [gotoPage, setGotoPage] = useState<string>('1');
11+
const [gotoError, setGotoError] = useState<string | null>(null);
12+
13+
return (
14+
<BrowserContext.Provider
15+
value={{
16+
currentPage,
17+
setCurrentPage,
18+
gotoPage,
19+
setGotoPage,
20+
gotoError,
21+
setGotoError,
22+
}}
23+
>
24+
{children}
25+
</BrowserContext.Provider>
26+
);
27+
};

src/contexts/LanguageContext/languageConfig.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export const translations = {
2525
goTo: 'إنتقال',
2626
firstPage: 'الصفحة الأولى',
2727
previousPage: 'الصفحة السابقة',
28+
lastPage: 'الصفحة السابقة',
2829
nextPage: 'الصفحة التالية',
2930
of: 'من',
3031
pageNumber: 'رقم الصفحة',
@@ -42,11 +43,12 @@ export const translations = {
4243
goTo: 'Go To',
4344
firstPage: 'First Page',
4445
previousPage: 'Previous Page',
46+
lastPage: 'Last Page',
4547
nextPage: 'Next Page',
4648
of: 'of',
4749
pageNumber: 'Page Number',
4850
pageNumberError: 'Invalid page number',
49-
}
51+
},
5052
};
5153

5254
export type TranslationKey = keyof (typeof translations)['ar'];
@@ -58,4 +60,5 @@ export interface LanguageContextType {
5860
isRTL: boolean;
5961
supportedLanguages: typeof SUPPORTED_LANGUAGES;
6062
isLoading: boolean;
63+
currentLanguage: string; // Add this property
6164
}

0 commit comments

Comments
 (0)