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" }, + }, }, }; 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/package-lock.json b/package-lock.json index f61e3e7..a3011b6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "hasInstallScript": true, "license": "MIT", "dependencies": { + "classnames": "^2.5.1", "ini": "^5.0.0", "next": "^15.0.0", "react": "19.0.0", @@ -46,6 +47,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", @@ -6726,6 +6728,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 +9233,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 +9617,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..682ac9e 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "build-storybook": "storybook build" }, "dependencies": { + "classnames": "^2.5.1", "ini": "^5.0.0", "next": "^15.0.0", "react": "19.0.0", @@ -73,9 +74,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 +81,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 +97,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 +108,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..ffbefe9 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 "vars"; +@import "mixins"; +@import "typography"; + +// 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 88% rename from src/ui/components/Header.stories.tsx rename to src/ui/components/atoms/Header/Header.stories.tsx index b99edfe..9e2d2e6 100644 --- a/src/ui/components/Header.stories.tsx +++ b/src/ui/components/atoms/Header/Header.stories.tsx @@ -1,8 +1,8 @@ import type { Meta, StoryObj } from "@storybook/react"; -import { Header } from "./Header"; +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 66% rename from src/ui/components/Header.tsx rename to src/ui/components/atoms/Header/Header.tsx index efdf22a..a3f79e5 100644 --- a/src/ui/components/Header.tsx +++ b/src/ui/components/atoms/Header/Header.tsx @@ -1,15 +1,17 @@ "use client"; import React from "react"; -import { User } from "../../models"; +import { User } from "@/models"; 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/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/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/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/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 new file mode 100644 index 0000000..911a2c0 --- /dev/null +++ b/src/ui/components/atoms/index.ts @@ -0,0 +1,4 @@ +export * from "./Cta"; +export * from "./Header"; +export * from "./NextImage"; +export * from "./Svg"; diff --git a/src/ui/components/index.ts b/src/ui/components/index.ts index eed75a1..3ce38df 100644 --- a/src/ui/components/index.ts +++ b/src/ui/components/index.ts @@ -1 +1,3 @@ -export * from "./Header"; \ No newline at end of file +export * from "./atoms"; +// export * from "./molecules"; +export * from "./organisms"; diff --git a/src/ui/components/molecules/index.ts b/src/ui/components/molecules/index.ts new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/ui/components/molecules/index.ts @@ -0,0 +1 @@ + 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..a5c1f57 --- /dev/null +++ b/src/ui/components/organisms/Downloader/Downloader.tsx @@ -0,0 +1,18 @@ +import Cta from "../../atoms/Cta"; + +const Downloader = () => { + return ( +
+

Download

+
    +
  • + + Top Secret + +
  • +
+
+ ); +}; + +export default Downloader; 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 new file mode 100644 index 0000000..4f4a272 --- /dev/null +++ b/src/ui/components/organisms/index.ts @@ -0,0 +1 @@ +export * from "./Downloader"; diff --git a/src/ui/pages/Home.tsx b/src/ui/pages/Home.tsx index dc7b7d1..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/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/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. */ diff --git a/webpack.config.js b/webpack.config.js index 93124db..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: { @@ -49,6 +65,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: { @@ -58,7 +75,7 @@ module.exports = { }, plugins: [ new MiniCssExtractPlugin({ - filename: "globals.css", + filename: "[name].css", }), new CopyWebpackPlugin({ patterns: [ @@ -73,4 +90,4 @@ module.exports = { ], }), ], -}; +}; \ No newline at end of file