Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions metro.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,15 @@ config.resolver = {
...config.resolver,
assetExts: config.resolver.assetExts.filter(ext => ext !== 'svg'),
sourceExts: [...config.resolver.sourceExts, 'svg'],
blockList: [
...(Array.isArray(config.resolver.blockList)
? config.resolver.blockList
: config.resolver.blockList
? [config.resolver.blockList]
: []),
/\/__tests__\//,
/\.test\.[jt]sx?$/,
],
};

module.exports = config;
68 changes: 68 additions & 0 deletions src/app/prepare-offline/BookChapterSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import React from 'react';
import { StyleSheet, View } from 'react-native';
import { PrepareOfflineBookGroup } from '../../types/prepareOffline/types';
import { theme } from '../../theme';
import { BookSectionHeader } from './BookSectionHeader';
import { ChapterSelectorGrid } from './ChapterSelectorGrid';

interface BookChapterSectionProps {
book: PrepareOfflineBookGroup;
expanded: boolean;
selectedIds: Set<number>;
onToggleExpanded: () => void;
onToggleChapter: (chapterId: number) => void;
onToggleBook: (book: PrepareOfflineBookGroup) => void;
isBookFullySelected: boolean;
}

export function BookChapterSection({
book,
expanded,
selectedIds,
onToggleExpanded,
onToggleChapter,
onToggleBook,
isBookFullySelected,
}: BookChapterSectionProps) {
const selectedCount = book.chapters.filter(ch =>
selectedIds.has(ch.id),
).length;

return (
<View style={styles.card}>
<BookSectionHeader
bookName={book.bookName}
expanded={expanded}
selectedCount={selectedCount}
totalChapters={book.chapters.length}
isBookFullySelected={isBookFullySelected}
onToggleExpanded={onToggleExpanded}
onToggleBook={() => onToggleBook(book)}
/>
{expanded ? (
<View style={styles.grid}>
<ChapterSelectorGrid
chapterIds={book.chapters.map(ch => ch.id)}
chapterNumbers={book.chapters.map(ch => ch.chapterNumber)}
selectedIds={selectedIds}
onToggleChapter={onToggleChapter}
/>
</View>
) : null}
</View>
);
}

const styles = StyleSheet.create({
card: {
overflow: 'hidden',
borderRadius: theme.radius.sm,
borderWidth: 1,
borderColor: theme.colors.border,
backgroundColor: theme.colors.cardBackground,
},
grid: {
paddingHorizontal: theme.spacing.sm,
paddingBottom: theme.spacing.sm,
},
});
111 changes: 111 additions & 0 deletions src/app/prepare-offline/BookSectionHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import React from 'react';
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import { ChevronDown, ChevronUp } from 'lucide-react-native';
import { theme, iconSizes, listIconStrokeWidth } from '../../theme';
import { SelectionCheckbox } from './SelectionCheckbox';

interface BookSectionHeaderProps {
bookName: string;
expanded: boolean;
selectedCount: number;
totalChapters: number;
isBookFullySelected: boolean;
onToggleExpanded: () => void;
onToggleBook: () => void;
}

export function BookSectionHeader({
bookName,
expanded,
selectedCount,
totalChapters,
isBookFullySelected,
onToggleExpanded,
onToggleBook,
}: BookSectionHeaderProps) {
const hasSelection = selectedCount > 0;

return (
<View style={styles.container}>
<TouchableOpacity
style={styles.titleButton}
onPress={onToggleExpanded}
accessibilityRole="button"
accessibilityState={{ expanded }}
accessibilityLabel={`${bookName}, ${selectedCount} of ${totalChapters} chapters selected`}
>
<Text style={styles.bookTitle}>{bookName}</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.selectAllButton}
onPress={onToggleBook}
accessibilityRole="button"
accessibilityLabel={`Select all chapters in ${bookName}`}
>
<SelectionCheckbox
selected={isBookFullySelected}
showCheck={hasSelection}
/>
<Text style={styles.selectAllLabel}>Select all</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.chevronButton}
onPress={onToggleExpanded}
accessibilityRole="button"
accessibilityLabel={`${expanded ? 'Collapse' : 'Expand'} ${bookName}`}
>
{expanded ? (
<ChevronUp
size={iconSizes.headerTab}
color={theme.colors.mutedForeground}
strokeWidth={listIconStrokeWidth}
/>
) : (
<ChevronDown
size={iconSizes.headerTab}
color={theme.colors.mutedForeground}
strokeWidth={listIconStrokeWidth}
/>
)}
</TouchableOpacity>
</View>
);
}

const styles = StyleSheet.create({
container: {
minHeight: 40,
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: theme.spacing.sm,
gap: theme.spacing.sm,
},
titleButton: {
flexShrink: 1,
minHeight: 40,
alignItems: 'center',
justifyContent: 'center',
},
bookTitle: {
fontSize: theme.typography.sizes.sm,
fontWeight: theme.typography.weights.semibold,
color: theme.colors.foreground,
},
selectAllButton: {
minHeight: 40,
flexDirection: 'row',
alignItems: 'center',
gap: theme.spacing.xs,
},
selectAllLabel: {
fontSize: theme.typography.sizes.xs,
color: theme.colors.mutedForeground,
},
chevronButton: {
minWidth: 36,
minHeight: 40,
marginLeft: 'auto',
alignItems: 'flex-end',
justifyContent: 'center',
},
});
103 changes: 103 additions & 0 deletions src/app/prepare-offline/ChapterSelectionAccordion.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import React from 'react';
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import { ChevronDown, ChevronUp } from 'lucide-react-native';
import { PrepareOfflineBookGroup } from '../../types/prepareOffline/types';
import { theme, iconSizes, listIconStrokeWidth } from '../../theme';
import { BookChapterSection } from './BookChapterSection';

interface ChapterSelectionAccordionProps {
title: string;
expanded: boolean;
onToggleExpanded: () => void;
books: PrepareOfflineBookGroup[];
selectedIds: Set<number>;
expandedBookIds: Set<number>;
onToggleBookExpanded: (bookId: number) => void;
onToggleChapter: (chapterId: number) => void;
onToggleBook: (book: PrepareOfflineBookGroup) => void;
isBookFullySelected: (book: PrepareOfflineBookGroup) => boolean;
}

export function ChapterSelectionAccordion({
title,
expanded,
onToggleExpanded,
books,
selectedIds,
expandedBookIds,
onToggleBookExpanded,
onToggleChapter,
onToggleBook,
isBookFullySelected,
}: ChapterSelectionAccordionProps) {
return (
<View style={styles.card}>
<TouchableOpacity
style={styles.header}
onPress={onToggleExpanded}
accessibilityRole="button"
accessibilityState={{ expanded }}
>
<Text style={styles.title}>{title}</Text>
{expanded ? (
<ChevronUp
size={iconSizes.headerTab}
color={theme.colors.mutedForeground}
strokeWidth={listIconStrokeWidth}
/>
) : (
<ChevronDown
size={iconSizes.headerTab}
color={theme.colors.mutedForeground}
strokeWidth={listIconStrokeWidth}
/>
)}
</TouchableOpacity>

{expanded ? (
<View style={styles.books}>
{books.map(book => (
<BookChapterSection
key={book.bookId}
book={book}
expanded={expandedBookIds.has(book.bookId)}
selectedIds={selectedIds}
onToggleExpanded={() => onToggleBookExpanded(book.bookId)}
onToggleChapter={onToggleChapter}
onToggleBook={onToggleBook}
isBookFullySelected={isBookFullySelected(book)}
/>
))}
</View>
) : null}
</View>
);
}

const styles = StyleSheet.create({
card: {
backgroundColor: theme.colors.cardBackground,
borderRadius: theme.radius.md,
borderWidth: 1,
borderColor: theme.colors.border,
paddingHorizontal: theme.spacing.md,
},
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
minHeight: 44,
paddingVertical: theme.spacing.sm,
gap: theme.spacing.sm,
},
title: {
flex: 1,
fontSize: theme.typography.sizes.sm,
fontWeight: theme.typography.weights.medium,
color: theme.colors.foreground,
},
books: {
gap: theme.spacing.xs,
paddingBottom: theme.spacing.md,
},
});
40 changes: 40 additions & 0 deletions src/app/prepare-offline/ChapterSelectorGrid.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import React from 'react';
import { fireEvent, render, screen } from '@testing-library/react-native';
import { ChapterSelectorGrid } from './ChapterSelectorGrid';

describe('ChapterSelectorGrid', () => {
it('renders chapter numbers and toggles selection', () => {
const onToggleChapter = jest.fn();
const selectedIds = new Set<number>();

const { rerender } = render(
<ChapterSelectorGrid
chapterIds={[1, 2]}
chapterNumbers={[3, 4]}
selectedIds={selectedIds}
onToggleChapter={onToggleChapter}
/>,
);

expect(screen.getByText('3')).toBeTruthy();
expect(screen.getByText('4')).toBeTruthy();

fireEvent.press(screen.getByLabelText('Chapter 3'));
expect(onToggleChapter).toHaveBeenCalledWith(1);

rerender(
<ChapterSelectorGrid
chapterIds={[1, 2]}
chapterNumbers={[3, 4]}
selectedIds={new Set([1])}
onToggleChapter={onToggleChapter}
/>,
);

expect(screen.getByLabelText('Chapter 3').props.accessibilityState).toEqual(
{
checked: true,
},
);
});
});
Loading
Loading