diff --git a/src/components/BlogSection/BlogSection.js b/src/components/BlogSection/BlogSection.js new file mode 100644 index 0000000..584f4eb --- /dev/null +++ b/src/components/BlogSection/BlogSection.js @@ -0,0 +1,152 @@ +import React from "react"; + +import PropTypes from "prop-types"; + +function classNames(...classes) { + return classes.filter(Boolean).join(" "); +} + +export default function BlogSection({ + title, + subtitle, + posts = [], + variants = "grid-image", + orientation = "row", +}) { + const containerClasses = classNames( + "bg-white py-20 sm:py-28", + orientation === "column" ? "max-w-2xl lg:max-w-4xl" : "max-w-7xl" + ); + + const headerClasses = classNames( + "text-balance text-4xl font-semibold tracking-tight text-gray-900 sm:text-5xl", + orientation !== "column" && "text-center" + ); + + const gridClasses = classNames( + orientation === "column" + ? "mt-10 space-y-16 border-t border-gray-200 pt-10 sm:mt-16 sm:pt-16" + : "mx-auto mt-16 grid max-w-2xl grid-cols-1 gap-x-8 gap-y-20 lg:mx-0 lg:max-w-none lg:grid-cols-3", + variants === "grid-text" && "border-t border-gray-200" + ); + + return ( +
+
+
+

{title}

+ {subtitle && ( +

{subtitle}

+ )} +
+
+ {posts?.map((post) => ( + + ))} +
+
+
+ ); +} + +BlogSection.propTypes = { + title: PropTypes.string.isRequired, + subtitle: PropTypes.string, + posts: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.number.isRequired, + title: PropTypes.string.isRequired, + href: PropTypes.string.isRequired, + description: PropTypes.string.isRequired, + imageUrl: PropTypes.string.isRequired, + date: PropTypes.string.isRequired, + datetime: PropTypes.string.isRequired, + category: PropTypes.shape({ + title: PropTypes.string.isRequired, + href: PropTypes.string.isRequired, + }).isRequired, + author: PropTypes.shape({ + name: PropTypes.string.isRequired, + role: PropTypes.string.isRequired, + href: PropTypes.string.isRequired, + imageUrl: PropTypes.string.isRequired, + }).isRequired, + }) + ).isRequired, + variants: PropTypes.oneOf(["grid-image", "grid-text"]), + orientation: PropTypes.oneOf(["row", "column"]), +}; + +// ImageGrid.defaultProps = { +// subtitle: "", +// variants: "grid-image", +// orientation: "row", +// }; diff --git a/src/components/BlogSection/BlogSection.stories.js b/src/components/BlogSection/BlogSection.stories.js new file mode 100644 index 0000000..a63b142 --- /dev/null +++ b/src/components/BlogSection/BlogSection.stories.js @@ -0,0 +1,97 @@ +import BlogSection from "./BlogSection"; +import { fn } from "@storybook/test"; + +export default { + title: "Marketing/BlogSection", + component: BlogSection, + argTypes: { + title: { control: "text" }, + subtitle: { control: "text" }, + variants: { + control: { + type: "select", + options: ["grid-image", "grid-text"], // Define available options + }, + description: "Choose between displaying a grid of images or text.", + }, + orientation: { + control: { + type: "select", + options: ["row", "column"], // Define available options + }, + description: "Set the layout orientation for the grid.", + }, + }, + tags: ["autodocs"], +}; + +const posts = [ + { + id: 1, + title: "Boost your conversion rate", + href: "#", + description: + "Illo sint voluptas. Error voluptates culpa eligendi. Hic vel totam vitae illo. Non aliquid explicabo necessitatibus unde. Sed exercitationem placeat consectetur nulla deserunt vel. Iusto corrupti dicta.", + imageUrl: + "https://images.unsplash.com/photo-1496128858413-b36217c2ce36?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=3603&q=80", + date: "Mar 16, 2020", + datetime: "2020-03-16", + category: { title: "Marketing", href: "#" }, + author: { + name: "Michael Foster", + role: "Co-Founder / CTO", + href: "#", + imageUrl: + "https://images.unsplash.com/photo-1519244703995-f4e0f30006d5?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80", + }, + }, + { + id: 2, + title: "How to use search engine optimization to drive sales", + href: "#", + description: + "Optio cum necessitatibus dolor voluptatum provident commodi et. Qui aperiam fugiat nemo cumque.", + imageUrl: + "https://images.unsplash.com/photo-1496128858413-b36217c2ce36?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=3603&q=80", + date: "Mar 10, 2020", + datetime: "2020-03-16", + category: { title: "Sales", href: "#" }, + author: { + name: "Michael Foster", + role: "Co-Founder / CTO", + href: "#", + imageUrl: + "https://images.unsplash.com/photo-1547586696-ea22b4d4235d?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=3270&q=80", + }, + }, + { + id: 3, + title: "Boost your conversion rate", + href: "#", + description: + "Illo sint voluptas. Error voluptates culpa eligendi. Hic vel totam vitae illo. Non aliquid explicabo necessitatibus unde. Sed exercitationem placeat consectetur nulla deserunt vel. Iusto corrupti dicta.", + imageUrl: + "https://images.unsplash.com/photo-1492724441997-5dc865305da7?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=3270&q=80", + date: "Mar 16, 2020", + datetime: "2020-03-16", + category: { title: "Marketing", href: "#" }, + author: { + name: "Michael Foster", + role: "Co-Founder / CTO", + href: "#", + imageUrl: + "https://images.unsplash.com/photo-1519244703995-f4e0f30006d5?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80", + }, + }, + // More posts... +]; + +export const Primary = { + args: { + title: "From the blog", + subtitle: "Learn how to grow your business with our expert advice.", + posts: posts, + variants: "grid-text", + orientation: "column", + }, +}; diff --git a/src/components/Input/Input.js b/src/components/Input/Input.js new file mode 100644 index 0000000..026d20e --- /dev/null +++ b/src/components/Input/Input.js @@ -0,0 +1,151 @@ +import { ExclamationCircleIcon } from "@heroicons/react/20/solid"; +import PropTypes from "prop-types"; +import React from "react"; + +/** + * @typedef {Object} InputFieldProps + * @property {string} [label] + * @property {string} [helperText] + * @property {string} [placeholder] + * @property {string} [type] + * @property {string} [value] + * @property {boolean} [isError] + * @property {boolean} [disabled] + * @property {string} [labelHint] + * @property {React.ReactNode} [leadingIcon] + * @property {React.ReactNode} [trailingIcon] + * @property {(e: React.ChangeEvent) => void} [onChange] + */ + +const inputClass = { + error: "text-red-900 ring-red-300 focus:ring-red-500", + default: "text-gray-900 ring-gray-300 focus:ring-primary-600", +}; + +const helperTextClass = { + error: "text-red-600", + default: "text-gray-500", +}; + +/** + * @param {InputFieldProps} props + */ + +export default function InputField({ + label, + helperText, + isError, + disabled, + labelHint, + leadingIcon, + trailingIcon, + placeholder, + value, + type, + onChange, +}) { + return ( +
+
+ {label && ( + + )} + {labelHint && ( + + {labelHint} + + )} +
+ +
+ {leadingIcon && ( +
+ {leadingIcon} +
+ )} + +
+ {isError && ( +
+
+ {helperText && ( +

+ {helperText} +

+ )} +
+ ); +} + +InputField.propTypes = { + /** The text label displayed above the input field */ + label: PropTypes.string, + + /** A short helper text displayed below the input field for additional information */ + helperText: PropTypes.string, + + /** Placeholder text displayed inside the input field when it's empty */ + placeholder: PropTypes.string, + + /** The type of input field, e.g., 'text', 'password', etc. */ + type: PropTypes.string, + + /** The current value of the input field */ + value: PropTypes.string, + + /** If true, displays the input field in an error state */ + isError: PropTypes.bool, + + /** If true, disables the input field, making it non-interactive */ + disabled: PropTypes.bool, + + /** Additional hint text displayed next to the label */ + labelHint: PropTypes.string, + + /** An optional icon component displayed at the beginning of the input field */ + leadingIcon: PropTypes.element, + + /** An optional icon component displayed at the end of the input field */ + trailingIcon: PropTypes.element, + + /** Callback function triggered when the input field's value changes */ + onChange: PropTypes.func, +}; diff --git a/src/components/Input/Input.stories.js b/src/components/Input/Input.stories.js new file mode 100644 index 0000000..d72e297 --- /dev/null +++ b/src/components/Input/Input.stories.js @@ -0,0 +1,125 @@ +import { + EnvelopeIcon, + QuestionMarkCircleIcon, +} from "@heroicons/react/20/solid"; +import InputField from "./Input"; +import { fn } from "@storybook/test"; + +export default { + title: "Atom/Input", + component: InputField, + argTypes: { + label: { control: "text" }, + helperText: { control: "text" }, + placeholder: { control: "text" }, + type: { control: "text" }, + value: { control: "text" }, + labelHint: { control: "text" }, + isError: { control: "boolean" }, + disabled: { control: "boolean" }, + leadingIcon: { + control: "object", + }, + trailingIcon: { + control: "object", + }, + }, + args: { onChange: fn() }, + tags: ["autodocs"], +}; + +export const Primary = { + args: { + label: "Email", + helperText: "We'll only use this for spam.", + placeholder: "say something", + isError: false, + disabled: false, + labelHint: "Optional", + type: "email", + onChange: fn(), + leadingIcon: ( +