From 0cb62d50581f32dbe900ad598cf74f6a9892b70f Mon Sep 17 00:00:00 2001 From: Stephen Savage Date: Thu, 27 Feb 2025 12:13:46 +0000 Subject: [PATCH 01/10] Added atomic structuring for components and added examples for each: NextImage, Cta, Carousel, Downloader --- jest.config.ts | 1 + jest.setup.ts | 16 +++ package-lock.json | 39 ++++++ package.json | 13 +- src/models/image-dto.ts | 27 ++++ src/models/link-dto.ts | 7 ++ src/styles/_mixins.scss | 47 ++++++- src/styles/_vars.scss | 16 +++ src/styles/globals.scss | 85 ++++++++++--- src/ui/components/atoms/Cta/Cta.module.scss | 94 ++++++++++++++ src/ui/components/atoms/Cta/Cta.stories.tsx | 59 +++++++++ src/ui/components/atoms/Cta/Cta.test.tsx | 58 +++++++++ src/ui/components/atoms/Cta/Cta.tsx | 77 ++++++++++++ src/ui/components/atoms/Cta/index.ts | 3 + .../{ => atoms/Header}/Header.stories.tsx | 2 +- .../components/{ => atoms/Header}/Header.tsx | 2 +- .../atoms/NextImage/NextImage.module.scss | 8 ++ .../atoms/NextImage/NextImage.test.tsx | 81 ++++++++++++ .../components/atoms/NextImage/NextImage.tsx | 55 +++++++++ src/ui/components/atoms/Svg/SvgArrowRight.tsx | 20 +++ .../components/atoms/Svg/SvgChevronRight.tsx | 20 +++ src/ui/components/atoms/Svg/SvgSearch.tsx | 26 ++++ src/ui/components/atoms/index.ts | 0 src/ui/components/index.ts | 2 +- .../molecules/Carousel/Carousel.module.scss | 68 +++++++++++ .../molecules/Carousel/Carousel.stories.tsx | 57 +++++++++ .../molecules/Carousel/Carousel.test.tsx | 16 +++ .../molecules/Carousel/Carousel.tsx | 74 +++++++++++ src/ui/components/molecules/Carousel/index.ts | 3 + src/ui/components/molecules/index.ts | 0 .../Downloader/Downloader.stories.tsx | 18 +++ .../organisms/Downloader/Downloader.tsx | 19 +++ src/ui/components/organisms/index.ts | 0 src/ui/pages/Home.tsx | 4 +- src/utils/splide/horizontal-wheel.ts | 115 ++++++++++++++++++ tsconfig.json | 13 +- 36 files changed, 1115 insertions(+), 30 deletions(-) create mode 100644 src/models/image-dto.ts create mode 100644 src/models/link-dto.ts create mode 100644 src/ui/components/atoms/Cta/Cta.module.scss create mode 100644 src/ui/components/atoms/Cta/Cta.stories.tsx create mode 100644 src/ui/components/atoms/Cta/Cta.test.tsx create mode 100644 src/ui/components/atoms/Cta/Cta.tsx create mode 100644 src/ui/components/atoms/Cta/index.ts rename src/ui/components/{ => atoms/Header}/Header.stories.tsx (94%) rename src/ui/components/{ => atoms/Header}/Header.tsx (88%) create mode 100644 src/ui/components/atoms/NextImage/NextImage.module.scss create mode 100644 src/ui/components/atoms/NextImage/NextImage.test.tsx create mode 100644 src/ui/components/atoms/NextImage/NextImage.tsx create mode 100644 src/ui/components/atoms/Svg/SvgArrowRight.tsx create mode 100644 src/ui/components/atoms/Svg/SvgChevronRight.tsx create mode 100644 src/ui/components/atoms/Svg/SvgSearch.tsx create mode 100644 src/ui/components/atoms/index.ts create mode 100644 src/ui/components/molecules/Carousel/Carousel.module.scss create mode 100644 src/ui/components/molecules/Carousel/Carousel.stories.tsx create mode 100644 src/ui/components/molecules/Carousel/Carousel.test.tsx create mode 100644 src/ui/components/molecules/Carousel/Carousel.tsx create mode 100644 src/ui/components/molecules/Carousel/index.ts create mode 100644 src/ui/components/molecules/index.ts create mode 100644 src/ui/components/organisms/Downloader/Downloader.stories.tsx create mode 100644 src/ui/components/organisms/Downloader/Downloader.tsx create mode 100644 src/ui/components/organisms/index.ts create mode 100644 src/utils/splide/horizontal-wheel.ts diff --git a/jest.config.ts b/jest.config.ts index 6468cad..c160ea7 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -1,6 +1,7 @@ export default { testEnvironment: "jsdom", moduleNameMapper: { + "^@/(.*)$": "/src/$1", "\\.(css|scss)$": "identity-obj-proxy", }, coverageProvider: "v8", diff --git a/jest.setup.ts b/jest.setup.ts index d0de870..432c4e8 100644 --- a/jest.setup.ts +++ b/jest.setup.ts @@ -1 +1,17 @@ import "@testing-library/jest-dom"; + +jest.mock("@splidejs/splide/css/core", () => ""); + +Object.defineProperty(window, "matchMedia", { + writable: true, + value: jest.fn().mockImplementation((query) => ({ + matches: false, + media: query, + onchange: null, + addListener: jest.fn(), // Deprecated + removeListener: jest.fn(), // Deprecated + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + dispatchEvent: jest.fn(), + })), +}); diff --git a/package-lock.json b/package-lock.json index f61e3e7..1fa7976 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,8 @@ "hasInstallScript": true, "license": "MIT", "dependencies": { + "@splidejs/react-splide": "^0.7.12", + "classnames": "^2.5.1", "ini": "^5.0.0", "next": "^15.0.0", "react": "19.0.0", @@ -46,6 +48,7 @@ "css-loader": "^7.1.2", "eslint": "^8.36.0", "eslint-plugin-storybook": "^0.11.3", + "identity-obj-proxy": "^3.0.0", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", "mini-css-extract-plugin": "^2.9.2", @@ -3373,6 +3376,19 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@splidejs/react-splide": { + "version": "0.7.12", + "resolved": "https://registry.npmjs.org/@splidejs/react-splide/-/react-splide-0.7.12.tgz", + "integrity": "sha512-UfXH+j47jsMc4x5HA/aOwuuHPqn6y9+ZTNYPWDRD8iLKvIVMZlzq2unjUEvyDAU+TTVPZOXkG2Ojeoz0P4AkZw==", + "dependencies": { + "@splidejs/splide": "^4.1.3" + } + }, + "node_modules/@splidejs/splide": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@splidejs/splide/-/splide-4.1.4.tgz", + "integrity": "sha512-5I30evTJcAJQXt6vJ26g2xEkG+l1nXcpEw4xpKh0/FWQ8ozmAeTbtniVtVmz2sH1Es3vgfC4SS8B2X4o5JMptA==" + }, "node_modules/@storybook/addon-actions": { "version": "8.6.0", "resolved": "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-8.6.0.tgz", @@ -6726,6 +6742,11 @@ "node": ">=0.10.0" } }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" + }, "node_modules/clean-css": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", @@ -9226,6 +9247,12 @@ "dev": true, "license": "MIT" }, + "node_modules/harmony-reflect": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/harmony-reflect/-/harmony-reflect-1.6.2.tgz", + "integrity": "sha512-HIp/n38R9kQjDEziXyDTuW3vvoxxyxjxFzXLrBr18uB47GnSt+G9D29fqrpM5ZkspMcPICud3XsBJQ4Y2URg8g==", + "dev": true + }, "node_modules/has-flag": { "version": "4.0.0", "dev": true, @@ -9604,6 +9631,18 @@ "postcss": "^8.1.0" } }, + "node_modules/identity-obj-proxy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz", + "integrity": "sha512-00n6YnVHKrinT9t0d9+5yZC6UBNJANpYEQvL2LlX6Ab9lnmxzIRcEmTPuyGScvl1+jKuCICX1Z0Ab1pPKKdikA==", + "dev": true, + "dependencies": { + "harmony-reflect": "^1.4.6" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/ieee754": { "version": "1.2.1", "dev": true, diff --git a/package.json b/package.json index 456a16a..9375e35 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,8 @@ "build-storybook": "storybook build" }, "dependencies": { + "@splidejs/react-splide": "^0.7.12", + "classnames": "^2.5.1", "ini": "^5.0.0", "next": "^15.0.0", "react": "19.0.0", @@ -73,9 +75,6 @@ "tsnode-di": "0.0.3" }, "devDependencies": { - "@testing-library/dom": "^10.4.0", - "@testing-library/jest-dom": "^6.6.3", - "@testing-library/react": "^16.2.0", "@chromatic-com/storybook": "^3.2.4", "@storybook/addon-essentials": "^8.5.8", "@storybook/addon-interactions": "^8.5.8", @@ -83,6 +82,9 @@ "@storybook/blocks": "^8.5.8", "@storybook/nextjs": "^8.5.8", "@storybook/test": "^8.5.8", + "@testing-library/dom": "^10.4.0", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.2.0", "@types/chai": "^4.3.4", "@types/cors": "^2.8.13", "@types/jest": "^29.5.14", @@ -96,9 +98,10 @@ "copy-webpack-plugin": "^12.0.2", "css-loader": "^7.1.2", "eslint": "^8.36.0", + "eslint-plugin-storybook": "^0.11.3", + "identity-obj-proxy": "^3.0.0", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", - "eslint-plugin-storybook": "^0.11.3", "mini-css-extract-plugin": "^2.9.2", "mocha": "^10.2.0", "nodemon": "^1.12.1", @@ -106,8 +109,8 @@ "nyc": "^15.1.0", "sass": "^1.85.0", "sass-loader": "^16.0.5", - "ts-jest": "^29.2.6", "storybook": "^8.5.8", + "ts-jest": "^29.2.6", "ts-loader": "^9.4.2", "ts-node": "^10.9.2", "typedoc": "^0.23.28", diff --git a/src/models/image-dto.ts b/src/models/image-dto.ts new file mode 100644 index 0000000..3f05bb3 --- /dev/null +++ b/src/models/image-dto.ts @@ -0,0 +1,27 @@ +export interface ImageDTO { + // id: string; + alt: string; + // filename: string; + // mimeType: string; + // filesize: number; + width: number; + height: number; + focalX?: number; + focalY?: number; + sizes?: { + [key: string]: ResizedImage; + }; + // createdAt: string; + // updatedAt: string; + url: string; + // thumbnailURL: string; +} + +export interface ResizedImage { + width: number; + height: number; + mimeType: string; + filesize: number; + filename: string; + url: string; +} diff --git a/src/models/link-dto.ts b/src/models/link-dto.ts new file mode 100644 index 0000000..bec99bf --- /dev/null +++ b/src/models/link-dto.ts @@ -0,0 +1,7 @@ +export interface LinkDTO { + variant?: "primary" | "secondary" | "icon" | "underline"; + href: string; + label: string; + newTab: boolean; + isHidden: boolean; +} diff --git a/src/styles/_mixins.scss b/src/styles/_mixins.scss index c4d4ffe..7336ee8 100644 --- a/src/styles/_mixins.scss +++ b/src/styles/_mixins.scss @@ -1,9 +1,54 @@ // _mixins.scss +@use "/src/styles/vars" as vars; + +@mixin grid-container($cols: repeat(12, 1fr), $gap: 12px, $gapMobile: 8px) { + display: grid; + grid-template-columns: $cols; + gap: $gap; + + @media screen and (max-width: vars.$breakpoints-md) { + gap: $gapMobile; + } +} + +/// Mixin to manage responsive breakpoints +/// @author Kitty Giraudel +/// @param {String} $breakpoint - Breakpoint name +/// @require $breakpoints +/// +@mixin respond-to($breakpoint) { + // If the key exists in the map + @if map-has-key(vars.$breakpoints, $breakpoint) { + // Prints a media query based on the value + @media (max-width: map-get(vars.$breakpoints, $breakpoint)) { + @content; + } + } + + // If the key doesn't exist in the map + @else { + @warn "Unfortunately, no value could be retrieved from `#{$breakpoint}`. " + + "Available breakpoints are: #{map-keys($breakpoints)}."; + } +} + +@mixin loading-shine { + background: linear-gradient(90deg, #0000 33%, rgba(255, 255, 255, 0.1) 50%, #0000 66%) rgba(255, 255, 255, 0.25); + background-size: 300% 100%; + animation: shine 2s infinite; + + @keyframes shine { + 0% { + background-position: right; + } + } +} + // Mixin for clearfix @mixin clearfix() { &::after { - content: ''; + content: ""; display: table; clear: both; } diff --git a/src/styles/_vars.scss b/src/styles/_vars.scss index ae4ec68..20f7c18 100644 --- a/src/styles/_vars.scss +++ b/src/styles/_vars.scss @@ -12,3 +12,19 @@ $base-line-height: 1.5; // Spacing variables $spacing-unit: 1rem; + +// Breakpoint variables +$breakpoints-xs: 375px; +$breakpoints-sm: 576px; +$breakpoints-md: 768px; +$breakpoints-lg: 992px; +$breakpoints-xl: 1280px; +$breakpoints-xxl: 1440px; +$breakpoints: ( + xs: $breakpoints-xs, + sm: $breakpoints-sm, + md: $breakpoints-md, + lg: $breakpoints-lg, + xl: $breakpoints-xl, + xxl: $breakpoints-xxl +) !default; diff --git a/src/styles/globals.scss b/src/styles/globals.scss index acc8e4c..7827e7b 100644 --- a/src/styles/globals.scss +++ b/src/styles/globals.scss @@ -1,30 +1,87 @@ // globals.scss // Import variables and mixins -@import 'vars'; -@import 'mixins'; -@import 'typography'; +@import "./mixins"; +@import "./typography"; +@import "./vars"; + +// for an intro on how this works, see https://www.youtube.com/watch?v=c13gpBrnGEw&t=839s +.content-grid, +.full-width { + --content-padding: 24px; + display: grid; + row-gap: 0; + grid-template-columns: + [full-width-start] minmax(var(--content-padding), 1fr) + [content-start] min( + calc(100% - (var(--content-padding) * 2)), + calc($breakpoints-xxl + (var(--content-padding) * 2)) + ) + [content-end] + minmax(var(--content-padding), 1fr) [full-width-end]; + + > :not(.full-width) { + grid-column: content; + } + + > .full-width { + grid-column: full-width; + } + + @media screen and (max-width: $breakpoints-lg) { + --content-padding: 16px; + } +} + +.disable-scroll { + overflow: hidden; +} + +// for screen reader only - hides elements from view +.sr-only { + position: absolute; + width: 1px; + height: 1px; + margin: -1px; + padding: 0; + overflow: hidden; + clip: rect(0, 0, 0, 0); + border: 0; +} -// Global reset or normalize styles * { margin: 0; padding: 0; box-sizing: border-box; } -// Body styles +html, body { - background-color: $background-color; - color: $text-color; - font-family: 'Roboto', sans-serif; - font-size: $base-font-size; - line-height: $base-line-height; + max-width: 100dvw; + overflow-x: hidden; +} + +// remove default styles +li { + list-style: none; +} + +a { + color: inherit; + text-decoration: none; +} + +button { + background: none; + color: inherit; + border: none; + padding: 0; + font: inherit; + cursor: pointer; } -// Utility classes -.container { - @include center(80%); - padding: $spacing-unit; +select { + font-family: inherit; } .clearfix { diff --git a/src/ui/components/atoms/Cta/Cta.module.scss b/src/ui/components/atoms/Cta/Cta.module.scss new file mode 100644 index 0000000..2768e7f --- /dev/null +++ b/src/ui/components/atoms/Cta/Cta.module.scss @@ -0,0 +1,94 @@ +@use "@/styles/vars.scss" as vars; + +.cta { + cursor: pointer; + display: inline-flex; + justify-content: center; + align-items: center; + padding: 8px 24px; + width: auto; + transition: all 0.3s ease; + svg *, + &::after { + transition: all 0.3s ease; + } + &.hidden { + display: none; + visibility: hidden; + } + + &.primary { + border-radius: 30px; + background: transparent; + border: 1.5px solid black; + color: black; + svg * { + stroke: black; + } + &:hover { + background: black; + color: white; + svg * { + stroke: white; + } + } + } + + &.secondary { + border-radius: 30px; + background: transparent; + border: 1.5px solid white; + color: white; + svg * { + stroke: white; + } + &:hover { + background: white; + color: black; + svg * { + stroke: black; + } + } + } + + &.icon { + padding: 0; + width: 40px; + height: 40px; + border-radius: 50%; + display: flex; + justify-content: center; + align-items: center; + background: transparent; + border: 1.5px solid black; + svg * { + stroke: black; + } + &:hover { + background: black; + svg * { + stroke: white; + } + } + } + + &.underline { + padding: 4px 0; + position: relative; + &::after { + content: ""; + position: absolute; + left: 0; + bottom: 0; + width: 100%; + height: 1.5px; + background: black; + opacity: 0; + } + &:hover { + &::after { + opacity: 1; + } + } + } +} diff --git a/src/ui/components/atoms/Cta/Cta.stories.tsx b/src/ui/components/atoms/Cta/Cta.stories.tsx new file mode 100644 index 0000000..5735f57 --- /dev/null +++ b/src/ui/components/atoms/Cta/Cta.stories.tsx @@ -0,0 +1,59 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { fn } from "@storybook/test"; +import Cta from "./Cta"; +import SvgChevronRight from "../Svg/SvgChevronRight"; + +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta: Meta = { + title: "Atoms/Cta", + component: Cta, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + layout: "centered", + }, + // More on argTypes: https://storybook.js.org/docs/api/argtypes + // argTypes: { + // backgroundColor: { control: "color" }, + // }, + // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args + args: { onClick: fn() }, +}; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const Primary: Story = { + args: { + variant: "primary", + children: "CTA", + }, +}; + +export const Secondary: Story = { + args: { + variant: "secondary", + children: "CTA", + }, + globals: { + backgrounds: { value: "dark" }, + }, +}; + +export const Icon: Story = { + args: { + variant: "icon", + }, + render: (args) => ( + + + + ), +}; + +export const Underline: Story = { + args: { + variant: "underline", + children: "CTA", + }, +}; diff --git a/src/ui/components/atoms/Cta/Cta.test.tsx b/src/ui/components/atoms/Cta/Cta.test.tsx new file mode 100644 index 0000000..942914d --- /dev/null +++ b/src/ui/components/atoms/Cta/Cta.test.tsx @@ -0,0 +1,58 @@ +import { fireEvent, render, screen } from "@testing-library/react"; +import Cta, { CtaProps } from "./Cta"; + +const dummyData: CtaProps = { + variant: "primary", +}; + +describe(Cta, () => { + it("should render a button element if no href is specified", () => { + const { getByRole } = render(); + const cta = getByRole("button"); + expect(cta).toBeInTheDocument(); + }); + + it("should render a Link element if a href is specified", () => { + const { getByRole } = render( + + ); + const cta = getByRole("link"); + expect(cta).toBeInTheDocument(); + expect(cta).toHaveAttribute("href", "https://www.google.com"); + }); + + it("should render the correct children text", () => { + render(Submit); + render( + + Submit + + ); + const button = screen.getByRole("button"); + const link = screen.getByRole("link"); + // Check if the button contains the correct text + expect(button).toHaveTextContent("Submit"); + expect(link).toHaveTextContent("Submit"); + }); + + it("should call the onClick callback when clicked", () => { + const handleClick = jest.fn(); // Mock function + render(); + render(); + const button = screen.getByRole("button"); + const link = screen.getByRole("link"); + fireEvent.click(button); + fireEvent.click(link); + // Expect the mock function to have been called twice + expect(handleClick).toHaveBeenCalledTimes(2); + }); + + // it("should not display if isHidden property is set to true", () => { + // render(); + // render(); + // const button = screen.queryByRole("button"); + // const link = screen.queryByRole("link"); + // expect(button).toHaveClass("hidden"); + // expect(link).toHaveClass("hidden"); + // }); +}); diff --git a/src/ui/components/atoms/Cta/Cta.tsx b/src/ui/components/atoms/Cta/Cta.tsx new file mode 100644 index 0000000..85b8b3b --- /dev/null +++ b/src/ui/components/atoms/Cta/Cta.tsx @@ -0,0 +1,77 @@ +"use client"; + +import classNames from "classnames"; +import styles from "./Cta.module.scss"; +import Link from "next/link"; +import { FC, PropsWithChildren } from "react"; + +export interface CtaProps extends PropsWithChildren { + className?: string; + variant: "primary" | "secondary" | "icon" | "underline"; + type?: "button" | "submit" | "reset"; + ariaLabel?: string; + href?: string; + newTab?: boolean; + download?: boolean; + onClick?: () => void; + isHidden?: boolean; +} + +const Cta: FC = ({ + className = "", + variant, + type, + ariaLabel, + href, + newTab = false, + download, + onClick = () => { + return; + }, + isHidden, + children, +}) => { + const classNamesString = classNames( + className, + styles.cta, + styles[`${variant}`], + { [styles.hidden]: isHidden } + ); + + return ( + <> + {href ? ( + { + onClick(); + }} + target={newTab ? "_blank" : undefined} + rel={newTab ? "noopener noreferrer" : undefined} + download={download} + > + {children} + + ) : ( + + )} + + ); +}; + +export default Cta; diff --git a/src/ui/components/atoms/Cta/index.ts b/src/ui/components/atoms/Cta/index.ts new file mode 100644 index 0000000..d2d422c --- /dev/null +++ b/src/ui/components/atoms/Cta/index.ts @@ -0,0 +1,3 @@ +import Cta from "./Cta"; + +export default Cta; diff --git a/src/ui/components/Header.stories.tsx b/src/ui/components/atoms/Header/Header.stories.tsx similarity index 94% rename from src/ui/components/Header.stories.tsx rename to src/ui/components/atoms/Header/Header.stories.tsx index b99edfe..7e57918 100644 --- a/src/ui/components/Header.stories.tsx +++ b/src/ui/components/atoms/Header/Header.stories.tsx @@ -2,7 +2,7 @@ import type { Meta, StoryObj } from "@storybook/react"; import { Header } from "./Header"; const meta: Meta = { - title: "Components/Header", + title: "Atoms/Header", component: Header, tags: ["autodocs"], argTypes: { diff --git a/src/ui/components/Header.tsx b/src/ui/components/atoms/Header/Header.tsx similarity index 88% rename from src/ui/components/Header.tsx rename to src/ui/components/atoms/Header/Header.tsx index efdf22a..b2516dc 100644 --- a/src/ui/components/Header.tsx +++ b/src/ui/components/atoms/Header/Header.tsx @@ -1,6 +1,6 @@ "use client"; import React from "react"; -import { User } from "../../models"; +import { User } from "@/models"; interface HeaderProps { user: User; diff --git a/src/ui/components/atoms/NextImage/NextImage.module.scss b/src/ui/components/atoms/NextImage/NextImage.module.scss new file mode 100644 index 0000000..ead7ad0 --- /dev/null +++ b/src/ui/components/atoms/NextImage/NextImage.module.scss @@ -0,0 +1,8 @@ +.imageWrapper { + position: relative; + width: inherit; + height: inherit; + display: flex; + justify-content: center; + align-items: center; +} diff --git a/src/ui/components/atoms/NextImage/NextImage.test.tsx b/src/ui/components/atoms/NextImage/NextImage.test.tsx new file mode 100644 index 0000000..bfc35f5 --- /dev/null +++ b/src/ui/components/atoms/NextImage/NextImage.test.tsx @@ -0,0 +1,81 @@ +import { render } from "@testing-library/react"; +import NextImage, { NextImageProps } from "./NextImage"; + +describe("NextImage", () => { + it("should render an image ", () => { + const dummyData: NextImageProps = { + image: { + url: "/wiser.svg", + alt: "wiser logo", + height: 100, + width: 100, + }, + }; + const { getByRole } = render(); + const imageElement = getByRole("img"); + expect(imageElement).toBeInTheDocument(); + expect(imageElement).toHaveAttribute("src", dummyData.image.url); + expect(imageElement).toHaveAttribute("alt", dummyData.image.alt); + }); + + it("should render an image with original image width and image height if width, height and fill are not specified", () => { + const dummyData: NextImageProps = { + image: { + url: "/wiser.svg", + alt: "wiser logo", + height: 100, + width: 100, + }, + }; + const { getByRole } = render(); + const imageElement = getByRole("img"); + expect(imageElement).toBeInTheDocument(); + expect(imageElement).toHaveAttribute("width", `${dummyData.image.width}`); + expect(imageElement).toHaveAttribute("height", `${dummyData.image.height}`); + }); + + it("should render an image with specified width and height if they are passed in as attributes", () => { + const dummyData: NextImageProps = { + image: { + url: "/wiser.svg", + alt: "wiser logo", + height: 100, + width: 100, + }, + width: 25, + height: 50, + }; + const { getByRole } = render(); + const imageElement = getByRole("img"); + expect(imageElement).toBeInTheDocument(); + expect(imageElement).toHaveAttribute("width", `${dummyData.width}`); + expect(imageElement).toHaveAttribute("height", `${dummyData.height}`); + }); + + it("should render an image with width and height of parent wrapper when 'fill' is specified", () => { + const dummyData: NextImageProps = { + image: { + url: "/wiser.svg", + alt: "wiser logo", + height: 100, + width: 100, + }, + fill: true, + }; + const { getByRole } = render( +
+ +
+ ); + const imageElement = getByRole("img"); + expect(imageElement).toBeInTheDocument(); + expect(getComputedStyle(imageElement).width).toBe("100%"); + expect(getComputedStyle(imageElement).height).toBe("100%"); + expect( + getComputedStyle(imageElement.parentElement!.parentElement!).width + ).toBe("25px"); + expect( + getComputedStyle(imageElement.parentElement!.parentElement!).height + ).toBe("50px"); + }); +}); diff --git a/src/ui/components/atoms/NextImage/NextImage.tsx b/src/ui/components/atoms/NextImage/NextImage.tsx new file mode 100644 index 0000000..a657c47 --- /dev/null +++ b/src/ui/components/atoms/NextImage/NextImage.tsx @@ -0,0 +1,55 @@ +import styles from "./NextImage.module.scss"; +import { ImageDTO } from "@/models/image-dto"; +import classNames from "classnames"; +import Image from "next/image"; +import { CSSProperties, FC } from "react"; + +export interface NextImageProps { + className?: string; + image: ImageDTO; + width?: number; + height?: number; + fill?: boolean; + objectFit?: "fill" | "contain" | "cover" | "none" | "scale-down"; + objectPosition?: string; + style?: CSSProperties; + priority?: boolean; +} + +const NextImage: FC = ({ + className = "", + image, + width, + height, + fill, + objectFit, + objectPosition, + style = {}, + priority, +}) => { + const fx = image.focalX ? image.focalX : 50; + const fy = image.focalY ? image.focalY : 50; + + return ( +
+ {image.alt} { + // ScrollTrigger.refresh(); + // }} + /> +
+ ); +}; + +export default NextImage; diff --git a/src/ui/components/atoms/Svg/SvgArrowRight.tsx b/src/ui/components/atoms/Svg/SvgArrowRight.tsx new file mode 100644 index 0000000..9125f69 --- /dev/null +++ b/src/ui/components/atoms/Svg/SvgArrowRight.tsx @@ -0,0 +1,20 @@ +const SvgArrowRight = () => { + return ( + + + + ); +}; + +export default SvgArrowRight; diff --git a/src/ui/components/atoms/Svg/SvgChevronRight.tsx b/src/ui/components/atoms/Svg/SvgChevronRight.tsx new file mode 100644 index 0000000..7a47160 --- /dev/null +++ b/src/ui/components/atoms/Svg/SvgChevronRight.tsx @@ -0,0 +1,20 @@ +const SvgChevronRight = () => { + return ( + + + + ); +}; + +export default SvgChevronRight; diff --git a/src/ui/components/atoms/Svg/SvgSearch.tsx b/src/ui/components/atoms/Svg/SvgSearch.tsx new file mode 100644 index 0000000..0b9a428 --- /dev/null +++ b/src/ui/components/atoms/Svg/SvgSearch.tsx @@ -0,0 +1,26 @@ +const SvgSearch = () => { + return ( + + + + + ); +}; + +export default SvgSearch; diff --git a/src/ui/components/atoms/index.ts b/src/ui/components/atoms/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/ui/components/index.ts b/src/ui/components/index.ts index eed75a1..1c94bc8 100644 --- a/src/ui/components/index.ts +++ b/src/ui/components/index.ts @@ -1 +1 @@ -export * from "./Header"; \ No newline at end of file +export * from "./atoms/Header/Header"; diff --git a/src/ui/components/molecules/Carousel/Carousel.module.scss b/src/ui/components/molecules/Carousel/Carousel.module.scss new file mode 100644 index 0000000..7015bf1 --- /dev/null +++ b/src/ui/components/molecules/Carousel/Carousel.module.scss @@ -0,0 +1,68 @@ +@use "@/styles/vars.scss" as vars; +@use "@/styles/mixins.scss" as mixins; + +.carousel { + &:global(.splide--draggable) { + cursor: grab; + &:active { + cursor: grabbing; + } + } + + .carouselGrid { + position: relative; + @include mixins.grid-container; + align-items: center; + } + + :global(.splide__track) { + z-index: 1; + grid-column: 2 / 12; + } + + :global(.splide__arrows) { + position: absolute; + width: 100%; + height: 100%; + @include mixins.grid-container; + :global(.splide__arrow__wrapper) { + width: 100%; + height: 100%; + cursor: default; + display: flex; + justify-content: center; + align-items: center; + } + :global(.splide__arrow__wrapper--prev) { + grid-column: 1 / 2; + } + :global(.splide__arrow__wrapper--next) { + grid-column: -2 / -1; + } + :global(.splide__arrow--prev) { + transform: rotate(180deg); + } + :global(.splide__arrow) { + &:disabled { + pointer-events: none; + opacity: 0.25; + } + } + } + + :global(.splide__pagination) { + margin-top: 16px; + grid-column: 2/12; + gap: 8px; + } + + :global(.splide__pagination__page) { + width: 8px; + height: 8px; + border-radius: 50%; + border: 1.5px solid black; + &:global(.is-active) { + background: black; + } + } +} diff --git a/src/ui/components/molecules/Carousel/Carousel.stories.tsx b/src/ui/components/molecules/Carousel/Carousel.stories.tsx new file mode 100644 index 0000000..92a75e9 --- /dev/null +++ b/src/ui/components/molecules/Carousel/Carousel.stories.tsx @@ -0,0 +1,57 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import Carousel from "./Carousel"; +import { ComponentProps } from "react"; + +type StoryProps = ComponentProps & { + numOfSlides: number; +}; + +const meta: Meta = { + title: "Molecules/Carousel", + component: Carousel, + parameters: { + layout: "fullscreen", + }, + args: { + numOfSlides: 6, + }, + decorators: [ + (Story) => ( +
+ +
+ ), + ], + render: ({ numOfSlides, ...args }) => ( +
+ { + const PHI = (1 + Math.sqrt(5)) / 2; + const n = i * PHI - Math.floor(i * PHI); + const hue = Math.floor(n * 256); + + return ( +
{`Slide ${i + 1}`}
+ ); + })} + /> +
+ ), +}; + +export default meta; +type Story = StoryObj; + +export const Primary: Story = { + args: {}, +}; diff --git a/src/ui/components/molecules/Carousel/Carousel.test.tsx b/src/ui/components/molecules/Carousel/Carousel.test.tsx new file mode 100644 index 0000000..f16a684 --- /dev/null +++ b/src/ui/components/molecules/Carousel/Carousel.test.tsx @@ -0,0 +1,16 @@ +import { render } from "@testing-library/react"; +import Carousel from "./Carousel"; + +const generateSlides = (numOfSlides: number) => { + return Array.from({ length: numOfSlides }, (_, i) => { + return
{`Slide ${i + 1}`}
; + }); +}; + +describe(Carousel, () => { + it("should render correct number of slides", () => { + const { queryAllByRole } = render(); + const slides = queryAllByRole("tabpanel"); + expect(slides).toHaveLength(3); + }); +}); diff --git a/src/ui/components/molecules/Carousel/Carousel.tsx b/src/ui/components/molecules/Carousel/Carousel.tsx new file mode 100644 index 0000000..71c6a36 --- /dev/null +++ b/src/ui/components/molecules/Carousel/Carousel.tsx @@ -0,0 +1,74 @@ +"use client"; + +import styles from "./Carousel.module.scss"; +import { FC, JSX } from "react"; +import classNames from "classnames"; +import { Splide, SplideTrack, SplideSlide } from "@splidejs/react-splide"; +import "@splidejs/splide/css/core"; +import Cta from "@/ui/components/atoms/Cta/Cta"; +import SvgArrowRight from "@/ui/components/atoms/Svg/SvgArrowRight"; +import HorizontalWheel from "@/utils/splide/horizontal-wheel"; + +export interface CarouselProps { + splideRef?: any; + className?: string; + ariaLabel?: string; + slides: JSX.Element[]; + options?: Record; +} + +const Carousel: FC = ({ + splideRef, + className = "", + ariaLabel, + slides, + options = {}, +}) => { + const defaultOptions = { + arrows: true, + autoHeight: true, + autoWidth: true, + gap: 12, + keyboard: true, + pagination: true, + updateOnMove: true, + wheelMinThreshold: 10, + wheelSleep: 350, + }; + + return ( + +
+ + {slides.map((slide, index) => { + return {slide}; + })} + + +
+
+ + + +
+
+ + + +
+
+
+ +
    +
    + ); +}; + +export default Carousel; diff --git a/src/ui/components/molecules/Carousel/index.ts b/src/ui/components/molecules/Carousel/index.ts new file mode 100644 index 0000000..7d6c38c --- /dev/null +++ b/src/ui/components/molecules/Carousel/index.ts @@ -0,0 +1,3 @@ +import Carousel from "./Carousel"; + +export default Carousel; diff --git a/src/ui/components/molecules/index.ts b/src/ui/components/molecules/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/ui/components/organisms/Downloader/Downloader.stories.tsx b/src/ui/components/organisms/Downloader/Downloader.stories.tsx new file mode 100644 index 0000000..9e164bc --- /dev/null +++ b/src/ui/components/organisms/Downloader/Downloader.stories.tsx @@ -0,0 +1,18 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import Downloader from "./Downloader"; + +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta: Meta = { + title: "Organisms/Downloader", + component: Downloader, + parameters: { + layout: "fullscreen", + }, +}; + +export default meta; +type Story = StoryObj; + +export const Primary: Story = { + args: {}, +}; diff --git a/src/ui/components/organisms/Downloader/Downloader.tsx b/src/ui/components/organisms/Downloader/Downloader.tsx new file mode 100644 index 0000000..6fbe5a1 --- /dev/null +++ b/src/ui/components/organisms/Downloader/Downloader.tsx @@ -0,0 +1,19 @@ +import Link from "next/link"; +import Cta from "../../atoms/Cta"; + +const Downloader = () => { + return ( +
    +

    Download

    +
      +
    • + + Top Secret + +
    • +
    +
    + ); +}; + +export default Downloader; diff --git a/src/ui/components/organisms/index.ts b/src/ui/components/organisms/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/ui/pages/Home.tsx b/src/ui/pages/Home.tsx index dc7b7d1..8846773 100644 --- a/src/ui/pages/Home.tsx +++ b/src/ui/pages/Home.tsx @@ -1,6 +1,6 @@ "use client"; import React from "react"; -import { Header } from "../components/Header"; +import { Header } from "../components/atoms/Header/Header"; import { USERS } from "../../data"; import { pickUser } from "../../utils/pickUser"; @@ -14,4 +14,4 @@ export const Home: React.FC = () => {

    This is a simple page component.

    ); -}; \ No newline at end of file +}; diff --git a/src/utils/splide/horizontal-wheel.ts b/src/utils/splide/horizontal-wheel.ts new file mode 100644 index 0000000..57f2130 --- /dev/null +++ b/src/utils/splide/horizontal-wheel.ts @@ -0,0 +1,115 @@ +import { EventInterface } from "@splidejs/splide"; +import { Splide, Components, Options } from "@splidejs/splide"; +import { BaseComponent } from "@splidejs/splide"; + +interface WheelComponent extends BaseComponent {} + +/** + * Extracts the timestamp from the event object. + * + * @param e - An Event object. + */ +function timeOf(e: Event): number { + return e.timeStamp; +} + +/** + * Call the `preventDefault()` of the provided event. + * + * @param e - An Event object. + * @param stopPropagation - Optional. Whether to stop the event propagation or not. + */ +function prevent(e: Event, stopPropagation?: boolean): void { + e.preventDefault(); + + if (stopPropagation) { + e.stopPropagation(); + e.stopImmediatePropagation(); + } +} + +/** + * Component for observing the horizontal mouse wheel and moving the slider. + * + * @param Splide - A Splide instance. + * @param Components - A collection of components. + * @param options - Options. + * + * @return A Wheel component object. + */ +function HorizontalWheel( + Splide: Splide, + Components: Components, + options: Options +): WheelComponent { + const { bind } = EventInterface(Splide); + + /** + * Holds the last time when the wheel moves the slider. + */ + let lastTime = 0; + let lastDeltaX = 0; + + /** + * Called when the component is mounted. + */ + function mount(): void { + bind(Components.Elements.root, "wheel", onWheel, { + passive: false, + capture: true, + }); + } + + /** + * Called when the user rotates the mouse wheel on the slider. + * + * @param e - A WheelEvent object. + */ + function onWheel(e: WheelEvent): void { + if (Math.abs(e.deltaY) < 5) { + // + if (e.cancelable) { + const { deltaX } = e; + const backwards = deltaX < 0; + const timeStamp = timeOf(e); + const min = options.wheelMinThreshold || 0; + const sleep = options.wheelSleep || 0; + + if ( + Math.abs(deltaX) > min && + timeStamp - lastTime > sleep && + Math.abs(deltaX) > Math.abs(lastDeltaX) + ) { + Splide.go(backwards ? "<" : ">"); + lastTime = timeStamp; + } + + lastDeltaX = deltaX; + + shouldPrevent(backwards) && prevent(e); + } + } + } + + /** + * Checks whether the component should prevent the default action of the wheel event or not. + * + * @param backwards - Set this to `true` for backwards direction. + * + * @return `true` if the action should be prevented. + */ + function shouldPrevent(backwards: boolean): boolean { + return ( + !options.releaseWheel || + // || Splide.state.is(MOVING) + Splide.state.is(4) || // MOVING = 4 + Components.Controller.getAdjacent(backwards) !== -1 + ); + } + + return { + mount, + }; +} + +export default HorizontalWheel; diff --git a/tsconfig.json b/tsconfig.json index 9ec8bdd..c48a296 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -35,14 +35,17 @@ /* Module Resolution Options */ "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ - // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + "paths": { + "@/*": ["./src/*"] + }, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ - "typeRoots": [ - "./node_modules/@types" - ], /* List of folders to include type definitions from. */ + // "typeRoots": [ + // "./node_modules/@types" + // ], /* List of folders to include type definitions from. */ "types": [ "node", - "jest" + "jest", + "@testing-library/jest-dom" ], /* Type declaration files to be included in compilation. */ "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ From 30e8c884bfe8ec62816d3f6a1bb185f1ababe99b Mon Sep 17 00:00:00 2001 From: Stephen Savage Date: Thu, 27 Feb 2025 12:14:44 +0000 Subject: [PATCH 02/10] amended storybook configuration --- .storybook/main.ts | 3 +++ .storybook/preview.ts | 11 +++++++++++ 2 files changed, 14 insertions(+) diff --git a/.storybook/main.ts b/.storybook/main.ts index a38730e..6b75ec0 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -12,5 +12,8 @@ const config: StorybookConfig = { name: "@storybook/nextjs", options: {}, }, + features: { + backgroundsStoryGlobals: true, + }, }; export default config; diff --git a/.storybook/preview.ts b/.storybook/preview.ts index 37914b1..ab06285 100644 --- a/.storybook/preview.ts +++ b/.storybook/preview.ts @@ -1,13 +1,24 @@ import type { Preview } from "@storybook/react"; +import "@/styles/globals.scss"; + const preview: Preview = { + // default auto-generation of docs: https://storybook.js.org/docs/writing-docs/autodocs + tags: ["autodocs"], parameters: { + docs: { + toc: true, // 👈 Enables the table of contents + }, controls: { matchers: { color: /(background|color)$/i, date: /Date$/i, }, }, + initialGlobals: { + // 👇 Set the initial background color + backgrounds: { value: "light" }, + }, }, }; From f64f4260a59b3b56efec9fc95b62958437f39d73 Mon Sep 17 00:00:00 2001 From: Stephen Savage Date: Thu, 27 Feb 2025 12:20:23 +0000 Subject: [PATCH 03/10] removed redundant svg files --- src/ui/components/atoms/Svg/SvgArrowRight.tsx | 20 -------------- src/ui/components/atoms/Svg/SvgSearch.tsx | 26 ------------------- .../molecules/Carousel/Carousel.tsx | 6 ++--- 3 files changed, 3 insertions(+), 49 deletions(-) delete mode 100644 src/ui/components/atoms/Svg/SvgArrowRight.tsx delete mode 100644 src/ui/components/atoms/Svg/SvgSearch.tsx diff --git a/src/ui/components/atoms/Svg/SvgArrowRight.tsx b/src/ui/components/atoms/Svg/SvgArrowRight.tsx deleted file mode 100644 index 9125f69..0000000 --- a/src/ui/components/atoms/Svg/SvgArrowRight.tsx +++ /dev/null @@ -1,20 +0,0 @@ -const SvgArrowRight = () => { - return ( - - - - ); -}; - -export default SvgArrowRight; diff --git a/src/ui/components/atoms/Svg/SvgSearch.tsx b/src/ui/components/atoms/Svg/SvgSearch.tsx deleted file mode 100644 index 0b9a428..0000000 --- a/src/ui/components/atoms/Svg/SvgSearch.tsx +++ /dev/null @@ -1,26 +0,0 @@ -const SvgSearch = () => { - return ( - - - - - ); -}; - -export default SvgSearch; diff --git a/src/ui/components/molecules/Carousel/Carousel.tsx b/src/ui/components/molecules/Carousel/Carousel.tsx index 71c6a36..1dbb453 100644 --- a/src/ui/components/molecules/Carousel/Carousel.tsx +++ b/src/ui/components/molecules/Carousel/Carousel.tsx @@ -6,7 +6,7 @@ import classNames from "classnames"; import { Splide, SplideTrack, SplideSlide } from "@splidejs/react-splide"; import "@splidejs/splide/css/core"; import Cta from "@/ui/components/atoms/Cta/Cta"; -import SvgArrowRight from "@/ui/components/atoms/Svg/SvgArrowRight"; +import SvgChevronRight from "@/ui/components/atoms/Svg/SvgChevronRight"; import HorizontalWheel from "@/utils/splide/horizontal-wheel"; export interface CarouselProps { @@ -55,12 +55,12 @@ const Carousel: FC = ({
    - +
    - +
    From 0dfed261e79e37acd2c18edd7ab70feaa86f0dff Mon Sep 17 00:00:00 2001 From: Stephen Savage Date: Thu, 27 Feb 2025 12:36:13 +0000 Subject: [PATCH 04/10] fixed exports for all components and atoms, molecules and organisms folders --- src/ui/components/atoms/Header/Header.stories.tsx | 2 +- src/ui/components/atoms/Header/Header.tsx | 4 +++- src/ui/components/atoms/Header/index.ts | 1 + src/ui/components/atoms/NextImage/index.ts | 1 + src/ui/components/atoms/Svg/index.ts | 1 + src/ui/components/atoms/index.ts | 3 +++ src/ui/components/index.ts | 4 +++- src/ui/components/molecules/Carousel/index.ts | 4 +--- src/ui/components/molecules/index.ts | 1 + src/ui/components/organisms/Downloader/index.ts | 1 + src/ui/components/organisms/index.ts | 1 + 11 files changed, 17 insertions(+), 6 deletions(-) create mode 100644 src/ui/components/atoms/Header/index.ts create mode 100644 src/ui/components/atoms/NextImage/index.ts create mode 100644 src/ui/components/atoms/Svg/index.ts create mode 100644 src/ui/components/organisms/Downloader/index.ts diff --git a/src/ui/components/atoms/Header/Header.stories.tsx b/src/ui/components/atoms/Header/Header.stories.tsx index 7e57918..9e2d2e6 100644 --- a/src/ui/components/atoms/Header/Header.stories.tsx +++ b/src/ui/components/atoms/Header/Header.stories.tsx @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from "@storybook/react"; -import { Header } from "./Header"; +import Header from "./Header"; const meta: Meta = { title: "Atoms/Header", diff --git a/src/ui/components/atoms/Header/Header.tsx b/src/ui/components/atoms/Header/Header.tsx index b2516dc..a3f79e5 100644 --- a/src/ui/components/atoms/Header/Header.tsx +++ b/src/ui/components/atoms/Header/Header.tsx @@ -6,10 +6,12 @@ interface HeaderProps { user: User; } -export const Header: React.FC = ({ user }) => { +const Header: React.FC = ({ user }) => { return (

    Hello, {user.name}!

    ); }; + +export default Header; diff --git a/src/ui/components/atoms/Header/index.ts b/src/ui/components/atoms/Header/index.ts new file mode 100644 index 0000000..d88c989 --- /dev/null +++ b/src/ui/components/atoms/Header/index.ts @@ -0,0 +1 @@ +export { default as Header } from "./Header"; diff --git a/src/ui/components/atoms/NextImage/index.ts b/src/ui/components/atoms/NextImage/index.ts new file mode 100644 index 0000000..7f64730 --- /dev/null +++ b/src/ui/components/atoms/NextImage/index.ts @@ -0,0 +1 @@ +export { default as NextImage } from "./NextImage"; diff --git a/src/ui/components/atoms/Svg/index.ts b/src/ui/components/atoms/Svg/index.ts new file mode 100644 index 0000000..efdcf1e --- /dev/null +++ b/src/ui/components/atoms/Svg/index.ts @@ -0,0 +1 @@ +export { default as SvgChevronRight } from "./SvgChevronRight"; diff --git a/src/ui/components/atoms/index.ts b/src/ui/components/atoms/index.ts index e69de29..b6da96f 100644 --- a/src/ui/components/atoms/index.ts +++ b/src/ui/components/atoms/index.ts @@ -0,0 +1,3 @@ +export * from "./Header"; +export * from "./Cta"; +export * from "./Svg"; diff --git a/src/ui/components/index.ts b/src/ui/components/index.ts index 1c94bc8..4a3b232 100644 --- a/src/ui/components/index.ts +++ b/src/ui/components/index.ts @@ -1 +1,3 @@ -export * from "./atoms/Header/Header"; +export * from "./atoms"; +export * from "./molecules"; +export * from "./organisms"; diff --git a/src/ui/components/molecules/Carousel/index.ts b/src/ui/components/molecules/Carousel/index.ts index 7d6c38c..d50f6cd 100644 --- a/src/ui/components/molecules/Carousel/index.ts +++ b/src/ui/components/molecules/Carousel/index.ts @@ -1,3 +1 @@ -import Carousel from "./Carousel"; - -export default Carousel; +export { default as Carousel } from "./Carousel"; diff --git a/src/ui/components/molecules/index.ts b/src/ui/components/molecules/index.ts index e69de29..2aa9303 100644 --- a/src/ui/components/molecules/index.ts +++ b/src/ui/components/molecules/index.ts @@ -0,0 +1 @@ +export * from "./Carousel"; diff --git a/src/ui/components/organisms/Downloader/index.ts b/src/ui/components/organisms/Downloader/index.ts new file mode 100644 index 0000000..c34e168 --- /dev/null +++ b/src/ui/components/organisms/Downloader/index.ts @@ -0,0 +1 @@ +export { default as Downloader } from "./Downloader"; diff --git a/src/ui/components/organisms/index.ts b/src/ui/components/organisms/index.ts index e69de29..4f4a272 100644 --- a/src/ui/components/organisms/index.ts +++ b/src/ui/components/organisms/index.ts @@ -0,0 +1 @@ +export * from "./Downloader"; From 78761598c3ebf24ba54ae1d12dd05e99fb5b1899 Mon Sep 17 00:00:00 2001 From: Stephen Savage Date: Thu, 27 Feb 2025 16:10:46 +0000 Subject: [PATCH 05/10] removed carousel component and fixed small bugs --- jest.setup.ts | 16 --- package-lock.json | 14 --- package.json | 1 - src/styles/globals.scss | 6 +- src/ui/components/atoms/index.ts | 3 +- .../molecules/Carousel/Carousel.module.scss | 68 ----------- .../molecules/Carousel/Carousel.stories.tsx | 57 --------- .../molecules/Carousel/Carousel.test.tsx | 16 --- .../molecules/Carousel/Carousel.tsx | 74 ----------- src/ui/components/molecules/Carousel/index.ts | 1 - src/ui/components/molecules/index.ts | 2 +- src/ui/pages/Home.tsx | 2 +- src/utils/splide/horizontal-wheel.ts | 115 ------------------ 13 files changed, 7 insertions(+), 368 deletions(-) delete mode 100644 src/ui/components/molecules/Carousel/Carousel.module.scss delete mode 100644 src/ui/components/molecules/Carousel/Carousel.stories.tsx delete mode 100644 src/ui/components/molecules/Carousel/Carousel.test.tsx delete mode 100644 src/ui/components/molecules/Carousel/Carousel.tsx delete mode 100644 src/ui/components/molecules/Carousel/index.ts delete mode 100644 src/utils/splide/horizontal-wheel.ts diff --git a/jest.setup.ts b/jest.setup.ts index 432c4e8..d0de870 100644 --- a/jest.setup.ts +++ b/jest.setup.ts @@ -1,17 +1 @@ import "@testing-library/jest-dom"; - -jest.mock("@splidejs/splide/css/core", () => ""); - -Object.defineProperty(window, "matchMedia", { - writable: true, - value: jest.fn().mockImplementation((query) => ({ - matches: false, - media: query, - onchange: null, - addListener: jest.fn(), // Deprecated - removeListener: jest.fn(), // Deprecated - addEventListener: jest.fn(), - removeEventListener: jest.fn(), - dispatchEvent: jest.fn(), - })), -}); diff --git a/package-lock.json b/package-lock.json index 1fa7976..a3011b6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,6 @@ "hasInstallScript": true, "license": "MIT", "dependencies": { - "@splidejs/react-splide": "^0.7.12", "classnames": "^2.5.1", "ini": "^5.0.0", "next": "^15.0.0", @@ -3376,19 +3375,6 @@ "@sinonjs/commons": "^3.0.0" } }, - "node_modules/@splidejs/react-splide": { - "version": "0.7.12", - "resolved": "https://registry.npmjs.org/@splidejs/react-splide/-/react-splide-0.7.12.tgz", - "integrity": "sha512-UfXH+j47jsMc4x5HA/aOwuuHPqn6y9+ZTNYPWDRD8iLKvIVMZlzq2unjUEvyDAU+TTVPZOXkG2Ojeoz0P4AkZw==", - "dependencies": { - "@splidejs/splide": "^4.1.3" - } - }, - "node_modules/@splidejs/splide": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@splidejs/splide/-/splide-4.1.4.tgz", - "integrity": "sha512-5I30evTJcAJQXt6vJ26g2xEkG+l1nXcpEw4xpKh0/FWQ8ozmAeTbtniVtVmz2sH1Es3vgfC4SS8B2X4o5JMptA==" - }, "node_modules/@storybook/addon-actions": { "version": "8.6.0", "resolved": "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-8.6.0.tgz", diff --git a/package.json b/package.json index 9375e35..682ac9e 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,6 @@ "build-storybook": "storybook build" }, "dependencies": { - "@splidejs/react-splide": "^0.7.12", "classnames": "^2.5.1", "ini": "^5.0.0", "next": "^15.0.0", diff --git a/src/styles/globals.scss b/src/styles/globals.scss index 7827e7b..ffbefe9 100644 --- a/src/styles/globals.scss +++ b/src/styles/globals.scss @@ -1,9 +1,9 @@ // globals.scss // Import variables and mixins -@import "./mixins"; -@import "./typography"; -@import "./vars"; +@import "vars"; +@import "mixins"; +@import "typography"; // for an intro on how this works, see https://www.youtube.com/watch?v=c13gpBrnGEw&t=839s .content-grid, diff --git a/src/ui/components/atoms/index.ts b/src/ui/components/atoms/index.ts index b6da96f..911a2c0 100644 --- a/src/ui/components/atoms/index.ts +++ b/src/ui/components/atoms/index.ts @@ -1,3 +1,4 @@ -export * from "./Header"; export * from "./Cta"; +export * from "./Header"; +export * from "./NextImage"; export * from "./Svg"; diff --git a/src/ui/components/molecules/Carousel/Carousel.module.scss b/src/ui/components/molecules/Carousel/Carousel.module.scss deleted file mode 100644 index 7015bf1..0000000 --- a/src/ui/components/molecules/Carousel/Carousel.module.scss +++ /dev/null @@ -1,68 +0,0 @@ -@use "@/styles/vars.scss" as vars; -@use "@/styles/mixins.scss" as mixins; - -.carousel { - &:global(.splide--draggable) { - cursor: grab; - &:active { - cursor: grabbing; - } - } - - .carouselGrid { - position: relative; - @include mixins.grid-container; - align-items: center; - } - - :global(.splide__track) { - z-index: 1; - grid-column: 2 / 12; - } - - :global(.splide__arrows) { - position: absolute; - width: 100%; - height: 100%; - @include mixins.grid-container; - :global(.splide__arrow__wrapper) { - width: 100%; - height: 100%; - cursor: default; - display: flex; - justify-content: center; - align-items: center; - } - :global(.splide__arrow__wrapper--prev) { - grid-column: 1 / 2; - } - :global(.splide__arrow__wrapper--next) { - grid-column: -2 / -1; - } - :global(.splide__arrow--prev) { - transform: rotate(180deg); - } - :global(.splide__arrow) { - &:disabled { - pointer-events: none; - opacity: 0.25; - } - } - } - - :global(.splide__pagination) { - margin-top: 16px; - grid-column: 2/12; - gap: 8px; - } - - :global(.splide__pagination__page) { - width: 8px; - height: 8px; - border-radius: 50%; - border: 1.5px solid black; - &:global(.is-active) { - background: black; - } - } -} diff --git a/src/ui/components/molecules/Carousel/Carousel.stories.tsx b/src/ui/components/molecules/Carousel/Carousel.stories.tsx deleted file mode 100644 index 92a75e9..0000000 --- a/src/ui/components/molecules/Carousel/Carousel.stories.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react"; -import Carousel from "./Carousel"; -import { ComponentProps } from "react"; - -type StoryProps = ComponentProps & { - numOfSlides: number; -}; - -const meta: Meta = { - title: "Molecules/Carousel", - component: Carousel, - parameters: { - layout: "fullscreen", - }, - args: { - numOfSlides: 6, - }, - decorators: [ - (Story) => ( -
    - -
    - ), - ], - render: ({ numOfSlides, ...args }) => ( -
    - { - const PHI = (1 + Math.sqrt(5)) / 2; - const n = i * PHI - Math.floor(i * PHI); - const hue = Math.floor(n * 256); - - return ( -
    {`Slide ${i + 1}`}
    - ); - })} - /> -
    - ), -}; - -export default meta; -type Story = StoryObj; - -export const Primary: Story = { - args: {}, -}; diff --git a/src/ui/components/molecules/Carousel/Carousel.test.tsx b/src/ui/components/molecules/Carousel/Carousel.test.tsx deleted file mode 100644 index f16a684..0000000 --- a/src/ui/components/molecules/Carousel/Carousel.test.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { render } from "@testing-library/react"; -import Carousel from "./Carousel"; - -const generateSlides = (numOfSlides: number) => { - return Array.from({ length: numOfSlides }, (_, i) => { - return
    {`Slide ${i + 1}`}
    ; - }); -}; - -describe(Carousel, () => { - it("should render correct number of slides", () => { - const { queryAllByRole } = render(); - const slides = queryAllByRole("tabpanel"); - expect(slides).toHaveLength(3); - }); -}); diff --git a/src/ui/components/molecules/Carousel/Carousel.tsx b/src/ui/components/molecules/Carousel/Carousel.tsx deleted file mode 100644 index 1dbb453..0000000 --- a/src/ui/components/molecules/Carousel/Carousel.tsx +++ /dev/null @@ -1,74 +0,0 @@ -"use client"; - -import styles from "./Carousel.module.scss"; -import { FC, JSX } from "react"; -import classNames from "classnames"; -import { Splide, SplideTrack, SplideSlide } from "@splidejs/react-splide"; -import "@splidejs/splide/css/core"; -import Cta from "@/ui/components/atoms/Cta/Cta"; -import SvgChevronRight from "@/ui/components/atoms/Svg/SvgChevronRight"; -import HorizontalWheel from "@/utils/splide/horizontal-wheel"; - -export interface CarouselProps { - splideRef?: any; - className?: string; - ariaLabel?: string; - slides: JSX.Element[]; - options?: Record; -} - -const Carousel: FC = ({ - splideRef, - className = "", - ariaLabel, - slides, - options = {}, -}) => { - const defaultOptions = { - arrows: true, - autoHeight: true, - autoWidth: true, - gap: 12, - keyboard: true, - pagination: true, - updateOnMove: true, - wheelMinThreshold: 10, - wheelSleep: 350, - }; - - return ( - -
    - - {slides.map((slide, index) => { - return {slide}; - })} - - -
    -
    - - - -
    -
    - - - -
    -
    -
    - -
      -
      - ); -}; - -export default Carousel; diff --git a/src/ui/components/molecules/Carousel/index.ts b/src/ui/components/molecules/Carousel/index.ts deleted file mode 100644 index d50f6cd..0000000 --- a/src/ui/components/molecules/Carousel/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as Carousel } from "./Carousel"; diff --git a/src/ui/components/molecules/index.ts b/src/ui/components/molecules/index.ts index 2aa9303..8b13789 100644 --- a/src/ui/components/molecules/index.ts +++ b/src/ui/components/molecules/index.ts @@ -1 +1 @@ -export * from "./Carousel"; + diff --git a/src/ui/pages/Home.tsx b/src/ui/pages/Home.tsx index 8846773..0cd74a5 100644 --- a/src/ui/pages/Home.tsx +++ b/src/ui/pages/Home.tsx @@ -1,6 +1,6 @@ "use client"; import React from "react"; -import { Header } from "../components/atoms/Header/Header"; +import Header from "../components/atoms/Header/Header"; import { USERS } from "../../data"; import { pickUser } from "../../utils/pickUser"; diff --git a/src/utils/splide/horizontal-wheel.ts b/src/utils/splide/horizontal-wheel.ts deleted file mode 100644 index 57f2130..0000000 --- a/src/utils/splide/horizontal-wheel.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { EventInterface } from "@splidejs/splide"; -import { Splide, Components, Options } from "@splidejs/splide"; -import { BaseComponent } from "@splidejs/splide"; - -interface WheelComponent extends BaseComponent {} - -/** - * Extracts the timestamp from the event object. - * - * @param e - An Event object. - */ -function timeOf(e: Event): number { - return e.timeStamp; -} - -/** - * Call the `preventDefault()` of the provided event. - * - * @param e - An Event object. - * @param stopPropagation - Optional. Whether to stop the event propagation or not. - */ -function prevent(e: Event, stopPropagation?: boolean): void { - e.preventDefault(); - - if (stopPropagation) { - e.stopPropagation(); - e.stopImmediatePropagation(); - } -} - -/** - * Component for observing the horizontal mouse wheel and moving the slider. - * - * @param Splide - A Splide instance. - * @param Components - A collection of components. - * @param options - Options. - * - * @return A Wheel component object. - */ -function HorizontalWheel( - Splide: Splide, - Components: Components, - options: Options -): WheelComponent { - const { bind } = EventInterface(Splide); - - /** - * Holds the last time when the wheel moves the slider. - */ - let lastTime = 0; - let lastDeltaX = 0; - - /** - * Called when the component is mounted. - */ - function mount(): void { - bind(Components.Elements.root, "wheel", onWheel, { - passive: false, - capture: true, - }); - } - - /** - * Called when the user rotates the mouse wheel on the slider. - * - * @param e - A WheelEvent object. - */ - function onWheel(e: WheelEvent): void { - if (Math.abs(e.deltaY) < 5) { - // - if (e.cancelable) { - const { deltaX } = e; - const backwards = deltaX < 0; - const timeStamp = timeOf(e); - const min = options.wheelMinThreshold || 0; - const sleep = options.wheelSleep || 0; - - if ( - Math.abs(deltaX) > min && - timeStamp - lastTime > sleep && - Math.abs(deltaX) > Math.abs(lastDeltaX) - ) { - Splide.go(backwards ? "<" : ">"); - lastTime = timeStamp; - } - - lastDeltaX = deltaX; - - shouldPrevent(backwards) && prevent(e); - } - } - } - - /** - * Checks whether the component should prevent the default action of the wheel event or not. - * - * @param backwards - Set this to `true` for backwards direction. - * - * @return `true` if the action should be prevented. - */ - function shouldPrevent(backwards: boolean): boolean { - return ( - !options.releaseWheel || - // || Splide.state.is(MOVING) - Splide.state.is(4) || // MOVING = 4 - Components.Controller.getAdjacent(backwards) !== -1 - ); - } - - return { - mount, - }; -} - -export default HorizontalWheel; From ff145366606e9ceac6b997ee780010c928052ff1 Mon Sep 17 00:00:00 2001 From: Stephen Savage Date: Thu, 27 Feb 2025 16:12:01 +0000 Subject: [PATCH 06/10] removed export of ./molecules as there are no components yet --- src/ui/components/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/components/index.ts b/src/ui/components/index.ts index 4a3b232..3ce38df 100644 --- a/src/ui/components/index.ts +++ b/src/ui/components/index.ts @@ -1,3 +1,3 @@ export * from "./atoms"; -export * from "./molecules"; +// export * from "./molecules"; export * from "./organisms"; From e5de6570a715ae7433792aa15dbc85e80e3c63e1 Mon Sep 17 00:00:00 2001 From: Stephen Savage Date: Thu, 27 Feb 2025 17:57:56 +0000 Subject: [PATCH 07/10] fixed TS6133 error --- src/ui/components/organisms/Downloader/Downloader.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ui/components/organisms/Downloader/Downloader.tsx b/src/ui/components/organisms/Downloader/Downloader.tsx index 6fbe5a1..a5c1f57 100644 --- a/src/ui/components/organisms/Downloader/Downloader.tsx +++ b/src/ui/components/organisms/Downloader/Downloader.tsx @@ -1,4 +1,3 @@ -import Link from "next/link"; import Cta from "../../atoms/Cta"; const Downloader = () => { From edb5cd8e35a03917fa6e051879dda335eb10b86d Mon Sep 17 00:00:00 2001 From: John Fedoruk Date: Thu, 27 Feb 2025 14:54:04 -0600 Subject: [PATCH 08/10] Fixed scss error --- webpack.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webpack.config.js b/webpack.config.js index 93124db..8596d33 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -58,7 +58,7 @@ module.exports = { }, plugins: [ new MiniCssExtractPlugin({ - filename: "globals.css", + filename: "[name].css", }), new CopyWebpackPlugin({ patterns: [ From b43cb60d70b3760809883d4c9210ff618b9e36b6 Mon Sep 17 00:00:00 2001 From: John Fedoruk Date: Mon, 3 Mar 2025 18:12:02 -0600 Subject: [PATCH 09/10] Fixed import issue with @ alias --- webpack.config.js | 1 + 1 file changed, 1 insertion(+) diff --git a/webpack.config.js b/webpack.config.js index 8596d33..1616c5f 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -49,6 +49,7 @@ module.exports = { alias: { react: path.dirname(require.resolve("react/package.json")), "react-dom": path.dirname(require.resolve("react-dom/package.json")), + "@": path.resolve(__dirname, "src"), }, }, externals: { From 0b94d29c7b5c72235fa7e37226320d12f4ea085c Mon Sep 17 00:00:00 2001 From: John Fedoruk Date: Mon, 3 Mar 2025 18:13:13 -0600 Subject: [PATCH 10/10] Fixed additional build errors related to SCSS --- webpack.config.js | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/webpack.config.js b/webpack.config.js index 1616c5f..582a13a 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -38,10 +38,26 @@ module.exports = { test: /\.scss$/, use: [ MiniCssExtractPlugin.loader, - "css-loader", + { + loader: "css-loader", + options: { + modules: { + auto: true, + localIdentName: "[local]_[hash:base64:5]", + }, + esModule: false, + }, + }, "sass-loader" ], }, + { + test: /\.(woff(2)?|ttf|eot|otf)$/, + type: 'asset/resource', + generator: { + filename: 'fonts/[name][ext]' + }, + }, ] }, resolve: { @@ -74,4 +90,4 @@ module.exports = { ], }), ], -}; +}; \ No newline at end of file