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
21 changes: 21 additions & 0 deletions app/components/OutlineDrawer/OutlineDrawer.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,24 @@
display: flex;
flex-direction: column;
}

/*
No results message styling
Displayed when search query returns no matching chapters or lessons
*/
.noResults {
text-align: center;
padding: 2rem 1rem;
color: var(--chakra-colors-gray-500);
}

.noResults p:first-child {
font-weight: 600;
margin-bottom: 0.5rem;
font-size: 1rem;
}

.noResults p:last-child {
font-size: 0.875rem;
opacity: 0.8;
}
88 changes: 74 additions & 14 deletions app/components/OutlineDrawer/OutlineDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,18 @@ import {
DrawerCloseButton,
DrawerBody,
Button,
Input,
InputGroup,
InputLeftElement,
} from "@chakra-ui/react";
import styles from "./OutlineDrawer.module.css";
import { ContentOutline } from "@/lib/types";
import ChapterItem from "../ChapterItem";
import { isChapterCompleted } from "@/lib/client-functions";
import Link from "next/link";
import CertificateButton from "../CertificateButton/CertificateButton";
import { useSearch } from "@/app/utils/hooks";
import { SearchIcon } from "@chakra-ui/icons";

export default function OutlineDrawer({
isOpen,
Expand All @@ -30,6 +35,16 @@ export default function OutlineDrawer({
activeChapterIndex: number;
activeStepIndex: number;
}) {
/**
* Initialize the search hook
*
* This hook provides:
* - searchQuery: Current search string
* - setSearchQuery: Function to update search
* - filteredOutline: Filtered chapters based on search
*/
const { searchQuery, setSearchQuery, filteredOutline } = useSearch(outline);

return (
<>
<Drawer
Expand All @@ -52,21 +67,66 @@ export default function OutlineDrawer({
</DrawerHeader>

<DrawerBody>
{/* Search Input Section */}
{/*
Added to allow users to search through chapters and lessons
This helps users quickly find specific topics without scrolling
*/}
<InputGroup mb={4} size="md">
<InputLeftElement pointerEvents="none">
<SearchIcon color="gray.400" />
</InputLeftElement>
<Input
placeholder="Search chapters and lessons..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
borderRadius="md"
focusBorderColor="blue.400"
_placeholder={{ color: "gray.400" }}
/>
</InputGroup>

<nav>
<ul className={styles.chapterItemsList}>
{outline.map((item, index) => (
<ChapterItem
isActive={index === activeChapterIndex}
isCompleted={isChapterCompleted(index, item.steps.length)}
index={index}
key={item.title}
title={item.title}
steps={item.steps}
activeStepIndex={activeStepIndex}
onClose={onClose}
/>
))}
</ul>
{/*
Display filtered outline instead of original outline
When search is active, only matching chapters/lessons are shown
*/}
{filteredOutline.length > 0 ? (
<ul className={styles.chapterItemsList}>
{filteredOutline.map((item, index) => {
// Find the original chapter index from the full outline
// This is needed to maintain correct chapter numbering and completion status
const originalIndex = outline.findIndex(
(chapter) => chapter.folderName === item.folderName
);

return (
<ChapterItem
isActive={originalIndex === activeChapterIndex}
isCompleted={isChapterCompleted(
originalIndex,
item.steps.length
)}
index={originalIndex}
key={item.title}
title={item.title}
steps={item.steps}
activeStepIndex={activeStepIndex}
onClose={onClose}
/>
);
})}
</ul>
) : (
/*
No results message
Shown when search query doesn't match any chapters or lessons
*/
<div className={styles.noResults}>
<p>No matching chapters or lessons found.</p>
<p>Try a different search term.</p>
</div>
)}
</nav>
</DrawerBody>
</DrawerContent>
Expand Down
47 changes: 47 additions & 0 deletions app/utils/hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { useState, useMemo } from "react";
import { ContentOutline, Chapter } from "@/lib/types";

/**
* Hook to search chapters and lessons by title
* Returns filtered outline based on search query
*/
export function useSearch(outline: ContentOutline) {
const [searchQuery, setSearchQuery] = useState("");

// Filter outline based on search query (memoized for performance)
const filteredOutline = useMemo(() => {
if (!searchQuery.trim()) {
return outline;
}

const query = searchQuery.toLowerCase().trim();

// Filter chapters and steps by search query
const filtered = outline
.map((chapter) => {
const chapterMatches = chapter.title.toLowerCase().includes(query);
const matchingSteps = chapter.steps.filter((step) =>
step.title.toLowerCase().includes(query)
);

// Include chapter if title or any step matches
if (chapterMatches || matchingSteps.length > 0) {
return {
...chapter,
steps: chapterMatches ? chapter.steps : matchingSteps,
};
}

return null;
})
.filter((chapter): chapter is Chapter => chapter !== null);

return filtered;
}, [searchQuery, outline]);

return {
searchQuery,
setSearchQuery,
filteredOutline,
};
}