diff --git a/README.md b/README.md
index b0f1cea..13ecf38 100644
--- a/README.md
+++ b/README.md
@@ -2,42 +2,61 @@
Hello there!
-Your task is to implement a new Stepper component
+### Task Overview
+
+Implemented a new Stepper component
based on [these specifications in Figma](https://www.figma.com/design/gvf4xjR6f9rvaj94BPFFyZ/Frontend-Coding-Challenge?node-id=0-1&t=tHGuIxWbpBVVsH5B-1).
-Make sure your component behaves as expected by
-creating tests for it.
## File structure
-The relevant files are these
+
```text
__tests__/
components/
stepper/
stepper.test.tsx
- -> Implement your tests here
+ -> Contains tests for the Stepper component, verifying that it renders correctly and displays the steps.
+ stepPage.test.tsx
+ -> Contains tests for the StepPage component, verifying that it renders the correct content based on the route, calls the `setCurrentStep` function with the correct index, and handles invalid routes.
+
components/
stepper/
- stepper.tsx
+ stepper.tsx
-> The stepper component
steps.ts
- -> You might want to use this to
- configure the steps
+ -> Configuring the steps
+ stepMapping.tsx
+ -> step mapping for each page
+ stepPage.ts
+ -> For creating dynamic routes
hooks/
useStepper.ts
-> Custom hook that handles navigation
+
+context/
+ stepperContext.tsx
+ -> Contains React context providers for managing application state- `StepperContext`
+
pages/
index.tsx
-> The main page (localhost:3000)
+ termin.tsx, fahrzeug.tsx, kontakt.tsx:
+ -> Page components that use the StepPage component.
```
+## Architecture Diagram
+
+
+
## Setting up everything
Install the dependencies
+
```bash
npm install
```
Start the dev server to get a preview
+
```bash
npm run dev
```
@@ -45,28 +64,19 @@ npm run dev
Check out the page on [localhost:3000](http://localhost:3000)
To run the tests
+
```bash
npm run test
```
-## Getting started
-
-Get started by having a look at the relevant files
-and then start implementing the Stepper component in
-`/components/stepper/stepper.tsx`.
-
-## Things to consider
-
-Please use **Tailwind CSS** to style the component.
-Please **do not install any additional packages**.
-
-Make sure the component
-
-- aligns **exactly** with the Figma spec (we already
-implemented the page layout for you,
-you just have to worry about the stepper component)
-- is **reusable**
-- works with **different sets of steps**
-- works well on **all breakpoints**
+## Features & Functionality
-Good luck! :)
\ No newline at end of file
+- Stepper Component implementation ✅
+- Steps configured ✅
+- useStepper hook to encapsulate the stepper logic and manage navigation of steps and routing ✅
+- Responsiveness ✅
+- StepperContext to manage the stepper state (steps, current step) and functions (handleNextStep, setCurrentStep) ✅
+- Dynamic step management ✅
+- Basic error handling to display an "Invalid Step" message if the route does not match a valid step. ✅
+- StepPage is a reusable component that renders the core layout of each step page ✅
+- Tests for Stepper component and StepPage ✅
diff --git a/__tests__/components/stepper/stepPage.test.tsx b/__tests__/components/stepper/stepPage.test.tsx
new file mode 100644
index 0000000..6746973
--- /dev/null
+++ b/__tests__/components/stepper/stepPage.test.tsx
@@ -0,0 +1,105 @@
+import React from "react";
+import { render, screen } from "@testing-library/react";
+import "@testing-library/jest-dom";
+import StepPage from "../../../components/stepper/stepPage";
+import { useStepperContext } from "../../../context/stepperContext";
+import { useRouter } from "next/router";
+
+jest.mock("next/router", () => ({
+ useRouter: jest.fn(),
+}));
+
+jest.mock("../../../context/stepperContext", () => ({
+ useStepperContext: jest.fn(),
+}));
+
+
+jest.mock("../../../components/stepper/stepMapping", () => {
+ const mockStepMapping = { step1: 1, step2: 2, termin: 1 };
+ return {
+ stepMapping: mockStepMapping,
+ };
+});
+
+describe("StepPage", () => {
+ const mockSteps = [
+ { title: "Step 1", isCompleted: false, navigateTo: "/step1" },
+ { title: "Step 2", isCompleted: false, navigateTo: "/step2" },
+ ];
+
+ const mockUseStepperContext = {
+ steps: mockSteps,
+ currentStep: 0,
+ handleNextStep: jest.fn(),
+ setCurrentStep: jest.fn(),
+ };
+
+ beforeEach(() => {
+ (useStepperContext as jest.Mock).mockReturnValue(mockUseStepperContext);
+ (useRouter as jest.Mock).mockReturnValue({
+ pathname: "/step1",
+ query: {},
+ push: jest.fn(),
+ events: {
+ emit: jest.fn(),
+ on: jest.fn(),
+ off: jest.fn(),
+ },
+ beforePopState: jest.fn(() => null),
+ prefetch: jest.fn(() => Promise.resolve()),
+ });
+ });
+
+ it("renders the Stepper component", () => {
+ (useRouter as jest.Mock).mockReturnValue({
+ pathname: "/step1",
+ query: {},
+ push: jest.fn(),
+ events: {
+ emit: jest.fn(),
+ on: jest.fn(),
+ off: jest.fn(),
+ },
+ beforePopState: jest.fn(() => null),
+ prefetch: jest.fn(() => Promise.resolve()),
+ });
+ render();
+ expect(screen.getByText("Step 1")).toBeInTheDocument();
+ });
+
+ it("calls setCurrentStep with the correct step index based on pathname", () => {
+ (useRouter as jest.Mock).mockReturnValue({
+ pathname: "/termin",
+ query: {},
+ push: jest.fn(),
+ events: {
+ emit: jest.fn(),
+ on: jest.fn(),
+ off: jest.fn(),
+ },
+ beforePopState: jest.fn(() => null),
+ prefetch: jest.fn(() => Promise.resolve()),
+ });
+
+ render();
+ expect(mockUseStepperContext.setCurrentStep).toHaveBeenCalledWith(1);
+ });
+
+ it("renders 'Invalid Step' if the pathname does not match any step", () => {
+ (useRouter as jest.Mock).mockReturnValue({
+ pathname: "/unknown",
+ query: {},
+ push: jest.fn(),
+ events: {
+ emit: jest.fn(),
+ on: jest.fn(),
+ off: jest.fn(),
+ },
+ beforePopState: jest.fn(() => null),
+ prefetch: jest.fn(() => Promise.resolve()),
+ });
+
+ const { container } = render();
+ expect(container).toHaveTextContent("Invalid Step");
+ });
+});
diff --git a/__tests__/components/stepper/stepper.test.tsx b/__tests__/components/stepper/stepper.test.tsx
index 9a9c705..69096ba 100644
--- a/__tests__/components/stepper/stepper.test.tsx
+++ b/__tests__/components/stepper/stepper.test.tsx
@@ -1,6 +1,61 @@
-// TODO: Implement your tests here
+import React from "react";
+import { render, screen } from "@testing-library/react";
+import "@testing-library/jest-dom";
+import Stepper from "../../../components/stepper/stepper";
+import { steps } from "../../../components/stepper/steps";
+
describe("Stepper", () => {
- it("TODO", () => {
- expect(true).toBe(false);
- });
+ it("renders all steps", () => {
+ render();
+
+ steps.forEach((step, index) => {
+ expect(screen.getByText(step.title)).toBeInTheDocument();
+ expect(screen.getByText(String(index + 1))).toBeInTheDocument();
+ });
+ });
+
+ it("highlights current and previous steps", () => {
+ const current = 2;
+ const stepsWithCompleted = steps.map((s, i) => ({ ...s, isCompleted: i <= current }));
+
+ render();
+
+ for (let i = 0; i <= current; i++) {
+ const stepCircle = screen.getByText(String(i + 1));
+ expect(stepCircle).toHaveClass("bg-blue-500");
+ }
+ });
+
+ it("renders correct number of connection lines", () => {
+ render();
+
+ const connectionLines = screen.getAllByTestId("connection-line");
+ expect(connectionLines).toHaveLength(steps.length - 1);
+ });
+
+ it("applies responsive classes", () => {
+ render();
+
+ const stepCircle = screen.getByText("1");
+ expect(stepCircle).toHaveClass("w-[30px]");
+ expect(stepCircle).toHaveClass("h-[30px]");
+ expect(stepCircle).toHaveClass("md:w-10");
+ expect(stepCircle).toHaveClass("md:h-10");
+
+ const stepTitle = screen.getByText(steps[0].title);
+ expect(stepTitle).toHaveClass("text-[12px]");
+ expect(stepTitle).toHaveClass("lg:text-[16px]");
+ });
+
+ it("renders correctly with last step as current", () => {
+ const lastIndex = steps.length - 1;
+ const stepsAllCompleted = steps.map((s) => ({ ...s, isCompleted: true }));
+
+ render();
+
+ steps.forEach((_, index) => {
+ const stepCircle = screen.getByText(String(index + 1));
+ expect(stepCircle).toHaveClass("bg-blue-500");
+ });
+ });
});
diff --git a/components/stepper/stepMapping.ts b/components/stepper/stepMapping.ts
new file mode 100644
index 0000000..ba5e431
--- /dev/null
+++ b/components/stepper/stepMapping.ts
@@ -0,0 +1,23 @@
+import { steps, Step } from "./steps";
+
+interface StepMapping {
+ [key: string]: number;
+}
+
+export const stepMapping: StepMapping = steps.reduce(
+ (accStepMapping: StepMapping, step: Step, stepIndex: number) => {
+ const pageName: string | null = step.navigateTo
+ ? step.navigateTo.replace("/", "")
+ : null;
+ // console.log(
+ // `Mapping step "${step.title}" to page "${pageName}" with index ${index}`
+ // );
+
+ if (pageName) {
+ accStepMapping[pageName] = stepIndex + 1;
+ }
+
+ return accStepMapping;
+ },
+ {}
+);
diff --git a/components/stepper/stepPage.tsx b/components/stepper/stepPage.tsx
new file mode 100644
index 0000000..ea9c8cb
--- /dev/null
+++ b/components/stepper/stepPage.tsx
@@ -0,0 +1,47 @@
+import { useEffect } from "react";
+import Header from "../../components/repareo/header";
+import MainWrapper from "../../components/repareo/mainWrapper";
+import StepperWrapper from "../../components/repareo/stepperWrapper";
+import Stepper from "../../components/stepper/stepper";
+import ButtonWrapper from "../../components/repareo/buttonWrapper";
+import Button from "../../components/repareo/button";
+import { useStepperContext } from "../../context/stepperContext";
+import { useRouter } from "next/router";
+import { stepMapping } from "./stepMapping";
+
+interface StepPageProps {}
+
+export default function StepPage({}: StepPageProps) {
+ const { steps, currentStep, handleNextStep, setCurrentStep } =
+ useStepperContext();
+
+ const router = useRouter();
+ const { pathname } = router;
+ const pageName = pathname.split("/").pop() || "";
+ const stepIndex = stepMapping[pageName];
+ useEffect(() => {
+ if (stepIndex) {
+ setCurrentStep(stepIndex);
+ }
+ }, [setCurrentStep, stepIndex]);
+
+ if (!stepIndex) {
+ return
Invalid Step
;
+ }
+
+ return (
+ <>
+
+
+
+
+
+
+ {currentStep < steps.length - 1 && (
+
+ )}
+
+
+ >
+ );
+}
diff --git a/components/stepper/stepper.tsx b/components/stepper/stepper.tsx
index fe3602e..6314a03 100644
--- a/components/stepper/stepper.tsx
+++ b/components/stepper/stepper.tsx
@@ -1,14 +1,42 @@
-interface StepperProps {}
+import { Step } from "./steps";
+interface StepperProps {
+ steps: Step[];
+ currentStep: number;
+}
+
+export default function Stepper({ steps, currentStep }: StepperProps) {
+ return (
+
+
+ {steps.map((step, index) => (
+
+
+ {index + 1}
+
+ {index < steps.length - 1 && (
+
+ )}
-export default function Stepper() {
- /*TODO: Replace this with the actual Stepper implementation*/
- return (
-
- {""}
-
- );
+
+ {step.title}
+
+
+ ))}
+
+
+ );
}
diff --git a/components/stepper/steps.ts b/components/stepper/steps.ts
index d840d60..a330349 100644
--- a/components/stepper/steps.ts
+++ b/components/stepper/steps.ts
@@ -1,5 +1,12 @@
-interface Step {
- title: string;
+export interface Step {
+ title: string;
+ isCompleted: boolean;
+ navigateTo: string | null;
}
-export const steps: Step[] = [];
+export const steps: Step[] = [
+ { title: "Service", isCompleted: false, navigateTo: "/termin" },
+ { title: "Termin", isCompleted: false, navigateTo: "/fahrzeug" },
+ { title: "Fahrzeug", isCompleted: false, navigateTo: "/kontakt" },
+ { title: "Kontakt", isCompleted: false, navigateTo: null },
+];
diff --git a/context/stepperContext.tsx b/context/stepperContext.tsx
new file mode 100644
index 0000000..b7fec49
--- /dev/null
+++ b/context/stepperContext.tsx
@@ -0,0 +1,44 @@
+import React, {
+ createContext,
+ useContext,
+ ReactNode,
+ useState,
+ useCallback,
+} from "react";
+import useStepper from "../hooks/useStepper";
+import { Step } from "../components/stepper/steps";
+
+interface StepperContextType {
+ steps: Step[];
+ currentStep: number;
+ handleNextStep: () => void;
+ setCurrentStep: (step: number) => void;
+}
+
+const StepperContext = createContext(undefined);
+
+export function StepperProvider({ children }: { children: ReactNode }) {
+ const [contextCurrentStep, setContextCurrentStep] = useState(0);
+
+ const onStepChange = useCallback((step: number) => {
+ setContextCurrentStep(step);
+ }, []);
+
+ const stepperHook = useStepper(onStepChange);
+
+ return (
+
+ {children}
+
+ );
+}
+
+export function useStepperContext() {
+ const context = useContext(StepperContext);
+ if (context === undefined) {
+ throw new Error("useStepperContext must be used within a StepperProvider");
+ }
+ return context;
+}
diff --git a/hooks/useStepper.ts b/hooks/useStepper.ts
index 588deeb..aaa8ede 100644
--- a/hooks/useStepper.ts
+++ b/hooks/useStepper.ts
@@ -1,11 +1,44 @@
-import { useState } from "react";
-
-export default function useStepper() {
- const [currentStep, setCurrentStep] = useState(0);
- function handleNextStep() {
- setCurrentStep((prev) => {
- return prev + 1;
- });
- }
- return { currentStep, handleNextStep };
+import { useState, useCallback } from "react";
+import { steps as initialSteps, Step } from "../components/stepper/steps";
+import { useRouter } from "next/router";
+
+export default function useStepper(onStepChange?: (step: number) => void) {
+ const router = useRouter();
+ const [stepsState, setStepsState] = useState(() =>
+ initialSteps.map((s) => ({ ...s }))
+ );
+ const [currentStep, setCurrentStep] = useState(0);
+
+ const updateStepCompletion = useCallback((stepIndex: number) => {
+ setStepsState((prevSteps) =>
+ prevSteps.map((step, index) =>
+ index < stepIndex ? { ...step, isCompleted: true } : step
+ )
+ );
+ }, []);
+
+ const setCurrentStepAndNotify = useCallback((newStep: number) => {
+ setCurrentStep(newStep);
+ updateStepCompletion(newStep);
+ if (onStepChange) {
+ onStepChange(newStep);
+ }
+ }, [onStepChange, updateStepCompletion]);
+
+ const handleNextStep = useCallback(() => {
+ const nextStep = Math.min(currentStep + 1, stepsState.length - 1);
+ updateStepCompletion(nextStep);
+ setCurrentStepAndNotify(nextStep);
+ const navigateTo = stepsState[currentStep]?.navigateTo ?? "";
+ if (navigateTo) {
+ router.push(navigateTo);
+ }
+ }, [currentStep, stepsState, router, setCurrentStepAndNotify, updateStepCompletion]);
+
+ return {
+ steps: stepsState,
+ currentStep,
+ handleNextStep,
+ setCurrentStep: setCurrentStepAndNotify
+ };
}
diff --git a/pages/_app.tsx b/pages/_app.tsx
index 5b48a79..d802510 100644
--- a/pages/_app.tsx
+++ b/pages/_app.tsx
@@ -1,8 +1,11 @@
import "../styles/globals.css";
import type { AppProps } from "next/app";
+import { StepperProvider } from "../context/stepperContext";
function MyApp({ Component, pageProps }: AppProps) {
- return ;
+ return
+
+ ;
}
export default MyApp;
diff --git a/pages/fahrzeug.tsx b/pages/fahrzeug.tsx
new file mode 100644
index 0000000..3045565
--- /dev/null
+++ b/pages/fahrzeug.tsx
@@ -0,0 +1,5 @@
+import StepPage from "../components/stepper/stepPage";
+
+export default function Termin() {
+ return ;
+}
\ No newline at end of file
diff --git a/pages/index.tsx b/pages/index.tsx
index f1ad62d..9f58f61 100644
--- a/pages/index.tsx
+++ b/pages/index.tsx
@@ -7,19 +7,19 @@ import Stepper from "../components/stepper/stepper";
import useStepper from "../hooks/useStepper";
export default function Home() {
- const { currentStep, handleNextStep } = useStepper();
- return (
- <>
-
-
-
- {/*TODO: Make sure the Stepper handles clicks on the button*/}
-
-
-
-
-
-
- >
- );
+ const { steps, currentStep, handleNextStep } = useStepper();
+ return (
+ <>
+
+
+
+ {/*TODO: Make sure the Stepper handles clicks on the button*/}
+
+
+
+
+
+
+ >
+ );
}
diff --git a/pages/kontakt.tsx b/pages/kontakt.tsx
new file mode 100644
index 0000000..3045565
--- /dev/null
+++ b/pages/kontakt.tsx
@@ -0,0 +1,5 @@
+import StepPage from "../components/stepper/stepPage";
+
+export default function Termin() {
+ return ;
+}
\ No newline at end of file
diff --git a/pages/termin.tsx b/pages/termin.tsx
new file mode 100644
index 0000000..3045565
--- /dev/null
+++ b/pages/termin.tsx
@@ -0,0 +1,5 @@
+import StepPage from "../components/stepper/stepPage";
+
+export default function Termin() {
+ return ;
+}
\ No newline at end of file
diff --git a/public/architectureDiagram.png b/public/architectureDiagram.png
new file mode 100644
index 0000000..b1f4d4b
Binary files /dev/null and b/public/architectureDiagram.png differ