diff --git a/__tests__/app/sorting/[algorithm]/page.test.tsx b/__tests__/app/sorting/[algorithm]/page.test.tsx deleted file mode 100644 index eb3b7d1..0000000 --- a/__tests__/app/sorting/[algorithm]/page.test.tsx +++ /dev/null @@ -1,144 +0,0 @@ -import React from "react"; -import { render, screen } from "@testing-library/react"; -import AlgorithmPage from "@/app/sorting/[algorithm]/page"; -import { AlgorithmProvider } from "@/context/AlgorithmContext"; - -// Mock the next/navigation module -jest.mock("next/navigation", () => ({ - useParams: jest.fn(() => ({ algorithm: "bubbleSort" })), - notFound: jest.fn(), -})); - -// Mock the algorithms module -jest.mock("@/lib/algorithms", () => { - const mockGetAlgorithmByName = jest.fn().mockReturnValue(() => ({ - steps: [ - { array: [5, 3, 8], comparing: [], swapped: false, completed: [] }, - { array: [3, 5, 8], comparing: [], swapped: false, completed: [2] }, - ], - name: "Bubble Sort", - key: "bubbleSort", - category: "sorting", - description: "A simple sorting algorithm", - timeComplexity: "O(n²)", - spaceComplexity: "O(1)", - reference: "https://en.wikipedia.org/wiki/Bubble_sort", - pseudoCode: ["procedure bubbleSort(A: list of sortable items)"], - })); - - return { - getAlgorithmByName: mockGetAlgorithmByName, - availableAlgorithms: [ - { - name: "Bubble Sort", - key: "bubbleSort", - category: "sorting", - description: "A simple sorting algorithm", - difficulty: "easy", - }, - ], - }; -}); - -// Mock the PageLayout component -jest.mock("@/components/layout/PageLayout", () => { - return ({ children, title, subtitle }: any) => ( -
-

{title}

-

{subtitle}

-
{children}
-
- ); -}); - -// Mock the AlgorithmVisualizer component -jest.mock("@/components/visualizer/AlgorithmVisualizer", () => { - return () => ( -
Algorithm Visualizer Mock
- ); -}); - -describe("AlgorithmPage", () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - it("should render the algorithm page with correct title and description", () => { - render( - - - - ); - - expect(screen.getByTestId("page-title")).toHaveTextContent("Bubble Sort"); - expect(screen.getByTestId("page-subtitle")).toHaveTextContent( - "A simple sorting algorithm" - ); - }); - - it("should render the AlgorithmVisualizer component", () => { - render( - - - - ); - - expect(screen.getByTestId("algorithm-visualizer")).toBeInTheDocument(); - }); - - it("should render algorithm not found message for invalid algorithm", () => { - // Mock the useParams to return an invalid algorithm - require("next/navigation").useParams.mockReturnValue({ - algorithm: "invalidAlgorithm", - }); - - // Mock the getAlgorithmByName to return null for invalid algorithm - require("@/lib/algorithms").getAlgorithmByName.mockReturnValue(null); - - // Mock the availableAlgorithms to not include the invalid algorithm - require("@/lib/algorithms").availableAlgorithms = []; - - render( - - - - ); - - // Use queryAllByText to handle multiple matches and check the first occurrence - const notFoundElements = screen.queryAllByText("Algorithm Not Found"); - expect(notFoundElements.length).toBeGreaterThan(0); - expect(notFoundElements[0]).toBeInTheDocument(); - }); - - it("should call getAlgorithmByName with the correct algorithm key", () => { - // Reset the useParams mock to return bubbleSort - require("next/navigation").useParams.mockReturnValue({ - algorithm: "bubbleSort", - }); - - // Reset algorithm mock data - require("@/lib/algorithms").availableAlgorithms = [ - { - name: "Bubble Sort", - key: "bubbleSort", - category: "sorting", - description: "A simple sorting algorithm", - difficulty: "easy", - }, - ]; - - // Get the mock function and clear it - const mockedGetAlgorithmByName = - require("@/lib/algorithms").getAlgorithmByName; - mockedGetAlgorithmByName.mockClear(); - - render( - - - - ); - - // Check that getAlgorithmByName was called with the right algorithm key - expect(mockedGetAlgorithmByName).toHaveBeenCalledWith("bubbleSort"); - }); -}); diff --git a/__tests__/components/AlgorithmCard.test.tsx b/__tests__/components/AlgorithmCard.test.tsx deleted file mode 100644 index 44a3b2e..0000000 --- a/__tests__/components/AlgorithmCard.test.tsx +++ /dev/null @@ -1,58 +0,0 @@ -// __tests__/components/AlgorithmCard.test.tsx -import React from "react"; -import { render, screen } from "@testing-library/react"; -import AlgorithmCard from "@/components/AlgorithmCard"; -import { AlgorithmInfo } from "@/lib/types"; - -// Mock the Next.js Link component -jest.mock("next/link", () => { - return ({ children, href }: { children: React.ReactNode; href: string }) => { - return {children}; - }; -}); - -describe("AlgorithmCard", () => { - const mockAlgorithm: AlgorithmInfo = { - name: "Bubble Sort", - key: "bubbleSort", - category: "sorting", - description: - "A simple sorting algorithm that repeatedly steps through the list.", - difficulty: "easy", - }; - - it("should render the algorithm name", () => { - render(); - expect(screen.getByText("Bubble Sort")).toBeInTheDocument(); - }); - - it("should render the algorithm description", () => { - render(); - expect( - screen.getByText( - "A simple sorting algorithm that repeatedly steps through the list." - ) - ).toBeInTheDocument(); - }); - - it("should contain a link to the algorithm visualization page", () => { - render(); - const visualizeLink = screen.getByText("Visualize"); - expect(visualizeLink.closest("a")).toHaveAttribute( - "href", - "/sorting/bubbleSort" - ); - }); - - it("should contain a link to the difficulty page", () => { - render(); - const difficultyLink = screen.getByText("easy"); - expect(difficultyLink.closest("a")).toHaveAttribute("href", "/easy"); - }); - - it("should contain a link to the category page", () => { - render(); - const categoryLink = screen.getByText("sorting"); - expect(categoryLink.closest("a")).toHaveAttribute("href", "/sorting"); - }); -}); diff --git a/__tests__/components/visualizer/SortingVisualization.test.tsx b/__tests__/components/visualizer/SortingVisualization.test.tsx deleted file mode 100644 index c8f38f1..0000000 --- a/__tests__/components/visualizer/SortingVisualization.test.tsx +++ /dev/null @@ -1,125 +0,0 @@ -// __tests__/components/visualizer/SortingVisualization.test.tsx -import React from "react"; -import { render, screen } from "@testing-library/react"; -import SortingVisualization from "@/components/visualizer/SortingVisualization"; -import { SortingStep } from "@/lib/types"; - -describe("SortingVisualization", () => { - const createMockStep = ( - array: number[] = [1, 2, 3], - comparing: number[] = [], - swapped: boolean = false, - completed: number[] = [] - ): SortingStep => ({ - array, - comparing, - swapped, - completed, - }); - - it("should render the array elements", () => { - const mockStep = createMockStep([5, 10, 15]); - render(); - - expect(screen.getByText("5")).toBeInTheDocument(); - expect(screen.getByText("10")).toBeInTheDocument(); - expect(screen.getByText("15")).toBeInTheDocument(); - }); - - it("should apply different colors based on element state", () => { - // Create a step with one element comparing, one completed, and one normal - const mockStep = createMockStep( - [5, 10, 15], - [1], // comparing index 1 - false, - [2] // completed index 2 - ); - - const { container } = render( - - ); - - // Get all the bar elements - const bars = container.querySelectorAll(".bar-chart"); - expect(bars.length).toBe(3); - - // Check class names for color styles - expect(bars[0]).toHaveClass("bg-blue-400"); // Normal bar - expect(bars[1]).toHaveClass("bg-yellow-400"); // Comparing bar - expect(bars[2]).toHaveClass("bg-green-400"); // Completed bar - }); - - it("should apply the swapped color when comparing and swapped is true", () => { - // Create a step with elements being compared and swapped - const mockStep = createMockStep( - [5, 10, 15], - [0, 1], // comparing indices 0 and 1 - true, // swapped is true - [] - ); - - const { container } = render( - - ); - - // Get the bar elements - const bars = container.querySelectorAll(".bar-chart"); - - // Both comparing bars should have the swapped color - expect(bars[0]).toHaveClass("bg-red-400"); - expect(bars[1]).toHaveClass("bg-red-400"); - }); - - it("should render bars with heights proportional to their values", () => { - const mockStep = createMockStep([5, 10, 15]); - const { container } = render( - - ); - - // Get all the bar elements - const bars = container.querySelectorAll(".bar-chart"); - - // Use type assertion to access style properties - const firstBarHeight = parseFloat((bars[0] as HTMLElement).style.height); - const secondBarHeight = parseFloat((bars[1] as HTMLElement).style.height); - const thirdBarHeight = parseFloat((bars[2] as HTMLElement).style.height); - - // Verify proper scaling (allow small margin for rounding) - expect(firstBarHeight).toBeGreaterThanOrEqual(32); - expect(firstBarHeight).toBeLessThanOrEqual(34); - - expect(secondBarHeight).toBeGreaterThanOrEqual(66); - expect(secondBarHeight).toBeLessThanOrEqual(68); - - expect(thirdBarHeight).toBeGreaterThanOrEqual(99); - expect(thirdBarHeight).toBeLessThanOrEqual(101); - - // Verify relative heights (proportions should be maintained) - expect(secondBarHeight).toBeCloseTo(firstBarHeight * 2, 0); - expect(thirdBarHeight).toBeCloseTo(firstBarHeight * 3, 0); - }); - - it("should adjust bar widths based on the number of elements", () => { - // Test with a small array - const smallStep = createMockStep([1, 2, 3]); - const { container: smallContainer } = render( - - ); - - // Test with a larger array - const largeStep = createMockStep([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); - const { container: largeContainer } = render( - - ); - - // Get the bars from both renders - const smallBars = smallContainer.querySelectorAll(".bar-chart"); - const largeBars = largeContainer.querySelectorAll(".bar-chart"); - - // The width of bars in the small array should be wider than in the large array - const smallBarWidth = parseFloat((smallBars[0] as HTMLElement).style.width); - const largeBarWidth = parseFloat((largeBars[0] as HTMLElement).style.width); - - expect(smallBarWidth).toBeGreaterThan(largeBarWidth); - }); -}); diff --git a/__tests__/components/visualizer/VisualizerControls.test.tsx b/__tests__/components/visualizer/VisualizerControls.test.tsx deleted file mode 100644 index 6ea2173..0000000 --- a/__tests__/components/visualizer/VisualizerControls.test.tsx +++ /dev/null @@ -1,159 +0,0 @@ -// __tests__/components/visualizer/VisualizerControls.test.tsx -import React from "react"; -import { render, screen, fireEvent } from "@testing-library/react"; -import VisualizerControls from "@/components/visualizer/VisualizerControls"; -import { AlgorithmProvider, useAlgorithm } from "@/context/AlgorithmContext"; - -// Mock the algorithms module -jest.mock("@/lib/algorithms", () => ({ - getAlgorithmByName: jest.fn().mockReturnValue(() => ({ - steps: [ - { array: [1, 2, 3], comparing: [], swapped: false, completed: [] }, - { array: [1, 2, 3], comparing: [0, 1], swapped: false, completed: [] }, - { array: [1, 2, 3], comparing: [], swapped: false, completed: [0] }, - ], - name: "Test Algorithm", - key: "testAlgorithm", - category: "sorting", - description: "Test description", - timeComplexity: "O(n)", - spaceComplexity: "O(1)", - reference: "test-reference", - pseudoCode: ["test pseudocode"], - })), -})); - -// Mock utility functions -jest.mock("@/lib/utils", () => ({ - saveState: jest.fn(), - loadState: jest.fn().mockReturnValue(null), - generateRandomArray: jest.fn().mockReturnValue([4, 2, 1]), -})); - -// Wrapper component to provide context and access props -const TestWrapper = ({ - onGenerateNewArray, -}: { - onGenerateNewArray: () => void; -}) => { - const { state } = useAlgorithm(); - - return ( - - ); -}; - -describe("VisualizerControls", () => { - // Create a mock for the generateNewArray function - const mockGenerateNewArray = jest.fn(); - - beforeEach(() => { - jest.clearAllMocks(); - }); - - it("should render control buttons", () => { - render( - - - - ); - - // Check that basic control buttons are rendered - expect(screen.getByText("Play")).toBeInTheDocument(); - expect(screen.getByText("Reset")).toBeInTheDocument(); - expect(screen.getByText("New Array")).toBeInTheDocument(); - }); - - it("should update state when play button is clicked", () => { - render( - - - - ); - - // Click the play button - fireEvent.click(screen.getByText("Play")); - - // The play button should be replaced with a pause button - expect(screen.getByText("Pause")).toBeInTheDocument(); - expect(screen.queryByText("Play")).not.toBeInTheDocument(); - }); - - it("should call onGenerateNewArray when New Array button is clicked", () => { - render( - - - - ); - - // Click the New Array button - fireEvent.click(screen.getByText("New Array")); - - // Check that the callback was called - expect(mockGenerateNewArray).toHaveBeenCalledTimes(1); - }); - - it("should display the correct step information", async () => { - render( - - - - ); - - // Wait for initialization - const stepText = await screen.findByText(/Step 1 of/); - expect(stepText).toBeInTheDocument(); - }); - - it("should update step when progress slider is changed", () => { - render( - - - - ); - - // Get the progress slider and change its value - const slider = screen.getByLabelText("Progress"); - fireEvent.change(slider, { target: { value: 1 } }); - - // Check that step information has updated (step 2 of 3) - expect(screen.getByText(/Step 2 of/)).toBeInTheDocument(); - }); - - it("should disable next button when on the last step", async () => { - render( - - - - ); - - // Move to the last step - const slider = screen.getByLabelText("Progress"); - fireEvent.change(slider, { target: { value: 2 } }); - - // Get the next button and check that it's disabled - const nextButton = screen.getByLabelText("Next"); - expect(nextButton).toBeDisabled(); - }); - - it("should disable prev button when on the first step", async () => { - render( - - - - ); - - // Should already be on the first step - const prevButton = screen.getByLabelText("Previous"); - expect(prevButton).toBeDisabled(); - - // Move to a later step and check that the button is enabled - const slider = screen.getByLabelText("Progress"); - fireEvent.change(slider, { target: { value: 1 } }); - expect(prevButton).not.toBeDisabled(); - }); -}); diff --git a/__tests__/context/AlgorithmContext.test.tsx b/__tests__/context/AlgorithmContext.test.tsx deleted file mode 100644 index 30f0b29..0000000 --- a/__tests__/context/AlgorithmContext.test.tsx +++ /dev/null @@ -1,340 +0,0 @@ -// __tests__/context/AlgorithmContext.test.tsx -import React from "react"; -import { render, screen, waitFor, act } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { AlgorithmProvider, useAlgorithm } from "@/context/AlgorithmContext"; -import { getAlgorithmByName } from "@/lib/algorithms"; - -// Mock the window.localStorage before importing context -const localStorageMock = (() => { - let store: Record = {}; - return { - getItem: jest.fn((key: string) => store[key] || null), - setItem: jest.fn((key: string, value: string) => { - store[key] = value; - }), - clear: jest.fn(() => { - store = {}; - }), - removeItem: jest.fn((key: string) => { - delete store[key]; - }), - }; -})(); - -// Replace the window.localStorage object with our mock -Object.defineProperty(window, "localStorage", { - value: localStorageMock, - writable: true, -}); - -// Mock the algorithms module -jest.mock("@/lib/algorithms", () => ({ - getAlgorithmByName: jest.fn().mockReturnValue(() => ({ - steps: [ - { array: [1, 2, 3], comparing: [], swapped: false, completed: [] }, - { array: [1, 2, 3], comparing: [0, 1], swapped: false, completed: [] }, - { array: [1, 2, 3], comparing: [], swapped: false, completed: [0] }, - ], - name: "Test Algorithm", - key: "testAlgorithm", - category: "sorting", - description: "Test description", - timeComplexity: "O(n)", - spaceComplexity: "O(1)", - reference: "test-reference", - pseudoCode: ["test pseudocode"], - })), -})); - -// Mock utility functions -jest.mock("@/lib/utils", () => { - const originalModule = jest.requireActual("@/lib/utils"); - - return { - ...originalModule, - saveState: jest.fn(), - loadState: jest.fn().mockReturnValue({ - data: [5, 3, 8], - speed: 3, - algorithm: "testAlgorithm", - }), - generateRandomArray: jest.fn().mockReturnValue([4, 2, 1]), - }; -}); - -// Test component that uses the algorithm context -const TestComponent = () => { - const { state, dispatch } = useAlgorithm(); - - return ( -
-
{state.currentStep}
-
{state.algorithm}
-
{state.speed}
-
- {state.isPlaying ? "playing" : "paused"} -
-
{state.data?.join(",") || "No data"}
-
- {state.visualizationData ? "Has Viz Data" : "No Viz Data"} -
- - - - - - - - - - - - -
- ); -}; - -describe("AlgorithmContext", () => { - // Reset timers and mocks between tests - beforeEach(() => { - jest.useFakeTimers(); - jest.clearAllMocks(); - }); - - afterEach(() => { - jest.runOnlyPendingTimers(); - jest.useRealTimers(); - }); - - it("should initialize with the correct values from localStorage", async () => { - render( - - - - ); - - // Wait for initial state to be loaded - await waitFor(() => { - expect(screen.getByTestId("algorithm")).toHaveTextContent( - "testAlgorithm" - ); - }); - - // Check all state values - expect(screen.getByTestId("speed")).toHaveTextContent("3"); - expect(screen.getByTestId("data")).toHaveTextContent("5,3,8"); - - // Verify visualization data is generated during initialization - await waitFor(() => { - expect(screen.getByTestId("viz-data")).toHaveTextContent("Has Viz Data"); - }); - }); - - it("should update step when next button is clicked", async () => { - const user = userEvent.setup({ delay: null }); - - render( - - - - ); - - // Wait for context to be initialized - await waitFor(() => { - expect(screen.getByTestId("viz-data")).toHaveTextContent("Has Viz Data"); - }); - - // Get initial step - expect(screen.getByTestId("current-step")).toHaveTextContent("0"); - - // Click the next button to increment the step - await user.click(screen.getByTestId("next-btn")); - - // Wait for the state update to be processed - await waitFor(() => { - expect(screen.getByTestId("current-step")).toHaveTextContent("1"); - }); - }); - - it("should toggle playing state", async () => { - const user = userEvent.setup({ delay: null }); - - render( - - - - ); - - // Wait for context to be initialized - await waitFor(() => { - expect(screen.getByTestId("viz-data")).toHaveTextContent("Has Viz Data"); - }); - - // Initially paused - expect(screen.getByTestId("is-playing")).toHaveTextContent("paused"); - - // Click play - await user.click(screen.getByTestId("play-btn")); - - // Wait for state update - await waitFor(() => { - expect(screen.getByTestId("is-playing")).toHaveTextContent("playing"); - }); - - // Click pause - await user.click(screen.getByTestId("pause-btn")); - - // Wait for state update - await waitFor(() => { - expect(screen.getByTestId("is-playing")).toHaveTextContent("paused"); - }); - }); - - it("should reset to step 0", async () => { - const user = userEvent.setup({ delay: null }); - - render( - - - - ); - - // Wait for context to be initialized - await waitFor(() => { - expect(screen.getByTestId("viz-data")).toHaveTextContent("Has Viz Data"); - }); - - // Move to step 1 - await user.click(screen.getByTestId("next-btn")); - - // Wait for the state update to be processed - await waitFor(() => { - expect(screen.getByTestId("current-step")).toHaveTextContent("1"); - }); - - // Reset to step 0 - await user.click(screen.getByTestId("reset-btn")); - - // Wait for the state update to be processed - await waitFor(() => { - expect(screen.getByTestId("current-step")).toHaveTextContent("0"); - }); - }); - - it("should automatically advance steps when playing", async () => { - const user = userEvent.setup({ delay: null }); - - render( - - - - ); - - // Wait for context to be initialized - await waitFor(() => { - expect(screen.getByTestId("viz-data")).toHaveTextContent("Has Viz Data"); - }); - - // Start playing - await user.click(screen.getByTestId("play-btn")); - - // Wait for state update - await waitFor(() => { - expect(screen.getByTestId("is-playing")).toHaveTextContent("playing"); - }); - - // Fast-forward time to trigger the interval - act(() => { - jest.advanceTimersByTime(1000); // Advance by 1 second - }); - - // Check that the step has advanced - await waitFor(() => { - expect(screen.getByTestId("current-step")).toHaveTextContent("1"); - }); - }); - - it("should generate new random data", async () => { - const user = userEvent.setup({ delay: null }); - - render( - - - - ); - - // Wait for context to be initialized - await waitFor(() => { - expect(screen.getByTestId("viz-data")).toHaveTextContent("Has Viz Data"); - expect(screen.getByTestId("data")).toHaveTextContent("5,3,8"); - }); - - // Reset mocks to track new calls - jest.clearAllMocks(); - - // Generate new data - await user.click(screen.getByTestId("new-data-btn")); - - // Wait for state to update - await waitFor(() => { - expect(screen.getByTestId("data")).toHaveTextContent("4,2,1"); - }); - }); -}); diff --git a/__tests__/lib/algorithms/bubbleSort.test.ts b/__tests__/lib/algorithms/bubbleSort.test.ts deleted file mode 100644 index 784e079..0000000 --- a/__tests__/lib/algorithms/bubbleSort.test.ts +++ /dev/null @@ -1,82 +0,0 @@ -// __tests__/lib/algorithms/bubbleSort.test.ts -import { bubbleSort } from "@/lib/algorithms/bubbleSort"; - -describe("Bubble Sort Algorithm", () => { - it("should return the correct visualization structure", () => { - const testArray = [5, 3, 8, 4, 2]; - const result = bubbleSort(testArray); - - // Check basic structure - expect(result).toHaveProperty("steps"); - expect(result).toHaveProperty("name", "Bubble Sort"); - expect(result).toHaveProperty("key", "bubbleSort"); - expect(result).toHaveProperty("category", "sorting"); - expect(result).toHaveProperty("timeComplexity", "O(n²)"); - expect(result).toHaveProperty("spaceComplexity", "O(1)"); - expect(result).toHaveProperty("pseudoCode"); - expect(Array.isArray(result.pseudoCode)).toBe(true); - }); - - it("should preserve the original array (not mutate it)", () => { - const testArray = [5, 3, 8, 4, 2]; - const originalArray = [...testArray]; - - bubbleSort(testArray); - - expect(testArray).toEqual(originalArray); - }); - - it("should correctly sort the array by the final step", () => { - const testArray = [5, 3, 8, 4, 2]; - const sortedArray = [...testArray].sort((a, b) => a - b); - - const result = bubbleSort(testArray); - const finalStepArray = result.steps[result.steps.length - 1].array; - - expect(finalStepArray).toEqual(sortedArray); - }); - - it("should handle arrays of length 1", () => { - const testArray = [1]; - const result = bubbleSort(testArray); - - expect(result.steps.length).toBeGreaterThan(0); - expect(result.steps[0].array).toEqual([1]); - expect(result.steps[result.steps.length - 1].array).toEqual([1]); - }); - - it("should handle empty arrays", () => { - const testArray: number[] = []; - const result = bubbleSort(testArray); - - expect(result.steps.length).toBeGreaterThan(0); - expect(result.steps[0].array).toEqual([]); - }); - - it("should handle already sorted arrays", () => { - const testArray = [1, 2, 3, 4, 5]; - const result = bubbleSort(testArray); - - // Check that the final state is the same as the initial sorted array - expect(result.steps[result.steps.length - 1].array).toEqual(testArray); - }); - - it("should mark elements as completed in the correct order", () => { - const testArray = [5, 3, 8, 4, 2]; - const result = bubbleSort(testArray); - - // In bubble sort, elements are completed from the end - // Check that the last step has all elements marked as completed - const finalStep = result.steps[result.steps.length - 1]; - expect(finalStep.completed.length).toBeGreaterThan(0); - - // The number of elements marked as completed should increase through the steps - let prevCompletedCount = -1; - for (const step of result.steps) { - if (step.completed.length > prevCompletedCount) { - prevCompletedCount = step.completed.length; - } - expect(step.completed.length).toBeGreaterThanOrEqual(prevCompletedCount); - } - }); -}); diff --git a/__tests__/lib/utils.test.ts b/__tests__/lib/utils.test.ts index e2dc826..a61a3e1 100644 --- a/__tests__/lib/utils.test.ts +++ b/__tests__/lib/utils.test.ts @@ -1,39 +1,6 @@ -import { - generateRandomArray, - getAlgorithmLabel, - getDifficulty, -} from "@/lib/utils"; - -// We need to mock localStorage before importing the module -const localStorageMock = (() => { - let store: Record = {}; - return { - getItem: jest.fn((key: string) => store[key] || null), - setItem: jest.fn((key: string, value: string) => { - store[key] = value; - }), - clear: jest.fn(() => { - store = {}; - }), - removeItem: jest.fn((key: string) => { - delete store[key]; - }), - }; -})(); - -// Replace the window.localStorage object with our mock -Object.defineProperty(window, "localStorage", { - value: localStorageMock, - writable: true, -}); +import { generateRandomArray } from "@/lib/utils"; describe("Utils Functions", () => { - // Reset mocks before each test - beforeEach(() => { - jest.clearAllMocks(); - localStorageMock.clear(); - }); - describe("generateRandomArray", () => { it("should generate a random array with the specified length", () => { const array = generateRandomArray(10, 50, 5); @@ -53,28 +20,4 @@ describe("Utils Functions", () => { expect(array1).not.toEqual(array2); }); }); - - describe("getAlgorithmLabel", () => { - it("should return the correct label for known algorithms", () => { - expect(getAlgorithmLabel("bubbleSort")).toBe("Bubble Sort"); - expect(getAlgorithmLabel("quickSort")).toBe("Quick Sort"); - expect(getAlgorithmLabel("mergeSort")).toBe("Merge Sort"); - }); - - it("should return the algorithm key for unknown algorithms", () => { - expect(getAlgorithmLabel("unknownAlgorithm")).toBe("unknownAlgorithm"); - }); - }); - - describe("getDifficulty", () => { - it("should return the correct difficulty for known algorithms", () => { - expect(getDifficulty("bubbleSort")).toBe("Easy"); - expect(getDifficulty("mergeSort")).toBe("Medium"); - expect(getDifficulty("heapSort")).toBe("Hard"); - }); - - it('should return "Unknown" for unknown algorithms', () => { - expect(getDifficulty("unknownAlgorithm")).toBe("Unknown"); - }); - }); }); diff --git a/app/[difficulty]/page.tsx b/app/difficulty/[difficulty]/page.tsx similarity index 85% rename from app/[difficulty]/page.tsx rename to app/difficulty/[difficulty]/page.tsx index 30cf684..b1403c1 100644 --- a/app/[difficulty]/page.tsx +++ b/app/difficulty/[difficulty]/page.tsx @@ -1,7 +1,7 @@ import { notFound } from "next/navigation"; import PageLayout from "@/components/layout/PageLayout"; import AlgorithmCard from "@/components/AlgorithmCard"; -import { availableAlgorithms } from "@/lib/algorithms"; +import { availableAlgorithms } from "@/lib/algorithms/metadata"; type DifficultyParams = { params: Promise<{ @@ -20,8 +20,8 @@ export default async function DifficultyPage(props: DifficultyParams) { } // Filter algorithms by the current difficulty - const filteredAlgorithms = availableAlgorithms.filter( - (algo) => algo.difficulty === difficulty + const filteredAlgorithms = Object.entries(availableAlgorithms).filter( + ([, algo]) => algo.difficulty === difficulty ); // Capitalize the first letter for display @@ -36,7 +36,7 @@ export default async function DifficultyPage(props: DifficultyParams) { {filteredAlgorithms.length > 0 ? (
{filteredAlgorithms.map((algorithm) => ( - + ))}
) : ( diff --git a/app/difficulties/page.tsx b/app/difficulty/page.tsx similarity index 77% rename from app/difficulties/page.tsx rename to app/difficulty/page.tsx index 3e8dda0..f5da07a 100644 --- a/app/difficulties/page.tsx +++ b/app/difficulty/page.tsx @@ -1,6 +1,6 @@ import Link from "next/link"; import PageLayout from "@/components/layout/PageLayout"; -import { availableAlgorithms } from "@/lib/algorithms"; +import { availableAlgorithms } from "@/lib/algorithms/metadata"; // Define type for card properties type CardProps = { @@ -12,20 +12,19 @@ type CardProps = { export default function DifficultiesPage() { // Get unique difficulties and count algorithms per difficulty - const difficultyData = availableAlgorithms.reduce((acc, algorithm) => { - const { difficulty } = algorithm; - if (!acc[difficulty]) { - acc[difficulty] = { - count: 0, - label: difficulty.charAt(0).toUpperCase() + difficulty.slice(1), - }; - } - acc[difficulty].count++; - return acc; - }, {} as Record); - - // Order by increasing difficulty - const orderedDifficulties = ["easy", "medium", "hard"]; + const difficultyData = Object.entries( + Object.entries(availableAlgorithms).reduce((acc, [, algorithm]) => { + const { difficulty } = algorithm; + if (!acc[difficulty]) { + acc[difficulty] = { + count: 0, + label: difficulty.charAt(0).toUpperCase() + difficulty.slice(1), + }; + } + acc[difficulty].count++; + return acc; + }, {} as Record) + ); // Define colors and descriptions for each difficulty const difficultyCardProps: Record = { @@ -55,14 +54,12 @@ export default function DifficultiesPage() { subtitle="Browse algorithms by their difficulty level from beginner to advanced." >
- {orderedDifficulties.map((difficulty) => { - const { count, label } = difficultyData[difficulty] || { - count: 0, - label: difficulty, - }; + {difficultyData.map((difficulty) => { + const label = difficulty[1].label.toLowerCase(); + const count = difficulty[1].count; // Get card props with fallback for unknown difficulties - const cardProps = difficultyCardProps[difficulty] || { + const cardProps = difficultyCardProps[label] || { color: "bg-gray-50 border-gray-200", textColor: "text-gray-800", icon: "?", @@ -71,12 +68,14 @@ export default function DifficultiesPage() { return (
-

{label}

+

+ {label} +

{cardProps.icon} diff --git a/app/page.tsx b/app/page.tsx index 009e5e3..fe4ddf4 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,57 +1,40 @@ import Link from "next/link"; import PageLayout from "@/components/layout/PageLayout"; import AlgorithmCard from "@/components/AlgorithmCard"; -import { availableAlgorithms } from "@/lib/algorithms"; +import { availableAlgorithms } from "@/lib/algorithms/metadata"; +import { AlgorithmInfo } from "@/lib/types"; export default function Home() { // Group algorithms by category - const algorithmsByCategory = availableAlgorithms.reduce((acc, algorithm) => { - const { category } = algorithm; - if (!acc[category]) { - acc[category] = []; - } - acc[category].push(algorithm); - return acc; - }, {} as Record); - - // Get unique categories - const categories = Object.keys(algorithmsByCategory); + const algorithmsByCategory = Object.entries( + Object.entries(availableAlgorithms).reduce((acc, [, algorithm]) => { + const { category } = algorithm; + if (!acc[category]) { + acc[category] = []; + } + acc[category].push(algorithm); + return acc; + }, {} as Record) + ); return ( -
-

Featured Algorithms

- -
- {availableAlgorithms - .filter( - (algo) => - algo.key === "bubbleSort" || - algo.key === "selectionSort" || - algo.key === "insertionSort" - ) - .map((algorithm) => ( - - ))} -
-
- - {categories.map((category) => ( -
+ {algorithmsByCategory.map((category) => ( +

- {category} Algorithms + {category[0]} Algorithms

- + View All
- {algorithmsByCategory[category].slice(0, 3).map((algorithm) => ( + {category[1].slice(0, 3).map((algorithm) => ( ))}
diff --git a/app/searching/[algorithm]/layout.tsx b/app/searching/[algorithm]/layout.tsx index d7ed62c..a2f884d 100644 --- a/app/searching/[algorithm]/layout.tsx +++ b/app/searching/[algorithm]/layout.tsx @@ -1,20 +1,18 @@ import { AlgorithmProvider } from "@/context/AlgorithmContext"; import { Metadata } from "next"; -import { availableAlgorithms } from "@/lib/algorithms"; +import { availableAlgorithms } from "@/lib/algorithms/metadata"; import { constructMetadata } from "@/lib/seo/metadata"; -import { getAlgorithmLabel } from "@/lib/utils"; type Props = { - params: { algorithm: string }; + params: Promise<{ algorithm: string }>; }; -export function generateMetadata({ params }: Props): Metadata { +export async function generateMetadata(props: Props): Promise { + const params = await props.params; const { algorithm } = params; // Find the algorithm in our available list - const algorithmInfo = availableAlgorithms.find( - (algo) => algo.key === algorithm - ); + const algorithmInfo = availableAlgorithms[algorithm]; if (!algorithmInfo) { return constructMetadata({ @@ -24,19 +22,18 @@ export function generateMetadata({ params }: Props): Metadata { }); } - const title = getAlgorithmLabel(algorithm); - const { description, difficulty, category } = algorithmInfo; + const { name, description, difficulty, category } = algorithmInfo; return constructMetadata({ - title, + title: name, description, path: `/searching/${algorithm}`, keywords: [ algorithm, - title.toLowerCase(), - `${title.toLowerCase()} visualization`, - `${title.toLowerCase()} explanation`, - `${title.toLowerCase()} algorithm`, + name.toLowerCase(), + `${name.toLowerCase()} visualization`, + `${name.toLowerCase()} explanation`, + `${name.toLowerCase()} algorithm`, `${difficulty} algorithm`, `${category} algorithm`, ], diff --git a/app/searching/[algorithm]/page.tsx b/app/searching/[algorithm]/page.tsx index ddc51d4..c50ca28 100644 --- a/app/searching/[algorithm]/page.tsx +++ b/app/searching/[algorithm]/page.tsx @@ -1,22 +1,20 @@ "use client"; import { useEffect } from "react"; -import { useParams } from "next/navigation"; +import { useParams, notFound } from "next/navigation"; import PageLayout from "@/components/layout/PageLayout"; import AlgorithmVisualizer from "@/components/visualizer/AlgorithmVisualizer"; import { useAlgorithm } from "@/context/AlgorithmContext"; -import { getAlgorithmByName, availableAlgorithms } from "@/lib/algorithms"; -import { getAlgorithmLabel, getRandomValueFromArray } from "@/lib/utils"; +import { getAlgorithmByName } from "@/lib/algorithms"; +import { getRandomValueFromArray } from "@/lib/utils"; +import { availableAlgorithms } from "@/lib/algorithms/metadata"; export default function SearchingAlgorithmPage() { const params = useParams(); const algorithmKey = params.algorithm as string; const { dispatch, state } = useAlgorithm(); - // Find algorithm info - const algorithmInfo = availableAlgorithms.find( - (algo) => algo.key === algorithmKey - ); + const algorithmInfo = availableAlgorithms[algorithmKey]; // Set the current algorithm and generate visualization useEffect(() => { @@ -25,19 +23,20 @@ export default function SearchingAlgorithmPage() { // Generate a new target value if none exists or when algorithm changes const target = - state.data.length > 0 - ? state.target || getRandomValueFromArray(state.data) - : 42; + state.target || + (state.data.length > 0 ? getRandomValueFromArray(state.data) : 42); dispatch({ type: "SET_TARGET", payload: target }); + // For binary search, ensure the array is sorted and the target exists + const data = [...state.data]; + // Generate visualization if not already generated OR if algorithm changed if (!state.visualizationData || algorithmKey !== state.algorithm) { const algorithmFunction = getAlgorithmByName(algorithmKey); if (algorithmFunction) { try { - console.log("Generating visualization with target:", target); - const viz = algorithmFunction(state.data, target); + const viz = algorithmFunction(data, target); dispatch({ type: "GENERATE_VISUALIZATION", payload: viz }); } catch (error) { console.error("Error generating visualization:", error); @@ -55,23 +54,13 @@ export default function SearchingAlgorithmPage() { ]); if (!algorithmInfo) { - return ( - -
-

Algorithm Not Found

-

- The algorithm you are looking for does not exist or is not - available. -

-
-
- ); + return notFound(); } return ( diff --git a/app/searching/page.tsx b/app/searching/page.tsx index 6f59ab1..52fba6b 100644 --- a/app/searching/page.tsx +++ b/app/searching/page.tsx @@ -1,11 +1,11 @@ import PageLayout from "@/components/layout/PageLayout"; import AlgorithmCard from "@/components/AlgorithmCard"; -import { availableAlgorithms } from "@/lib/algorithms"; +import { availableAlgorithms } from "@/lib/algorithms/metadata"; export default function SearchingAlgorithms() { // Filter algorithms to only show searching ones - const searchingAlgorithms = availableAlgorithms.filter( - (algo) => algo.category === "searching" + const searchingAlgorithms = Object.entries(availableAlgorithms).filter( + ([, algo]) => algo.category === "searching" ); return ( @@ -15,7 +15,7 @@ export default function SearchingAlgorithms() { >
{searchingAlgorithms.map((algorithm) => ( - + ))}
diff --git a/app/sitemap.ts b/app/sitemap.ts index 38f0f15..e23369e 100644 --- a/app/sitemap.ts +++ b/app/sitemap.ts @@ -1,5 +1,5 @@ import { MetadataRoute } from "next"; -import { availableAlgorithms } from "@/lib/algorithms"; +import { availableAlgorithms } from "@/lib/algorithms/metadata"; import { APP_URL } from "@/constants/URL"; export default function sitemap(): MetadataRoute.Sitemap { @@ -20,7 +20,7 @@ export default function sitemap(): MetadataRoute.Sitemap { priority: 0.8, }, { - url: `${baseUrl}/difficulties`, + url: `${baseUrl}/difficulty`, lastModified: new Date(), changeFrequency: "monthly", priority: 0.8, @@ -44,19 +44,19 @@ export default function sitemap(): MetadataRoute.Sitemap { priority: 0.9, }, { - url: `${baseUrl}/easy`, + url: `${baseUrl}/difficulty/easy`, lastModified: new Date(), changeFrequency: "monthly", priority: 0.8, }, { - url: `${baseUrl}/medium`, + url: `${baseUrl}/difficulty/medium`, lastModified: new Date(), changeFrequency: "monthly", priority: 0.8, }, { - url: `${baseUrl}/hard`, + url: `${baseUrl}/difficulty/hard`, lastModified: new Date(), changeFrequency: "monthly", priority: 0.8, @@ -64,16 +64,16 @@ export default function sitemap(): MetadataRoute.Sitemap { ]; // Generate algorithm-specific pages - const algorithmPages: MetadataRoute.Sitemap = availableAlgorithms.map( - (algorithm) => { - return { - url: `${baseUrl}/${algorithm.category}/${algorithm.key}`, - lastModified: new Date(), - changeFrequency: "monthly", - priority: 0.7, - }; - } - ); + const algorithmPages: MetadataRoute.Sitemap = Object.entries( + availableAlgorithms + ).map((algorithm) => { + return { + url: `${baseUrl}/${algorithm[1].category}/${algorithm[1].key}`, + lastModified: new Date(), + changeFrequency: "monthly", + priority: 0.7, + }; + }); return [...staticPages, ...algorithmPages]; } diff --git a/app/sorting/[algorithm]/layout.tsx b/app/sorting/[algorithm]/layout.tsx index 1050442..b43b79c 100644 --- a/app/sorting/[algorithm]/layout.tsx +++ b/app/sorting/[algorithm]/layout.tsx @@ -1,21 +1,18 @@ import React from "react"; import { AlgorithmProvider } from "@/context/AlgorithmContext"; import { Metadata } from "next"; -import { availableAlgorithms } from "@/lib/algorithms"; +import { availableAlgorithms } from "@/lib/algorithms/metadata"; import { constructMetadata } from "@/lib/seo/metadata"; -import { getAlgorithmLabel } from "@/lib/utils"; type Props = { - params: { algorithm: string }; + params: Promise<{ algorithm: string }>; }; -export function generateMetadata({ params }: Props): Metadata { +export async function generateMetadata(props: Props): Promise { + const params = await props.params; const { algorithm } = params; - // Find the algorithm in our available list - const algorithmInfo = availableAlgorithms.find( - (algo) => algo.key === algorithm - ); + const algorithmInfo = availableAlgorithms[algorithm]; if (!algorithmInfo) { return constructMetadata({ @@ -25,19 +22,18 @@ export function generateMetadata({ params }: Props): Metadata { }); } - const title = getAlgorithmLabel(algorithm); - const { description, difficulty, category } = algorithmInfo; + const { name, description, difficulty, category } = algorithmInfo; return constructMetadata({ - title, + title: name, description, path: `/sorting/${algorithm}`, keywords: [ algorithm, - title.toLowerCase(), - `${title.toLowerCase()} visualization`, - `${title.toLowerCase()} explanation`, - `${title.toLowerCase()} algorithm`, + name.toLowerCase(), + `${name.toLowerCase()} visualization`, + `${name.toLowerCase()} explanation`, + `${name.toLowerCase()} algorithm`, `${difficulty} algorithm`, `${category} algorithm`, ], diff --git a/app/sorting/[algorithm]/opengraph-image.tsx b/app/sorting/[algorithm]/opengraph-image.tsx index 636a996..83e71db 100644 --- a/app/sorting/[algorithm]/opengraph-image.tsx +++ b/app/sorting/[algorithm]/opengraph-image.tsx @@ -1,6 +1,5 @@ import { ImageResponse } from "next/og"; -import { getAlgorithmLabel } from "@/lib/utils"; -import { availableAlgorithms } from "@/lib/algorithms"; +import { availableAlgorithms } from "@/lib/algorithms/metadata"; import { APP_URL } from "@/constants/URL"; export const runtime = "edge"; @@ -27,12 +26,8 @@ export default async function Image({ }) { const { algorithm } = params; - // Find the algorithm info - const algorithmInfo = availableAlgorithms.find( - (algo) => algo.key === algorithm - ); + const algorithmInfo = availableAlgorithms[algorithm]; - const title = getAlgorithmLabel(algorithm); const description = algorithmInfo?.description || "Interactive algorithm visualization"; const difficulty = algorithmInfo?.difficulty || "unknown"; @@ -110,7 +105,7 @@ export default async function Image({ marginBottom: 20, }} > - {title} + {algorithmInfo.name}
algo.key === algorithmKey - ); + const algorithmInfo = availableAlgorithms[algorithmKey]; // Set the current algorithm and generate visualization useEffect(() => { @@ -56,8 +53,8 @@ export default function AlgorithmPage() { return ( diff --git a/app/sorting/page.tsx b/app/sorting/page.tsx index 0af3f04..d0a233e 100644 --- a/app/sorting/page.tsx +++ b/app/sorting/page.tsx @@ -1,11 +1,11 @@ import PageLayout from "@/components/layout/PageLayout"; import AlgorithmCard from "@/components/AlgorithmCard"; -import { availableAlgorithms } from "@/lib/algorithms"; +import { availableAlgorithms } from "@/lib/algorithms/metadata"; export default function SortingAlgorithms() { // Filter algorithms to only show sorting ones - const sortingAlgorithms = availableAlgorithms.filter( - (algo) => algo.category === "sorting" + const sortingAlgorithms = Object.entries(availableAlgorithms).filter( + ([, algo]) => algo.category === "sorting" ); return ( @@ -15,7 +15,7 @@ export default function SortingAlgorithms() { >
{sortingAlgorithms.map((algorithm) => ( - + ))}
diff --git a/components/AlgorithmCard.tsx b/components/AlgorithmCard.tsx index 09bc4b0..0f44ec0 100644 --- a/components/AlgorithmCard.tsx +++ b/components/AlgorithmCard.tsx @@ -15,7 +15,7 @@ export default function AlgorithmCard({ algorithm }: AlgorithmCardProps) {

{name}

{difficulty} diff --git a/components/layout/Navbar.tsx b/components/layout/Navbar.tsx index 64bd004..2c8a55c 100644 --- a/components/layout/Navbar.tsx +++ b/components/layout/Navbar.tsx @@ -2,9 +2,109 @@ import Link from "next/link"; import { usePathname } from "next/navigation"; +import { useEffect, useState } from "react"; + +const GitHubIcon = () => ( + +); + +const MenuIcon = () => ( + +); + +const CloseIcon = () => ( + +); + +const navLinks = [ + { href: "/", label: "Home" }, + { href: "/sorting", label: "Sorting" }, + { href: "/searching", label: "Searching" }, + { href: "/graph", label: "Graph" }, + { href: "/difficulty", label: "Difficulty" }, +]; export default function Navbar() { const pathname = usePathname(); + const [isMenuOpen, setIsMenuOpen] = useState(false); + + useEffect(() => { + if (!isMenuOpen) return; + + const handleOutsideClick = (event: MouseEvent) => { + const target = event.target as HTMLElement; + if ( + !target.closest("#mobile-menu-button") && + !target.closest("#mobile-menu") + ) { + setIsMenuOpen(false); + } + }; + + const handleEsc = (event: KeyboardEvent) => { + if (event.key === "Escape") { + setIsMenuOpen(false); + } + }; + + // Prevent scrolling when mobile menu is open + document.body.style.overflow = "hidden"; + + document.addEventListener("mousedown", handleOutsideClick); + document.addEventListener("keydown", handleEsc); + + return () => { + document.body.style.overflow = ""; + document.removeEventListener("mousedown", handleOutsideClick); + document.removeEventListener("keydown", handleEsc); + }; + }, [isMenuOpen]); + + const isActive = (path: string) => { + if (path === "/") { + return pathname === path; + } + return pathname === path || pathname.startsWith(`${path}/`); + }; return (
@@ -29,49 +129,71 @@ export default function Navbar() { -
-
+ +
+
+ {navLinks.map((link) => ( + + {link.label} + + ))} +
+
); } diff --git a/components/visualizer/AlgorithmInfo.tsx b/components/visualizer/AlgorithmInfo.tsx index f743ac9..4ee589d 100644 --- a/components/visualizer/AlgorithmInfo.tsx +++ b/components/visualizer/AlgorithmInfo.tsx @@ -46,6 +46,20 @@ export default function AlgorithmInfo({ algorithm }: AlgorithmInfoProps) {
+
+

+ Difficulty +

+
+ + {algorithm.difficulty} + +
+
+

Reference diff --git a/components/visualizer/AlgorithmPseudocode.tsx b/components/visualizer/AlgorithmPseudocode.tsx index 5b6d5d8..6efc753 100644 --- a/components/visualizer/AlgorithmPseudocode.tsx +++ b/components/visualizer/AlgorithmPseudocode.tsx @@ -11,10 +11,7 @@ export default function AlgorithmPseudocode({

Pseudocode

-
+
           {code.map((line, index) => {
             // Calculate indentation to display nested blocks properly
diff --git a/components/visualizer/AlgorithmVisualizer.tsx b/components/visualizer/AlgorithmVisualizer.tsx
index 95fcb15..d48f896 100644
--- a/components/visualizer/AlgorithmVisualizer.tsx
+++ b/components/visualizer/AlgorithmVisualizer.tsx
@@ -12,7 +12,7 @@ import { SearchStep, SortingStep } from "@/lib/types";
 
 export default function AlgorithmVisualizer() {
   const { state, dispatch } = useAlgorithm();
-  const { currentStep, algorithm, data, visualizationData } = state;
+  const { currentStep, algorithm, data, visualizationData, target } = state;
 
   // Generate a new random array
   const handleGenerateNewArray = () => {
@@ -25,8 +25,14 @@ export default function AlgorithmVisualizer() {
     const algorithmFunction = getAlgorithmByName(algorithm);
     if (algorithmFunction) {
       try {
-        // For search algorithms, the target will be set in the reducer
-        const viz = algorithmFunction(data, state.target);
+        const newData = [...state.data];
+
+        // Special handling for binary search - ensure sorted array and target exists
+        if (algorithm === "binarySearch") {
+          newData.sort((a, b) => a - b);
+        }
+
+        const viz = algorithmFunction(newData, target);
         dispatch({ type: "GENERATE_VISUALIZATION", payload: viz });
       } catch (error) {
         console.error("Error generating visualization:", error);
diff --git a/components/visualizer/SearchVisualization.tsx b/components/visualizer/SearchVisualization.tsx
index 47bb8c0..5fe3b2f 100644
--- a/components/visualizer/SearchVisualization.tsx
+++ b/components/visualizer/SearchVisualization.tsx
@@ -32,17 +32,14 @@ export default function SearchVisualization({
   };
 
   const updateTarget = (newTarget: number) => {
-    console.log(newTarget);
     // Update the target value
     dispatch({ type: "SET_TARGET", payload: newTarget });
 
     // Regenerate visualization with the new target
     const algorithmFunction = getAlgorithmByName(state.algorithm);
-    console.log(algorithmFunction);
     if (algorithmFunction) {
       try {
         const viz = algorithmFunction(state.data, newTarget);
-        console.log(viz);
         dispatch({ type: "GENERATE_VISUALIZATION", payload: viz });
       } catch (error) {
         console.error("Error generating visualization:", error);
@@ -114,10 +111,15 @@ export default function SearchVisualization({
                 Index: {index}
               
{index === current && ( -
+
Current
)} + {value === target && ( +
+ Target +
+ )}
); })} diff --git a/lib/algorithms/index.ts b/lib/algorithms/index.ts index f64b5d0..c65bb2f 100644 --- a/lib/algorithms/index.ts +++ b/lib/algorithms/index.ts @@ -1,14 +1,12 @@ -import { AlgorithmVisualization, AlgorithmInfo } from "../types"; -import { bubbleSort } from "./bubbleSort"; -import { selectionSort } from "./selectionSort"; -import { insertionSort } from "./insertionSort"; -import { mergeSort } from "./mergeSort"; -import { quickSort } from "./quickSort"; -import { heapSort } from "./heapSort"; -import { linearSearch } from "./linearSearch"; - -// Default target value for search algorithms -const DEFAULT_TARGET = 42; +import { AlgorithmVisualization } from "../types"; +import { bubbleSort } from "./sorting/bubbleSort"; +import { selectionSort } from "./sorting/selectionSort"; +import { insertionSort } from "./sorting/insertionSort"; +import { mergeSort } from "./sorting/mergeSort"; +import { quickSort } from "./sorting/quickSort"; +import { heapSort } from "./sorting/heapSort"; +import { linearSearch } from "./searching/linearSearch"; +import { binarySearch } from "./searching/binarySearch"; // Map algorithm names to their implementation functions const algorithms: Record< @@ -21,8 +19,8 @@ const algorithms: Record< mergeSort, quickSort, heapSort, - linearSearch: (array: number[]) => linearSearch(array, DEFAULT_TARGET), - // Add more algorithms as they are implemented + linearSearch, + binarySearch, }; // Get algorithm function by name @@ -32,66 +30,6 @@ export function getAlgorithmByName( return algorithms[name] || null; } -// List available algorithms with metadata -export const availableAlgorithms: AlgorithmInfo[] = [ - { - name: "Bubble Sort", - key: "bubbleSort", - category: "sorting", - description: - "A simple sorting algorithm that repeatedly steps through the list, compares adjacent elements and swaps them if they are in the wrong order.", - difficulty: "easy", - }, - { - name: "Selection Sort", - key: "selectionSort", - category: "sorting", - description: - "The selection sort algorithm sorts an array by repeatedly finding the minimum element from the unsorted part and putting it at the beginning.", - difficulty: "easy", - }, - { - name: "Insertion Sort", - key: "insertionSort", - category: "sorting", - description: - "Insertion sort iterates through an array and at each iteration it removes one element, finds the location where it belongs and inserts it there.", - difficulty: "easy", - }, - { - name: "Merge Sort", - key: "mergeSort", - category: "sorting", - description: - "Merge sort is a divide and conquer algorithm that divides the input array into two halves, recursively sorts them, and then merges the sorted halves.", - difficulty: "medium", - }, - { - name: "Quick Sort", - key: "quickSort", - category: "sorting", - description: - "Quick sort is a divide and conquer algorithm that picks an element as a pivot and partitions the array around the pivot.", - difficulty: "medium", - }, - { - name: "Heap Sort", - key: "heapSort", - category: "sorting", - description: - "Heap sort is a comparison-based sorting algorithm that uses a binary heap data structure to build a max-heap and then extract elements in order.", - difficulty: "hard", - }, - { - name: "Linear Search", - key: "linearSearch", - category: "searching", - description: - "Linear search sequentially checks each element of the list until it finds an element that matches the target value.", - difficulty: "easy", - }, -]; - // Export all algorithms export { bubbleSort, @@ -101,4 +39,5 @@ export { quickSort, heapSort, linearSearch, + binarySearch, }; diff --git a/lib/algorithms/metadata.ts b/lib/algorithms/metadata.ts new file mode 100644 index 0000000..15e1c26 --- /dev/null +++ b/lib/algorithms/metadata.ts @@ -0,0 +1,76 @@ +import { AlgorithmInfo } from "../types"; + +export const availableAlgorithms: Record = { + bubbleSort: { + name: "Bubble Sort", + key: "bubbleSort", + category: "sorting", + subtitle: "Simple but inefficient comparison sort", + description: + "A simple sorting algorithm that repeatedly steps through the list, compares adjacent elements and swaps them if they are in the wrong order. With a time complexity of O(n²), it's inefficient for large datasets but is easy to implement and understand. The algorithm gets its name because smaller elements 'bubble' to the top of the list with each iteration.", + difficulty: "easy", + }, + selectionSort: { + name: "Selection Sort", + key: "selectionSort", + category: "sorting", + subtitle: "In-place comparison sort with O(n²) complexity", + description: + "The selection sort algorithm sorts an array by repeatedly finding the minimum element from the unsorted part and putting it at the beginning. Unlike bubble sort, it makes only O(n) swaps, making it useful when write/swap operations are expensive. However, its O(n²) time complexity makes it inefficient for large datasets. Selection sort is not stable, meaning equal elements might not maintain their relative positions.", + difficulty: "easy", + }, + insertionSort: { + name: "Insertion Sort", + key: "insertionSort", + category: "sorting", + subtitle: "Efficient for small data sets and nearly sorted arrays", + description: + "Insertion sort iterates through an array and at each iteration it removes one element, finds the location where it belongs and inserts it there. While it has an average time complexity of O(n²), it performs exceptionally well on small or nearly-sorted arrays with best-case O(n) performance. Insertion sort is an adaptive, stable, in-place algorithm that works similarly to how people sort playing cards in their hands.", + difficulty: "easy", + }, + mergeSort: { + name: "Merge Sort", + key: "mergeSort", + category: "sorting", + subtitle: "Stable, divide-and-conquer algorithm with O(n log n) complexity", + description: + "Merge sort is a divide and conquer algorithm that divides the input array into two halves, recursively sorts them, and then merges the sorted halves. With a consistent O(n log n) time complexity regardless of input data, it outperforms simpler algorithms on large datasets. While requiring O(n) auxiliary space for the merging process, it guarantees stability and is particularly efficient for linked lists. Merge sort is often used in external sorting when data doesn't fit in memory.", + difficulty: "medium", + }, + quickSort: { + name: "Quick Sort", + key: "quickSort", + category: "sorting", + subtitle: "Fast, in-place sorting with average O(n log n) performance", + description: + "Quick sort is a divide and conquer algorithm that picks an element as a pivot and partitions the array around the pivot. With an average time complexity of O(n log n) and minimal space requirements, it's typically faster in practice than other O(n log n) algorithms like merge sort. However, it has a worst-case time complexity of O(n²) with poor pivot selection and is not stable. Quick sort is widely used and serves as the foundation for many programming language sorting implementations.", + difficulty: "medium", + }, + heapSort: { + name: "Heap Sort", + key: "heapSort", + category: "sorting", + subtitle: "Comparison-based sort using binary heap structure", + description: + "Heap sort is a comparison-based sorting algorithm that uses a binary heap data structure to build a max-heap and then repeatedly extracts the maximum element. With a guaranteed O(n log n) time complexity regardless of input data and O(1) auxiliary space, it combines many advantages of insertion sort and merge sort. While not stable and slightly slower than quick sort in practice, heap sort provides reliable performance without the risk of worst-case scenarios, making it valuable for systems requiring consistent performance.", + difficulty: "hard", + }, + linearSearch: { + name: "Linear Search", + key: "linearSearch", + category: "searching", + subtitle: "Simple O(n) search through unsorted collections", + description: + "Linear search sequentially checks each element of the list until it finds an element that matches the target value. With a time complexity of O(n), it's the simplest searching algorithm but becomes inefficient for large datasets. One advantage is that it works on unsorted arrays and doesn't require any preprocessing. Linear search is practical for small arrays or when the target is likely to be found early in the sequence. It's also useful when searching rarely happens or when elements are frequently added and removed.", + difficulty: "easy", + }, + binarySearch: { + name: "Binary Search", + key: "binarySearch", + category: "searching", + subtitle: "Efficient O(log n) search for sorted collections", + description: + "Binary search finds the position of a target value within a sorted array by repeatedly dividing the search interval in half. With a logarithmic time complexity of O(log n), it's dramatically more efficient than linear search for large datasets. Binary search requires the array to be sorted beforehand, making it ideal for situations where searching occurs frequently on relatively static data. This algorithm is the foundation for many efficient data structures like binary search trees and is widely used in database systems, dictionaries, and numerous programming applications.", + difficulty: "medium", + }, +}; diff --git a/lib/algorithms/searching/binarySearch.ts b/lib/algorithms/searching/binarySearch.ts new file mode 100644 index 0000000..0f74072 --- /dev/null +++ b/lib/algorithms/searching/binarySearch.ts @@ -0,0 +1,95 @@ +import { AlgorithmVisualization, SearchStep } from "../../types"; +import { createVisualization } from "../utils"; + +export function binarySearch( + array: number[], + target: number = 42 +): AlgorithmVisualization { + const steps: SearchStep[] = []; + // Make a copy of the array and sort it (binary search requires a sorted array) + const arr = [...array].sort((a, b) => a - b); + const visited: number[] = []; + + // Initial state + steps.push({ + array: [...arr], + current: -1, + target: target, + found: false, + visited: [], + }); + + let left = 0; + let right = arr.length - 1; + let found = false; + + while (left <= right) { + const mid = Math.floor((left + right) / 2); + + // Add current index to visited + visited.push(mid); + + // Record the step of examining the middle value + steps.push({ + array: [...arr], + current: mid, + target: target, + found: false, + visited: [...visited], + }); + + // Check if middle element is the target + if (arr[mid] === target) { + // Found the target + steps.push({ + array: [...arr], + current: mid, + target: target, + found: true, + visited: [...visited], + }); + found = true; + break; + } else if (arr[mid] < target) { + // Target is in the right half + left = mid + 1; + } else { + // Target is in the left half + right = mid - 1; + } + } + + // If target was not found + if (!found) { + steps.push({ + array: [...arr], + current: -1, + target: target, + found: false, + visited: [...visited], + }); + } + + return createVisualization("binarySearch", steps, { + timeComplexity: "O(log n)", + spaceComplexity: "O(1)", + reference: "https://en.wikipedia.org/wiki/Binary_search_algorithm", + pseudoCode: [ + "procedure binarySearch(arr: sorted array, target: element)", + " left := 0", + " right := length(arr) - 1", + " while left <= right do", + " mid := floor((left + right) / 2)", + " if arr[mid] = target then", + " return mid // Target found at index mid", + " else if arr[mid] < target then", + " left := mid + 1 // Target is in the right half", + " else", + " right := mid - 1 // Target is in the left half", + " end if", + " end while", + " return -1 // Target not found", + "end procedure", + ], + }); +} diff --git a/lib/algorithms/linearSearch.ts b/lib/algorithms/searching/linearSearch.ts similarity index 68% rename from lib/algorithms/linearSearch.ts rename to lib/algorithms/searching/linearSearch.ts index acf022e..124c41f 100644 --- a/lib/algorithms/linearSearch.ts +++ b/lib/algorithms/searching/linearSearch.ts @@ -1,5 +1,5 @@ -// lib/algorithms/linearSearch.ts -import { AlgorithmVisualization, SearchStep } from "../types"; +import { AlgorithmVisualization, SearchStep } from "../../types"; +import { createVisualization } from "../utils"; export function linearSearch( array: number[], @@ -9,14 +9,11 @@ export function linearSearch( const arr = [...array]; const visited: number[] = []; - // Set a default target if one is not provided or not found in the array - const targetValue = target || 42; - // Initial state steps.push({ array: [...arr], current: -1, - target: targetValue, + target: target, found: false, visited: [], }); @@ -30,18 +27,18 @@ export function linearSearch( steps.push({ array: [...arr], current: i, - target: targetValue, + target: target, found: false, visited: [...visited], }); // Check if current element equals target - if (arr[i] === targetValue) { + if (arr[i] === target) { // Found the target, record final step steps.push({ array: [...arr], current: i, - target: targetValue, + target: target, found: true, visited: [...visited], }); @@ -54,19 +51,13 @@ export function linearSearch( steps.push({ array: [...arr], current: -1, - target: targetValue, + target: target, found: false, visited: [...visited], }); } - return { - steps, - name: "Linear Search", - key: "linearSearch", - category: "searching", - description: - "Linear search is a simple search algorithm that finds the position of a target value within a list by checking each element sequentially until a match is found or the end of the list is reached.", + return createVisualization("linearSearch", steps, { timeComplexity: "O(n)", spaceComplexity: "O(1)", reference: "https://en.wikipedia.org/wiki/Linear_search", @@ -80,5 +71,5 @@ export function linearSearch( " return -1 // Target not found", "end procedure", ], - }; + }); } diff --git a/lib/algorithms/bubbleSort.ts b/lib/algorithms/sorting/bubbleSort.ts similarity index 84% rename from lib/algorithms/bubbleSort.ts rename to lib/algorithms/sorting/bubbleSort.ts index 83af5be..1eadc5e 100644 --- a/lib/algorithms/bubbleSort.ts +++ b/lib/algorithms/sorting/bubbleSort.ts @@ -1,4 +1,5 @@ -import { AlgorithmVisualization, SortingStep } from "../types"; +import { AlgorithmVisualization, SortingStep } from "../../types"; +import { createVisualization } from "../utils"; export function bubbleSort(array: number[]): AlgorithmVisualization { const steps: SortingStep[] = []; @@ -55,13 +56,7 @@ export function bubbleSort(array: number[]): AlgorithmVisualization { if (!swapped) break; } - return { - steps, - name: "Bubble Sort", - key: "bubbleSort", - category: "sorting", - description: - "A simple sorting algorithm that repeatedly steps through the list, compares adjacent elements and swaps them if they are in the wrong order.", + return createVisualization("bubbleSort", steps, { timeComplexity: "O(n²)", spaceComplexity: "O(1)", reference: "https://en.wikipedia.org/wiki/Bubble_sort", @@ -80,5 +75,5 @@ export function bubbleSort(array: number[]): AlgorithmVisualization { " until not swapped", "end procedure", ], - }; + }); } diff --git a/lib/algorithms/heapSort.ts b/lib/algorithms/sorting/heapSort.ts similarity index 91% rename from lib/algorithms/heapSort.ts rename to lib/algorithms/sorting/heapSort.ts index c1c973c..146e637 100644 --- a/lib/algorithms/heapSort.ts +++ b/lib/algorithms/sorting/heapSort.ts @@ -1,4 +1,5 @@ -import { AlgorithmVisualization, SortingStep } from "../types"; +import { AlgorithmVisualization, SortingStep } from "../../types"; +import { createVisualization } from "../utils"; export function heapSort(array: number[]): AlgorithmVisualization { const steps: SortingStep[] = []; @@ -56,13 +57,7 @@ export function heapSort(array: number[]): AlgorithmVisualization { completed: [...completed], }); - return { - steps, - name: "Heap Sort", - key: "heapSort", - category: "sorting", - description: - "A comparison-based sorting algorithm that uses a binary heap data structure. It divides the input into a sorted and an unsorted region, and iteratively shrinks the unsorted region by extracting the largest element and moving it to the sorted region.", + return createVisualization("heapSort", steps, { timeComplexity: "O(n log n)", spaceComplexity: "O(1)", reference: "https://en.wikipedia.org/wiki/Heapsort", @@ -99,7 +94,7 @@ export function heapSort(array: number[]): AlgorithmVisualization { " end if", "end procedure", ], - }; + }); } function buildMaxHeap( diff --git a/lib/algorithms/insertionSort.ts b/lib/algorithms/sorting/insertionSort.ts similarity index 85% rename from lib/algorithms/insertionSort.ts rename to lib/algorithms/sorting/insertionSort.ts index 3852edb..5cfcb50 100644 --- a/lib/algorithms/insertionSort.ts +++ b/lib/algorithms/sorting/insertionSort.ts @@ -1,4 +1,5 @@ -import { AlgorithmVisualization, SortingStep } from "../types"; +import { AlgorithmVisualization, SortingStep } from "../../types"; +import { createVisualization } from "../utils"; export function insertionSort(array: number[]): AlgorithmVisualization { const steps: SortingStep[] = []; @@ -64,13 +65,7 @@ export function insertionSort(array: number[]): AlgorithmVisualization { }); } - return { - steps, - name: "Insertion Sort", - key: "insertionSort", - category: "sorting", - description: - "Insertion sort iterates through an array and at each iteration it removes one element, finds the location where it belongs and inserts it there.", + return createVisualization("insertionSort", steps, { timeComplexity: "O(n²)", spaceComplexity: "O(1)", reference: "https://en.wikipedia.org/wiki/Insertion_sort", @@ -88,5 +83,5 @@ export function insertionSort(array: number[]): AlgorithmVisualization { " end for", "end procedure", ], - }; + }); } diff --git a/lib/algorithms/mergeSort.ts b/lib/algorithms/sorting/mergeSort.ts similarity index 93% rename from lib/algorithms/mergeSort.ts rename to lib/algorithms/sorting/mergeSort.ts index e4986cf..3d73b2d 100644 --- a/lib/algorithms/mergeSort.ts +++ b/lib/algorithms/sorting/mergeSort.ts @@ -1,4 +1,5 @@ -import { AlgorithmVisualization, SortingStep } from "../types"; +import { AlgorithmVisualization, SortingStep } from "../../types"; +import { createVisualization } from "../utils"; export function mergeSort(array: number[]): AlgorithmVisualization { const steps: SortingStep[] = []; @@ -29,13 +30,7 @@ export function mergeSort(array: number[]): AlgorithmVisualization { completed: finalCompleted, }); - return { - steps, - name: "Merge Sort", - key: "mergeSort", - category: "sorting", - description: - "A divide and conquer algorithm that divides the input array in two halves, recursively sorts them, and then merges the sorted halves.", + return createVisualization("mergeSort", steps, { timeComplexity: "O(n log n)", spaceComplexity: "O(n)", reference: "https://en.wikipedia.org/wiki/Merge_sort", @@ -75,7 +70,7 @@ export function mergeSort(array: number[]): AlgorithmVisualization { " end for", "end procedure", ], - }; + }); } function mergeSortHelper( diff --git a/lib/algorithms/quickSort.ts b/lib/algorithms/sorting/quickSort.ts similarity index 92% rename from lib/algorithms/quickSort.ts rename to lib/algorithms/sorting/quickSort.ts index be4d390..bf7b79b 100644 --- a/lib/algorithms/quickSort.ts +++ b/lib/algorithms/sorting/quickSort.ts @@ -1,4 +1,5 @@ -import { AlgorithmVisualization, SortingStep } from "../types"; +import { AlgorithmVisualization, SortingStep } from "../../types"; +import { createVisualization } from "../utils"; export function quickSort(array: number[]): AlgorithmVisualization { const steps: SortingStep[] = []; @@ -26,13 +27,7 @@ export function quickSort(array: number[]): AlgorithmVisualization { completed: finalCompleted, }); - return { - steps, - name: "Quick Sort", - key: "quickSort", - category: "sorting", - description: - "A divide and conquer algorithm that picks an element as a pivot and partitions the array around the chosen pivot.", + return createVisualization("quickSort", steps, { timeComplexity: "O(n log n) - average, O(n²) - worst case", spaceComplexity: "O(log n)", reference: "https://en.wikipedia.org/wiki/Quicksort", @@ -58,7 +53,7 @@ export function quickSort(array: number[]): AlgorithmVisualization { " return i + 1", "end procedure", ], - }; + }); } function quickSortHelper( diff --git a/lib/algorithms/selectionSort.ts b/lib/algorithms/sorting/selectionSort.ts similarity index 86% rename from lib/algorithms/selectionSort.ts rename to lib/algorithms/sorting/selectionSort.ts index 99fcb6c..9646988 100644 --- a/lib/algorithms/selectionSort.ts +++ b/lib/algorithms/sorting/selectionSort.ts @@ -1,4 +1,5 @@ -import { AlgorithmVisualization, SortingStep } from "../types"; +import { AlgorithmVisualization, SortingStep } from "../../types"; +import { createVisualization } from "../utils"; export function selectionSort(array: number[]): AlgorithmVisualization { const steps: SortingStep[] = []; @@ -74,13 +75,7 @@ export function selectionSort(array: number[]): AlgorithmVisualization { completed: [...completed], }); - return { - steps, - name: "Selection Sort", - key: "selectionSort", - category: "sorting", - description: - "The selection sort algorithm sorts an array by repeatedly finding the minimum element from the unsorted part and putting it at the beginning.", + return createVisualization("selectionSort", steps, { timeComplexity: "O(n²)", spaceComplexity: "O(1)", reference: "https://en.wikipedia.org/wiki/Selection_sort", @@ -100,5 +95,5 @@ export function selectionSort(array: number[]): AlgorithmVisualization { " end for", "end procedure", ], - }; + }); } diff --git a/lib/algorithms/utils.ts b/lib/algorithms/utils.ts new file mode 100644 index 0000000..19fc359 --- /dev/null +++ b/lib/algorithms/utils.ts @@ -0,0 +1,33 @@ +import { AlgorithmVisualization, SearchStep, SortingStep } from "../types"; +import { availableAlgorithms } from "./metadata"; + +/** + * Helper function to create an AlgorithmVisualization object from steps and algorithm-specific details + * This eliminates duplication between the metadata and the algorithm implementation + */ +export function createVisualization( + key: keyof typeof availableAlgorithms, + steps: SortingStep[] | SearchStep[], + details: { + timeComplexity: string; + spaceComplexity: string; + reference: string; + pseudoCode: string[]; + } +): AlgorithmVisualization { + const metadata = availableAlgorithms[key]; + + if (!metadata) { + throw new Error(`Metadata for algorithm "${String(key)}" not found`); + } + + return { + steps, + name: metadata.name, + key: metadata.key, + category: metadata.category, + difficulty: metadata.difficulty, + description: metadata.description, + ...details, + }; +} diff --git a/lib/types.ts b/lib/types.ts index ab59780..e92568c 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -22,6 +22,7 @@ export interface AlgorithmVisualization { reference: string; pseudoCode: string[]; category: string; + difficulty: string; key: string; } @@ -35,6 +36,7 @@ export interface AlgorithmInfo { name: string; key: string; category: AlgorithmCategory; + subtitle: string; description: string; difficulty: "easy" | "medium" | "hard"; } diff --git a/lib/utils.ts b/lib/utils.ts index 791ad20..fbfd39b 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -9,51 +9,8 @@ export function generateRandomArray( ); } -export function getAlgorithmLabel(algorithmKey: string): string { - const labels: Record = { - bubbleSort: "Bubble Sort", - selectionSort: "Selection Sort", - insertionSort: "Insertion Sort", - mergeSort: "Merge Sort", - quickSort: "Quick Sort", - heapSort: "Heap Sort", - linearSearch: "Linear Search", - }; - - return labels[algorithmKey] || algorithmKey; -} - -export function getDifficulty(algorithmKey: string): string { - const difficulties: Record = { - bubbleSort: "Easy", - selectionSort: "Easy", - insertionSort: "Easy", - mergeSort: "Medium", - quickSort: "Medium", - heapSort: "Hard", - linearSearch: "Easy", - }; - - return difficulties[algorithmKey] || "Unknown"; -} - export function getRandomValueFromArray(array: number[]): number { if (array.length === 0) return 42; const randomIndex = Math.floor(Math.random() * array.length); return array[randomIndex]; } - -// Ensure the target value exists in the array for demo purposes -export function ensureTargetInArray(array: number[], target: number): number[] { - if (array.length === 0) return [target]; - - // If target is not in the array, replace a random element with it - if (!array.includes(target)) { - const newArray = [...array]; - const randomIndex = Math.floor(Math.random() * newArray.length); - newArray[randomIndex] = target; - return newArray; - } - - return array; -}