├── .eslintrc.cjs
├── .gitignore
├── .storybook
├── main.ts
└── preview.ts
├── README.md
├── index.html
├── package-lock.json
├── package.json
├── public
└── vite.svg
├── src
├── assets
│ ├── react.svg
│ └── vc-logo.png
├── components
│ ├── avatar
│ │ ├── avatar.stories.tsx
│ │ ├── avatar.tsx
│ │ ├── index.ts
│ │ └── styled.ts
│ ├── button
│ │ ├── button.stories.tsx
│ │ ├── button.tsx
│ │ ├── index.ts
│ │ └── styled.ts
│ ├── card
│ │ ├── card.stories.tsx
│ │ ├── card.tsx
│ │ ├── index.ts
│ │ └── styled.ts
│ ├── form-control
│ │ ├── form-control.stories.tsx
│ │ ├── form-control.tsx
│ │ ├── index.ts
│ │ └── styled.ts
│ ├── header
│ │ ├── header.stories.tsx
│ │ ├── header.tsx
│ │ ├── index.css
│ │ └── index.ts
│ ├── index.ts
│ ├── link
│ │ ├── index.ts
│ │ ├── link.stories.tsx
│ │ ├── link.tsx
│ │ └── styled.ts
│ ├── login
│ │ ├── index.ts
│ │ ├── login.stories.tsx
│ │ ├── login.tsx
│ │ ├── styled.ts
│ │ └── use-login-logic.ts
│ ├── modal
│ │ ├── index.css
│ │ ├── index.ts
│ │ ├── modal.stories.tsx
│ │ ├── modal.tsx
│ │ └── styled.ts
│ ├── search
│ │ ├── index.ts
│ │ ├── search.stories.tsx
│ │ ├── search.tsx
│ │ └── styled.ts
│ ├── select
│ │ ├── index.ts
│ │ ├── select.stories.tsx
│ │ ├── select.tsx
│ │ ├── styled.ts
│ │ └── use-select-logic.ts
│ ├── skeleton
│ │ ├── index.ts
│ │ ├── skeleton.stories.tsx
│ │ ├── skeleton.tsx
│ │ └── styled.ts
│ ├── spinner
│ │ ├── index.ts
│ │ ├── spinner.stories.tsx
│ │ ├── spinner.tsx
│ │ ├── styled.ts
│ │ └── utils.ts
│ ├── text-input
│ │ ├── index.ts
│ │ ├── styled.ts
│ │ ├── text-input.stories.tsx
│ │ └── text-input.tsx
│ └── typography
│ │ ├── index.ts
│ │ ├── styled.ts
│ │ ├── typography.stories.tsx
│ │ └── typography.tsx
├── config
│ ├── fonts
│ │ ├── Poppins-Bold.ttf
│ │ ├── Poppins-Light.ttf
│ │ ├── Poppins-Medium.ttf
│ │ ├── Poppins-Regular.ttf
│ │ ├── Poppins-SemiBold.ttf
│ │ ├── Poppins-Thin.ttf
│ │ └── styles.css
│ ├── global.styles.ts
│ └── sizes.ts
├── icons
│ ├── ArrowDown.tsx
│ ├── Cross.tsx
│ ├── Download.tsx
│ ├── Search.tsx
│ ├── User.tsx
│ ├── arrow-down.svg
│ ├── cross.svg
│ ├── download.svg
│ ├── index.ts
│ ├── index.tsx
│ ├── search.svg
│ └── user.svg
├── index.tsx
└── vite-env.d.ts
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: { browser: true, es2020: true },
4 | extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:react-hooks/recommended', 'plugin:storybook/recommended'],
5 | ignorePatterns: ['dist', '.eslintrc.cjs'],
6 | parser: '@typescript-eslint/parser',
7 | plugins: ['react-refresh'],
8 | rules: {
9 | 'react-refresh/only-export-components': [
10 | 'warn',
11 | { allowConstantExport: true },
12 | ],
13 | },
14 | }
15 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | lib
13 | storybook-static/
14 | dist-ssr
15 | *.local
16 |
17 | # Editor directories and files
18 | .vscode/*
19 | !.vscode/extensions.json
20 | .idea
21 | .DS_Store
22 | *.suo
23 | *.ntvs*
24 | *.njsproj
25 | *.sln
26 | *.sw?
27 |
--------------------------------------------------------------------------------
/.storybook/main.ts:
--------------------------------------------------------------------------------
1 | import type { StorybookConfig } from "@storybook/react-vite";
2 |
3 | const config: StorybookConfig = {
4 | stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
5 | addons: [
6 | "@storybook/addon-onboarding",
7 | "@storybook/addon-links",
8 | "@storybook/addon-essentials",
9 | "@chromatic-com/storybook",
10 | "@storybook/addon-interactions",
11 | ],
12 | framework: {
13 | name: "@storybook/react-vite",
14 | options: {},
15 | },
16 | docs: {
17 | autodocs: "tag",
18 | },
19 | };
20 | export default config;
21 |
--------------------------------------------------------------------------------
/.storybook/preview.ts:
--------------------------------------------------------------------------------
1 | import type { Preview } from "@storybook/react";
2 |
3 | const preview: Preview = {
4 | parameters: {
5 | controls: {
6 | matchers: {
7 | color: /(background|color)$/i,
8 | date: /Date$/i,
9 | },
10 | },
11 | },
12 | };
13 |
14 | export default preview;
15 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vinyl Component Blocks ⚡️
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | Vinyl Component Blocks is a UI component library designed to help you quickly build beautiful and functional user interfaces for your web applications. This library provides a collection of reusable and customizable React components that cover a wide range of UI elements.
28 |
29 |
30 |
31 |
32 | ## Introduction
33 |
34 | Offering a set of commonly used components that are designed to be easy to use, highly customizable, and visually appealing. Whether you're building a simple login form, a complex dashboard interface or seeking inspiration while building components in your development process, Vinyl Component(VC) Blocks has the basic components you need to create a polished user experience.
35 |
36 |
37 |
38 | ## Installation and Setup 🧱
39 |
40 | You can install Vinyl Component Blocks via npm or yarn:
41 |
42 | ```bash
43 | npm install vinyl-component-blocks
44 | ```
45 |
46 | or
47 |
48 | ```bash
49 | yarn add vinyl-component-blocks
50 | ```
51 |
52 | ## Usage
53 |
54 | Once installed, you can import the components you need from Vinyl Component Blocks and use them in your React application:
55 |
56 | ```jsx
57 | import React from "react";
58 | import {
59 | Button,
60 | FormControl,
61 | Link,
62 | Search,
63 | Select,
64 | Skeleton,
65 | Spinner,
66 | TextInput,
67 | Typography,
68 | Avatar,
69 | Card,
70 | Modal,
71 | Header,
72 | } from "vinyl-component-blocks";
73 | ```
74 |
75 | ### Demo Example Combining Components
76 |
77 | ```jsx
78 | import React from "react";
79 | import { Button, Card, Avatar } from "vinyl-component-blocks";
80 |
81 | const DemoComponent = () => (
82 |
83 |
84 |
85 | John Doe
86 | Lorem ipsum dolor sit amet, consectetur adipiscing elit.
87 |
88 | Read More
89 |
90 |
91 |
92 | );
93 |
94 | export default DemoComponent;
95 | ```
96 |
97 |
98 |
99 | - Providing a hands-on experience with the Vinyl Component Blocks library, showcasing the versatility and functionality of each component. Key components here include the `Header` and `Login` UI components.
100 |
101 |
102 |
103 | ```jsx
104 | import React from "react";
105 | import { Button, Card, Avatar, Header, Login } from "vinyl-component-blocks";
106 |
107 | const DemoComponent = () => (
108 |
126 | );
127 |
128 | export default DemoComponent;
129 | ```
130 |
131 | ## Components
132 |
133 | ### Button
134 |
135 | | Prop | Type | Description |
136 | | --------- | ----------- | ----------------------------------------------------------- |
137 | | type | string | Button type (`default`, `danger`, `ghost`, `secondary` ) |
138 | | size | string | Button size (`default`, `large`, `small`) |
139 | | disabled | boolean | Disable button |
140 | | onClick | function | Click event handler |
141 | | children | ReactNode | Button content |
142 | | icon | ElementType | Icon component to display alongside the button text |
143 | | className | string | Additional CSS classes for styling |
144 | | loading | boolean | Specify whether the button should display a loading spinner |
145 | | href | string | URL for the button if it should act as a link |
146 | | as | ElementType | HTML element type for the button |
147 | | to | string | Destination path for the link if using the 'as' prop |
148 | | ...rest | any | Additional props for customization |
149 |
150 |
151 |
152 |
153 | ### FormControl
154 |
155 | | Prop | Type | Description |
156 | | ---------- | --------- | ---------------------------------- |
157 | | label | string | Label for the form control |
158 | | htmlFor | string | ID of the form control element |
159 | | error | string | Error message to display |
160 | | hint | string | Hint message to display |
161 | | disabled | boolean | Disable the form control |
162 | | className | string | Additional CSS classes for styling |
163 | | children | ReactNode | Form control element |
164 | | onBlur | function | Blur event handler |
165 | | onFocus | function | Focus event handler |
166 | | forceLabel | boolean | Force label to always show |
167 | | ...rest | any | Additional props for customization |
168 |
169 |
170 |
171 |
172 | ### Link
173 |
174 | | Prop | Type | Description |
175 | | --------- | ----------- | ---------------------------------- |
176 | | disabled | boolean | Disable the link |
177 | | className | string | Additional CSS classes for styling |
178 | | children | ReactNode | Link text |
179 | | as | ElementType | HTML element type (default is `a`) |
180 | | href | string | Link URL |
181 | | ...rest | any | Additional props for customization |
182 |
183 |
184 |
185 | ### Search
186 |
187 | | Prop | Type | Description |
188 | | ----------- | ------------- | ------------------------------------------------------ |
189 | | className | string | Additional CSS classes for styling |
190 | | size | ComponentSize | Size of the search input (`default`, `small`, `large`) |
191 | | width | string | Width of the search input |
192 | | value | string | Current value of the search input |
193 | | onChange | function | Change event handler for the search input |
194 | | placeholder | string | Placeholder text for the search input |
195 |
196 |
197 |
198 | ### Select
199 |
200 | | Prop | Type | Description |
201 | | ----------- | -------------- | ------------------------------------------------------ |
202 | | className | string | Additional CSS classes for styling |
203 | | size | ComponentSize | Size of the select input (`default`, `small`, `large`) |
204 | | disabled | boolean | Disable the select input |
205 | | error | boolean | Indicate error state |
206 | | width | string | Width of the select input |
207 | | option | SelectOption | Selected option |
208 | | listOptions | SelectOption[] | Array of options for the select input |
209 | | onChange | function | Change event handler for the select input |
210 | | placeholder | string | Placeholder text for the select input |
211 |
212 |
213 |
214 |
215 |
216 |
217 | ### Skeleton
218 |
219 | | Prop | Type | Description |
220 | | ------------ | ------ | ------------------------------------- |
221 | | width | number | Width of the skeleton element |
222 | | height | number | Height of the skeleton element |
223 | | className | string | Additional CSS classes for styling |
224 | | borderRadius | string | Border radius of the skeleton element |
225 |
226 |
227 |
228 | ### Spinner
229 |
230 | | Prop | Type | Description |
231 | | --------- | ------- | ---------------------------------- |
232 | | size | number | Size of the spinner |
233 | | className | string | Additional CSS classes for styling |
234 | | light | boolean | Use light spinner variant |
235 |
236 |
237 |
238 | ### TextInput
239 |
240 | | Prop | Type | Description |
241 | | ----------- | ------------- | ----------------------------------------------- |
242 | | icon | ElementType | Icon component to display |
243 | | size | ComponentSize | Size of the input (`default`, `small`, `large`) |
244 | | disabled | boolean | Disable the input |
245 | | error | boolean | Indicate error state |
246 | | value | string | Current value of the input |
247 | | onChange | function | Change event handler for the input |
248 | | placeholder | string | Placeholder text for the input |
249 | | width | string | Width of the input |
250 | | readonly | boolean | Make the input read-only |
251 | | clearable | boolean | Allow clearing the input |
252 | | ...rest | any | Additional props for customization |
253 |
254 |
255 |
256 |
257 |
258 | ### Typography
259 |
260 | | Prop | Type | Description |
261 | | --------- | --------- | ------------------------------------------ |
262 | | variant | string | Typography variant (`h1`, `h2`, `h3`) |
263 | | align | string | Text alignment (`center`, `right`, `left`) |
264 | | className | string | Additional CSS classes for styling |
265 | | children | ReactNode | Text content |
266 |
267 |
268 |
269 | ### Avatar
270 |
271 | | Prop | Type | Description |
272 | | -------- | ------ | ------------------------------- |
273 | | imageSrc | string | Source URL for the avatar image |
274 |
275 |
276 |
277 | ### Card
278 |
279 | | Prop | Type | Description |
280 | | --------------- | --------- | ------------------------------------------ |
281 | | backgroundColor | string | Background color of the card |
282 | | children | ReactNode | Content to be displayed within the card |
283 | | padding | string | Padding around the content inside the card |
284 |
285 |
286 |
287 | ### Modal
288 |
289 | | Prop | Type | Description |
290 | | -------- | --------- | ---------------------------------------- |
291 | | children | ReactNode | Content to be displayed within the modal |
292 |
293 |
294 |
295 | ### Header
296 |
297 | | Prop | Type | Description |
298 | | --------------- | -------- | --------------------------------------- |
299 | | user | User | User object |
300 | | onLogin | function | Event handler for login action |
301 | | onLogout | function | Event handler for logout action |
302 | | onCreateAccount | function | Event handler for create account action |
303 |
304 |
305 |
306 |
307 | ### Login
308 |
309 | | Prop | Type | Description |
310 | | ------------- | -------- | ---------------------------------------- |
311 | | onSubmit | function | Event handler for form submission |
312 | | initialValues | object | Initial values for the form fields |
313 | | loading | boolean | Specify whether to display loading state |
314 | | registerLink | string | Link for user registration |
315 |
316 |
317 |
318 |
319 |
320 | ## External Resources and Dependencies 🌐
321 |
322 | The Vinyl Component Blocks library relies on the following external resources and dependencies:
323 |
324 | - **Styled Components**: Utilized for customizable styling of components. CSS-in-JS library.
325 |
326 | - **React**: Serves as the core of the Vinyl Component Blocks, providing the necessary infrastructure for building user interfaces in JavaScript.
327 |
328 | - **TypeScript**: Vinyl Component Blocks is super strictly typed. TypeScript as a strict syntactical superset of JavaScript adds static typing to the library. It enhances developer productivity and code quality.
329 |
330 | - **Email Validator**: Used for validating emails, for the input-component most specifically. Email Validator is a dependency that helps ensure that the entered email addresses conform to standard email format rules, enhancing the reliability and accuracy of user input validation.
331 |
332 | ## Demo Examples 🛠️
333 |
334 | ### Customizing TextInput Component
335 |
336 | ```jsx
337 | import React from "react";
338 | import { TextInput } from "vinyl-component-blocks";
339 |
340 | const CustomTextInput = () => (
341 | console.log(e.target.value)}
349 | placeholder="Enter text here"
350 | width="200px"
351 | readonly={false}
352 | clearable={true}
353 | />
354 | );
355 |
356 | export default CustomTextInput;
357 | ```
358 |
359 | ### Demo Example, Simple Login UI
360 |
361 | ```jsx
362 | import React from "react";
363 | import { Header, Login } from "vinyl-component-blocks";
364 |
365 | const HeaderWithLogin = () => (
366 |
367 | console.log("Logged out")}
370 | />
371 | console.log("Logged in")}
373 | onCreateAccount={() => console.log("Creating account")}
374 | />
375 |
376 | );
377 |
378 | export default HeaderWithLogin;
379 | ```
380 |
381 | ### Customizing Components with Custom CSS Styles ✨
382 |
383 | Customizing components with custom CSS styles allows you to tailor the appearance of UI components to match the specific design requirements of your project. Applying custom CSS classes to components, you can override default styles and apply unique visual enhancements. This approach offers flexibility and control over the presentation of components.
384 |
385 | #### Customizing Card Component
386 |
387 | ```jsx
388 | import React from "react";
389 | import { Card } from "vinyl-component-blocks";
390 | import "./custom-styles.css";
391 |
392 | const CustomCard = () => (
393 |
398 | This is a Custom Card
399 |
400 | Stop reinventing the wheel! Our well-documented component library empowers
401 | you to concentrate on building unique features and functionalities.
402 |
403 |
404 | );
405 |
406 | export default CustomCard;
407 | ```
408 |
409 | #### Customizing TextInput Component
410 |
411 | ```jsx
412 | import React from "react";
413 | import { TextInput } from "vinyl-component-blocks";
414 | import "./custom-styles.css";
415 |
416 | const CustomTextInput = () => (
417 | console.log(e.target.value)}
425 | placeholder="Enter text here"
426 | width="200px"
427 | readonly={false}
428 | clearable={true}
429 | />
430 | );
431 |
432 | export default CustomTextInput;
433 | ```
434 |
435 | In the `custom-styles.css` file:
436 |
437 | ```css
438 | /* custom-styles.css */
439 | .custom-card {
440 | box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
441 | border-radius: 12px;
442 | }
443 |
444 | .custom-text-input {
445 | border: 2px solid #cccccc;
446 | border-radius: 8px;
447 | }
448 | ```
449 |
450 | ## Contributing
451 |
452 | ### Adding New Components
453 |
454 | Contributions from developers who feel the need to expand the library with new components or features. Whether you've identified a potential component, have an idea for a new feature, or want to enhance existing functionality, your contributions are valuable to the community.
455 |
456 | If you find something that piques your interest or if the project has inspired you, feel free to contribute by submitting pull requests(PRs). Your contributions help improve the library for everyone. Don't forget to leave a star ⭐ on GitHub to show your support!
457 |
458 | ## Conclusion
459 |
460 | Vinyl Component Blocks offers a comprehensive collection of UI components that are easy to use and highly customizable. Say goodbye to design inconsistencies.
461 |
462 | So, stop reinventing the wheel! Our well-documented component library empowers you to create, clone and build unique component features and functionalities to your taste.
463 |
464 | Stop repeating work! Install, call, use, modify.
465 | `npm install vinyl-component-blocks`.
466 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Block Test
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vinyl-component-blocks",
3 | "version": "1.1.6",
4 | "main": "lib/index.js",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "compile-icons": "npx @svgr/cli -d src/icons src/icons --typescript",
9 | "copy-files": "copyfiles --up 1 src/config/fonts/* lib/",
10 | "build": "tsc && vite build",
11 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
12 | "preview": "vite preview",
13 | "storybook": "storybook dev -p 6006",
14 | "build-storybook": "storybook build",
15 | "prebuild": "npm run compile-icons && npm run copy-files",
16 | "prepublish": "npm run build"
17 | },
18 | "dependencies": {
19 | "email-validator": "^2.0.4",
20 | "react": "^18.2.0",
21 | "react-dom": "^18.2.0",
22 | "styled-components": "^6.1.9"
23 | },
24 | "devDependencies": {
25 | "@chromatic-com/storybook": "^1.3.3",
26 | "@storybook/addon-essentials": "^8.0.9",
27 | "@storybook/addon-interactions": "^8.0.9",
28 | "@storybook/addon-links": "^8.0.9",
29 | "@storybook/addon-onboarding": "^8.0.9",
30 | "@storybook/blocks": "^8.0.9",
31 | "@storybook/react": "^8.0.9",
32 | "@storybook/react-vite": "^8.0.9",
33 | "@storybook/test": "^8.0.9",
34 | "@svgr/cli": "^8.1.0",
35 | "@types/react": "^18.2.15",
36 | "@types/react-dom": "^18.2.7",
37 | "@types/styled-components": "^5.1.34",
38 | "@typescript-eslint/eslint-plugin": "^6.0.0",
39 | "@typescript-eslint/parser": "^6.0.0",
40 | "@vitejs/plugin-react": "^4.0.3",
41 | "copyfiles": "^2.4.1",
42 | "eslint": "^8.45.0",
43 | "eslint-plugin-react-hooks": "^4.6.0",
44 | "eslint-plugin-react-refresh": "^0.4.3",
45 | "eslint-plugin-storybook": "^0.8.0",
46 | "storybook": "^8.0.9",
47 | "typescript": "^5.0.2",
48 | "vite": "^4.4.5"
49 | },
50 | "keywords": [
51 | "UI",
52 | "components",
53 | "design-system",
54 | "auth-ui",
55 | "library",
56 | "accessibility",
57 | "open-source",
58 | "reusable-component",
59 | "design-engineering"
60 | ],
61 | "repository": {
62 | "type": "git",
63 | "url": "https://github.com/Vinyl-Davyl/vinyl-component-blocks"
64 | },
65 | "author": {
66 | "email": "okononfuadavid@gmail.com",
67 | "name": "Vinyl-Davyl (Okononfua O David)",
68 | "url": "https://vinyldavyl.xyz"
69 | },
70 | "license": "MIT",
71 | "description": "Modular, Reusable, and Styled UI Component Library. Stop repeating work, install, call, use, modify."
72 | }
73 |
--------------------------------------------------------------------------------
/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/react.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/vc-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vinyl-Davyl/vinyl-component-blocks/cdb592e13255fae06946ad1de1a5c4dcf1f59a12/src/assets/vc-logo.png
--------------------------------------------------------------------------------
/src/components/avatar/avatar.stories.tsx:
--------------------------------------------------------------------------------
1 | import { Meta, StoryObj } from "@storybook/react";
2 | import Avatar from ".";
3 |
4 | const meta: Meta = {
5 | title: "Components/Avatar",
6 | component: Avatar,
7 | parameters: {
8 | layout: "centered",
9 | },
10 | tags: ["autodocs"],
11 | };
12 |
13 | // Export the metadata as default
14 | export default meta;
15 |
16 | type Story = StoryObj;
17 |
18 | export const Primary: Story = {
19 | args: {
20 | imageSrc:
21 | "https://miro.medium.com/v2/resize:fit:740/1*ooOH6jo8I0ns0J-BE0SAow.jpeg",
22 | },
23 | };
24 |
--------------------------------------------------------------------------------
/src/components/avatar/avatar.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { AvatarImage } from "./styled";
3 |
4 | export type AvatarProps = {
5 | imageSrc?: string;
6 | };
7 |
8 | const Avatar: React.ForwardRefRenderFunction = (
9 | props,
10 | ref
11 | ) => {
12 | const { imageSrc } = props;
13 |
14 | return (
15 |
18 | );
19 | };
20 |
21 | export default React.forwardRef(Avatar);
22 |
--------------------------------------------------------------------------------
/src/components/avatar/index.ts:
--------------------------------------------------------------------------------
1 | import Avatar from "./avatar";
2 |
3 | export * from "./avatar";
4 |
5 | export default Avatar;
6 |
--------------------------------------------------------------------------------
/src/components/avatar/styled.ts:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | interface StyledAvatarProps {
4 | imageSrc?: string;
5 | }
6 |
7 | export const AvatarImage = styled.img`
8 | margin: 0px 20px 0px 0px;
9 | height: 40px;
10 | border-radius: 50%;
11 | `;
12 |
--------------------------------------------------------------------------------
/src/components/button/button.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Meta, StoryObj } from "@storybook/react";
3 |
4 | import Button, { ButtonProps } from ".";
5 | import styled from "styled-components";
6 | import DownloadIcon from "../../icons/Download";
7 |
8 | const meta: Meta = {
9 | title: "Components/Button",
10 | component: Button,
11 | parameters: {
12 | layout: "centered",
13 | },
14 | tags: ["autodocs"],
15 | };
16 |
17 | export default meta;
18 |
19 | type Story = StoryObj;
20 |
21 | export const Default: Story = {
22 | args: {
23 | children: "Default Button",
24 | },
25 | };
26 |
27 | export const DangerButton: Story = {
28 | args: {
29 | children: "Danger Button",
30 | type: "danger",
31 | },
32 | };
33 |
34 | export const Ghost: Story = {
35 | args: {
36 | children: "Ghost Button",
37 | type: "ghost",
38 | },
39 | };
40 |
41 | export const Secondary: Story = {
42 | args: {
43 | children: "Secondary Button",
44 | type: "secondary",
45 | },
46 | };
47 |
48 | export const Disabled: Story = {
49 | args: {
50 | children: "Disabled Button",
51 | disabled: true,
52 | },
53 | };
54 |
55 | export const Loading: Story = {
56 | args: {
57 | children: "Button",
58 | loading: true,
59 | },
60 | };
61 |
62 | export const WithIcon: Story = {
63 | args: {
64 | icon: DownloadIcon,
65 | children: "Download",
66 | },
67 | };
68 |
69 | const ButtonRow = styled.div`
70 | display: flex;
71 | align-items: flex-start;
72 | margin-bottom: 10px;
73 | & > * {
74 | margin-right: 10px;
75 | }
76 | `;
77 |
78 | export const Sizes = () => {
79 | return (
80 | <>
81 |
82 | Large
83 | Default
84 | Small
85 |
86 |
87 |
88 | Large
89 |
90 |
91 | Default
92 |
93 |
94 | Small
95 |
96 |
97 |
98 |
99 | Large
100 |
101 |
102 | Default
103 |
104 |
105 | Small
106 |
107 |
108 |
109 |
110 | Large
111 |
112 |
113 | Default
114 |
115 |
116 | Small
117 |
118 |
119 |
120 |
121 | Large
122 |
123 |
124 | Default
125 |
126 |
127 | Small
128 |
129 |
130 | >
131 | );
132 | };
133 |
--------------------------------------------------------------------------------
/src/components/button/button.tsx:
--------------------------------------------------------------------------------
1 | import React, { ElementType, MouseEventHandler, ReactNode } from 'react';
2 | import { StyledButton, StyledIcon } from './styled';
3 | import Spinner from '../spinner';
4 | import { ComponentSize } from '../../config/sizes';
5 |
6 | export type ButtonType = 'default' | 'danger' | 'ghost' | 'secondary';
7 |
8 | interface BaseButtonProps {
9 | type?: ButtonType;
10 | icon?: ElementType;
11 | size?: ComponentSize;
12 | className?: string;
13 | children?: ReactNode;
14 | disabled?: boolean;
15 | loading?: boolean;
16 | }
17 |
18 | type HTMLButtonProps = {
19 | onClick?: MouseEventHandler;
20 | } & BaseButtonProps;
21 |
22 | /**
23 | * If href is supplied, button becomes an anchor link
24 | */
25 | type HTMLAnchorProps = {
26 | href?: string;
27 | } & BaseButtonProps;
28 |
29 | /**
30 | * If `as` is supplied, button becomes a custom html node specified in `as`
31 | */
32 | type CustomNodeProps = {
33 | as?: ElementType;
34 | to?: string;
35 | } & BaseButtonProps;
36 |
37 | export type ButtonProps = HTMLButtonProps & HTMLAnchorProps & CustomNodeProps;
38 |
39 | const Button: React.ForwardRefRenderFunction = (props, ref) => {
40 | const {
41 | type = 'default',
42 | icon,
43 | size = 'default',
44 | className,
45 | children,
46 | disabled = false,
47 | loading,
48 | onClick,
49 | href,
50 | as,
51 | to,
52 | } = props;
53 |
54 | const styles = {
55 | innerType: type,
56 | size,
57 | disabled,
58 | withText: children != null
59 | }
60 |
61 | const spinnerStyles = {
62 | size: size === 'large' ? 25 : size === 'default' ? 20 : 15,
63 | light: true,
64 | }
65 |
66 | const childrenWithIcon = !icon ? children : (
67 | <>
68 | {children}
69 |
70 | >
71 | );
72 |
73 | if (as && !disabled) {
74 | return (
75 |
82 | {loading ? (
83 | <>
84 | Loading
85 |
86 | >
87 | ) : childrenWithIcon}
88 |
89 | )
90 | }
91 |
92 | if (href && !disabled) {
93 | return (
94 | }
98 | className={className}
99 | {...styles}
100 | >
101 | {loading ? (
102 | <>
103 | Loading
104 |
105 | >
106 | ) : childrenWithIcon}
107 |
108 | );
109 | }
110 |
111 | return (
112 | }
117 | className={className}
118 | {...styles}
119 | >
120 | {loading ? (
121 | <>
122 | Loading
123 |
124 | >
125 | ) : childrenWithIcon }
126 |
127 | );
128 | }
129 |
130 | export default React.forwardRef(Button);
--------------------------------------------------------------------------------
/src/components/button/index.ts:
--------------------------------------------------------------------------------
1 | import Button from './button';
2 |
3 | export * from './button';
4 |
5 | export default Button;
--------------------------------------------------------------------------------
/src/components/button/styled.ts:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 | import { ComponentSize, heights, sidePaddings } from "../../config/sizes";
3 | import { ButtonType } from "./button";
4 |
5 | type StateColors = {
6 | regular: string;
7 | hover: string;
8 | };
9 |
10 | const typeColors: { [key in ButtonType]: StateColors } = {
11 | default: {
12 | regular: "#1ea7fd",
13 | hover: "#2e27cc",
14 | },
15 | danger: {
16 | regular: "#d93848",
17 | hover: "#eb4d5d",
18 | },
19 | ghost: {
20 | regular: "transparent",
21 | hover: "#dbdbdb",
22 | },
23 | secondary: {
24 | regular: "#000",
25 | hover: "#3d3d3d",
26 | },
27 | };
28 |
29 | interface StyledButtonProps {
30 | innerType: ButtonType;
31 | size: ComponentSize;
32 | withText: boolean;
33 | }
34 |
35 | /* Real tag is assigned dynamically */
36 | export const StyledButton = styled.button`
37 | display: flex;
38 | align-items: center;
39 | justify-content: center;
40 |
41 | /* Add margin in case of loading or icon */
42 | & > *:nth-child(1) {
43 | margin-left: ${(pr) => (pr.withText ? 7 : 0)}px;
44 | }
45 | font-size: 15px;
46 |
47 | border: none;
48 | border-radius: 3em;
49 | cursor: pointer;
50 | background-color: ${(pr) => typeColors[pr.innerType].regular};
51 | padding: 0 ${(pr) => sidePaddings[pr.size]}px;
52 | height: ${(pr) => heights[pr.size]}px;
53 | color: ${(pr) =>
54 | pr.innerType === "ghost" ? typeColors["default"].regular : "#fff"};
55 | ${(pr) =>
56 | pr.disabled
57 | ? `
58 | background-color: #a6a6a6;
59 | color: #5e5e5e;
60 | cursor: not-allowed;
61 |
62 | &:hover {
63 | background-color: #a6a6a6 !important;
64 | color: #5e5e5e !important;
65 | }
66 | `
67 | : ""}
68 | outline: none;
69 |
70 | &:focus {
71 | box-shadow:
72 | 0 0 0 1px #fff,
73 | 0 0 0 2px ${(pr) => typeColors[pr.innerType].regular};
74 | }
75 | &:hover {
76 | background-color: ${(pr) => typeColors[pr.innerType].hover};
77 | }
78 | `;
79 |
80 | export const StyledIcon = styled.div`
81 | height: 20px;
82 | `;
83 |
--------------------------------------------------------------------------------
/src/components/card/card.stories.tsx:
--------------------------------------------------------------------------------
1 | import { Meta, StoryObj } from "@storybook/react";
2 | import React from "react";
3 | import Card from "./index";
4 |
5 | const meta: Meta = {
6 | title: "Components/Card",
7 | component: Card,
8 | parameters: {
9 | layout: "centered",
10 | },
11 | tags: ["autodocs"],
12 | };
13 |
14 | // Export the metadata as default
15 | export default meta;
16 |
17 | type Story = StoryObj;
18 |
19 | export const Default: Story = {
20 | // Normal.args = {children: }
21 | args: {
22 | children:
,
23 | },
24 | };
25 |
26 | export const Primary: Story = {
27 | args: {
28 | children: (
29 |
30 | Lorem ipsum dolor sit amet consectetur, adipisicing elit. Vero enim
31 | sapiente ea a, minima ratione quibusdam deserunt eius voluptatum quidem,
32 | architecto sequi corporis reprehenderit? Eos et aspernatur eaque
33 | voluptatibus laboriosam.
34 |
35 | ),
36 | },
37 | };
38 |
--------------------------------------------------------------------------------
/src/components/card/card.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { CardBody } from "./styled";
3 |
4 | export type CardProps = {
5 | backgroundColor?: string;
6 | children?: any;
7 | padding?: string;
8 | };
9 |
10 | const Card: React.ForwardRefRenderFunction = (
11 | props,
12 | ref
13 | ) => {
14 | const { backgroundColor, children, padding } = props;
15 | return (
16 |
17 | {children}
18 |
19 | );
20 | };
21 |
22 | export default React.forwardRef(Card);
23 |
--------------------------------------------------------------------------------
/src/components/card/index.ts:
--------------------------------------------------------------------------------
1 | import Card from "./card";
2 |
3 | export * from "./card";
4 |
5 | export default Card;
6 |
--------------------------------------------------------------------------------
/src/components/card/styled.ts:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | interface CardProps {
4 | backgroundColor?: string;
5 | children?: any;
6 | padding?: string;
7 | }
8 |
9 | export const CardBody = styled.div`
10 | background-color: ${(props) => props.backgroundColor ?? "white"};
11 | padding: ${(props) => props?.padding ?? "20px 30px 10px 30px"};
12 | max-width: max-content;
13 | border-radius: 8px;
14 | /* Add shadows to create the "card" effect */
15 | box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.2);
16 | transition: 0.3s;
17 | `;
18 |
--------------------------------------------------------------------------------
/src/components/form-control/form-control.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Meta } from "@storybook/react";
3 |
4 | import FormControl from ".";
5 | import TextInput from "../text-input";
6 | import Select, { SelectOption } from "../select";
7 | import { validate as validateEmail } from "email-validator";
8 |
9 | const meta: Meta = {
10 | title: "Components/FormControl",
11 | component: FormControl,
12 | parameters: {
13 | layout: "centered",
14 | },
15 | tags: ["autodocs"],
16 | };
17 |
18 | export default meta;
19 |
20 | export const FormInput = () => {
21 | const [value, setValue] = React.useState("");
22 | const [isValid, setIsValid] = React.useState(false);
23 | const [isVisited, setIsVisited] = React.useState(false);
24 | const shouldShowError = !isValid && isVisited;
25 | const onChange = (event: React.FormEvent) => {
26 | const { value } = event.currentTarget;
27 | setIsValid(validateEmail(value));
28 | setValue(value);
29 | };
30 |
31 | return (
32 | setIsVisited(true)}
38 | >
39 |
47 |
48 | );
49 | };
50 |
51 | export const DisabledFormInput = () => {
52 | return (
53 |
59 |
60 |
61 | );
62 | };
63 |
64 | const selectOptions = [
65 | { label: "Country 1", value: "country-1" },
66 | { label: "Country 2", value: "country-2" },
67 | { label: "Country 3", value: "country-3" },
68 | ];
69 |
70 | export const FormSelect = () => {
71 | const [option, setOption] = React.useState(
72 | selectOptions[0]
73 | );
74 | const [isVisited, setIsVisited] = React.useState(false);
75 | const shouldShowError = !option && isVisited;
76 |
77 | return (
78 | setIsVisited(true)}
85 | >
86 | {
94 | setOption(option);
95 | }}
96 | />
97 |
98 | );
99 | };
100 |
--------------------------------------------------------------------------------
/src/components/form-control/form-control.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode, useState } from 'react';
2 | import { StyledFormControl, StyledLabel, StyledError, StyledHint } from './styled';
3 |
4 | export interface FormControlProps {
5 | label?: string;
6 | htmlFor?: string;
7 | error?: string;
8 | hint?: string;
9 | /* Disabled forces to always show label */
10 | disabled?: boolean;
11 | className?: string;
12 | children: ReactNode;
13 | onBlur?: (e: React.FocusEvent) => void;
14 | onFocus?: (e: React.FocusEvent) => void;
15 | /* Forces to always show label */
16 | forceLabel?: boolean;
17 | }
18 |
19 | const FormControl: React.ForwardRefRenderFunction = (props, ref) => {
20 | const {
21 | label = '',
22 | htmlFor = '',
23 | error = '',
24 | hint = '',
25 | disabled = false,
26 | className,
27 | children,
28 | onBlur = () => {},
29 | onFocus = () => {},
30 | forceLabel = false,
31 | } = props;
32 |
33 | const [focused, setFocused] = useState(false);
34 |
35 | return (
36 | { setFocused(true); onFocus(e); }}
40 | onBlur={(e) => { setFocused(false); onBlur(e); }}
41 | >
42 | {label ? {label} : null}
47 | {children}
48 | {error ? {error} : (
49 | hint ? {hint} : null
50 | )}
51 |
52 | );
53 | }
54 |
55 | export default React.forwardRef(FormControl);
--------------------------------------------------------------------------------
/src/components/form-control/index.ts:
--------------------------------------------------------------------------------
1 | import FormControl from './form-control';
2 |
3 | export * from './form-control';
4 |
5 | export default FormControl;
--------------------------------------------------------------------------------
/src/components/form-control/styled.ts:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | export const StyledFormControl = styled.div`
4 | box-sizing: border-box;
5 | position: relative;
6 | margin-bottom: 20px;
7 | `;
8 |
9 | interface StyledLabelProps {
10 | focused: boolean;
11 | error: boolean;
12 | }
13 |
14 | export const StyledLabel = styled.label`
15 | display: block;
16 | position: relative;
17 | z-index: 2;
18 | transform: ${pr => pr.focused ? 'translateY(0)' : 'translateY(20px)'};
19 | opacity: ${pr => pr.focused ? 1 : 0};
20 | visibility: ${pr => pr.focused ? 'visible' : 'hidden'};
21 | transition: all 0.2s ease-in;
22 | font-size: 0.8em;
23 | font-weight: 500;
24 | margin-bottom: 5px;
25 | color: ${pr => pr.error ? '#d93848' : '#000'};
26 | `;
27 |
28 | const StyledCaption = styled.span`
29 | position: absolute;
30 | font-size: 0.75em;
31 | `;
32 |
33 | export const StyledError = styled(StyledCaption)`
34 | color: #d93848;
35 | `;
36 | export const StyledHint = styled(StyledCaption)`
37 | color: #545454;
38 | font-style: italic;
39 | `;
--------------------------------------------------------------------------------
/src/components/header/header.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from "@storybook/react";
2 | import { fn } from "@storybook/test";
3 |
4 | import Header from "./Header";
5 |
6 | const meta = {
7 | title: "Components/Header",
8 | component: Header,
9 | // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
10 | tags: ["autodocs"],
11 | parameters: {
12 | // More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
13 | layout: "fullscreen",
14 | },
15 | args: {
16 | onLogin: fn(),
17 | onLogout: fn(),
18 | onCreateAccount: fn(),
19 | },
20 | } satisfies Meta;
21 |
22 | export default meta;
23 | type Story = StoryObj;
24 |
25 | export const LoggedIn: Story = {
26 | args: {
27 | user: {
28 | name: "Vinyl Davyl",
29 | },
30 | },
31 | };
32 |
33 | export const LoggedOut: Story = {};
34 |
--------------------------------------------------------------------------------
/src/components/header/header.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import Button from "../button";
4 | import "./index.css";
5 |
6 | type User = {
7 | name: string;
8 | };
9 |
10 | interface HeaderProps {
11 | user?: User;
12 | onLogin?: () => void;
13 | onLogout?: () => void;
14 | onCreateAccount?: () => void;
15 | }
16 |
17 | const Header: React.FC = ({
18 | user,
19 | onLogin,
20 | onLogout,
21 | onCreateAccount,
22 | }: HeaderProps) => (
23 |
72 | );
73 |
74 | export default Header;
75 |
--------------------------------------------------------------------------------
/src/components/header/index.css:
--------------------------------------------------------------------------------
1 | .storybook-header {
2 | font-family: "Nunito Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
3 | border-bottom: 1px solid rgba(0, 0, 0, 0.1);
4 | padding: 15px 20px;
5 | display: flex;
6 | align-items: center;
7 | justify-content: space-between;
8 | }
9 |
10 | .storybook-header svg {
11 | display: inline-block;
12 | vertical-align: top;
13 | }
14 |
15 | .storybook-header h1 {
16 | font-weight: 700;
17 | font-size: 20px;
18 | line-height: 1;
19 | margin: 6px 0 6px 10px;
20 | display: inline-block;
21 | vertical-align: top;
22 | }
23 |
24 | .storybook-header .right-pane {
25 | display: flex;
26 | align-items: center;
27 | }
28 |
29 | .storybook-header button + button {
30 | margin-left: 10px;
31 | }
32 |
33 | .storybook-header .welcome {
34 | color: #333;
35 | font-size: 14px;
36 | margin-right: 10px;
37 | }
38 |
--------------------------------------------------------------------------------
/src/components/header/index.ts:
--------------------------------------------------------------------------------
1 | import Header from "./header";
2 |
3 | export * from "./header";
4 |
5 | export default Header;
6 |
--------------------------------------------------------------------------------
/src/components/index.ts:
--------------------------------------------------------------------------------
1 | export { default as Button } from "./button";
2 |
3 | export { default as FormControl } from "./form-control";
4 |
5 | export { default as Link } from "./link";
6 |
7 | export { default as Search } from "./search";
8 |
9 | export { default as Select } from "./select";
10 |
11 | export { default as Skeleton } from "./skeleton";
12 |
13 | export { default as Spinner } from "./spinner";
14 |
15 | export { default as TextInput } from "./text-input";
16 |
17 | export { default as Login } from "./login";
18 |
19 | export { default as Typography } from "./typography";
20 |
21 | export { default as Avatar } from "./avatar";
22 |
23 | export { default as Card } from "./card";
24 |
25 | export { default as Modal } from "./modal";
26 |
27 | export { default as Header } from "./header";
28 |
--------------------------------------------------------------------------------
/src/components/link/index.ts:
--------------------------------------------------------------------------------
1 | import Link from './link';
2 |
3 | export * from './link';
4 |
5 | export default Link;
--------------------------------------------------------------------------------
/src/components/link/link.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Meta, StoryObj } from "@storybook/react";
3 |
4 | import Link, { LinkProps } from ".";
5 |
6 | const meta: Meta = {
7 | title: "Components/Link",
8 | component: Link,
9 | parameters: {
10 | layout: "centered",
11 | },
12 | tags: ["autodocs"],
13 | };
14 |
15 | export default meta;
16 |
17 | type Story = StoryObj;
18 |
19 | export const Default: Story = {
20 | args: {
21 | children: "Link",
22 | href: "#",
23 | },
24 | };
25 |
26 | export const CustomElement: Story = {
27 | args: {
28 | children: "I am span",
29 | href: "#",
30 | as: "span",
31 | },
32 | };
33 |
34 | export const Disabled: Story = {
35 | args: {
36 | children: "Disabled link",
37 | href: "#",
38 | disabled: true,
39 | },
40 | };
41 |
--------------------------------------------------------------------------------
/src/components/link/link.tsx:
--------------------------------------------------------------------------------
1 | import React, { ElementType, HTMLAttributes, ReactNode } from 'react';
2 | import { StyledLink } from './styled';
3 |
4 | export interface LinkProps extends Omit, 'as' | 'disabled'> {
5 | disabled?: boolean;
6 | className?: string;
7 | children: ReactNode;
8 | as?: ElementType;
9 | href?: string;
10 | }
11 |
12 | const Link: React.ForwardRefRenderFunction = (props, ref) => {
13 | const {
14 | className,
15 | disabled = false,
16 | children,
17 | as = 'a',
18 | href,
19 | } = props;
20 |
21 | return (
22 | {children}
29 | );
30 | }
31 |
32 |
33 | export default React.forwardRef(Link);
--------------------------------------------------------------------------------
/src/components/link/styled.ts:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | interface StyledLinkProps {
4 | disabled: boolean;
5 | }
6 |
7 | export const StyledLink = styled.a`
8 | display: inline-block;
9 | box-sizing: border-box;
10 | color: #0018cf;
11 | cursor: pointer;
12 | text-decoration: none;
13 | margin: 0;
14 | padding: 0;
15 | font-size: 15px;
16 |
17 | &:active, &:visited {
18 | color: #0018cf;
19 | text-decoration: none;
20 | }
21 | &:focus, &:hover {
22 | color: #6874cc;
23 | }
24 |
25 | ${pr => pr.disabled ? `
26 | cursor: not-allowed;
27 | &, &:focus, &:visited, &:hover {
28 | color: #5e5e5e;
29 | }
30 | ` : ''}
31 | `
--------------------------------------------------------------------------------
/src/components/login/index.ts:
--------------------------------------------------------------------------------
1 | import Login from './login';
2 |
3 | export * from './login';
4 |
5 | export default Login;
--------------------------------------------------------------------------------
/src/components/login/login.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Meta, StoryObj } from "@storybook/react";
3 |
4 | import Login, { LoginProps } from ".";
5 |
6 | const meta: Meta = {
7 | title: "Components/Login",
8 | component: Login,
9 | parameters: {
10 | layout: "centered",
11 | },
12 | tags: ["autodocs"],
13 | };
14 |
15 | export default meta;
16 |
17 | type Story = StoryObj;
18 |
19 | export const Default: Story = {
20 | args: {
21 | onSubmit: (values) => alert(JSON.stringify(values, undefined, 2)),
22 | },
23 | };
24 |
--------------------------------------------------------------------------------
/src/components/login/login.tsx:
--------------------------------------------------------------------------------
1 | import React, { ChangeEvent, HTMLAttributes } from "react";
2 | import useLoginLogic from "./use-login-logic";
3 | import FormControl from "../form-control";
4 | import TextInput from "../text-input";
5 | import Button from "../button";
6 | import { ButtonWrapper, StyledForm } from "./styled";
7 | import Typography from "../typography";
8 | import Link from "../link";
9 |
10 | export interface LoginModel {
11 | email?: string;
12 | password?: string;
13 | }
14 |
15 | export type SubmitHandler = (fieldValues: LoginModel) => void;
16 |
17 | export interface LoginProps
18 | extends Omit, "onSubmit"> {
19 | onSubmit?: SubmitHandler;
20 | initialValues?: LoginModel;
21 | loading?: boolean;
22 | registerLink?: string;
23 | }
24 |
25 | const Login: React.ForwardRefRenderFunction = (
26 | props,
27 | ref
28 | ) => {
29 | const {
30 | onSubmit = () => {},
31 | initialValues = {},
32 | loading = false,
33 | registerLink = "#",
34 | } = props;
35 |
36 | const { values, errors, handleChange, handleSubmit } = useLoginLogic(
37 | onSubmit,
38 | initialValues
39 | );
40 | return (
41 |
42 |
43 | Sign In
44 |
45 |
51 | ) =>
56 | handleChange("email")(e.currentTarget.value)
57 | }
58 | error={!!errors.email}
59 | />
60 |
61 |
67 | ) =>
73 | handleChange("password")(e.currentTarget.value)
74 | }
75 | error={!!errors.password}
76 | />
77 |
78 |
79 | Create account
80 |
81 | Proceed
82 |
83 |
84 |
85 | );
86 | };
87 |
88 | export default React.forwardRef(Login);
89 |
--------------------------------------------------------------------------------
/src/components/login/styled.ts:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | export const ButtonWrapper = styled.div`
4 | display: flex;
5 | justify-content: space-between;
6 | align-items: center;
7 | margin-top: 40px;
8 | `;
9 |
10 | export const StyledForm = styled.form`
11 | padding: 40px 25px;
12 | width: 450px;
13 | border: 1px solid #333333;
14 | border-radius: 0.4em;
15 | `;
16 |
--------------------------------------------------------------------------------
/src/components/login/use-login-logic.ts:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import { LoginModel, SubmitHandler } from "./login";
3 | import { validate as validateEmail } from 'email-validator';
4 |
5 | export type ErrorModel = {[key in keyof LoginModel]?: string};
6 |
7 | const validate = (
8 | values: LoginModel,
9 | ) => {
10 | const errors: ErrorModel = {};
11 |
12 | if (!values.password) {
13 | errors.password = 'Field is required';
14 | }
15 |
16 | if (!values.email) {
17 | errors.email = 'Field is required';
18 | } else if (!validateEmail(values.email)) {
19 | errors.email = 'Please type a valid email address'
20 | }
21 |
22 | return errors;
23 | };
24 |
25 | const useLoginLogic = (
26 | onSubmit: SubmitHandler,
27 | initialValues: LoginModel,
28 | ): {
29 | values: LoginModel,
30 | errors: ErrorModel,
31 | handleChange: (fieldName: string) => (value: any) => void,
32 | handleSubmit: (event: any) => void,
33 | } => {
34 | const [values, setValues] = useState(initialValues);
35 | const [errors, setErrors] = useState({});
36 | const [isSubmitting, setIsSubmitting] = useState(false);
37 |
38 |
39 | useEffect(() => {
40 | if (Object.keys(errors).length === 0 && isSubmitting) {
41 | onSubmit(values);
42 | }
43 | setIsSubmitting(false);
44 | }, [errors]);
45 |
46 | const handleSubmit = (
47 | event: any,
48 | ) => {
49 | if (event) event.preventDefault();
50 |
51 | setErrors(validate(values));
52 | setIsSubmitting(true);
53 | };
54 |
55 | const handleChange = (fieldName: string) => (value: any) => {
56 | setValues((values: LoginModel): LoginModel => ({
57 | ...values,
58 | [fieldName]: value,
59 | }));
60 | };
61 |
62 | return {
63 | handleChange,
64 | handleSubmit,
65 | values,
66 | errors,
67 | };
68 | };
69 |
70 | export default useLoginLogic;
--------------------------------------------------------------------------------
/src/components/modal/index.css:
--------------------------------------------------------------------------------
1 | .modal {
2 | position: fixed;
3 | left: 0;
4 | top: 0;
5 | width: 100%;
6 | height: 100%;
7 | background-color: rgba(0, 0, 0, 0.5);
8 | opacity: 0;
9 | visibility: hidden;
10 | transform: scale(1.1);
11 | transition:
12 | visibility 0s linear 0.25s,
13 | opacity 0.25s 0s,
14 | transform 0.25s;
15 | z-index: 200;
16 | }
17 |
18 | .modal-content {
19 | position: absolute;
20 | top: 50%;
21 | left: 50%;
22 | transform: translate(-50%, -50%);
23 | background-color: white;
24 | padding: 1rem 1.5rem;
25 | border-radius: 0.5rem;
26 | }
27 |
28 | .close-button {
29 | float: right;
30 | width: 1.5rem;
31 | line-height: 1.5rem;
32 | text-align: center;
33 | cursor: pointer;
34 | border-radius: 0.25rem;
35 | }
36 | .close-button:hover {
37 | background-color: darkgray;
38 | }
39 | .show-modal {
40 | opacity: 1;
41 | display: block;
42 | visibility: visible;
43 | transform: scale(1);
44 | transition:
45 | visibility 0s linear 0s,
46 | opacity 0.25s 0s,
47 | transform 0.25s;
48 | }
49 |
--------------------------------------------------------------------------------
/src/components/modal/index.ts:
--------------------------------------------------------------------------------
1 | import Modal from "./modal";
2 |
3 | export * from "./modal";
4 |
5 | export default Modal;
6 |
--------------------------------------------------------------------------------
/src/components/modal/modal.stories.tsx:
--------------------------------------------------------------------------------
1 | import { Meta, StoryObj } from "@storybook/react";
2 | import Modal from ".";
3 | import React from "react";
4 |
5 | const meta: Meta = {
6 | title: "Components/Modal",
7 | component: Modal,
8 | parameters: {
9 | layout: "centered",
10 | },
11 | tags: ["autodocs"],
12 | };
13 |
14 | // Export the metadata as default
15 | export default meta;
16 |
17 | type Story = StoryObj;
18 |
19 | export const Default: Story = {
20 | args: {
21 | children: (
22 | Ensure a cohesive user experience across your application.
25 | }
26 | />
27 | ),
28 | },
29 | };
30 |
--------------------------------------------------------------------------------
/src/components/modal/modal.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | // import { StyledModal, ModalContent, CloseButton, ShowModal } from "./styled";
3 | import "./index.css";
4 |
5 | export type ModalProps = {
6 | children: any;
7 | };
8 |
9 | const Modal: React.ForwardRefRenderFunction = (
10 | props,
11 | ref
12 | ) => {
13 | const { children } = props;
14 | const [showModal, setShowModal] = useState(true);
15 | function toggleModal() {
16 | let tempModal = showModal;
17 | setShowModal(!tempModal);
18 | }
19 |
20 | return (
21 |
22 |
23 |
24 |
25 | x
26 |
27 | {children}
28 |
29 |
30 |
31 | // <>
32 | //
36 | //
37 | //
38 | // x
39 | //
40 | // {children}
41 | //
42 | //
43 | // >
44 | );
45 | };
46 |
47 | export default React.forwardRef(Modal);
48 |
--------------------------------------------------------------------------------
/src/components/modal/styled.ts:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | const modalBackgroundColor = "rgba(0, 0, 0, 0.5)";
4 |
5 | export const StyledModal = styled.div`
6 | position: fixed;
7 | left: 0;
8 | top: 0;
9 | width: 100%;
10 | height: 100%;
11 | background-color: ${modalBackgroundColor};
12 | opacity: 0;
13 | visibility: hidden;
14 | transform: scale(1.1);
15 | transition:
16 | visibility 0s linear 0.25s,
17 | opacity 0.25s 0s,
18 | transform 0.25s;
19 | z-index: 200;
20 | `;
21 |
22 | export const ModalContent = styled.div`
23 | position: absolute;
24 | top: 50%;
25 | left: 50%;
26 | transform: translate(-50%, -50%);
27 | background-color: white;
28 | padding: 1rem 1.5rem;
29 | border-radius: 0.5rem;
30 | `;
31 |
32 | export const CloseButton = styled.button`
33 | float: right;
34 | width: 1.5rem;
35 | line-height: 1.5rem;
36 | text-align: center;
37 | cursor: pointer;
38 | border-radius: 0.25rem;
39 |
40 | &:hover {
41 | background-color: darkgray;
42 | }
43 | `;
44 |
45 | export const ShowModal = styled(StyledModal)`
46 | opacity: 1;
47 | display: block;
48 | visibility: visible;
49 | transform: scale(1);
50 | transition:
51 | visibility 0s linear 0s,
52 | opacity 0.25s 0s,
53 | transform 0.25s;
54 | `;
55 |
--------------------------------------------------------------------------------
/src/components/search/index.ts:
--------------------------------------------------------------------------------
1 | import Search from './search';
2 |
3 | export * from './search';
4 |
5 | export default Search;
--------------------------------------------------------------------------------
/src/components/search/search.stories.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { Meta } from "@storybook/react";
3 |
4 | import Search from ".";
5 |
6 | const meta: Meta = {
7 | title: "Components/Search",
8 | component: Search,
9 | parameters: {
10 | layout: "centered",
11 | },
12 | tags: ["autodocs"],
13 | };
14 |
15 | export default meta;
16 |
17 | export const Default = () => {
18 | const [value, setValue] = useState("");
19 |
20 | return (
21 | setValue(e.currentTarget.value)}
26 | />
27 | );
28 | };
29 |
--------------------------------------------------------------------------------
/src/components/search/search.tsx:
--------------------------------------------------------------------------------
1 | import React, { ChangeEventHandler } from 'react';
2 | import Button from '../button';
3 | import TextInput from '../text-input';
4 | import { ComponentSize } from '../../config/sizes';
5 | import { SearchWrapper } from './styled';
6 | import SearchIcon from '../../icons/Search';
7 |
8 | export interface SearchProps {
9 | className?: string;
10 | size?: ComponentSize;
11 | width?: string;
12 | value?: string;
13 | onChange?: ChangeEventHandler;
14 | placeholder?: string;
15 | }
16 |
17 | const Search: React.ForwardRefRenderFunction = (props, ref) => {
18 | const {
19 | size = 'default',
20 | className,
21 | value,
22 | onChange,
23 | placeholder,
24 | width = '100%',
25 | } = props;
26 |
27 | const textInputStyles = {
28 | size,
29 | width: '100%',
30 | placeholder
31 | }
32 |
33 | return (
34 |
39 |
46 |
47 |
48 | );
49 | }
50 |
51 | export default React.forwardRef(Search);
--------------------------------------------------------------------------------
/src/components/search/styled.ts:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | interface SearchWrapperProps {
4 | width: string;
5 | }
6 |
7 | export const SearchWrapper = styled.div`
8 | width: ${pr => pr.width};
9 |
10 | display: flex;
11 | `;
--------------------------------------------------------------------------------
/src/components/select/index.ts:
--------------------------------------------------------------------------------
1 | import Select from './select';
2 |
3 | export * from './select';
4 |
5 | export default Select;
--------------------------------------------------------------------------------
/src/components/select/select.stories.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { Meta } from "@storybook/react";
3 |
4 | import Select, { SelectOption } from ".";
5 | import styled from "styled-components";
6 |
7 | const meta: Meta = {
8 | title: "Components/Select",
9 | component: Select,
10 | parameters: {
11 | layout: "centered",
12 | },
13 | tags: ["autodocs"],
14 | };
15 |
16 | export default meta;
17 |
18 | const listOptions = [
19 | { label: "Item 1", value: "item-1" },
20 | { label: "Item 2", value: "item-2" },
21 | { label: "Item 3", value: "item-3" },
22 | ];
23 |
24 | export const Default = () => {
25 | const [option, setOption] = useState(
26 | listOptions[1]
27 | );
28 | return (
29 | {
35 | setOption(option);
36 | }}
37 | />
38 | );
39 | };
40 |
41 | export const WithError = () => {
42 | const [option, setOption] = useState(
43 | listOptions[1]
44 | );
45 | return (
46 | {
53 | setOption(option);
54 | }}
55 | />
56 | );
57 | };
58 |
59 | export const Disabled = () => {
60 | const [option, setOption] = useState(
61 | listOptions[1]
62 | );
63 | return (
64 | {
71 | setOption(option);
72 | }}
73 | />
74 | );
75 | };
76 |
77 | const Row = styled.div`
78 | display: flex;
79 | align-items: flex-start;
80 | margin-bottom: 10px;
81 | & > * {
82 | margin-right: 10px;
83 | }
84 | `;
85 |
86 | export const Sizes = () => {
87 | return (
88 |
89 |
90 |
91 |
92 |
93 | );
94 | };
95 |
--------------------------------------------------------------------------------
/src/components/select/select.tsx:
--------------------------------------------------------------------------------
1 | import React, { HTMLAttributes, useRef } from 'react';
2 | import TextInput from '../text-input';
3 | import { ComponentSize } from '../../config/sizes';
4 | import {
5 | StyledWrapper,
6 | StyledHeader,
7 | StyledArrow,
8 | StyledPopover,
9 | List,
10 | ListItem
11 | } from './styled';
12 | import { useSelectLogic } from './use-select-logic';
13 |
14 | export type SelectOption = {
15 | label: string;
16 | value: string;
17 | }
18 |
19 | export type SelectChangeHandler = (option: SelectOption | undefined) => void;
20 |
21 | export interface SelectProps extends Omit, 'className' | 'width' | 'option' | 'onChange'> {
22 | className?: string;
23 | size?: ComponentSize;
24 | disabled?: boolean;
25 | error?: boolean;
26 | width?: string;
27 | option?: SelectOption;
28 | listOptions?: SelectOption[];
29 | onChange?: SelectChangeHandler;
30 | placeholder?: string;
31 | }
32 |
33 | const Select: React.ForwardRefRenderFunction = (props, ref) => {
34 | const {
35 | size = 'default',
36 | className,
37 | disabled = false,
38 | error = false,
39 | option,
40 | listOptions,
41 | onChange,
42 | placeholder,
43 | width = '100%',
44 | } = props;
45 |
46 | const textInputStyles = {
47 | size,
48 | disabled,
49 | error,
50 | width: '100%',
51 | placeholder,
52 | readonly: true,
53 | }
54 |
55 | const headerRef = useRef(null);
56 | const popoverRef = useRef(null);
57 | const {
58 | onOptionClick,
59 | optionsListVisible,
60 | setOptionsListVisible
61 | } = useSelectLogic(onChange, { headerRef, popoverRef }, disabled);
62 |
63 | return (
64 |
69 | setOptionsListVisible(true)}
72 | >
73 |
77 |
78 |
79 |
84 |
85 | {listOptions?.map((option) => (
86 | onOptionClick(option)}
90 | >{option.label}
91 | ))}
92 |
93 |
94 |
95 | );
96 | }
97 |
98 | export default React.forwardRef(Select);
--------------------------------------------------------------------------------
/src/components/select/styled.ts:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 | import { ComponentSize, heights, sidePaddings } from "../../config/sizes";
3 | import ArrowDown from '../../icons/ArrowDown';
4 |
5 | interface StyledWrapperProps {
6 | width: string;
7 | innerSize: ComponentSize;
8 | }
9 |
10 | export const StyledWrapper = styled.div`
11 | box-sizing: border-box;
12 | position: relative;
13 | width: ${pr => pr.width};
14 | height: ${ pr => heights[pr.innerSize]}px;
15 | `;
16 |
17 | export const StyledHeader = styled.div`
18 | box-sizing: border-box;
19 | position: relative;
20 |
21 | input:not([disabled]) {
22 | cursor: pointer;
23 | }
24 | `;
25 |
26 | interface StyledArrowProps {
27 | innerSize: ComponentSize;
28 | upwards: boolean;
29 | }
30 |
31 | export const StyledArrow = styled(ArrowDown).withConfig({
32 | shouldForwardProp: (prop, defPropValFN) =>
33 | !["innerSize", "upwards"].includes(prop) && defPropValFN(prop),
34 | })`
35 | box-sizing: border-box;
36 | position: absolute;
37 | right: ${ pr => sidePaddings[pr.innerSize]}px;
38 | top: 50%;
39 | transform: ${ pr => pr.upwards ? 'translateY(-50%) rotateZ(180deg)' : 'translateY(-50%)' };
40 | transition: transform 0.3s;
41 |
42 | pointer-events: none;
43 | height: 15px;
44 | `;
45 |
46 | interface StyledPopoverProps {
47 | width: string;
48 | visible: boolean;
49 | }
50 |
51 | export const StyledPopover = styled.div`
52 | position: absolute;
53 | bottom: 0;
54 | left: 0;
55 | transform: translateY(100%);
56 |
57 | width: ${pr => pr.width};
58 | box-sizing: border-box;
59 | padding: 15px 0;
60 | background-color: #fff;
61 | box-shadow: 0px 2px 2px -1px rgba(0,0,0,0.2), 0px 4px 5px 1px rgba(0,0,0,0.14), 0px 1px 7px 1px rgba(0,0,0,0.12);
62 |
63 | transition: opacity 0.3s;
64 |
65 | visibility: ${pr => pr.visible ? 'visible' : 'hidden'};
66 | opacity: ${pr => pr.visible ? 1 : 0};
67 | `;
68 |
69 | export const List = styled.ul`
70 | box-sizing: border-box;
71 | list-style: none;
72 | margin: 0;
73 | padding: 0;
74 | `;
75 |
76 |
77 | interface ListItemProps {
78 | innerSize: ComponentSize;
79 | }
80 |
81 | export const ListItem = styled.li`
82 | box-sizing: border-box;
83 | list-style: none;
84 | margin: 0;
85 | padding: 6px ${ pr => sidePaddings[pr.innerSize]}px;
86 | cursor: pointer;
87 |
88 | &:hover {
89 | background-color: #EEEEEE;
90 | }
91 | `;
--------------------------------------------------------------------------------
/src/components/select/use-select-logic.ts:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState, useCallback } from 'react';
2 | import { SelectOption, SelectChangeHandler } from './select';
3 |
4 | interface Refs {
5 | headerRef: React.MutableRefObject;
6 | popoverRef: React.MutableRefObject;
7 | }
8 |
9 | export const useSelectLogic = (
10 | onChange: SelectChangeHandler = () => {},
11 | { headerRef, popoverRef }: Refs,
12 | disabled: boolean
13 | ) => {
14 | const [optionsListVisible, __setOptionsListVisible] = useState(false);
15 | const onOptionClick = (option: SelectOption) => {
16 | onChange(option);
17 | setOptionsListVisible(false);
18 | }
19 |
20 | const setOptionsListVisible = useCallback((isVisible: boolean) => {
21 | if (!disabled) {
22 | __setOptionsListVisible(isVisible);
23 | }
24 | }, [disabled]);
25 |
26 | useEffect(() => {
27 | /**
28 | * Alert if clicked on outside of element
29 | */
30 | const handleClickOutside = (event: MouseEvent) => {
31 | if (
32 | headerRef.current
33 | && popoverRef.current
34 | && !headerRef.current.contains(event.target as Node)
35 | && !popoverRef.current.contains(event.target as Node)
36 | ) {
37 | setOptionsListVisible(false);
38 | }
39 | }
40 |
41 | document.addEventListener("mousedown", handleClickOutside);
42 | return () => {
43 | document.removeEventListener("mousedown", handleClickOutside);
44 | };
45 | }, [headerRef, popoverRef, setOptionsListVisible]);
46 |
47 | return {
48 | onOptionClick,
49 | optionsListVisible,
50 | setOptionsListVisible
51 | };
52 | }
--------------------------------------------------------------------------------
/src/components/skeleton/index.ts:
--------------------------------------------------------------------------------
1 | import Skeleton from './skeleton';
2 |
3 | export * from './skeleton';
4 |
5 | export default Skeleton;
--------------------------------------------------------------------------------
/src/components/skeleton/skeleton.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Meta, StoryObj } from "@storybook/react";
3 |
4 | import Skeleton, { SkeletonProps } from ".";
5 |
6 | const meta: Meta = {
7 | title: "Components/Skeleton",
8 | component: Skeleton,
9 | parameters: {
10 | layout: "centered",
11 | },
12 | tags: ["autodocs"],
13 | };
14 |
15 | // Export the metadata as default
16 | export default meta;
17 |
18 | type Story = StoryObj;
19 |
20 | export const Default: Story = {
21 | args: {
22 | width: 200,
23 | height: 50,
24 | },
25 | };
26 |
27 | export const BorderRadius: Story = {
28 | args: {
29 | width: 150,
30 | height: 150,
31 | borderRadius: "50%",
32 | },
33 | };
34 |
--------------------------------------------------------------------------------
/src/components/skeleton/skeleton.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyledSkeleton } from './styled';
3 |
4 | export type SkeletonProps = {
5 | width?: number;
6 | height?: number;
7 | className?: string;
8 | borderRadius: string;
9 | }
10 |
11 | const Skeleton: React.ForwardRefRenderFunction = (props, ref) => {
12 | const {
13 | className,
14 | width = 100,
15 | height = 100,
16 | borderRadius = 'none',
17 | } = props;
18 |
19 | return (
20 |
27 | );
28 | }
29 |
30 |
31 | export default React.forwardRef(Skeleton);
--------------------------------------------------------------------------------
/src/components/skeleton/styled.ts:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | interface StyledSkeletonProps {
4 | width: number;
5 | height: number;
6 | borderRadius: string;
7 | }
8 |
9 | export const StyledSkeleton = styled.div`
10 | position: relative;
11 | overflow: hidden;
12 | width: ${pr => pr.width}px;
13 | height: ${pr => pr.height}px;
14 | background-color: #e8e8e8;
15 | border-radius: ${pr => pr.borderRadius};
16 |
17 | &:before {
18 | content: '';
19 | display: block;
20 | position: absolute;
21 | left: -${pr => pr.width}px;
22 | top: 0;
23 | height: ${pr => pr.height}px;
24 | width: ${pr => pr.width}px;
25 | background-color: #cccccc;
26 | animation: load 1.5s cubic-bezier(0.4, 0.0, 0.2, 1);
27 | animation-iteration-count: infinite;
28 | }
29 |
30 | @keyframes load {
31 | from {
32 | left: -${pr => pr.width}px;
33 | }
34 | to {
35 | left: 100%;
36 | }
37 | }
38 | `;
--------------------------------------------------------------------------------
/src/components/spinner/index.ts:
--------------------------------------------------------------------------------
1 | import Spinner from './spinner';
2 |
3 | export * from './spinner';
4 |
5 | export default Spinner;
--------------------------------------------------------------------------------
/src/components/spinner/spinner.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Meta, StoryObj } from "@storybook/react";
3 |
4 | import Spinner, { SpinnerProps } from ".";
5 | import styled from "styled-components";
6 |
7 | const meta: Meta = {
8 | title: "Components/Spinner",
9 | component: Spinner,
10 | parameters: {
11 | layout: "centered",
12 | },
13 | tags: ["autodocs"],
14 | };
15 |
16 | // Export the metadata as default
17 | export default meta;
18 |
19 | type Story = StoryObj;
20 |
21 | export const Default: Story = {
22 | args: {
23 | size: 50,
24 | },
25 | };
26 |
27 | const DarkBackground = styled.div`
28 | display: flex;
29 | align-items: center;
30 | justify-content: center;
31 | width: 50px;
32 | height: 50px;
33 | background-color: #2c2c59;
34 | `;
35 |
36 | export const Light: React.FC = () => (
37 |
38 |
39 |
40 | );
41 |
--------------------------------------------------------------------------------
/src/components/spinner/spinner.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyledSpinner } from './styled';
3 |
4 | export type SpinnerProps = {
5 | size?: number;
6 | className?: string;
7 | light?: boolean;
8 | }
9 |
10 | const Spinner: React.ForwardRefRenderFunction = (props, ref) => {
11 | const {
12 | className,
13 | size = 30,
14 | light = false,
15 | } = props;
16 |
17 | return (
18 |
24 | );
25 | }
26 |
27 |
28 | export default React.forwardRef(Spinner);
--------------------------------------------------------------------------------
/src/components/spinner/styled.ts:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 | import { getSpinnerWidth } from "./utils";
3 |
4 | interface StyledSpinnerProps {
5 | size: number;
6 | light: boolean;
7 | }
8 |
9 | export const StyledSpinner = styled.span`
10 | box-sizing: border-box;
11 | display: block;
12 | border: ${pr => getSpinnerWidth(pr.size)}px solid transparent;
13 | border-top: ${pr => getSpinnerWidth(pr.size)}px solid ${pr => pr.light ? '#f7f7f7' : '#3d4ed1'};
14 | border-right: ${pr => getSpinnerWidth(pr.size)}px solid ${pr => pr.light ? '#f7f7f7' : '#3d4ed1'};
15 | border-bottom: ${pr => getSpinnerWidth(pr.size)}px solid ${pr => pr.light ? '#f7f7f7' : '#3d4ed1'};
16 | border-radius: 50%;
17 | width: ${pr => pr.size}px;
18 | height: ${pr => pr.size}px;
19 | animation: spin 1s linear infinite;
20 |
21 | @keyframes spin {
22 | 0% { transform: rotate(0deg); }
23 | 100% { transform: rotate(360deg); }
24 | }
25 | `;
--------------------------------------------------------------------------------
/src/components/spinner/utils.ts:
--------------------------------------------------------------------------------
1 | export const getSpinnerWidth = (size: number): number => {
2 | switch (true) {
3 | case (size < 15):
4 | return size / 3.5;
5 | case (size < 50):
6 | return size / 5;
7 | case (size < 100):
8 | return size / 7.5;
9 | case (size >= 100):
10 | return size / 10;
11 | default:
12 | return size / 10;
13 | }
14 | }
--------------------------------------------------------------------------------
/src/components/text-input/index.ts:
--------------------------------------------------------------------------------
1 | import TextInput from './text-input';
2 |
3 | export * from './text-input';
4 |
5 | export default TextInput;
--------------------------------------------------------------------------------
/src/components/text-input/styled.ts:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 | import { ComponentSize, heights, sidePaddings } from "../../config/sizes";
3 | import CrossIcon from '../../icons/Cross';
4 |
5 | interface StyledWrapperProps {
6 | width: string;
7 | innerSize: ComponentSize;
8 | }
9 |
10 | export const StyledWrapper = styled.div`
11 | position: relative;
12 | width: ${pr => pr.width};
13 | height: ${ pr => heights[pr.innerSize]}px;
14 | `;
15 |
16 | interface StyledTextInputProps {
17 | innerSize: ComponentSize;
18 | disabled: boolean;
19 | error: boolean;
20 | width: string;
21 | withIcon: boolean;
22 | withCross: boolean;
23 | }
24 |
25 |
26 | /* Real tag is assigned dynamically */
27 | export const StyledTextInput = styled.input`
28 | box-sizing: border-box;
29 | position: relative;
30 | background-color: ${ (pr) => pr.error ? '#ffe3e6' : '#EEEEEE' };
31 | padding: 0;
32 | padding-left: ${ pr => sidePaddings[pr.innerSize] + (pr.withIcon ? sidePaddings[pr.innerSize] + 10/* icon */ : 0) }px;
33 | padding-right: ${ pr => sidePaddings[pr.innerSize] + (pr.withCross ? sidePaddings[pr.innerSize]/* cross */ : 0) }px;
34 | height: ${ pr => heights[pr.innerSize]}px;
35 | width: ${pr => pr.width};
36 | border: none;
37 | color: #000;
38 | ${ pr => pr.disabled ? `
39 | background-color: #a6a6a6;
40 | color: #5e5e5e;
41 | cursor: not-allowed;
42 |
43 | &:hover {
44 | background-color: #a6a6a6 !important;
45 | color: #5e5e5e !important;
46 | }
47 | ` : ''}
48 | border-radius: 0;
49 | outline: none;
50 | transition: 0.1s ease-out;
51 |
52 | box-shadow: inset 0 0 0 2px ${pr => pr.error ? '#d93848' : 'transparent'};
53 | &:focus {
54 | box-shadow: inset 0 0 0 2px ${pr => pr.error ? '#d93848' : '#000'};
55 | }
56 | `;
57 |
58 | interface StyledIconProps {
59 | innerSize: ComponentSize;
60 | }
61 |
62 | export const StyledIcon = styled.div.withConfig({
63 | shouldForwardProp: (prop, defPropValFN) =>
64 | !["innerSize"].includes(prop) && defPropValFN(prop),
65 | })`
66 | box-sizing: border-box;
67 | position: absolute;
68 | left: ${ pr => sidePaddings[pr.innerSize]}px;
69 | top: 50%;
70 | transform: translateY(-50%);
71 |
72 | height: 15px;
73 | width: 15px;
74 | `;
75 |
76 | interface StyledCrossProps {
77 | innerSize: ComponentSize;
78 | }
79 |
80 | export const StyledCross = styled(CrossIcon)`
81 | box-sizing: border-box;
82 | position: absolute;
83 | right: ${ pr => sidePaddings[pr.innerSize]}px;
84 | top: 50%;
85 | transform: translateY(-50%);
86 | cursor: pointer;
87 |
88 | height: 15px;
89 | `;
--------------------------------------------------------------------------------
/src/components/text-input/text-input.stories.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { Meta, StoryObj } from "@storybook/react";
3 |
4 | import TextInput, { TextInputProps } from ".";
5 | import styled from "styled-components";
6 | import UserIcon from "../../icons/User";
7 |
8 | const meta: Meta = {
9 | title: "Components/TextInput",
10 | component: TextInput,
11 | parameters: {
12 | layout: "centered",
13 | },
14 | tags: ["autodocs"],
15 | };
16 |
17 | // Export the metadata as default
18 | export default meta;
19 |
20 | type Story = StoryObj;
21 |
22 | export const Default: Story = {
23 | args: {
24 | width: "250px",
25 | },
26 | };
27 |
28 | export const Placeholder: Story = {
29 | args: {
30 | placeholder: "Placeholder",
31 | width: "250px",
32 | },
33 | };
34 |
35 | export const WithIcon: Story = {
36 | args: {
37 | placeholder: "Login",
38 | icon: UserIcon,
39 | width: "250px",
40 | },
41 | };
42 |
43 | export const WithError: Story = {
44 | args: {
45 | placeholder: "Wrong input",
46 | width: "250px",
47 | error: true,
48 | },
49 | };
50 |
51 | export const Disabled: Story = {
52 | args: {
53 | placeholder: "Disabled",
54 | width: "250px",
55 | disabled: true,
56 | },
57 | };
58 |
59 | export const ReadOnly: Story = {
60 | args: {
61 | placeholder: "Not editable",
62 | width: "250px",
63 | readonly: true,
64 | },
65 | };
66 |
67 | export const Clearable = () => {
68 | const [value, setValue] = useState("");
69 |
70 | return (
71 | setValue(e.currentTarget.value)}
76 | clearable
77 | />
78 | );
79 | };
80 |
81 | const Row = styled.div`
82 | display: flex;
83 | align-items: flex-start;
84 | margin-bottom: 10px;
85 | & > * {
86 | margin-right: 10px;
87 | }
88 | `;
89 |
90 | export const Sizes = () => {
91 | return (
92 | <>
93 |
94 |
95 |
96 |
97 |
98 | >
99 | );
100 | };
101 |
--------------------------------------------------------------------------------
/src/components/text-input/text-input.tsx:
--------------------------------------------------------------------------------
1 | import React, { ChangeEventHandler, HTMLAttributes } from 'react';
2 | import { ComponentSize } from '../../config/sizes';
3 | import { StyledTextInput, StyledWrapper, StyledIcon, StyledCross } from './styled';
4 |
5 | export interface TextInputProps extends Omit, 'size' | 'disabled' | 'onChange'> {
6 | icon?: React.ElementType;
7 | size?: ComponentSize;
8 | disabled?: boolean;
9 | error?: boolean;
10 | value?: string;
11 | width?: string;
12 | onChange?: ChangeEventHandler;
13 | /* Useful when input is controlled by another element */
14 | readonly?: boolean;
15 | clearable?: boolean;
16 | [key: string]: any;
17 | }
18 |
19 | const TextInput: React.ForwardRefRenderFunction = (props, ref) => {
20 | const {
21 | icon,
22 | size = 'default',
23 | className,
24 | disabled = false,
25 | error = false,
26 | value,
27 | onChange,
28 | placeholder,
29 | width = '100%',
30 | readonly = false,
31 | clearable = false,
32 | ...rest
33 | } = props;
34 |
35 | const styles = {
36 | innerSize: size,
37 | disabled: disabled,
38 | error,
39 | width,
40 | withIcon: icon !== undefined,
41 | withCross: clearable
42 | };
43 |
44 | return (
45 |
46 |
57 |
58 | {clearable && value ? (
59 |
62 | onChange
63 | && onChange({ currentTarget: { value: '' } } as any)
64 | }
65 | />
66 | ) : null}
67 |
68 | );
69 | }
70 |
71 | export default React.forwardRef(TextInput);
--------------------------------------------------------------------------------
/src/components/typography/index.ts:
--------------------------------------------------------------------------------
1 | import Typography from "./typography";
2 |
3 | export * from "./typography";
4 |
5 | export default Typography;
6 |
--------------------------------------------------------------------------------
/src/components/typography/styled.ts:
--------------------------------------------------------------------------------
1 | import styled, { css } from "styled-components";
2 |
3 | const headingMargin = css`
4 | margin: 15px 0;
5 | `;
6 |
7 | interface HeaderProps {
8 | variant?: "h1" | "h2" | "h3";
9 | align?: "center" | "right" | "left";
10 | }
11 |
12 | export const H1 = styled.h1`
13 | font-size: 64px;
14 | text-align: ${(pr) => (pr.align ? pr.align : "left")};
15 | ${headingMargin}
16 | `;
17 |
18 | export const H2 = styled.h2`
19 | font-size: 48px;
20 | text-align: ${(pr) => (pr.align ? pr.align : "left")};
21 | ${headingMargin}
22 | `;
23 |
24 | export const H3 = styled.h3`
25 | font-size: 24px;
26 | text-align: ${(pr) => (pr.align ? pr.align : "left")};
27 | ${headingMargin}
28 | `;
29 |
--------------------------------------------------------------------------------
/src/components/typography/typography.stories.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { Meta, StoryObj } from "@storybook/react";
3 |
4 | import Typography, { TypographyProps } from ".";
5 |
6 | const meta: Meta = {
7 | title: "Components/Typography",
8 | component: Typography,
9 | parameters: {
10 | layout: "centered",
11 | },
12 | tags: ["autodocs"],
13 | };
14 |
15 | // Export the metadata as default
16 | export default meta;
17 |
18 | export const Sizes = () => {
19 | return (
20 | <>
21 |
22 | Ensure a cohesive user experience across your application with a
23 | centralized library of reusable UI components. Say goodbye to design
24 | inconsistencies. Stop reinventing the wheel! Our well-documented
25 | component library empowers you to concentrate on building unique
26 | features and functionalities.
27 |
28 |
29 |
30 | Ensure a cohesive user experience across your application with a
31 | centralized library of reusable UI components. Say goodbye to design
32 | inconsistencies. Stop reinventing the wheel! Our well-documented
33 | component library empowers you to concentrate on building unique
34 | features and functionalities.
35 |
36 |
37 |
38 | Ensure a cohesive user experience across your application with a
39 | centralized library of reusable UI components. Say goodbye to design
40 | inconsistencies. Stop reinventing the wheel! Our well-documented
41 | component library empowers you to concentrate on building unique
42 | features and functionalities.
43 |
44 | >
45 | );
46 | };
47 |
--------------------------------------------------------------------------------
/src/components/typography/typography.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode } from "react";
2 | import { H1, H2, H3 } from "./styled";
3 |
4 | export type TypographyProps = {
5 | variant?: "h1" | "h2" | "h3";
6 | align?: "center" | "right" | "left";
7 | className?: string;
8 | children: ReactNode;
9 | };
10 |
11 | const Typography: React.ForwardRefRenderFunction<
12 | HTMLParagraphElement,
13 | TypographyProps
14 | > = (props, ref) => {
15 | const { variant = "h1", align = "left", className, children } = props;
16 |
17 | const HeadingComponent = (() => {
18 | switch (variant) {
19 | case "h1":
20 | return H1;
21 | case "h2":
22 | return H2;
23 | case "h3":
24 | return H3;
25 | default:
26 | return H1;
27 | }
28 | })();
29 |
30 | return (
31 |
32 | {children}
33 |
34 | );
35 | };
36 |
37 | export default React.forwardRef(Typography);
38 |
--------------------------------------------------------------------------------
/src/config/fonts/Poppins-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vinyl-Davyl/vinyl-component-blocks/cdb592e13255fae06946ad1de1a5c4dcf1f59a12/src/config/fonts/Poppins-Bold.ttf
--------------------------------------------------------------------------------
/src/config/fonts/Poppins-Light.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vinyl-Davyl/vinyl-component-blocks/cdb592e13255fae06946ad1de1a5c4dcf1f59a12/src/config/fonts/Poppins-Light.ttf
--------------------------------------------------------------------------------
/src/config/fonts/Poppins-Medium.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vinyl-Davyl/vinyl-component-blocks/cdb592e13255fae06946ad1de1a5c4dcf1f59a12/src/config/fonts/Poppins-Medium.ttf
--------------------------------------------------------------------------------
/src/config/fonts/Poppins-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vinyl-Davyl/vinyl-component-blocks/cdb592e13255fae06946ad1de1a5c4dcf1f59a12/src/config/fonts/Poppins-Regular.ttf
--------------------------------------------------------------------------------
/src/config/fonts/Poppins-SemiBold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vinyl-Davyl/vinyl-component-blocks/cdb592e13255fae06946ad1de1a5c4dcf1f59a12/src/config/fonts/Poppins-SemiBold.ttf
--------------------------------------------------------------------------------
/src/config/fonts/Poppins-Thin.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vinyl-Davyl/vinyl-component-blocks/cdb592e13255fae06946ad1de1a5c4dcf1f59a12/src/config/fonts/Poppins-Thin.ttf
--------------------------------------------------------------------------------
/src/config/fonts/styles.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: 'Poppins';
3 | src: url('./Poppins-Thin.ttf') format("truetype");
4 | font-weight: 200;
5 | font-style: normal;
6 | }
7 | @font-face {
8 | font-family: 'Poppins';
9 | src: url('./Poppins-Light.ttf') format("truetype");
10 | font-weight: 300;
11 | font-style: normal;
12 | }
13 | @font-face {
14 | font-family: 'Poppins';
15 | src: url('./Poppins-Regular.ttf') format("truetype");
16 | font-weight: 400;
17 | font-style: normal;
18 | }
19 | @font-face {
20 | font-family: 'Poppins';
21 | src: url('./Poppins-Medium.ttf') format("truetype");
22 | font-weight: 500;
23 | font-style: normal;
24 | }
25 | @font-face {
26 | font-family: 'Poppins';
27 | src: url('./Poppins-SemiBold.ttf') format("truetype");
28 | font-weight: 600;
29 | font-style: normal;
30 | }
31 | @font-face {
32 | font-family: 'Poppins';
33 | src: url('./Poppins-Bold.ttf') format("truetype");
34 | font-weight: 700;
35 | font-style: normal;
36 | }
--------------------------------------------------------------------------------
/src/config/global.styles.ts:
--------------------------------------------------------------------------------
1 | import { createGlobalStyle } from 'styled-components';
2 | import './fonts/styles.css';
3 |
4 | export const GlobalStyles = createGlobalStyle`
5 | * {
6 | font-family: 'Poppins';
7 | box-sizing: border-box;
8 | margin: 0;
9 | padding: 0;
10 | }
11 | `;
--------------------------------------------------------------------------------
/src/config/sizes.ts:
--------------------------------------------------------------------------------
1 | export type ComponentSize = 'default' | 'large' | 'small';
2 |
3 | export const sidePaddings: {[key in ComponentSize]: number} = {
4 | large: 30,
5 | default: 25,
6 | small: 20,
7 | }
8 |
9 | export const heights: {[key in ComponentSize]: number} = {
10 | large: 55,
11 | default: 45,
12 | small: 35,
13 | }
--------------------------------------------------------------------------------
/src/icons/ArrowDown.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import type { SVGProps } from "react";
3 | const SvgArrowDown = (props: SVGProps) => (
4 |
5 |
6 |
7 | );
8 | export default SvgArrowDown;
9 |
--------------------------------------------------------------------------------
/src/icons/Cross.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import type { SVGProps } from "react";
3 | const SvgCross = (props: SVGProps) => (
4 |
5 |
6 |
7 | );
8 | export default SvgCross;
9 |
--------------------------------------------------------------------------------
/src/icons/Download.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import type { SVGProps } from "react";
3 | const SvgDownload = (props: SVGProps) => (
4 |
11 |
12 |
13 | );
14 | export default SvgDownload;
15 |
--------------------------------------------------------------------------------
/src/icons/Search.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import type { SVGProps } from "react";
3 | const SvgSearch = (props: SVGProps) => (
4 |
11 |
12 |
13 | );
14 | export default SvgSearch;
15 |
--------------------------------------------------------------------------------
/src/icons/User.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import type { SVGProps } from "react";
3 | const SvgUser = (props: SVGProps) => (
4 |
10 |
11 |
12 | );
13 | export default SvgUser;
14 |
--------------------------------------------------------------------------------
/src/icons/arrow-down.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/src/icons/cross.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/download.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
7 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/icons/index.ts:
--------------------------------------------------------------------------------
1 | export { default as ArrowDown } from "./ArrowDown";
2 | export { default as Cross } from "./Cross";
3 | export { default as Download } from "./Download";
4 | export { default as Search } from "./Search";
5 | export { default as User } from "./User";
6 |
--------------------------------------------------------------------------------
/src/icons/index.tsx:
--------------------------------------------------------------------------------
1 | export { default as ArrowDown } from './ArrowDown'
2 | export { default as Cross } from './Cross'
3 | export { default as Download } from './Download'
4 | export { default as Search } from './Search'
5 | export { default as User } from './User'
--------------------------------------------------------------------------------
/src/icons/search.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
7 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/icons/user.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | export * from "./components";
2 |
3 | export * from './config/global.styles';
4 |
--------------------------------------------------------------------------------
/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "useDefineForClassFields": true,
5 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
6 | "module": "ESNext",
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "bundler",
11 | "allowImportingTsExtensions": true,
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "jsx": "react-jsx",
15 | "outDir": "lib",
16 | "declaration": true,
17 | "sourceMap": true,
18 | "declarationMap": true,
19 | "emitDeclarationOnly": true,
20 |
21 | /* Linting */
22 | "strict": true,
23 | "noUnusedLocals": true,
24 | "noUnusedParameters": true,
25 | "noFallthroughCasesInSwitch": true
26 | },
27 | "include": ["src"],
28 | // exclude stories files(for demonstration purpose only)
29 | "exclude": [
30 | "node_modules",
31 | "**/*.test.ts",
32 | "**/*.test.tsx",
33 | "**/*.stories.tsx",
34 | "./src/setupTests.ts"
35 | ],
36 | "references": [{ "path": "./tsconfig.node.json" }]
37 | }
38 |
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "skipLibCheck": true,
5 | "module": "ESNext",
6 | "moduleResolution": "bundler",
7 | "allowSyntheticDefaultImports": true
8 | },
9 | "include": ["vite.config.ts"]
10 | }
11 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import react from '@vitejs/plugin-react'
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [react()],
7 | })
8 |
--------------------------------------------------------------------------------