From e227e0c65828e55d11f29473d89ee1b9f2f6f5fd Mon Sep 17 00:00:00 2001 From: adilallo <39313955+adilallo@users.noreply.github.com> Date: Mon, 2 Feb 2026 14:43:32 -0700 Subject: [PATCH 1/4] Implementation of icon card --- app/components-preview/page.tsx | 28 +++++ .../IconCard/IconCard.container.tsx | 35 ++++++ app/components/IconCard/IconCard.types.ts | 16 +++ app/components/IconCard/IconCard.view.tsx | 38 ++++++ app/components/IconCard/index.tsx | 2 + public/assets/Vector_Dao.svg | 20 ++++ public/assets/Vector_Default.svg | 3 + public/assets/Vector_MutualAid.svg | 15 +++ public/assets/Vector_OpenSource.svg | 15 +++ public/assets/Vector_Shapes.svg | 19 +++ public/assets/Vector_WorkerCoop.svg | 15 +++ stories/IconCard.stories.js | 80 +++++++++++++ tests/components/IconCard.test.tsx | 110 ++++++++++++++++++ 13 files changed, 396 insertions(+) create mode 100644 app/components/IconCard/IconCard.container.tsx create mode 100644 app/components/IconCard/IconCard.types.ts create mode 100644 app/components/IconCard/IconCard.view.tsx create mode 100644 app/components/IconCard/index.tsx create mode 100644 public/assets/Vector_Dao.svg create mode 100644 public/assets/Vector_Default.svg create mode 100644 public/assets/Vector_MutualAid.svg create mode 100644 public/assets/Vector_OpenSource.svg create mode 100644 public/assets/Vector_Shapes.svg create mode 100644 public/assets/Vector_WorkerCoop.svg create mode 100644 stories/IconCard.stories.js create mode 100644 tests/components/IconCard.test.tsx diff --git a/app/components-preview/page.tsx b/app/components-preview/page.tsx index d23e021..633710d 100644 --- a/app/components-preview/page.tsx +++ b/app/components-preview/page.tsx @@ -9,6 +9,8 @@ import Progress from "../components/Progress"; import Create from "../components/Create"; import Input from "../components/Input"; import InputWithCounter from "../components/InputWithCounter"; +import IconCard from "../components/IconCard"; +import { getAssetPath } from "../../lib/assetUtils"; export default function ComponentsPreview() { const [alertVisible, setAlertVisible] = useState({ @@ -413,6 +415,32 @@ export default function ComponentsPreview() { + + {/* IconCard Component Section */} +
+

+ IconCard Component +

+ +
+
+ + } + title="Worker's cooperatives" + description="Employee-owned businesses often need to clarify how power is shared, decisions are made, and how processes operate within their organizations." + onClick={() => console.log("IconCard clicked")} + /> +
+
+
); diff --git a/app/components/IconCard/IconCard.container.tsx b/app/components/IconCard/IconCard.container.tsx new file mode 100644 index 0000000..205b82d --- /dev/null +++ b/app/components/IconCard/IconCard.container.tsx @@ -0,0 +1,35 @@ +"use client"; + +import { memo } from "react"; +import { IconCardView } from "./IconCard.view"; +import type { IconCardProps } from "./IconCard.types"; + +const IconCardContainer = memo( + ({ icon, title, description, className = "", onClick }) => { + const handleClick = () => { + if (onClick) onClick(); + }; + + const handleKeyDown = (event: React.KeyboardEvent) => { + if (event.key === "Enter" || event.key === " ") { + event.preventDefault(); + handleClick(); + } + }; + + return ( + + ); + }, +); + +IconCardContainer.displayName = "IconCard"; + +export default IconCardContainer; diff --git a/app/components/IconCard/IconCard.types.ts b/app/components/IconCard/IconCard.types.ts new file mode 100644 index 0000000..04f6c11 --- /dev/null +++ b/app/components/IconCard/IconCard.types.ts @@ -0,0 +1,16 @@ +export interface IconCardProps { + icon: React.ReactNode; + title: string; + description: string; + className?: string; + onClick?: () => void; +} + +export interface IconCardViewProps { + icon: React.ReactNode; + title: string; + description: string; + className: string; + onClick: () => void; + onKeyDown: (event: React.KeyboardEvent) => void; +} diff --git a/app/components/IconCard/IconCard.view.tsx b/app/components/IconCard/IconCard.view.tsx new file mode 100644 index 0000000..d911e72 --- /dev/null +++ b/app/components/IconCard/IconCard.view.tsx @@ -0,0 +1,38 @@ +"use client"; + +import type { IconCardViewProps } from "./IconCard.types"; + +export function IconCardView({ + icon, + title, + description, + className, + onClick, + onKeyDown, +}: IconCardViewProps) { + return ( +
+ {/* Icon */} +
+ {icon} +
+ + {/* Title - Centered with auto space above and below */} +

+ {title} +

+ + {/* Description */} +

+ {description} +

+
+ ); +} diff --git a/app/components/IconCard/index.tsx b/app/components/IconCard/index.tsx new file mode 100644 index 0000000..ed26dd5 --- /dev/null +++ b/app/components/IconCard/index.tsx @@ -0,0 +1,2 @@ +export { default } from "./IconCard.container"; +export type { IconCardProps } from "./IconCard.types"; diff --git a/public/assets/Vector_Dao.svg b/public/assets/Vector_Dao.svg new file mode 100644 index 0000000..3581b6a --- /dev/null +++ b/public/assets/Vector_Dao.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/public/assets/Vector_Default.svg b/public/assets/Vector_Default.svg new file mode 100644 index 0000000..2bd6a9b --- /dev/null +++ b/public/assets/Vector_Default.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/Vector_MutualAid.svg b/public/assets/Vector_MutualAid.svg new file mode 100644 index 0000000..b4654f8 --- /dev/null +++ b/public/assets/Vector_MutualAid.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/public/assets/Vector_OpenSource.svg b/public/assets/Vector_OpenSource.svg new file mode 100644 index 0000000..49ae29b --- /dev/null +++ b/public/assets/Vector_OpenSource.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/public/assets/Vector_Shapes.svg b/public/assets/Vector_Shapes.svg new file mode 100644 index 0000000..d8e7d57 --- /dev/null +++ b/public/assets/Vector_Shapes.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/public/assets/Vector_WorkerCoop.svg b/public/assets/Vector_WorkerCoop.svg new file mode 100644 index 0000000..cb4b277 --- /dev/null +++ b/public/assets/Vector_WorkerCoop.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/stories/IconCard.stories.js b/stories/IconCard.stories.js new file mode 100644 index 0000000..e6bab37 --- /dev/null +++ b/stories/IconCard.stories.js @@ -0,0 +1,80 @@ +import IconCard from "../app/components/IconCard"; +import { getAssetPath } from "../lib/assetUtils"; + +export default { + title: "Components/IconCard", + component: IconCard, + parameters: { + layout: "centered", + docs: { + description: { + component: + "An interactive card component that displays an icon, title, and description. Features hover states, keyboard navigation, and accessibility support. Use Tab key to test focus indicators and Enter/Space to activate.", + }, + }, + }, + argTypes: { + icon: { + control: false, + description: "The icon element to display at the top of the card", + }, + title: { + control: { type: "text" }, + description: "The main title of the card", + }, + description: { + control: { type: "text" }, + description: "The description text displayed in uppercase", + }, + onClick: { action: "clicked" }, + }, + tags: ["autodocs"], +}; + +// Worker's Coop icon +const WorkerCoopIcon = () => ( + +); + +export const Default = { + args: { + icon: , + title: "Worker's cooperatives", + description: + "Employee-owned businesses often need to clarify how power is shared, decisions are made, and how processes operate within their organizations.", + }, +}; + +export const WithLongTitle = { + args: { + icon: , + title: "This is a very long title that might wrap to multiple lines", + description: + "Employee-owned businesses often need to clarify how power is shared.", + }, +}; + +export const WithShortDescription = { + args: { + icon: , + title: "Worker's cooperatives", + description: "Short description", + }, +}; + +export const Interactive = { + args: { + icon: , + title: "Clickable Card", + description: "This card has an onClick handler", + onClick: () => { + console.log("Card clicked!"); + }, + }, +}; diff --git a/tests/components/IconCard.test.tsx b/tests/components/IconCard.test.tsx new file mode 100644 index 0000000..18ff92f --- /dev/null +++ b/tests/components/IconCard.test.tsx @@ -0,0 +1,110 @@ +import React from "react"; +import { describe, it, expect, vi } from "vitest"; +import { render, screen, fireEvent } from "@testing-library/react"; +import "@testing-library/jest-dom/vitest"; +import IconCard from "../../app/components/IconCard"; +import { + componentTestSuite, + type ComponentTestSuiteConfig, +} from "../utils/componentTestSuite"; + +type IconCardProps = React.ComponentProps; + +const baseProps: IconCardProps = { + icon:
Icon
, + title: "Worker's cooperatives", + description: "Employee-owned businesses often need to clarify how power is shared", +}; + +const config: ComponentTestSuiteConfig = { + component: IconCard, + name: "IconCard", + props: baseProps, + requiredProps: ["icon", "title", "description"], + optionalProps: { + className: "custom-class", + onClick: vi.fn(), + }, + primaryRole: "button", + testCases: { + renders: true, + accessibility: true, + keyboardNavigation: true, + disabledState: false, + errorState: false, + }, +}; + +componentTestSuite(config); + +describe("IconCard (behavioral tests)", () => { + it("calls onClick when clicked", () => { + const handleClick = vi.fn(); + render( + Icon} + title="Test Title" + description="Test Description" + onClick={handleClick} + />, + ); + const card = screen.getByRole("button"); + fireEvent.click(card); + expect(handleClick).toHaveBeenCalledTimes(1); + }); + + it("calls onClick when Enter key is pressed", () => { + const handleClick = vi.fn(); + render( + Icon} + title="Test Title" + description="Test Description" + onClick={handleClick} + />, + ); + const card = screen.getByRole("button"); + fireEvent.keyDown(card, { key: "Enter" }); + expect(handleClick).toHaveBeenCalledTimes(1); + }); + + it("calls onClick when Space key is pressed", () => { + const handleClick = vi.fn(); + render( + Icon} + title="Test Title" + description="Test Description" + onClick={handleClick} + />, + ); + const card = screen.getByRole("button"); + fireEvent.keyDown(card, { key: " " }); + expect(handleClick).toHaveBeenCalledTimes(1); + }); + + it("renders icon, title, and description", () => { + render( + Icon} + title="Worker's cooperatives" + description="Employee-owned businesses" + />, + ); + expect(screen.getByTestId("icon")).toBeInTheDocument(); + expect(screen.getByText("Worker's cooperatives")).toBeInTheDocument(); + expect(screen.getByText("Employee-owned businesses")).toBeInTheDocument(); + }); + + it("has proper ARIA label", () => { + render( + Icon} + title="Test Title" + description="Test Description" + />, + ); + const card = screen.getByRole("button"); + expect(card).toHaveAttribute("aria-label", "Test Title: Test Description"); + }); +}); From 4ab37afaf391f9d0a194921bb9f6fdf93bae31ef Mon Sep 17 00:00:00 2001 From: adilallo <39313955+adilallo@users.noreply.github.com> Date: Mon, 2 Feb 2026 14:55:49 -0700 Subject: [PATCH 2/4] Fix icon card component and tests to pass --- app/components-preview/page.tsx | 4 +++- app/components/IconCard/IconCard.view.tsx | 8 ++++---- app/components/InputWithCounter/InputWithCounter.types.ts | 2 -- tests/components/IconCard.test.tsx | 3 ++- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/app/components-preview/page.tsx b/app/components-preview/page.tsx index 633710d..ac3f7ae 100644 --- a/app/components-preview/page.tsx +++ b/app/components-preview/page.tsx @@ -436,7 +436,9 @@ export default function ComponentsPreview() { } title="Worker's cooperatives" description="Employee-owned businesses often need to clarify how power is shared, decisions are made, and how processes operate within their organizations." - onClick={() => console.log("IconCard clicked")} + onClick={() => { + // IconCard clicked handler + }} /> diff --git a/app/components/IconCard/IconCard.view.tsx b/app/components/IconCard/IconCard.view.tsx index d911e72..c4b462a 100644 --- a/app/components/IconCard/IconCard.view.tsx +++ b/app/components/IconCard/IconCard.view.tsx @@ -24,10 +24,10 @@ export function IconCardView({ {icon} - {/* Title - Centered with auto space above and below */} -

- {title} -

+ {/* Title - Centered with auto space above and below */} +

+ {title} +

{/* Description */}

diff --git a/app/components/InputWithCounter/InputWithCounter.types.ts b/app/components/InputWithCounter/InputWithCounter.types.ts index ec6d43c..c04ee47 100644 --- a/app/components/InputWithCounter/InputWithCounter.types.ts +++ b/app/components/InputWithCounter/InputWithCounter.types.ts @@ -1,4 +1,3 @@ -/* eslint-disable no-unused-vars, @typescript-eslint/no-unused-vars */ export interface InputWithCounterProps { label?: string; placeholder?: string; @@ -9,4 +8,3 @@ export interface InputWithCounterProps { className?: string; inputClassName?: string; } -/* eslint-enable no-unused-vars, @typescript-eslint/no-unused-vars */ \ No newline at end of file diff --git a/tests/components/IconCard.test.tsx b/tests/components/IconCard.test.tsx index 18ff92f..5b050a8 100644 --- a/tests/components/IconCard.test.tsx +++ b/tests/components/IconCard.test.tsx @@ -13,7 +13,8 @@ type IconCardProps = React.ComponentProps; const baseProps: IconCardProps = { icon:

Icon
, title: "Worker's cooperatives", - description: "Employee-owned businesses often need to clarify how power is shared", + description: + "Employee-owned businesses often need to clarify how power is shared", }; const config: ComponentTestSuiteConfig = { From 1ec9e9d63917d85a6281ebc3ac68213f259d820e Mon Sep 17 00:00:00 2001 From: adilallo <39313955+adilallo@users.noreply.github.com> Date: Mon, 2 Feb 2026 15:51:52 -0700 Subject: [PATCH 3/4] Update Create.stories.js --- stories/Create.stories.js | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/stories/Create.stories.js b/stories/Create.stories.js index cea920c..61c58f8 100644 --- a/stories/Create.stories.js +++ b/stories/Create.stories.js @@ -1,4 +1,3 @@ -import React, { useState } from "react"; import Create from "../app/components/Create"; import Input from "../app/components/Input"; @@ -44,20 +43,10 @@ export default { }; const Template = (args) => { - const [isOpen, setIsOpen] = useState(args.isOpen || false); - return ( -
- - setIsOpen(false)}> - {args.children} - -
+ {}}> + {args.children} + ); }; From 9a42035b0ccc1abf56c7ebb68718f1ed92112f2a Mon Sep 17 00:00:00 2001 From: adilallo <39313955+adilallo@users.noreply.github.com> Date: Mon, 2 Feb 2026 18:56:47 -0700 Subject: [PATCH 4/4] Update and resolve storybook issues --- .storybook/preview.js | 10 +++++++--- stories/Progress.stories.js | 30 +++++++++++++++++------------- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/.storybook/preview.js b/.storybook/preview.js index aadfe40..333f813 100644 --- a/.storybook/preview.js +++ b/.storybook/preview.js @@ -1,5 +1,7 @@ import "../app/globals.css"; import "./fonts.css"; +import { MessagesProvider } from "../app/contexts/MessagesContext"; +import messages from "../messages/en/index"; /** @type { import('@storybook/react').Preview } */ const preview = { @@ -13,9 +15,11 @@ const preview = { }, decorators: [ (Story) => ( -
- -
+ +
+ +
+
), ], }; diff --git a/stories/Progress.stories.js b/stories/Progress.stories.js index 2e3be3c..2e3ce5c 100644 --- a/stories/Progress.stories.js +++ b/stories/Progress.stories.js @@ -39,58 +39,62 @@ export const Default = { args: { progress: "3-2", }, - render: (args) => , + render: (args) => ( +
+ +
+ ), }; export const AllStates = { args: {}, render: (_args) => (
-
+

1-0

-
+

1-1

-
+

1-2

-
+

1-3

-
+

1-4

-
+

1-5

-
+

2-0

-
+

2-1

-
+

2-2

-
+

3-0

-
+

3-1

-
+

3-2