();
38 |
39 | renderHook(() => {
40 | useEventListener('click', eventListener, targetElement);
41 | });
42 | expect(eventListener).toHaveBeenCalledTimes(0);
43 | });
44 | });
45 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "airbnb-typescript",
4 | "airbnb/hooks",
5 | "plugin:@typescript-eslint/recommended",
6 | "plugin:jest/recommended",
7 | "prettier",
8 | "prettier/react",
9 | "prettier/@typescript-eslint",
10 | "plugin:prettier/recommended"
11 | ],
12 | "plugins": ["react", "@typescript-eslint", "jest"],
13 | "env": {
14 | "browser": true,
15 | "es6": true,
16 | "jest": true
17 | },
18 | "globals": {
19 | "Atomics": "readonly",
20 | "SharedArrayBuffer": "readonly"
21 | },
22 | "parser": "@typescript-eslint/parser",
23 | "parserOptions": {
24 | "createDefaultProgram": true,
25 | "ecmaFeatures": {
26 | "jsx": true
27 | },
28 | "ecmaVersion": 2018,
29 | "sourceType": "module",
30 | "project": "./tsconfig.json"
31 | },
32 | "rules": {
33 | "sort-imports": [
34 | 2,
35 | {
36 | "ignoreDeclarationSort": true,
37 | "memberSyntaxSortOrder": ["none", "single", "multiple", "all"]
38 | }
39 | ],
40 | "jsx-a11y/label-has-associated-control": [ "error", {
41 | "required": {
42 | "some": [ "nesting", "id" ]
43 | }
44 | }],
45 | "no-unused-vars": 0,
46 | "@typescript-eslint/no-unused-vars": ["error"],
47 | "import/no-unresolved": 0,
48 | "react/jsx-props-no-spreading": 0,
49 | "import/prefer-default-export": 0,
50 | "import/no-extraneous-dependencies": 0,
51 | "no-restricted-globals": 0,
52 | "no-param-reassign": 0,
53 | "@typescript-eslint/no-unused-expressions": 0,
54 | "react/react-in-jsx-scope": 0,
55 | "react/prop-types": 0,
56 | "jsx-a11y/anchor-is-valid": 0,
57 | "@typescript-eslint/no-var-requires": 0
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/components/atoms/Button/Button.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { IButton } from './Button.model';
3 | import {
4 | ButtonIcon,
5 | DisableButton,
6 | FillButton,
7 | OutlineButton,
8 | TransparentButton,
9 | } from './Button.styled';
10 |
11 | const Button = ({
12 | children,
13 | icon: Icon,
14 | iconPos = null,
15 | onClick,
16 | filled,
17 | outline,
18 | disabled,
19 | transparent,
20 | ...props
21 | }: IButton): JSX.Element => {
22 | const content = () => {
23 | if (Icon && iconPos) {
24 | return (
25 |
26 |
32 |
33 |
34 | {children}
35 |
36 | );
37 | }
38 | return {children} ;
39 | };
40 |
41 | if (outline) {
42 | return (
43 |
44 | {content()}
45 |
46 | );
47 | }
48 |
49 | if (transparent) {
50 | return (
51 |
52 | {content()}
53 |
54 | );
55 | }
56 |
57 | if (disabled) {
58 | return (
59 |
60 | {content()}
61 |
62 | );
63 | }
64 |
65 | return (
66 |
67 | {content()}
68 |
69 | );
70 | };
71 |
72 | export default Button;
73 |
--------------------------------------------------------------------------------
/src/components/organisms/CourseList/CourseElement/CourseElement.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import CourseTags from 'components/molecules/CourseTags';
4 | import { useTextClip } from 'hooks';
5 | import { IProps } from './CourseElement.model';
6 | import {
7 | CourseDetails,
8 | Score,
9 | ScoreCard,
10 | ScoreText,
11 | StyledCourseElement,
12 | StyledCourseImageWrapper,
13 | StyledCourseInfo,
14 | StyledCourseLevel,
15 | StyledHeader,
16 | } from './CourseElement.styled';
17 |
18 | const CourseElement = ({
19 | author,
20 | level,
21 | image,
22 | description,
23 | price,
24 | score,
25 | tags,
26 | recommendation = false,
27 | releaseDate,
28 | ...props
29 | }: IProps): JSX.Element => {
30 | const descriptionText = useTextClip(description);
31 |
32 | return (
33 |
34 |
35 |
36 | {level}
37 |
38 |
39 | {descriptionText}
40 |
41 | Author: {author}
42 | Release date: {releaseDate.toLocaleDateString()}
43 | Price: {price}$
44 |
45 |
46 |
47 |
48 |
49 |
50 | Total score
51 | {`${score} / 10`}
52 | {recommendation && (
53 |
54 | Recommended by senior
55 |
56 | )}
57 |
58 |
59 | );
60 | };
61 |
62 | export default CourseElement;
63 |
--------------------------------------------------------------------------------
/.storybook/main.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const svgs = path.resolve(__dirname, '../src/assets/svg');
3 |
4 | module.exports = {
5 | stories: ['../src/**/*.stories.@(js|jsx|ts|tsx)'],
6 | addons: [
7 | '@storybook/addon-links',
8 | '@storybook/addon-essentials',
9 | {
10 | name: '@storybook/preset-create-react-app',
11 | options: {
12 | craOverrides: {
13 | // disable file loader for svgs from preset-create-react-app whitch override SB webpack configuration and broke svgs
14 | fileLoaderExcludes: ['svg'],
15 | },
16 | },
17 | },
18 | ],
19 | typescript: {
20 | check: false,
21 | checkOptions: {},
22 | reactDocgen: 'react-docgen-typescript',
23 | reactDocgenTypescriptOptions: {
24 | shouldExtractLiteralValuesFromEnum: true,
25 | propFilter: (prop) => (prop.parent ? !/node_modules/.test(prop.parent.fileName) : true),
26 | },
27 | },
28 | webpackFinal: async (config) => {
29 | config.resolve.modules = [...(config.resolve.modules || []), path.resolve(__dirname, '../src')];
30 | config.module.rules.push(
31 | {
32 | // rule for svgs from svg dricetory read as SVG Component
33 | test: /\.svg$/,
34 | issuer: {
35 | test: /\.(js|ts)x?$/,
36 | },
37 | include: svgs,
38 | use: [
39 | {
40 | loader: '@svgr/webpack',
41 | options: {
42 | svgo: true,
43 | },
44 | },
45 | ],
46 | },
47 | {
48 | // rule for svgs from images dricetory read as url for img tag
49 | exclude: svgs,
50 | test: /\.svg$/,
51 | use: [
52 | {
53 | loader: 'url-loader',
54 | },
55 | ],
56 | }
57 | );
58 |
59 | return config;
60 | },
61 | };
62 |
--------------------------------------------------------------------------------
/src/assets/svg/logo-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
11 |
15 |
16 |
--------------------------------------------------------------------------------
/.storybook/preview.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { addDecorator } from '@storybook/react';
3 | import GlobalStyles from '../src/styles/GlobalStyles';
4 |
5 | addDecorator((story) => (
6 | <>
7 |
8 | {story()}
9 | >
10 | ));
11 |
12 | export const parameters = {
13 | actions: { argTypesRegex: '^on[A-Z].*' },
14 | viewport: {
15 | viewports: {
16 | mobile: {
17 | name: 'iPhone X',
18 | styles: {
19 | width: '375px',
20 | height: '812px',
21 | },
22 | },
23 | tablet: {
24 | name: 'iPad',
25 | styles: {
26 | width: '768px',
27 | height: '1024px',
28 | },
29 | },
30 | laptop: {
31 | name: 'Laptop',
32 | styles: {
33 | width: '1024px',
34 | height: '768px',
35 | },
36 | },
37 | desktop: {
38 | name: 'Desktop',
39 | styles: {
40 | width: '1440px',
41 | height: '1024px',
42 | },
43 | },
44 | },
45 | },
46 | };
47 |
48 | // Replace next/image for Storybook (currently, we don't use this solution)
49 | //
50 | // import * as nextImage from 'next/image';
51 | //
52 | // Object.defineProperty(nextImage, 'default', {
53 | // configurable: true,
54 | // value: (props) => {
55 | // const { width, height } = props
56 | // const ratio = (height / width) * 100
57 | // return (
58 | //
63 | //
72 | //
73 | // )
74 | // },
75 | // })
76 |
--------------------------------------------------------------------------------
/src/styles/colors.ts:
--------------------------------------------------------------------------------
1 | const colors = {
2 | text: {
3 | Primary20: '#FFFFFF',
4 | Primary40: '#1C7CED',
5 | Primary60: '#0D54A5',
6 | Secondary20: '#FFFFFF',
7 | Secondary40: '#FFFFFF',
8 | Secondary60: '#C48C2A',
9 | NeutralBlack: '#FFFFFF',
10 | Neutral20: '#5D6371',
11 | Neutral40: '#FFFFFF',
12 | Neutral60: '#5D6371',
13 | Neutral80: '#5D6371',
14 | NeutralWhite: '#5D6371',
15 | Success20: '#FFFFFF',
16 | Success40: '#FFFFFF,',
17 | Success60: '#006629',
18 | Error20: '#FFFFFF',
19 | Error40: '#FFFFFF',
20 | Error60: '#700000',
21 | Warning20: '#FFFFFF',
22 | Warning40: '#FFFFFF',
23 | Warning60: '#7E6301',
24 | Information20: '#FFFFFF',
25 | Information40: '#FFFFFF',
26 | Information60: '#026688',
27 | addons: {
28 | Blue10: '#B0D1F9',
29 | Blue20: '#4694F0',
30 | Blue30: '#1066CC',
31 | Blue40: '#0B478C',
32 | },
33 | },
34 | background: {
35 | Primary20: '#0D54A5',
36 | Primary40: '#1C7CED',
37 | Primary60: '#B4D2F8',
38 | Secondary20: '#C48C2A',
39 | Secondary40: '#F59E05',
40 | Secondary60: '#FFBF4F',
41 | NeutralBlack: '#191D24',
42 | Neutral20: '#5D6371',
43 | Neutral40: '#9096A3',
44 | Neutral60: '#B7BDC8',
45 | Neutral80: '#E8EAEE',
46 | NeutralWhite: '#FFFFFF',
47 | Success20: '#006629',
48 | Success40: '#1FE46E,',
49 | Success60: '#5FEC98',
50 | Error20: '#700000',
51 | Error40: '#D81414',
52 | Error60: '#F27D7D',
53 | Warning20: '#7E6301',
54 | Warning40: '#FFCD1C',
55 | Warning60: '#FFE78F',
56 | Information20: '#026688',
57 | Information40: '#1CC8FF',
58 | Information60: '#A3EAFF',
59 | addons: {
60 | Blue10: '#B0D1F9',
61 | Blue20: '#4694F0',
62 | Blue30: '#1066CC',
63 | Blue40: '#0B478C',
64 | },
65 | },
66 | };
67 |
68 | export default colors;
69 |
--------------------------------------------------------------------------------
/src/components/atoms/Input/Input.stories.tsx:
--------------------------------------------------------------------------------
1 | import { InputIcon } from 'assets/svg';
2 | import { Meta } from '@storybook/react/types-6-0';
3 | import Input from './Input';
4 | import { IProps } from './Input.model';
5 |
6 | export default {
7 | title: 'atoms/Input',
8 | component: Input,
9 | argTypes: {
10 | type: {
11 | control: {
12 | type: 'select',
13 | options: ['text', 'password', 'email', 'date', 'number'],
14 | },
15 | },
16 | label: {
17 | type: 'string',
18 | },
19 | className: {
20 | control: {
21 | disable: true,
22 | },
23 | },
24 | icon: {
25 | control: {
26 | disable: true,
27 | },
28 | },
29 | ref: {
30 | control: {
31 | disable: true,
32 | },
33 | },
34 | id: {
35 | control: {
36 | disable: true,
37 | },
38 | },
39 | },
40 | } as Meta;
41 |
42 | export const Default = ({ placeholder, type, label, icon }: IProps): JSX.Element => (
43 |
44 | );
45 |
46 | export const InputWithPlaceholder = ({ type, label, icon }: IProps): JSX.Element => (
47 |
48 | );
49 |
50 | export const InputWithIcon = ({ type, placeholder, label }: IProps): JSX.Element => (
51 |
52 | );
53 |
54 | export const InputWithLabel = ({
55 | type,
56 | placeholder,
57 | icon,
58 | label = 'Example',
59 | }: IProps): JSX.Element => (
60 |
61 | );
62 |
63 | export const InputWithLabelAndIcon = ({
64 | type,
65 | placeholder,
66 | label = 'Example',
67 | }: IProps): JSX.Element => (
68 |
69 | );
70 |
--------------------------------------------------------------------------------
/src/components/organisms/CourseList/CourseElement/CourseElement.styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import Header from 'components/atoms/Header';
3 | import typography from 'styles/typography';
4 | import colors from 'styles/colors';
5 |
6 | export const StyledCourseElement = styled.div`
7 | display: flex;
8 | justify-content: space-between;
9 | max-width: 1016px;
10 | max-height: 184px;
11 | `;
12 |
13 | export const StyledCourseImageWrapper = styled.div`
14 | position: relative;
15 | margin-right: 32px;
16 | background-color: #1cc8ff;
17 | img {
18 | max-width: 140px;
19 | height: 184px;
20 | }
21 | `;
22 |
23 | export const StyledCourseLevel = styled.div`
24 | position: absolute;
25 | bottom: 0;
26 | width: 100%;
27 | height: 55px;
28 | border-radius: 0px 0px 4px 4px;
29 | background-color: rgba(25, 29, 36, 0.15);
30 | ${typography.body.bold.L}
31 | color: ${colors.text.Primary20};
32 | line-height: 55px;
33 | text-align: center;
34 | `;
35 |
36 | export const StyledCourseInfo = styled.div`
37 | max-width: 500px;
38 | `;
39 |
40 | export const StyledHeader = styled(Header)`
41 | ${typography.header.bold.M}
42 | margin: 16px 0;
43 | `;
44 |
45 | export const ScoreCard = styled.div`
46 | margin-left: 10%;
47 | padding: 25px 20px;
48 | max-width: 208px;
49 | border: 1px solid rgba(232, 234, 238, 0.75);
50 | border-radius: 4px;
51 | box-shadow: 0px 4px 8px rgba(232, 234, 238, 0.15), 0px 8px 16px rgba(232, 234, 238, 0.25);
52 | text-align: center;
53 | `;
54 |
55 | export const Score = styled(Header)`
56 | margin: 4px 0;
57 | width: 100%;
58 | font-size: 50px;
59 | `;
60 |
61 | export const ScoreText = styled.p`
62 | margin: 0;
63 | ${typography.body.M}
64 | color: ${colors.text.Neutral20};
65 | span {
66 | font-weight: ${typography.body.bold.M};
67 | }
68 | `;
69 |
70 | export const CourseDetails = styled.div`
71 | margin: 16px 0;
72 | span {
73 | margin-right: 12px;
74 | ${typography.body.M}
75 | color: ${colors.text.Neutral20};
76 |
77 | &:last-child {
78 | margin-right: 0;
79 | }
80 | }
81 | `;
82 |
--------------------------------------------------------------------------------
/src/components/atoms/Button/Button.test.tsx:
--------------------------------------------------------------------------------
1 | import Button from 'components/atoms/Button';
2 | import { ReactComponent as icon } from 'assets/svg/button-icon.svg';
3 | import { cleanup, render } from '@testing-library/react';
4 |
5 | afterEach(cleanup);
6 |
7 | describe('Button Component', () => {
8 | test('default button render', () => {
9 | const { getByTestId } = render(Click );
10 | expect(getByTestId('fill')).toBeInTheDocument();
11 | });
12 |
13 | test('render button with text', () => {
14 | const { getByText, getByTestId } = render(Click );
15 | expect(getByText('Click')).toHaveTextContent(/click/i);
16 | expect(getByTestId('fill')).toBeInTheDocument();
17 | });
18 |
19 | test('that fill button is default', () => {
20 | const { getByTestId } = render(Click );
21 | expect(getByTestId('fill')).toBeInTheDocument();
22 | });
23 |
24 | test('render button with particular props', () => {
25 | const { queryByTestId, rerender } = render(Click );
26 | expect(queryByTestId('outline')).toBeInTheDocument();
27 |
28 | rerender(Click );
29 | expect(queryByTestId('disabled')).toBeInTheDocument();
30 |
31 | rerender(Click );
32 | expect(queryByTestId('transparent')).toBeInTheDocument();
33 |
34 | rerender(Click );
35 | expect(queryByTestId('fill')).toBeInTheDocument();
36 | });
37 |
38 | test('checks that icon has rendered', () => {
39 | const { getByTestId } = render(
40 |
41 | Click
42 |
43 | );
44 | expect(getByTestId('fill').querySelector('svg')).toBeInTheDocument();
45 | });
46 |
47 | test('if icon or iconPos not passed then icon shouldnt render', () => {
48 | const { getByTestId, rerender } = render(Click );
49 | expect(getByTestId('fill').querySelector('svg')).not.toBeInTheDocument();
50 |
51 | rerender(Click );
52 | expect(getByTestId('fill').querySelector('svg')).not.toBeInTheDocument();
53 | });
54 | });
55 |
--------------------------------------------------------------------------------
/src/components/atoms/Toast/Toast.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { ToastIconError, ToastIconInfo, ToastIconSuccess } from 'assets/svg';
3 | import { DefaultToastHeaders, DefaultToastInfo, IProps } from './Toast.model';
4 | import { ToastContent, ToastHeader, ToastIconWrapper, ToastInfo } from './Toast.styled';
5 |
6 | const Toast = ({ header, info, type, ...props }: IProps): JSX.Element => {
7 | switch (type) {
8 | case 'success':
9 | return (
10 |
11 |
12 |
13 |
14 |
15 | {header || DefaultToastHeaders.SUCCESS}
16 | {info || DefaultToastInfo.SUCCESS}
17 |
18 |
19 | );
20 | case 'error':
21 | return (
22 |
23 |
24 |
25 |
26 |
27 | {header || DefaultToastHeaders.ERROR}
28 | {info || DefaultToastInfo.ERROR}
29 |
30 |
31 | );
32 | case 'info':
33 | return (
34 |
35 |
36 |
37 |
38 |
39 | {header || DefaultToastHeaders.INFO}
40 | {info || DefaultToastInfo.INFO}
41 |
42 |
43 | );
44 | default:
45 | return (
46 |
47 |
48 |
49 |
50 |
51 | {header || DefaultToastHeaders.ERROR}
52 | {info && {info} }
53 |
54 |
55 | );
56 | }
57 | };
58 |
59 | export default Toast;
60 |
--------------------------------------------------------------------------------
/src/components/molecules/MultiStepForm/MultiStepFormBody.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { useFormContext } from 'react-hook-form';
3 | import _ from 'lodash';
4 |
5 | import { useFormProgress } from 'hooks';
6 | import ActionButtons from './ActionButtons';
7 | import { IFormData, IMultiStepForm } from './MultiStep.model';
8 | import { StyledForm, StyledTitle } from './MultiStepForm.styled';
9 | import ProgressBar from './ProgressBar';
10 |
11 | const MultiStepFormBody = ({
12 | children,
13 | onNext,
14 | onBack,
15 | onSubmit,
16 | onCancel,
17 | title,
18 | }: IMultiStepForm): JSX.Element => {
19 | const { trigger, watch, errors, clearErrors } = useFormContext();
20 | const { currentStep, goForward, goBack, goTo } = useFormProgress();
21 |
22 | const lastStep = React.Children.count(children) - 1;
23 | const steps = React.Children.toArray(children);
24 | const amountOfSteps = Array.from(Array(steps.length).keys());
25 |
26 | const formData = watch();
27 |
28 | const [form, setForm] = useState({});
29 |
30 | const handleSubmit = (data: IFormData): void => {
31 | if (_.isEmpty(errors)) {
32 | onSubmit(data);
33 | }
34 | };
35 |
36 | const updateCurrentForm = () => setForm((state) => ({ ...state, ...formData }));
37 |
38 | const handleNext = async () => {
39 | const status = await trigger();
40 |
41 | if (!status) return;
42 |
43 | updateCurrentForm();
44 |
45 | if (currentStep === lastStep) {
46 | handleSubmit({ ...form, ...formData });
47 | return;
48 | }
49 |
50 | goForward();
51 | clearErrors();
52 | onNext(formData);
53 | };
54 |
55 | const handleBack = () => {
56 | updateCurrentForm();
57 |
58 | goBack();
59 | onBack(formData);
60 | };
61 |
62 | return (
63 | <>
64 | {title && {title} }
65 |
71 | {steps[currentStep]}
72 |
79 | >
80 | );
81 | };
82 |
83 | export default MultiStepFormBody;
84 |
--------------------------------------------------------------------------------
/src/components/molecules/CourseTags/CourseTags.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import Tag from 'components/atoms/Tag';
3 | import { Tags, TagsEndCounter, TagsLabel, TagsWrapper } from './CourseTags.styled';
4 | import { IProps } from './CourseTags.model';
5 |
6 | const CourseTags = ({ tags, ...props }: IProps): JSX.Element => {
7 | let tagsWidthSummary = 0;
8 | const containerWidth = 300;
9 | let outTags = 0;
10 | const [moreTags, setMoreTags] = useState(outTags);
11 | const [showTags, setShowTags] = useState(false);
12 | const [tagsToRender, setTagsToRender] = useState(null);
13 |
14 | const tagsToShow: any = tags.map((tag) =>
15 | tagsWidthSummary < containerWidth ? (
16 |
17 | {tag.children}
18 |
19 | ) : (
20 | <>{console.log('Add to +more counter')}>
21 | )
22 | );
23 |
24 | const getWidth = (width: number): void => {
25 | tagsWidthSummary += width;
26 | if (tagsWidthSummary > containerWidth) {
27 | outTags += 1;
28 | tagsToShow.pop();
29 | setTimeout(() => {
30 | setTagsToRender(tagsToShow.slice(0, tagsToShow.length - 1));
31 | setMoreTags(outTags + 1);
32 | setShowTags(true);
33 | }, 10);
34 | }
35 |
36 | if (tagsWidthSummary <= containerWidth) {
37 | setTimeout(() => {
38 | setTagsToRender(tagsToShow);
39 | setShowTags(true);
40 | }, 10);
41 | }
42 | };
43 | useEffect(() => {
44 | const tagsToCount = tags.map((tag) =>
45 | tagsWidthSummary < containerWidth ? (
46 |
52 | {console.log('render')}
53 | {tag.children}
54 |
55 | ) : (
56 | <>{console.log('Add to +more counter')}>
57 | )
58 | );
59 | setTagsToRender(tagsToCount);
60 | // eslint-disable-next-line
61 | }, [tags, tagsWidthSummary]);
62 |
63 | return (
64 |
65 | Tags:
66 | {tagsToRender}
67 | +{moreTags} more
68 |
69 | );
70 | };
71 |
72 | export default CourseTags;
73 |
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | name: "CodeQL"
2 |
3 | on:
4 | push:
5 | branches: [master]
6 | pull_request:
7 | # The branches below must be a subset of the branches above
8 | branches: [master]
9 | schedule:
10 | - cron: '0 5 * * 3'
11 |
12 | jobs:
13 | analyze:
14 | name: Analyze
15 | runs-on: ubuntu-latest
16 |
17 | strategy:
18 | fail-fast: false
19 | matrix:
20 | # Override automatic language detection by changing the below list
21 | # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
22 | language: ['javascript']
23 | # Learn more...
24 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
25 |
26 | steps:
27 | - name: Checkout repository
28 | uses: actions/checkout@v2
29 | with:
30 | # We must fetch at least the immediate parents so that if this is
31 | # a pull request then we can checkout the head.
32 | fetch-depth: 2
33 |
34 | # If this run was triggered by a pull request event, then checkout
35 | # the head of the pull request instead of the merge commit.
36 | - run: git checkout HEAD^2
37 | if: ${{ github.event_name == 'pull_request' }}
38 |
39 | # Initializes the CodeQL tools for scanning.
40 | - name: Initialize CodeQL
41 | uses: github/codeql-action/init@v1
42 | with:
43 | languages: ${{ matrix.language }}
44 | # If you wish to specify custom queries, you can do so here or in a config file.
45 | # By default, queries listed here will override any specified in a config file.
46 | # Prefix the list here with "+" to use these queries and those in the config file.
47 | # queries: ./path/to/local/query, your-org/your-repo/queries@main
48 |
49 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
50 | # If this step fails, then you should remove it and run the build manually (see below)
51 | - name: Autobuild
52 | uses: github/codeql-action/autobuild@v1
53 |
54 | # ℹ️ Command-line programs to run using the OS shell.
55 | # 📚 https://git.io/JvXDl
56 |
57 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
58 | # and modify them (or add more) to build your code if your project
59 | # uses a compiled language
60 |
61 | #- run: |
62 | # make bootstrap
63 | # make release
64 |
65 | - name: Perform CodeQL Analysis
66 | uses: github/codeql-action/analyze@v1
--------------------------------------------------------------------------------
/src/styles/typography.ts:
--------------------------------------------------------------------------------
1 | const defaultStyles = `
2 | font-family: Manrope;
3 | font-style: normal;
4 | font-weight: normal;
5 | color: #000000;
6 | line-height: 135%;`;
7 |
8 | const typography = {
9 | globalStyles: defaultStyles,
10 | header: {
11 | XXL: `
12 | ${defaultStyles}
13 | font-size: 37px;
14 | letter-spacing: -.02em;`,
15 | XL: `
16 | ${defaultStyles}
17 | font-size: 33px;
18 | letter-spacing: -.01em;`,
19 | L: `
20 | ${defaultStyles}
21 | font-size: 29px;
22 | letter-spacing: 0;`,
23 | M: `
24 | ${defaultStyles}
25 | font-size: 25px;
26 | letter-spacing: .01em;`,
27 | S: `
28 | ${defaultStyles};
29 | font-size: 21px;
30 | letter-spacing: .01em;`,
31 | bold: {
32 | XXL: `
33 | ${defaultStyles}
34 | font-weight: bold;
35 | font-size: 37px;
36 | letter-spacing: -.02em;`,
37 | XL: `
38 | ${defaultStyles}
39 | font-weight: bold;
40 | font-size: 33px;
41 | letter-spacing: -.01em;`,
42 | L: `
43 | ${defaultStyles}
44 | font-weight: bold;
45 | font-size: 29px;
46 | letter-spacing: 0;`,
47 | M: `
48 | ${defaultStyles}
49 | font-weight: bold;
50 | font-size: 25px;
51 | letter-spacing: .01em;`,
52 | S: `
53 | ${defaultStyles}
54 | font-weight: bold;
55 | font-size: 21px;
56 | letter-spacing: .01em;`,
57 | },
58 | },
59 | body: {
60 | L: `
61 | ${defaultStyles}
62 | font-size: 17px;
63 | letter-spacing: 0;`,
64 | M: `
65 | ${defaultStyles}
66 | font-size: 15px;
67 | letter-spacing: .01em;`,
68 | S: `
69 | ${defaultStyles}
70 | font-size: 13px;
71 | letter-spacing: .02em;`,
72 | XS: `
73 | ${defaultStyles};
74 | font-size: 11px;
75 | letter-spacing: .03em;
76 | `,
77 | bold: {
78 | L: `
79 | ${defaultStyles}
80 | font-weight: bold;
81 | font-size: 17px;
82 | letter-spacing: 0;`,
83 | M: `
84 | ${defaultStyles}
85 | font-weight: bold;
86 | font-size: 15px;
87 | letter-spacing: .01em;`,
88 | S: `
89 | ${defaultStyles}
90 | font-weight: bold;
91 | font-size: 13px;
92 | letter-spacing: .02em;`,
93 | XS: `
94 | ${defaultStyles};
95 | font-size: 11px;
96 | font-weight: bold;
97 | letter-spacing: .03em;
98 | `,
99 | },
100 | },
101 | };
102 |
103 | export default typography;
104 |
--------------------------------------------------------------------------------
/src/components/atoms/Toast/Toast.test.tsx:
--------------------------------------------------------------------------------
1 | import { render } from '@testing-library/react';
2 | import Toast from './Toast';
3 | import { DefaultToastHeaders, DefaultToastInfo } from './Toast.model';
4 |
5 | describe('Toast', () => {
6 | test('render default Toast component without info', () => {
7 | const { getByTestId, getByText } = render( );
8 | expect(getByTestId('toastDefault')).toBeInTheDocument();
9 | expect(getByText(DefaultToastHeaders.ERROR)).toBeInTheDocument();
10 | });
11 | test('render default Toast component with info', () => {
12 | const info = 'Test info';
13 | const { getByTestId, getByText } = render( );
14 | expect(getByTestId('toastDefault')).toBeInTheDocument();
15 | expect(getByText(DefaultToastHeaders.ERROR)).toBeInTheDocument();
16 | });
17 | test('render success Toast component with default header and info', () => {
18 | const { getByTestId, getByText } = render( );
19 | expect(getByTestId('toastSuccess')).toBeInTheDocument();
20 | expect(getByText(DefaultToastHeaders.SUCCESS)).toBeInTheDocument();
21 | expect(getByText(DefaultToastInfo.SUCCESS)).toBeInTheDocument();
22 | });
23 | test('render error Toast component with default header and info', () => {
24 | const { getByTestId, getByText } = render( );
25 | expect(getByTestId('toastError')).toBeInTheDocument();
26 | expect(getByText(DefaultToastHeaders.ERROR)).toBeInTheDocument();
27 | expect(getByText(DefaultToastInfo.ERROR)).toBeInTheDocument();
28 | });
29 | test('render info Toast component with default header and info', () => {
30 | const { getByTestId, getByText } = render( );
31 | expect(getByTestId('toastInfo')).toBeInTheDocument();
32 | expect(getByText(DefaultToastHeaders.INFO)).toBeInTheDocument();
33 | expect(getByText(DefaultToastInfo.INFO)).toBeInTheDocument();
34 | });
35 | test('render success Toast component with header and info props', () => {
36 | const header = 'Test header';
37 | const info = 'Test info';
38 | const { getByText } = render( );
39 | expect(getByText(header)).toBeInTheDocument();
40 | expect(getByText(info)).toBeInTheDocument();
41 | });
42 | test('render error Toast component with header and info props', () => {
43 | const header = 'Test header';
44 | const info = 'Test info';
45 | const { getByText } = render( );
46 | expect(getByText(header)).toBeInTheDocument();
47 | expect(getByText(info)).toBeInTheDocument();
48 | });
49 | test('render info Toast component with header and info props', () => {
50 | const header = 'Test header';
51 | const info = 'Test info';
52 | const { getByText } = render( );
53 | expect(getByText(header)).toBeInTheDocument();
54 | expect(getByText(info)).toBeInTheDocument();
55 | });
56 | });
57 |
--------------------------------------------------------------------------------
/src/components/atoms/Button/Button.styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import typography from 'styles/typography';
3 | import colors from 'styles/colors';
4 | import { IconPosition } from './Button.model';
5 |
6 | const BaseButton = styled.button<{ iconPos: IconPosition }>`
7 | box-sizing: border-box;
8 | display: flex;
9 | padding: 14px 24px;
10 | height: 52px;
11 | border: none;
12 | border-radius: 6px;
13 | ${typography.body.bold.L}
14 | font-weight: 600;
15 | outline: none;
16 | cursor: pointer;
17 | div {
18 | display: flex;
19 | flex-direction: ${({ iconPos }) => (iconPos === 'right' ? 'row-reverse' : 'row')};
20 | align-content: space-between;
21 | align-items: center;
22 | height: 24px;
23 | }
24 | span {
25 | height: 24px;
26 | color: #fff;
27 | }
28 | `;
29 |
30 | export const FillButton = styled(BaseButton)`
31 | background-color: ${colors.background.Primary40};
32 | color: ${colors.text.Primary20};
33 | `;
34 |
35 | export const OutlineButton = styled(BaseButton)`
36 | position: relative;
37 | background-color: ${colors.background.NeutralWhite};
38 | color: ${colors.text.Primary40};
39 | ::after {
40 | content: '';
41 | position: absolute;
42 | top: 0px;
43 | left: 0px;
44 | z-index: 2;
45 | width: calc(100% - 2px);
46 | height: calc(100% - 2px);
47 | transform: translate(1px);
48 | border: 1px solid ${colors.text.Primary40};
49 | border-radius: 6px;
50 | background: transparent;
51 | }
52 | span {
53 | color: ${colors.text.Primary40};
54 | }
55 | `;
56 |
57 | export const DisableButton = styled(BaseButton)`
58 | background-color: ${colors.background.Neutral80};
59 | span {
60 | color: ${colors.background.Neutral40};
61 | }
62 | `;
63 |
64 | export const TransparentButton = styled(BaseButton)`
65 | background: transparent;
66 | span {
67 | color: ${colors.text.Primary40};
68 | }
69 | `;
70 |
71 | const setIconMargin = (pos: IconPosition) => {
72 | switch (pos) {
73 | case 'left':
74 | return '0 14px 0 0';
75 | case 'right':
76 | return '0 0 0 14px';
77 | default:
78 | return '0';
79 | }
80 | };
81 |
82 | const setBackgroundColor = (fill: boolean, outline: boolean, transparent: boolean): string => {
83 | if (fill) {
84 | return colors.background.NeutralWhite;
85 | }
86 | if (outline || transparent) {
87 | return colors.background.Primary40;
88 | }
89 |
90 | return colors.background.Neutral40;
91 | };
92 |
93 | export const ButtonIcon = styled.div<{
94 | iconPos: IconPosition;
95 | filled: boolean;
96 | transparent: boolean;
97 | outline: boolean;
98 | }>`
99 | margin: ${({ iconPos }) => setIconMargin(iconPos)};
100 | height: 24px;
101 | svg {
102 | width: 24px;
103 | height: 24px;
104 | path {
105 | fill: ${({ filled, transparent, outline }) =>
106 | setBackgroundColor(filled, transparent, outline)};
107 | }
108 | }
109 | `;
110 |
--------------------------------------------------------------------------------
/src/components/molecules/SelectInput/Select/Select.tsx:
--------------------------------------------------------------------------------
1 | import React, { MouseEvent, useEffect, useRef, useState } from 'react';
2 | import { ArrowIcon } from 'assets/images';
3 | import { useOutsideClick } from 'hooks';
4 | import {
5 | StyledArrowImage,
6 | StyledInput,
7 | StyledSelect,
8 | StyledSelectCaption,
9 | StyledTopWrapper,
10 | StyledWrapper,
11 | } from './Select.styled';
12 | import Option from '../Option';
13 | import { ISelect } from './Select.model';
14 | import { ISingleOption } from '../SelectInput.model';
15 |
16 | const Select = ({
17 | isMulti,
18 | options,
19 | onChange,
20 | selectCaption,
21 | inputPlaceholder,
22 | isOpen,
23 | }: ISelect): JSX.Element => {
24 | const [mutableOptions, setMutableOptions] = useState(options);
25 | const [selectedOptions, setSelectedOptions] = useState([]);
26 | const [currentOptionId, setCurrentOptionId] = useState('');
27 | const [isOpenState, setIsOpenState] = useState(isOpen || false);
28 | const containerRef = useRef(null);
29 |
30 | useOutsideClick(containerRef, () => {
31 | setIsOpenState(false);
32 | });
33 |
34 | const doesArrayConsistValue = (id: string) => {
35 | return selectedOptions.filter((element: ISingleOption) => element.id === id).length;
36 | };
37 |
38 | const isInputTextMatch = (inputText: string, value: string) => {
39 | const regex = new RegExp(`^${inputText}`, 'i');
40 | return regex.test(value);
41 | };
42 |
43 | const updateSelectedOptions = (option: ISingleOption): void => {
44 | const { id } = option;
45 | setCurrentOptionId(id);
46 | if (isMulti) {
47 | if (!doesArrayConsistValue(id)) {
48 | setSelectedOptions([option, ...selectedOptions]);
49 | } else {
50 | const newArray = selectedOptions.filter((element: ISingleOption) => element.id !== id);
51 | setSelectedOptions([...newArray]);
52 | }
53 | } else {
54 | setSelectedOptions([option]);
55 | }
56 | };
57 |
58 | const filterOptions = (e: React.ChangeEvent) => {
59 | const inputValue = e.target.value;
60 | const filteredOptions = options.filter((option) => isInputTextMatch(inputValue, option.value));
61 | setMutableOptions(filteredOptions);
62 | };
63 |
64 | const toggleList = (e: MouseEvent) => {
65 | const clickedElementName = (e.target as HTMLDivElement).localName;
66 | setMutableOptions(options);
67 | if (clickedElementName !== 'input') {
68 | setIsOpenState(!isOpenState);
69 | }
70 | };
71 | useEffect(() => {
72 | onChange(selectedOptions);
73 | }, [selectedOptions, onChange]);
74 |
75 | return (
76 |
77 |
78 | {isOpenState ? (
79 |
80 | ) : (
81 | {selectCaption}
82 | )}
83 |
84 |
85 | {isOpenState && (
86 |
87 | {mutableOptions.map((option) => (
88 |
96 | ))}
97 |
98 | )}
99 |
100 | );
101 | };
102 |
103 | export default Select;
104 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "frontend",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start -p 80",
9 | "storybook": "start-storybook -p 6006 -s public",
10 | "build-storybook": "build-storybook -s public",
11 | "lint:css": "stylelint './src/**/*.ts'",
12 | "postinstall": "husky install",
13 | "test": "cross-env CI=true react-scripts test --color --env=jsdom",
14 | "test:watch": "npm test --env=jsdom",
15 | "test:coverage": "cross-env CI=true react-scripts test --env=jsdom --coverage"
16 | },
17 | "dependencies": {
18 | "@commitlint/cli": "^11.0.0",
19 | "@reduxjs/toolkit": "^1.5.0",
20 | "@types/react-dom": "^16.9.0",
21 | "@types/react-redux": "^7.1.16",
22 | "@types/react-router-dom": "^5.1.5",
23 | "@types/styled-components": "^5.1.7",
24 | "@typescript-eslint/eslint-plugin": "^4.15.0",
25 | "@typescript-eslint/parser": "^4.15.0",
26 | "cross-env": "^7.0.3",
27 | "eslint": "^7.19.0",
28 | "eslint-config-airbnb": "^18.2.1",
29 | "eslint-config-airbnb-typescript": "^12.3.1",
30 | "eslint-config-prettier": "^7.2.0",
31 | "eslint-plugin-import": "^2.22.1",
32 | "eslint-plugin-jest": "^24.1.3",
33 | "eslint-plugin-jsx-a11y": "^6.4.1",
34 | "eslint-plugin-prettier": "^3.3.1",
35 | "eslint-plugin-react": "^7.22.0",
36 | "eslint-plugin-react-hooks": "^4.2.0",
37 | "lint-staged": "^10.5.4",
38 | "next": "10.2.1",
39 | "prettier": "^2.2.1",
40 | "react": "17.0.1",
41 | "react-dom": "17.0.1",
42 | "react-hook-form": "^6.15.4",
43 | "react-redux": "^7.2.2",
44 | "react-router-dom": "^5.2.0",
45 | "react-scripts": "^4.0.1",
46 | "styled-components": "^5.2.1",
47 | "stylelint": "^13.9.0",
48 | "stylelint-config-recommended": "^3.0.0",
49 | "stylelint-config-styled-components": "^0.1.1",
50 | "stylelint-order": "^4.1.0",
51 | "stylelint-processor-styled-components": "^1.10.0"
52 | },
53 | "devDependencies": {
54 | "@babel/core": "^7.12.10",
55 | "@storybook/addon-actions": "^6.1.21",
56 | "@storybook/addon-essentials": "^6.1.21",
57 | "@storybook/addon-info": "^5.3.21",
58 | "@storybook/addon-links": "^6.1.21",
59 | "@storybook/addon-viewport": "^6.1.21",
60 | "@storybook/addons": "^6.1.21",
61 | "@storybook/node-logger": "^6.1.21",
62 | "@storybook/preset-create-react-app": "^3.1.7",
63 | "@storybook/react": "^6.1.21",
64 | "@storybook/theming": "^6.1.21",
65 | "@svgr/webpack": "^5.5.0",
66 | "@testing-library/jest-dom": "^5.11.9",
67 | "@testing-library/react": "^11.2.5",
68 | "@testing-library/react-hooks": "^5.1.0",
69 | "@testing-library/user-event": "^12.8.1",
70 | "@types/lodash": "^4.14.168",
71 | "@types/node": "^14.14.20",
72 | "@types/react": "^17.0.0",
73 | "awesome-typescript-loader": "^5.2.1",
74 | "babel-loader": "^8.2.2",
75 | "babel-plugin-inline-react-svg": "^2.0.0",
76 | "babel-plugin-styled-components": "^1.12.0",
77 | "babel-preset-react-app": "^9.1.0",
78 | "husky": "^5.0.9",
79 | "next-images": "^1.6.2",
80 | "react-docgen-typescript-loader": "^3.7.2",
81 | "storybook": "^6.1.21",
82 | "storybook-addon-next-router": "^2.0.4",
83 | "typescript": "^4.1.3",
84 | "url-loader": "^4.1.1"
85 | },
86 | "lint-staged": {
87 | "*.{js,jsx,ts,tsx}": [
88 | "prettier --config .prettierrc --write",
89 | "eslint --fix"
90 | ],
91 | "*.styled.{js,jsx,ts,tsx}": [
92 | "npm run lint:css"
93 | ]
94 | },
95 | "jest": {
96 | "collectCoverageFrom": [
97 | "src/**/*.tsx",
98 | "src/**/*.ts",
99 | "!src/index.tsx",
100 | "!src/**/index.tsx",
101 | "!src/**/index.ts",
102 | "!src/**/*.styled.ts",
103 | "!src/**/*.stories.tsx"
104 | ]
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/src/components/atoms/Button/Button.stories.tsx:
--------------------------------------------------------------------------------
1 | import { ButtonIcon } from 'assets/svg';
2 | import Button from './Button';
3 | import { IButton } from './Button.model';
4 |
5 | export default {
6 | title: 'atoms/Buttton',
7 | component: Button,
8 | args: {
9 | children: 'Click',
10 | icon: ButtonIcon,
11 | iconPos: null,
12 | fill: true,
13 | transparent: false,
14 | disabled: false,
15 | outline: false,
16 | },
17 | argTypes: {
18 | children: {
19 | control: {
20 | type: 'text',
21 | },
22 | },
23 | iconPos: {
24 | control: {
25 | type: 'inline-radio',
26 | options: ['left', 'right', null],
27 | },
28 | },
29 | icon: {
30 | control: {
31 | disable: true,
32 | },
33 | },
34 | className: {
35 | control: {
36 | disable: true,
37 | },
38 | },
39 | fill: {
40 | control: {
41 | type: 'boolean',
42 | },
43 | },
44 | disabled: {
45 | control: {
46 | type: 'boolean',
47 | },
48 | },
49 | transparent: {
50 | control: {
51 | type: 'boolean',
52 | },
53 | },
54 | outline: {
55 | control: {
56 | type: 'boolean',
57 | },
58 | },
59 | },
60 | };
61 |
62 | export const Default = ({
63 | children,
64 | iconPos,
65 | icon,
66 | filled,
67 | outline,
68 | disabled,
69 | transparent,
70 | }: IButton): JSX.Element => (
71 |
79 | {children}
80 |
81 | );
82 |
83 | export const FillWithIconLeft = ({ children, icon }: IButton): JSX.Element => (
84 |
85 | {children}
86 |
87 | );
88 |
89 | export const FillWithIconRight = ({ children, icon }: IButton): JSX.Element => (
90 |
91 | {children}
92 |
93 | );
94 |
95 | export const FillWithoutIcon = ({ children, icon }: IButton): JSX.Element => (
96 |
97 | {children}
98 |
99 | );
100 |
101 | export const OutlineWithIconLeft = ({ children, icon }: IButton): JSX.Element => (
102 |
103 | {children}
104 |
105 | );
106 |
107 | export const OutlineWithIconRight = ({ children, icon }: IButton): JSX.Element => (
108 |
109 | {children}
110 |
111 | );
112 |
113 | export const OutlineWithoutIcon = ({ children, icon }: IButton): JSX.Element => (
114 |
115 | {children}
116 |
117 | );
118 |
119 | export const DisabledWithIconLeft = ({ children, icon }: IButton): JSX.Element => (
120 |
121 | {children}
122 |
123 | );
124 |
125 | export const DisabledWithIconRight = ({ children, icon }: IButton): JSX.Element => (
126 |
127 | {children}
128 |
129 | );
130 |
131 | export const DisabledWithoutIcon = ({ children, icon }: IButton): JSX.Element => (
132 |
133 | {children}
134 |
135 | );
136 |
137 | export const TransparentWithIconLeft = ({ children, icon }: IButton): JSX.Element => (
138 |
139 | {children}
140 |
141 | );
142 |
143 | export const TransparentWithIconRight = ({ children, icon }: IButton): JSX.Element => (
144 |
145 | {children}
146 |
147 | );
148 |
149 | export const TransparentWithoutIcon = ({ children, icon }: IButton): JSX.Element => (
150 |
151 | {children}
152 |
153 | );
154 |
--------------------------------------------------------------------------------
/src/components/atoms/Ranges/LevelRange/LevelRange.styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import colors from 'styles/colors';
3 | import typography from 'styles/typography';
4 |
5 | export const Container = styled.div`
6 | display: flex;
7 | justify-content: center;
8 | align-items: center;
9 | max-width: 328px;
10 | height: 72px;
11 | `;
12 | export const Label = styled.label`
13 | position: relative;
14 | margin-top: -10px;
15 | `;
16 |
17 | export const ThumbWrapper = styled.div<{ value: number }>`
18 | position: absolute;
19 | bottom: -6px;
20 | left: ${({ value }) => `${value}%`};
21 | z-index: 2;
22 | width: 24px;
23 | height: 24px;
24 | transform: translateX(-50%);
25 | `;
26 |
27 | export const ThumbSlider = styled.span<{ value: number }>`
28 | position: absolute;
29 | top: 0;
30 | width: 22px;
31 | height: 22px;
32 | border: 2px solid ${colors.background.Primary20};
33 | border-radius: 50%;
34 | background: ${colors.background.NeutralWhite};
35 | cursor: pointer;
36 | ::before {
37 | content: '${({ value }) => value}';
38 | position: absolute;
39 | top: -25px;
40 | display: block;
41 | width: 100%;
42 | color: ${colors.background.Primary20};
43 | text-align: center;
44 | ${typography.body.L}
45 | cursor: pointer;
46 | }
47 | `;
48 |
49 | export const Input = styled.input<{ value: number }>`
50 | -webkit-appearance: none;
51 | -moz-appearance: none;
52 | position: relative;
53 | z-index: 5;
54 | margin: 0;
55 | width: 328px;
56 | height: 6px;
57 | border-radius: 50px;
58 | background: transparent;
59 | ::-webkit-slider-thumb {
60 | -webkit-appearance: none;
61 | position: relative;
62 | z-index: 5;
63 | width: 22px;
64 | height: 22px;
65 | border-radius: 50%;
66 | background-color: transparent;
67 | cursor: pointer;
68 | }
69 | ::-moz-range-thumb {
70 | -webkit-appearance: none;
71 | -moz-appearance: none;
72 | position: relative;
73 | z-index: 5;
74 | width: 22px;
75 | height: 22px;
76 | border: none;
77 | border-radius: 50%;
78 | background-color: transparent;
79 | cursor: pointer;
80 | }
81 | `;
82 |
83 | export const Datalist = styled.datalist`
84 | position: absolute;
85 | top: 10px;
86 | left: 0;
87 | display: flex;
88 | width: 100%;
89 | height: 6px;
90 | background: transparent;
91 | @supports (-moz-appearance: none) {
92 | top: 12px;
93 | }
94 | `;
95 |
96 | export const Option = styled.option`
97 | display: block;
98 | padding-top: 18px;
99 | width: calc(328px / 4);
100 | height: 6px;
101 | background-color: transparent;
102 | ::before {
103 | position: absolute;
104 | top: 0;
105 | right: 0;
106 | width: calc(328px / 4);
107 | height: 6px;
108 | content: '';
109 | }
110 | ::after {
111 | content: attr(aria-label);
112 | display: block;
113 | ${typography.body.L}
114 | text-align: center;
115 | }
116 | :nth-of-type(1) {
117 | ::before {
118 | left: 0;
119 | border-radius: 50px 0 0 50px;
120 | background-color: ${colors.background.addons.Blue10};
121 | }
122 | ::after {
123 | color: ${colors.text.addons.Blue10};
124 | }
125 | }
126 | :nth-of-type(2) {
127 | ::before {
128 | left: 25%;
129 | background-color: ${colors.background.addons.Blue20};
130 | }
131 | ::after {
132 | color: ${colors.text.addons.Blue20};
133 | }
134 | }
135 | :nth-of-type(3) {
136 | ::before {
137 | left: 50%;
138 | background-color: ${colors.background.addons.Blue30};
139 | }
140 | ::after {
141 | color: ${colors.text.addons.Blue30};
142 | }
143 | }
144 | :nth-of-type(4) {
145 | ::before {
146 | left: 75%;
147 | border-radius: 0 50px 50px 0;
148 | background-color: ${colors.background.addons.Blue40};
149 | }
150 | ::after {
151 | color: ${colors.text.addons.Blue40};
152 | }
153 | }
154 | `;
155 |
--------------------------------------------------------------------------------
/.github/workflows/frontend_aws_deploy.yml:
--------------------------------------------------------------------------------
1 | # This workflow will build and push a new container image to Amazon ECR,
2 | # and then will deploy a new task definition to Amazon ECS, when a release is created
3 | #
4 | # To use this workflow, you will need to complete the following set-up steps:
5 | #
6 | # 1. Create an ECR repository to store your images.
7 | # For example: `aws ecr create-repository --repository-name my-ecr-repo --region us-east-2`.
8 | # Replace the value of `ECR_REPOSITORY` in the workflow below with your repository's name.
9 | # Replace the value of `aws-region` in the workflow below with your repository's region.
10 | #
11 | # 2. Create an ECS task definition, an ECS cluster, and an ECS service.
12 | # For example, follow the Getting Started guide on the ECS console:
13 | # https://us-east-2.console.aws.amazon.com/ecs/home?region=us-east-2#/firstRun
14 | # Replace the values for `service` and `cluster` in the workflow below with your service and cluster names.
15 | #
16 | # 3. Store your ECS task definition as a JSON file in your repository.
17 | # The format should follow the output of `aws ecs register-task-definition --generate-cli-skeleton`.
18 | # Replace the value of `task-definition` in the workflow below with your JSON file's name.
19 | # Replace the value of `container-name` in the workflow below with the name of the container
20 | # in the `containerDefinitions` section of the task definition.
21 | #
22 | # 4. Store an IAM user access key in GitHub Actions secrets named `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`.
23 | # See the documentation for each action used below for the recommended IAM policies for this IAM user,
24 | # and best practices on handling the access key credentials.
25 |
26 | name: Deploy to Amazon ECS
27 |
28 | on:
29 | push:
30 | branches: [master]
31 | workflow_dispatch:
32 | inputs:
33 | home:
34 | description: 'location'
35 | required: false
36 |
37 | jobs:
38 | deploy:
39 | name: Deploy
40 | runs-on: ubuntu-latest
41 |
42 | steps:
43 | - name: Checkout
44 | uses: actions/checkout@v2
45 |
46 | - name: Configure AWS credentials
47 | uses: aws-actions/configure-aws-credentials@v1
48 | with:
49 | aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
50 | aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
51 | aws-region: eu-central-1
52 |
53 | - name: Login to Amazon ECR
54 | id: login-ecr
55 | uses: aws-actions/amazon-ecr-login@v1
56 |
57 | - name: Build, tag, and push image to Amazon ECR
58 | id: build-image
59 | env:
60 | ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
61 | ECR_REPOSITORY: geeks_frontend
62 | IMAGE_TAG: ${{ github.sha }}
63 | run: |
64 | # Build a docker container and
65 | # push it to ECR so that it can
66 | # be deployed to ECS.
67 | docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
68 | docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
69 | echo "::set-output name=image::$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG"
70 |
71 | - name: Fill in the new image ID in the Amazon ECS task definition
72 | id: task-def
73 | uses: aws-actions/amazon-ecs-render-task-definition@v1
74 | with:
75 | task-definition: ./frontend_task_definition.json
76 | container-name: geeks_frontend
77 | image: ${{ steps.build-image.outputs.image }}
78 |
79 | - name: Deploy Amazon ECS task definition
80 | uses: aws-actions/amazon-ecs-deploy-task-definition@v1
81 | with:
82 | task-definition: ${{ steps.task-def.outputs.task-definition }}
83 | service: geeks_frontend
84 | cluster: GeeksAcademy
85 | wait-for-service-stability: false
86 |
87 | #TODO can be deleted once we will use Application Load Balancers and dynamic ports
88 | - name: Stop current task
89 | run: |
90 | #!/bin/bash
91 | tasks=`aws ecs list-tasks --cluster GeeksAcademy | jq '.taskArns'`
92 | for task in $tasks; do
93 | name=""
94 | if [ ${#task} -gt 1 ]; then
95 | task=`echo $task | tr -d '\"'`
96 | name=`aws ecs describe-tasks --cluster GeeksAcademy --tasks $task | jq '.tasks' | jq '.[0].overrides.containerOverrides' | jq '.[0].name' | tr -d '\"'`
97 | if [[ $name == "geeks_frontend" ]]; then
98 | echo $name
99 | aws ecs stop-task --cluster GeeksAcademy --task $task
100 | fi
101 | fi
102 | done
--------------------------------------------------------------------------------
/src/assets/svg/logo-text.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
11 |
15 |
19 |
23 |
27 |
31 |
35 |
39 |
43 |
47 |
51 |
55 |
59 |
63 |
67 |
68 |
--------------------------------------------------------------------------------