diff --git a/.eslintrc.json b/.eslintrc.json index 9c74a81..9ee8816 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -32,24 +32,5 @@ "caughtErrorsIgnorePattern": "^_" } ] - /* "import/order": [ - "error", - { - "groups": ["builtin", "external", "internal"], - "pathGroups": [ - { - "pattern": "react", - "group": "external", - "position": "before" - } - ], - "pathGroupsExcludedImportTypes": ["react"], - "newlines-between": "always", - "alphabetize": { - "order": "asc", - "caseInsensitive": true - } - } - ] */ } } diff --git a/README.md b/README.md index f189dd5..b70cc4c 100644 --- a/README.md +++ b/README.md @@ -70,13 +70,11 @@ yarn run test:coverage``` script ## Resources -- [Product design](https://www.figma.com/file/3awAF5mKSCZVdhfnmu8R7g/Service-Dey?type=design&node-id=100-42&t=SkxXK9zT64bhFQfD-0) - - [Local URL](http://localhost:3000) -- [Staging URL](https://x10-staging.vercel.app/) +- [Staging URL](https://x10-staging.netlify.app/) -- [Production URL](https://x10.com) +- [Production URL](https://x10.dev) ## PR convention @@ -96,7 +94,7 @@ For example, image 2 and give the images a title e.g New login page ## Code convention -We use the "next/core-web-vitals" coding standard which is enforced by an eslint-extension we use. So, ensure to go through at least each file within the folders to get an overview of our coding standards like "imports patterns", "export patterns", "named regular functions", "arrow function", "anonymous function, please don't do this one, we want all our functions to be named at least a-named-function-expression :)" +We try as much as possible to follow the best and latest code standards. So, ensure to go through at least each file within the folders to get an overview of our coding standards like "imports patterns", "export patterns", "named regular functions", "arrow function", "anonymous function, please don't do this one, we want all our functions to be named at least a-named-function-expression :)" ## Folder naming convention diff --git a/docs/contribution.md b/docs/contribution.md deleted file mode 100644 index e69de29..0000000 diff --git a/docs/maintenance.md b/docs/maintenance.md deleted file mode 100644 index e69de29..0000000 diff --git a/package.json b/package.json index 165d546..4430c52 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,9 @@ }, "dependencies": { "@radix-ui/react-accordion": "^1.1.2", + "@radix-ui/react-dialog": "^1.0.5", + "@radix-ui/react-icons": "^1.3.0", + "@tanstack/react-query": "^5.28.4", "axios": "^1.6.8", "js-crypto-hmac": "^1.0.7", "react": "^18.2.0", diff --git a/public/android-chrome-192x192.png b/public/android-chrome-192x192.png new file mode 100644 index 0000000..38c3c89 Binary files /dev/null and b/public/android-chrome-192x192.png differ diff --git a/public/android-chrome-512x512.png b/public/android-chrome-512x512.png new file mode 100644 index 0000000..028b87d Binary files /dev/null and b/public/android-chrome-512x512.png differ diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png new file mode 100644 index 0000000..0212658 Binary files /dev/null and b/public/apple-touch-icon.png differ diff --git a/public/favicon-16x16.png b/public/favicon-16x16.png new file mode 100644 index 0000000..14ad50a Binary files /dev/null and b/public/favicon-16x16.png differ diff --git a/public/favicon-32x32.png b/public/favicon-32x32.png new file mode 100644 index 0000000..6839b80 Binary files /dev/null and b/public/favicon-32x32.png differ diff --git a/public/favicon.ico b/public/favicon.ico index a11777c..3b52633 100644 Binary files a/public/favicon.ico and b/public/favicon.ico differ diff --git a/public/logo192.png b/public/logo192.png deleted file mode 100644 index fc44b0a..0000000 Binary files a/public/logo192.png and /dev/null differ diff --git a/public/logo512.png b/public/logo512.png deleted file mode 100644 index a4e47a6..0000000 Binary files a/public/logo512.png and /dev/null differ diff --git a/public/manifest.json b/public/manifest.json index 080d6c7..bf49366 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -1,21 +1,21 @@ { - "short_name": "React App", - "name": "Create React App Sample", + "short_name": "x10", + "name": "x10", "icons": [ { - "src": "favicon.ico", - "sizes": "64x64 32x32 24x24 16x16", - "type": "image/x-icon" + "src": "/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" }, { - "src": "logo192.png", - "type": "image/png", - "sizes": "192x192" + "src": "/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" }, { - "src": "logo512.png", - "type": "image/png", - "sizes": "512x512" + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" } ], "start_url": ".", diff --git a/src/design-system/.gitkeep b/src/design-system/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/design-system/assets/icn-empty.png b/src/design-system/assets/icn-empty.png new file mode 100644 index 0000000..0250738 Binary files /dev/null and b/src/design-system/assets/icn-empty.png differ diff --git a/src/design-system/assets/icn-logo.png b/src/design-system/assets/icn-logo.png new file mode 100644 index 0000000..98755b9 Binary files /dev/null and b/src/design-system/assets/icn-logo.png differ diff --git a/src/design-system/assets/index.ts b/src/design-system/assets/index.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/src/design-system/assets/index.ts @@ -0,0 +1 @@ +export {}; diff --git a/src/design-system/colors/index.stories.tsx b/src/design-system/colors/index.stories.tsx new file mode 100644 index 0000000..0880d66 --- /dev/null +++ b/src/design-system/colors/index.stories.tsx @@ -0,0 +1,91 @@ +import React from 'react'; + +import { StoryObj, Meta } from '@storybook/react'; + +import { colors } from './index'; + +type ColorsPropTypes = { + color: string; +}; + +const Colors = ({ color }: ColorsPropTypes) => ( +
+
+
+); + +const meta: Meta = { + title: 'COLORS', + component: Colors, +}; + +export default meta; + +type Story = StoryObj; + +export const Primary400: Story = { args: { color: colors.primary400 } }; +export const Primary300: Story = { args: { color: colors.primary300 } }; +export const Primary200: Story = { args: { color: colors.primary200 } }; +export const Primary100: Story = { args: { color: colors.primary100 } }; + +export const Secondary400: Story = { args: { color: colors.secondary400 } }; +export const Secondary300: Story = { args: { color: colors.secondary300 } }; +export const Secondary200: Story = { args: { color: colors.secondary200 } }; +export const Secondary100: Story = { args: { color: colors.secondary100 } }; + +export const White400: Story = { args: { color: colors.white400 } }; +export const White300: Story = { args: { color: colors.white300 } }; +export const White200: Story = { args: { color: colors.white200 } }; +export const White100: Story = { args: { color: colors.white100 } }; + +export const Dark500: Story = { args: { color: colors.dark500 } }; +export const Dark400: Story = { args: { color: colors.dark400 } }; +export const Dark300: Story = { args: { color: colors.dark300 } }; +export const Dark200: Story = { args: { color: colors.dark200 } }; +export const Dark100: Story = { args: { color: colors.dark100 } }; + +export const Grey300: Story = { args: { color: colors.grey300 } }; +export const Grey200: Story = { args: { color: colors.grey200 } }; +export const Grey100: Story = { args: { color: colors.grey100 } }; + +export const Accent400: Story = { args: { color: colors.accent400 } }; +export const Accent300: Story = { args: { color: colors.accent300 } }; +export const Accent200: Story = { args: { color: colors.accent200 } }; +export const Accent100: Story = { args: { color: colors.accent100 } }; + +export const Success400: Story = { args: { color: colors.success400 } }; +export const Success300: Story = { args: { color: colors.success300 } }; +export const Success200: Story = { args: { color: colors.success200 } }; +export const Success100: Story = { args: { color: colors.success100 } }; + +export const Error400: Story = { args: { color: colors.error400 } }; +export const Error300: Story = { args: { color: colors.error300 } }; +export const Error200: Story = { args: { color: colors.error200 } }; +export const Error100: Story = { args: { color: colors.error100 } }; + +export const Warning400: Story = { args: { color: colors.warning400 } }; +export const Warning300: Story = { args: { color: colors.warning300 } }; +export const Warning200: Story = { args: { color: colors.warning200 } }; +export const Warning100: Story = { args: { color: colors.warning100 } }; + +export const Magenta400: Story = { args: { color: colors.magenta400 } }; +export const Magenta300: Story = { args: { color: colors.magenta300 } }; +export const Magenta200: Story = { args: { color: colors.magenta200 } }; +export const Magenta100: Story = { args: { color: colors.magenta100 } }; + +export const Neutral400: Story = { args: { color: colors.neutral400 } }; +export const Neutral300: Story = { args: { color: colors.neutral300 } }; diff --git a/src/design-system/colors/index.test.ts b/src/design-system/colors/index.test.ts new file mode 100644 index 0000000..30b382b --- /dev/null +++ b/src/design-system/colors/index.test.ts @@ -0,0 +1,61 @@ +import { colors } from './index'; + +describe('colors', () => { + it('should confirm colors are valid', () => { + expect({ + white400: '#fff', + white300: '#FDFEFF', + white200: '#F7F7F7', + white100: '#F7F7F7', + + warning400: '#FF7A00', + warning300: '#FF9534', + warning200: '#FFB067', + warning100: '#FFD0A5', + warning50: '#FFF7EF', + + secondary400: '#181818', + secondary300: '#737373', + secondary200: '#B0B0B0', + secondary100: '#DFDFDF', + + error400: '#FF0000', + error300: '#FF5656', + error200: '#FDA1A1', + error100: '#FCDEDE', + + success400: '#02A543', + success300: '#5CC486', + success200: '#A2DDB9', + success100: '#D4EEDE', + + magenta400: '#CD00D1', + magenta300: '#DE5BE0', + magenta200: '#EBA1ED', + magenta100: '#F4D4F5', + + primary400: '#0038FF', + primary300: '#5B7FFE', + primary200: '#A1B5FD', + primary100: '#D4DDFC', + + dark500: '#000', + dark400: '#212121', + dark300: '#4D4D4D', + dark200: '#999999', + dark100: '#C9C9C9', + + grey300: '#242426', + grey200: '#4f4e50', + grey100: '#cacacb', + + accent400: '#E7E7E7', + accent300: '#EEEEEE', + accent200: '#F3F3F3', + accent100: '#F7F7F7', + + neutral400: '#6f6c90', + neutral300: '#d9dbe9', + }).toMatchObject(colors); + }); +}); diff --git a/src/design-system/colors/index.ts b/src/design-system/colors/index.ts new file mode 100644 index 0000000..de74ecb --- /dev/null +++ b/src/design-system/colors/index.ts @@ -0,0 +1,65 @@ +/** + * @colors this is our colors token. + * + * @sample + * ```ts + * const H1 = styled.h1` + * color: ${(props) => props.theme.colors.warning400}; + * `; + * ``` + */ +export const colors = { + white400: '#fff', + white300: '#FDFEFF', + white200: '#F7F7F7', + white100: '#F7F7F7', + + warning400: '#FF7A00', + warning300: '#FF9534', + warning200: '#FFB067', + warning100: '#FFD0A5', + warning50: '#FFF7EF', + + secondary400: '#181818', + secondary300: '#737373', + secondary200: '#B0B0B0', + secondary100: '#DFDFDF', + + error400: '#FF0000', + error300: '#FF5656', + error200: '#FDA1A1', + error100: '#FCDEDE', + + success400: '#02A543', + success300: '#5CC486', + success200: '#A2DDB9', + success100: '#D4EEDE', + + magenta400: '#CD00D1', + magenta300: '#DE5BE0', + magenta200: '#EBA1ED', + magenta100: '#F4D4F5', + + primary400: '#0038FF', + primary300: '#5B7FFE', + primary200: '#A1B5FD', + primary100: '#D4DDFC', + + dark500: '#000', + dark400: '#212121', + dark300: '#4D4D4D', + dark200: '#999999', + dark100: '#C9C9C9', + + grey300: '#242426', + grey200: '#4f4e50', + grey100: '#cacacb', + + accent400: '#E7E7E7', + accent300: '#EEEEEE', + accent200: '#F3F3F3', + accent100: '#F7F7F7', + + neutral400: '#6f6c90', + neutral300: '#d9dbe9', +} as const; diff --git a/src/design-system/design-tokens.ts b/src/design-system/design-tokens.ts new file mode 100644 index 0000000..5f663c7 --- /dev/null +++ b/src/design-system/design-tokens.ts @@ -0,0 +1,10 @@ +import * as assets from './assets'; + +import { colors } from './colors'; +import { typography } from './typography'; + +export const designTokens = { + colors, + typography, + assets, +}; diff --git a/src/design-system/global-styles.ts b/src/design-system/global-styles.ts new file mode 100644 index 0000000..53fe585 --- /dev/null +++ b/src/design-system/global-styles.ts @@ -0,0 +1,155 @@ +/** + * We use RadixUI and Styled-components together. + * In a situation where we need to style a Radix primitive, + * we then integrate it with the styled component like shown below: + * ```ts + * 'use client'; + * import styled from 'styled-components'; + * import * as Accordion from '@radix-ui/react-accordion'; + * + * const AccordionRoot = styled(Accordion.Root)` + * background-color: red; + * `; + * const AccordionItem = styled(Accordion.Item)` + * background-color: pink; + * `; + * const AccordionTrigger = styled(Accordion.Trigger)` + * background-color: green; + * `; + * const AccordionContent = styled(Accordion.Content)` + * color: orange; + * `; + * + * export default function Home() { + * return ( + * + * + * Is it accessible? + * + * Yes. It adheres to the WAI-ARIA design pattern. + * + * + * + * ); + *} + * + * ``` + */ + +import styled, { createGlobalStyle } from 'styled-components'; + +import { designTokens } from './design-tokens'; + +const theme = { + colors: designTokens.colors, + typography: designTokens.typography, +}; + +export type Theme = Record<'theme', typeof theme>; + +const SkipToMainContent = styled.a` + background-color: ${(props) => props.theme.colors.warning400}; + border: solid 0.0625rem ${(props) => props.theme.colors.dark400}; + color: ${(props) => props.theme.colors.white400}; + border-radius: ${(props) => props.theme.typography.borderRadius.md}; + font-size: ${(props) => props.theme.typography.bodyText.fontSize}; + padding: 0.5rem; + position: absolute; + left: 0; + top: -300px; + z-index: 100; + transition: top 0.5s ease-out; + outline: none; + + &:focus { + position: absolute; + left: 0; + top: 0; + transition: top 0.3s ease-out; + } +`; + +const GlobalStyles = createGlobalStyle` + *, + *::before, + *::after { + margin: 0; + padding: 0; + box-sizing: border-box; + } + + html, body { + font-family: ${({ theme }) => theme.typography.fontFamilies.primary}; + font-size: ${({ theme }) => theme.typography.bodyText.fontSize}; + font-weight: ${({ theme }) => theme.typography.bodyText.fontWeight}; + line-height: ${({ theme }) => theme.typography.lineHeight.xs}; + text-rendering: optimizeLegibility; + scroll-behavior: smooth; + width: 100%; + height: 100%; + position: relative; + } + + body { + background: ${({ theme }) => theme.colors.white300}; + display: flex; + display: -webkit-flex; + flex-direction: column; + } + + body main#main { + flex-grow: 1; + } + + h1, h2, h3, h4, h5, h6 { + margin-bottom: ${({ theme }) => theme.typography.lineHeight.md}; + } + + img { + max-width: 100%; + display: block; + height: auto; + } + + ul, ol { + list-style: none; + } + + a { + color: inherit; + text-decoration: none; + } + + select, + button, + [type="submit"], + [type="reset"], + [type="button"]{ + cursor: pointer; + &:disabled{ + opacity: 0.5; + cursor: not-allowed; + } + } + + input[type='color'], + input[type='date'], + input[type='datetime'], + input[type='datetime-local'], + input[type='email'], + input[type='month'], + input[type='number'], + input[type='password'], + input[type='search'], + input[type='tel'], + input[type='text'], + input[type='time'], + input[type='url'], + input[type='week'], + select:focus, + textarea { + font-size: 1rem; + } +`; + +export { SkipToMainContent, GlobalStyles, theme }; diff --git a/src/design-system/index.ts b/src/design-system/index.ts new file mode 100644 index 0000000..59be39d --- /dev/null +++ b/src/design-system/index.ts @@ -0,0 +1,2 @@ +export * from './design-tokens'; +export * from './global-styles'; diff --git a/src/design-system/typography/index.stories.tsx b/src/design-system/typography/index.stories.tsx new file mode 100644 index 0000000..d444aaf --- /dev/null +++ b/src/design-system/typography/index.stories.tsx @@ -0,0 +1,86 @@ +import React from 'react'; + +import { StoryObj, Meta } from '@storybook/react'; + +import { typography } from './index'; + +interface TypographyPropTypes extends React.CSSProperties { + children: React.ReactNode; +} + +const Typography = ({ children, ...restProps }: TypographyPropTypes) => ( +

{children}

+); + +const meta: Meta = { + title: 'TYPOGRAPHY', + component: Typography, +}; + +export default meta; + +type Story = StoryObj; + +export const TitleH1: Story = { + args: { + fontWeight: typography.title1.fontWeight, + fontSize: typography.title1.fontSize, + children: 'TitleH1', + }, +}; + +export const TitleH2: Story = { + args: { + fontWeight: typography.title2.fontWeight, + fontSize: typography.title2.fontSize, + children: 'TitleH2', + }, +}; + +export const TitleH3: Story = { + args: { + fontWeight: typography.title3.fontWeight, + fontSize: typography.title3.fontSize, + children: 'TitleH3', + }, +}; + +export const BoldBody: Story = { + args: { + fontWeight: typography.title1.fontWeight, + fontSize: typography.title1.fontSize, + children: 'TitleH1', + }, +}; + +export const Subtitle: Story = { + args: { + fontWeight: typography.subtitle.fontWeight, + fontSize: typography.subtitle.fontSize, + children: 'Subtitle', + }, +}; + +export const BodyText: Story = { + args: { + fontWeight: typography.bodyText.fontWeight, + fontSize: typography.bodyText.fontSize, + children: 'BodyText', + }, +}; + +export const SmallText: Story = { + args: { + fontWeight: typography.smallText.fontWeight, + fontSize: typography.smallText.fontSize, + children: 'SmallText', + }, +}; + +export const PreText: Story = { + args: { + fontWeight: typography.preText.fontWeight, + fontSize: typography.preText.fontSize, + children: 'PreText', + }, +}; diff --git a/src/design-system/typography/index.test.ts b/src/design-system/typography/index.test.ts new file mode 100644 index 0000000..418c525 --- /dev/null +++ b/src/design-system/typography/index.test.ts @@ -0,0 +1,60 @@ +import { typography } from '.'; + +describe('typography', () => { + it('should confirm typographies are valid', () => { + expect(typography).toMatchObject({ + fontFamilies: { + primary: '"Poppins", sans-serif', + secondary: 'Inter', + }, + zIndexes: { + step1: '100', + step2: '200', + step3: '300', + overlay: '500', + modal: '1000', + max: '99999', + }, + lineHeight: { + xxs: '1.3', + xs: '1.5', + sm: '1.75', + md: '2', + lg: '2.25', + xl: '2.5', + xxl: '2.75', + }, + space: { + xs: '0.25rem', + sm: '0.5rem', + md: '1rem', + lg: '1.5rem', + xl: '2rem', + xxl: '3rem', + xxxl: '6rem', + }, + pageWidth: { + minWidth: '100%', + mobileStartWidth: '100%', + mobileEndWidth: '767px', + desktopStartWidth: '768px', + desktopEndWidth: '1440px', + }, + borderRadius: { + sm: '8px', + md: '11px', + lg: '14px', + xl: '25px', + }, + breakpoints: ['40em', '48em', '62em', '80em'], + title1: { fontWeight: 'bold', fontSize: '65px' }, + title2: { fontWeight: 'bold', fontSize: '50px' }, + title3: { fontWeight: 'bold', fontSize: '35px' }, + subtitle: { fontWeight: '500px', fontSize: '24px' }, + boldBody: { fontWeight: 'bold', fontSize: '17px' }, + bodyText: { fontWeight: '400px', fontSize: '16px' }, + smallText: { fontWeight: '400px', fontSize: '14px' }, + preText: { fontWeight: '400px', fontSize: '10px' }, + }); + }); +}); diff --git a/src/design-system/typography/index.ts b/src/design-system/typography/index.ts new file mode 100644 index 0000000..5050cac --- /dev/null +++ b/src/design-system/typography/index.ts @@ -0,0 +1,67 @@ +/** + * @typography this is our typographic design-token. + * we try to follow the css field convention for + * easy usage. + * + * @sample + * ```ts + * const H1 = styled.h1` + * font-weight: ${(props) => props.theme.typography.title1.fontWeight}; + * font-size: ${(props) => props.theme.typography.title1.fontSize}; + * `; + * ``` + */ +export const typography = { + fontFamilies: { + primary: '"Poppins", sans-serif', + secondary: 'Inter', + }, + zIndexes: { + step1: '100', + step2: '200', + step3: '300', + overlay: '500', + modal: '1000', + max: '99999', + }, + lineHeight: { + xxs: '1.3', + xs: '1.5', + sm: '1.75', + md: '2', + lg: '2.25', + xl: '2.5', + xxl: '2.75', + }, + space: { + xs: '0.25rem', + sm: '0.5rem', + md: '1rem', + lg: '1.5rem', + xl: '2rem', + xxl: '3rem', + xxxl: '6rem', + }, + pageWidth: { + minWidth: '100%', + mobileStartWidth: '100%', + mobileEndWidth: '767px', + desktopStartWidth: '768px', + desktopEndWidth: '1440px', + }, + borderRadius: { + sm: '8px', + md: '11px', + lg: '14px', + xl: '25px', + }, + breakpoints: ['40em', '48em', '62em', '80em'], + title1: { fontWeight: 'bold', fontSize: '65px' }, + title2: { fontWeight: 'bold', fontSize: '50px' }, + title3: { fontWeight: 'bold', fontSize: '35px' }, + subtitle: { fontWeight: '500px', fontSize: '24px' }, + boldBody: { fontWeight: 'bold', fontSize: '17px' }, + bodyText: { fontWeight: '400px', fontSize: '16px' }, + smallText: { fontWeight: '400px', fontSize: '14px' }, + preText: { fontWeight: '400px', fontSize: '10px' }, +} as const; diff --git a/src/global-store/index.tsx b/src/global-store/index.tsx new file mode 100644 index 0000000..1e8ea50 --- /dev/null +++ b/src/global-store/index.tsx @@ -0,0 +1,51 @@ +/** + * @About this folder contains the application global state(state shared across the app) + * + * @Usage all global context should be exposed through this store. + * + * @Note only global context should be exposed through this file meaning, + * if a context value is needed in a singular parent, then it should be + * exposed from here. Mind you, this pattern is modular, meaning each context + * value is a standalone module. + * + * @Sample + * ```ts + * export function GlobalStore(props: GlobalStorePropTypes) { + * const a = useAPresenter(); // this is a separate logical module in its on file + * const b = useBPresenter(); // this is a separate logical module in its on file + * const c = useCPresenter(); // this is a separate logical module in its on file + * const values = React.useMemo(() => ({ a,b,c }), [a,b,c ]); + * + * return ( + * {props.children}; + * ) + * } + * ``` + */ +import React from 'react'; + +import { createContext } from 'shared/utils'; +import {} from 'shared/models'; + +/** + * Context + */ +type GlobalStoreContextType = {}; + +const [GlobalStoreProvider, useGlobalStore] = + createContext('GlobalStoreContext'); + +/** + * Component + */ +type GlobalStorePropTypes = { + children: React.ReactNode; +}; + +function GlobalStore(props: GlobalStorePropTypes) { + const values = React.useMemo(() => ({}), []); + + return {props.children}; +} + +export { GlobalStore, useGlobalStore }; diff --git a/src/index.tsx b/src/index.tsx index 74cff11..ba489e3 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,19 +1,40 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; +import { ThemeProvider } from 'styled-components'; +import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; +import { QueryClientProvider, QueryClient } from '@tanstack/react-query'; + import { App } from 'app'; import { startMockServer } from 'test'; +import { GlobalStore } from 'global-store'; import { reportWebVitals, Natives } from 'configs'; +import { ErrorBoundary, InternetNotifier } from 'shared/components'; import { unregisterServiceWorker } from 'service-worker-registration'; +import { SkipToMainContent, GlobalStyles, theme } from 'design-system'; Natives.bind(); +const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 5000 } } }); + const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement); startMockServer().finally(() => { root.render( - + + + + Skip to main content + + + + + + + + + ); }); diff --git a/src/shared/components/.gitkeep b/src/shared/components/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/shared/components/error-boundary/icon-json.png b/src/shared/components/error-boundary/icon-json.png new file mode 100644 index 0000000..be844e0 Binary files /dev/null and b/src/shared/components/error-boundary/icon-json.png differ diff --git a/src/shared/components/error-boundary/icon-refresh.png b/src/shared/components/error-boundary/icon-refresh.png new file mode 100644 index 0000000..19660d5 Binary files /dev/null and b/src/shared/components/error-boundary/icon-refresh.png differ diff --git a/src/shared/components/error-boundary/index.component.tsx b/src/shared/components/error-boundary/index.component.tsx new file mode 100644 index 0000000..9f11939 --- /dev/null +++ b/src/shared/components/error-boundary/index.component.tsx @@ -0,0 +1,54 @@ +import React from 'react'; + +import { __DEV__ } from 'shared/utils'; +import { Heading2 } from 'shared/components'; + +import iconJson from './icon-json.png'; +import iconRefresh from './icon-refresh.png'; + +import { Wrapper, Refresh } from './index.styles'; + +type ErrorBoundaryPropTypes = { + children: React.ReactNode; +}; + +type ErrorBoundaryStateTypes = { + hasError: boolean; +}; + +export class ErrorBoundary extends React.Component< + ErrorBoundaryPropTypes, + ErrorBoundaryStateTypes +> { + constructor(props: ErrorBoundaryPropTypes) { + super(props); + + this.state = { hasError: false } as ErrorBoundaryStateTypes; + } + public static getDerivedStateFromError(_error: Error) { + return { hasError: true }; + } + + public componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { + window.x10.println?.group('COMPONENT RENDERING ERROR 🚨'); + window.x10.println?.error({ error, errorInfo }); + window.x10.println?.groupEnd(); + } + + public render() { + if (this.state.hasError) { + return ( + + + Oops, compilation error {``} + + + Try again? + + + ); + } + + return this.props.children; + } +} diff --git a/src/shared/components/error-boundary/index.stories.tsx b/src/shared/components/error-boundary/index.stories.tsx new file mode 100644 index 0000000..a1cde57 --- /dev/null +++ b/src/shared/components/error-boundary/index.stories.tsx @@ -0,0 +1,26 @@ +import React from 'react'; + +import { StoryFn, Meta } from '@storybook/react'; + +import { throwError } from 'shared/utils'; + +import { ErrorBoundary } from './index.component'; + +export default { + title: 'Components/ErrorBoundary', + component: ErrorBoundary, +} as Meta; + +function ThrowError() { + React.useEffect(() => { + throwError('ErrorBoundarySimulationError', 'Lets simulate our ErrorBoundary Abilities 🛠️'); + }, []); + + return

This can never be rendered, are you checking it in the DOM ☕️

; +} + +export const Primary: StoryFn = () => ( + + + +); diff --git a/src/shared/components/error-boundary/index.styles.tsx b/src/shared/components/error-boundary/index.styles.tsx new file mode 100644 index 0000000..dedb6d8 --- /dev/null +++ b/src/shared/components/error-boundary/index.styles.tsx @@ -0,0 +1,34 @@ +import styled from 'styled-components'; + +import { Container } from 'shared/layouts'; + +const Wrapper = styled(Container)` + margin: 0 auto; + text-align: center; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + gap: 1rem; + + & > img { + width: 300px; + } +`; + +const Refresh = styled.button` + display: flex; + justify-content: center; + align-items: center; + padding: 10px; + + & span { + padding-left: 0.5rem; + } + + & img { + width: 25px; + } +`; + +export { Wrapper, Refresh }; diff --git a/src/shared/components/error-boundary/index.test.tsx b/src/shared/components/error-boundary/index.test.tsx new file mode 100644 index 0000000..a701c84 --- /dev/null +++ b/src/shared/components/error-boundary/index.test.tsx @@ -0,0 +1,51 @@ +import React from 'react'; + +import { throwError } from 'shared/utils'; + +import { ErrorBoundary } from './index.component'; +import { renderWithOptions, screen, fireEvent, waitFor } from '../../../test'; + +jest.mock('../../utils/env/index.util.ts', () => ({ + __DEV__: true, + isBrowser: true, +})); +jest.mock('next/image', () => ({ + __esModule: true, + default: ( + props: JSX.IntrinsicAttributes & + React.ClassAttributes & + React.ImgHTMLAttributes + ) => , +})); + +function ThrowError() { + throwError('ErrorBoundarySimulationError', 'Lets simulate our ErrorBoundary Abilities 🛠️'); + + return

This can never be rendered, are you checking it in the DOM ☕️

; +} + +describe('first', () => { + it('should render child component when its error free', () => { + renderWithOptions( + +

Foo Bar Baz

+
+ ); + + expect(screen.getByText('Foo Bar Baz')).toBeInTheDocument(); + }); + + it('should render ErrorBoundary component when child component is not error free', async () => { + renderWithOptions( + + + + ); + + expect(screen.getByText('Oops, compilation error ')).toBeInTheDocument(); + + await waitFor(() => { + fireEvent.click(screen.getByText('Try again?')); + }); + }); +}); diff --git a/src/shared/components/headings/index.component.tsx b/src/shared/components/headings/index.component.tsx new file mode 100644 index 0000000..072bad6 --- /dev/null +++ b/src/shared/components/headings/index.component.tsx @@ -0,0 +1,31 @@ +import styled from 'styled-components'; + +const Heading1 = styled.h1` + color: ${(props) => props.theme.colors.secondary400}; + font-weight: ${(props) => props.theme.typography.title1.fontWeight}; + font-size: ${(props) => props.theme.typography.title1.fontSize}; + line-height: ${(props) => props.theme.typography.lineHeight.xxs}; +`; + +const Heading2 = styled.h2` + color: ${(props) => props.theme.colors?.secondary400}; + font-weight: ${(props) => props.theme.typography?.title2.fontWeight}; + font-size: ${(props) => props.theme.typography?.title2.fontSize}; + line-height: ${(props) => props.theme.typography?.lineHeight.xxs}; +`; + +const Heading3 = styled.h3` + color: ${(props) => props.theme.colors.secondary400}; + font-weight: ${(props) => props.theme.typography.title3.fontWeight}; + font-size: ${(props) => props.theme.typography.title3.fontSize}; + line-height: ${(props) => props.theme.typography.lineHeight.xxs}; +`; + +const Heading4 = styled.h4` + color: ${(props) => props.theme.colors.secondary400}; + font-weight: ${(props) => props.theme.typography.bodyText.fontWeight}; + font-size: ${(props) => props.theme.typography.bodyText.fontSize}; + line-height: ${(props) => props.theme.typography.lineHeight.xxs}; +`; + +export { Heading1, Heading2, Heading3, Heading4 }; diff --git a/src/shared/components/headings/index.stories.tsx b/src/shared/components/headings/index.stories.tsx new file mode 100644 index 0000000..d5ffea3 --- /dev/null +++ b/src/shared/components/headings/index.stories.tsx @@ -0,0 +1,18 @@ +import React from 'react'; + +import { StoryFn, Meta } from '@storybook/react'; + +import { Heading1, Heading2, Heading3, Heading4 } from './index.component'; + +export default { + title: 'Components/Headings', + component: Heading1, +} as Meta; + +export const H1: StoryFn = () => Heading 1; + +export const H2: StoryFn = () => Heading 2; + +export const H3: StoryFn = () => Heading 3; + +export const H4: StoryFn = () => Heading 4; diff --git a/src/shared/components/headings/index.test.tsx b/src/shared/components/headings/index.test.tsx new file mode 100644 index 0000000..0604230 --- /dev/null +++ b/src/shared/components/headings/index.test.tsx @@ -0,0 +1,25 @@ +import { renderWithOptions, screen } from '../../../test'; + +import { Heading1, Heading2, Heading3, Heading4 } from './index.component'; + +describe('', () => { + it('should render Heading1', () => { + renderWithOptions(Heading 1); + expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent('Heading 1'); + }); + + it('should render Heading2', () => { + renderWithOptions(Heading 2); + expect(screen.getByRole('heading', { level: 2 })).toHaveTextContent('Heading 2'); + }); + + it('should render Heading3', () => { + renderWithOptions(Heading 3); + expect(screen.getByRole('heading', { level: 3 })).toHaveTextContent('Heading 3'); + }); + + it('should render Heading4', () => { + renderWithOptions(Heading 4); + expect(screen.getByRole('heading', { level: 4 })).toHaveTextContent('Heading 4'); + }); +}); diff --git a/src/shared/components/index.ts b/src/shared/components/index.ts new file mode 100644 index 0000000..b63aca8 --- /dev/null +++ b/src/shared/components/index.ts @@ -0,0 +1,3 @@ +export * from './headings/index.component'; +export * from './error-boundary/index.component'; +export * from './internet-notifier/index.component'; diff --git a/src/shared/components/internet-notifier/index.component.tsx b/src/shared/components/internet-notifier/index.component.tsx new file mode 100644 index 0000000..954aee4 --- /dev/null +++ b/src/shared/components/internet-notifier/index.component.tsx @@ -0,0 +1,36 @@ +'use client'; + +import React from 'react'; +import * as Dialog from '@radix-ui/react-dialog'; + +import { useId } from '@radix-ui/react-id'; +import { CrossCircledIcon } from '@radix-ui/react-icons'; + +import { designTokens } from 'design-system'; + +import { useInternetNotifier } from './useInternetNotifier.presenter'; +import { Content, Close, Title, Description } from './index.styles'; + +export function InternetNotifier() { + const closeBtnId = useId('closeBtn'); + const data = useInternetNotifier(); + + return ( + + + + + + + {data.title} 🛜 + {data.message} + + + + ); +} diff --git a/src/shared/components/internet-notifier/index.model.ts b/src/shared/components/internet-notifier/index.model.ts new file mode 100644 index 0000000..19af38a --- /dev/null +++ b/src/shared/components/internet-notifier/index.model.ts @@ -0,0 +1,23 @@ +export class InternetNotifierModel { + public offlineListener( + listener: (_ev: Event) => any, + options?: boolean | AddEventListenerOptions + ) { + window.addEventListener('offline', listener, options); + } + + public onlineListener( + listener: (_ev: Event) => any, + options?: boolean | AddEventListenerOptions + ) { + window.addEventListener('online', listener, options); + } + + public unsubscribeOnline(listener: (_ev: Event) => any) { + window.removeEventListener('online', listener); + } + + public unsubscribeOffline(listener: (_ev: Event) => any) { + window.removeEventListener('offline', listener); + } +} diff --git a/src/shared/components/internet-notifier/index.stories.tsx b/src/shared/components/internet-notifier/index.stories.tsx new file mode 100644 index 0000000..6913cde --- /dev/null +++ b/src/shared/components/internet-notifier/index.stories.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import { StoryFn, Meta } from '@storybook/react'; + +import { InternetNotifier } from './index.component'; + +export default { + title: 'Components/InternetNotifier', + component: InternetNotifier, +} as Meta; + +export const TurnOnAndOffYourWifiToSee: StoryFn = () => ( + +); diff --git a/src/shared/components/internet-notifier/index.styles.tsx b/src/shared/components/internet-notifier/index.styles.tsx new file mode 100644 index 0000000..7f554eb --- /dev/null +++ b/src/shared/components/internet-notifier/index.styles.tsx @@ -0,0 +1,41 @@ +import * as Dialog from '@radix-ui/react-dialog'; + +import { styled } from 'styled-components'; + +import { designTokens } from 'design-system'; + +const Content = styled(Dialog.Content)` + background-color: ${({ theme }) => theme.colors.white400}; + border-radius: ${({ theme }) => theme.typography.borderRadius.lg}; + box-shadow: 3px 3px 10px 0px #dee2eb; + color: ${({ theme }) => theme.colors.dark500}; + width: 98%; + max-width: 500px; + height: 120px; + padding: 0.5rem 1rem; + position: fixed; + top: 5px; + right: 5px; + z-index: ${designTokens.typography.zIndexes.max}; +`; + +const Close = styled(Dialog.Close)` + background-color: transparent; + border: 0; + outline: none; + display: flex; + justify-content: flex-end; + width: 100%; +`; + +const Title = styled(Dialog.Title)` + font-size: ${({ theme }) => theme.typography.boldBody.fontSize}; + font-weight: ${({ theme }) => theme.typography.boldBody.fontWeight}; + padding-bottom: 0.5rem; +`; + +const Description = styled(Dialog.Description)` + padding-top: 0.5rem; +`; + +export { Content, Close, Title, Description }; diff --git a/src/shared/components/internet-notifier/index.test.tsx b/src/shared/components/internet-notifier/index.test.tsx new file mode 100644 index 0000000..b823d8e --- /dev/null +++ b/src/shared/components/internet-notifier/index.test.tsx @@ -0,0 +1,39 @@ +import React from 'react'; + +import { screen, renderWithOptions, fireEvent, act } from '../../../test'; + +import { InternetNotifier } from './index.component'; + +function mockInternetConnection(status: string) { + const event = new window.Event(status); + + act(() => { + window.dispatchEvent(event); + }); +} + +describe('', () => { + it('should render offline component', () => { + renderWithOptions(); + + mockInternetConnection('offline'); + + expect(screen.getByText('Gone offline 🛜')).toBeInTheDocument(); + expect(screen.getByText('You are no longer connected to the internet.')).toBeInTheDocument(); + + fireEvent.click(screen.getByRole('button')); + expect(screen.queryByText('Gone offline')).not.toBeInTheDocument(); + }); + + it('should render online component', () => { + renderWithOptions(); + + mockInternetConnection('online'); + + expect(screen.getByText('Back online 🛜')).toBeInTheDocument(); + expect(screen.getByText('You are now connected to the internet.')).toBeInTheDocument(); + + fireEvent.click(screen.getByRole('button')); + expect(screen.queryByText('Back online')).not.toBeInTheDocument(); + }); +}); diff --git a/src/shared/components/internet-notifier/useInternetNotifier.presenter.ts b/src/shared/components/internet-notifier/useInternetNotifier.presenter.ts new file mode 100644 index 0000000..031192d --- /dev/null +++ b/src/shared/components/internet-notifier/useInternetNotifier.presenter.ts @@ -0,0 +1,56 @@ +import { useRef, useState, useCallback, useEffect } from 'react'; + +import { useBoolean } from 'shared/hooks'; + +import { InternetNotifierModel } from './index.model'; + +const INTERNET_STATES = { + DEFAULT: 'DEFAULT', + GONE_OFFLINE: 'GONE_OFFLINE', + BACK_ONLINE: 'BACK_ONLINE', +} as const; + +export function useInternetNotifier() { + const { current: model } = useRef(new InternetNotifierModel()); + const timeoutIdRef = useRef | null>(null); + const [state, setState] = useState(INTERNET_STATES.DEFAULT); + const [shownNotifier, { setToFalse: onHideNotifier, setToTrue: onShowNotifier }] = useBoolean(); + + const automaticallyHideNotifier = useCallback(() => { + timeoutIdRef.current = setTimeout(() => onHideNotifier(), 10000); + }, [onHideNotifier]); + + const offlineListener = useCallback(() => { + setState(INTERNET_STATES.GONE_OFFLINE); + onShowNotifier(); + automaticallyHideNotifier(); + }, [automaticallyHideNotifier, onShowNotifier]); + + const onlineListener = useCallback(() => { + setState(INTERNET_STATES.BACK_ONLINE); + onShowNotifier(); + automaticallyHideNotifier(); + }, [automaticallyHideNotifier, onShowNotifier]); + + useEffect(() => { + model.offlineListener(offlineListener); + model.onlineListener(onlineListener); + + return () => { + model.unsubscribeOffline(offlineListener); + model.unsubscribeOnline(onlineListener); + clearTimeout(timeoutIdRef.current!); + }; + }, [model, offlineListener, onlineListener]); + + const isBackOnline = state === INTERNET_STATES.BACK_ONLINE; + + return { + open: shownNotifier, + title: isBackOnline ? 'Back online' : 'Gone offline', + message: isBackOnline + ? 'You are now connected to the internet.' + : 'You are no longer connected to the internet.', + onHideNotifier, + }; +} diff --git a/src/shared/helpers/.gitkeep b/src/shared/helpers/.gitkeep new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/src/shared/helpers/.gitkeep @@ -0,0 +1 @@ +export {}; diff --git a/src/shared/hooks/.gitkeep b/src/shared/hooks/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/shared/hooks/index.ts b/src/shared/hooks/index.ts new file mode 100644 index 0000000..158c533 --- /dev/null +++ b/src/shared/hooks/index.ts @@ -0,0 +1 @@ +export * from './useBoolean'; diff --git a/src/shared/hooks/useBoolean.ts b/src/shared/hooks/useBoolean.ts new file mode 100644 index 0000000..d6652d3 --- /dev/null +++ b/src/shared/hooks/useBoolean.ts @@ -0,0 +1,13 @@ +import { useCallback, useState } from 'react'; + +type Returnee = [boolean, { setToTrue(): void; setToFalse(): void; toggle(): void }]; + +export function useBoolean(initial: boolean | (() => boolean) = false): Returnee { + const [state, setState] = useState(initial); + + const setToTrue = useCallback(() => setState(true), []); + const setToFalse = useCallback(() => setState(false), []); + const toggle = useCallback(() => setState((prevState) => !prevState), []); + + return [state, { setToTrue, setToFalse, toggle }]; +} diff --git a/src/shared/layouts/.gitkeep b/src/shared/layouts/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/shared/layouts/container/index.layout.tsx b/src/shared/layouts/container/index.layout.tsx new file mode 100644 index 0000000..17f101e --- /dev/null +++ b/src/shared/layouts/container/index.layout.tsx @@ -0,0 +1,14 @@ +import styled from 'styled-components'; + +export const Container = styled.div` + /** + * @mobileStyle + */ + padding: 1rem 2rem; + /** + * @desktopStyle + */ + @media (min-width: ${({ theme }) => theme.typography.pageWidth.desktopStartWidth}) { + padding: 1rem 6rem; + } +`; diff --git a/src/shared/layouts/container/index.stories.tsx b/src/shared/layouts/container/index.stories.tsx new file mode 100644 index 0000000..24f6ad2 --- /dev/null +++ b/src/shared/layouts/container/index.stories.tsx @@ -0,0 +1,16 @@ +import React from 'react'; + +import { StoryFn, Meta } from '@storybook/react'; + +import { Container } from './index.layout'; + +export default { + title: 'Layouts/Container', + component: Container, +} as Meta; + +export const Primary: StoryFn = () => ( + + Feel free to reduce the screen resolution to notice the padding variation based on resolution + +); diff --git a/src/shared/layouts/container/index.test.tsx b/src/shared/layouts/container/index.test.tsx new file mode 100644 index 0000000..1e93ffb --- /dev/null +++ b/src/shared/layouts/container/index.test.tsx @@ -0,0 +1,15 @@ +import { renderWithOptions, screen } from '../../../test'; + +import { Container } from './index.layout'; + +describe('', () => { + it('should render component', () => { + renderWithOptions( + +

Foo Bar Bax

+
+ ); + + expect(screen.getByText('Foo Bar Bax')).toBeInTheDocument(); + }); +}); diff --git a/src/shared/layouts/index.ts b/src/shared/layouts/index.ts new file mode 100644 index 0000000..8c39815 --- /dev/null +++ b/src/shared/layouts/index.ts @@ -0,0 +1,2 @@ +export * from './main/index.layout'; +export * from './container/index.layout'; diff --git a/src/shared/layouts/main/index.layout.tsx b/src/shared/layouts/main/index.layout.tsx new file mode 100644 index 0000000..f963554 --- /dev/null +++ b/src/shared/layouts/main/index.layout.tsx @@ -0,0 +1,10 @@ +import React from 'react'; + +type PrimitiveMainPropTypes = React.ComponentPropsWithoutRef<'main'>; +type MainElement = React.ElementRef<'main'>; + +export const Main = React.forwardRef( + function Main(props, forwardedRef) { + return
; + } +); diff --git a/src/shared/layouts/main/index.test.tsx b/src/shared/layouts/main/index.test.tsx new file mode 100644 index 0000000..b19d0db --- /dev/null +++ b/src/shared/layouts/main/index.test.tsx @@ -0,0 +1,15 @@ +import { render, screen } from '@testing-library/react'; + +import { Main } from './index.layout'; + +describe('
', () => { + it('should render component', () => { + render( +
+

Foo Bar Baz

+
+ ); + + expect(screen.getByText('Foo Bar Baz')).toBeInTheDocument(); + }); +}); diff --git a/src/shared/models/.gitkeep b/src/shared/models/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/shared/models/history/index.model.ts b/src/shared/models/history/index.model.ts index d376585..43b9829 100644 --- a/src/shared/models/history/index.model.ts +++ b/src/shared/models/history/index.model.ts @@ -1,4 +1,4 @@ -export class History { +export class HistoryModel { public delete() {} public deleteAll() {} diff --git a/src/shared/models/index.ts b/src/shared/models/index.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/src/shared/models/index.ts @@ -0,0 +1 @@ +export {}; diff --git a/src/shared/models/navigator/index.model.ts b/src/shared/models/navigator/index.model.ts new file mode 100644 index 0000000..0c84c7e --- /dev/null +++ b/src/shared/models/navigator/index.model.ts @@ -0,0 +1,11 @@ +export class NavigatorModel { + public append() {} + + public prepend() {} + + public insert() {} + + public remove() {} + + public lookup() {} +} diff --git a/src/shared/models/navigator/useNavigator.presenter.ts b/src/shared/models/navigator/useNavigator.presenter.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/src/shared/models/navigator/useNavigator.presenter.ts @@ -0,0 +1 @@ +export {}; diff --git a/src/shared/models/settings/index.model.ts b/src/shared/models/settings/index.model.ts index 3ee7a0b..04d576e 100644 --- a/src/shared/models/settings/index.model.ts +++ b/src/shared/models/settings/index.model.ts @@ -1,4 +1,4 @@ -export class Settings { +export class SettingsModel { public set defaultTextEngine(_name: string) {} public get defaultTextEngine() { diff --git a/src/shared/utils/compose-events/index.test.ts b/src/shared/utils/compose-events/index.test.ts new file mode 100644 index 0000000..db662f6 --- /dev/null +++ b/src/shared/utils/compose-events/index.test.ts @@ -0,0 +1,15 @@ +import React from 'react'; + +import { composeEvents } from './index.util'; + +describe('composeEvents', () => { + const mockedEvent = { target: { value: 'testValue' } } as unknown as React.SyntheticEvent; + + it('should confirm that eventHandlers gets the rightful events', () => { + const eventHandler1 = (event: typeof mockedEvent) => expect(event).toEqual(mockedEvent); + const eventHandler2 = (event: typeof mockedEvent) => expect(event).toEqual(mockedEvent); + const composedEventHandler = composeEvents(eventHandler1, eventHandler2); + + composedEventHandler(mockedEvent); + }); +}); diff --git a/src/shared/utils/compose-events/index.util.ts b/src/shared/utils/compose-events/index.util.ts new file mode 100644 index 0000000..c2cd8e8 --- /dev/null +++ b/src/shared/utils/compose-events/index.util.ts @@ -0,0 +1,9 @@ +import { SyntheticEvent } from 'react'; + +export function composeEvents(...handlers: Array<(ev: SyntheticEvent) => void>) { + return function onEvent(ev: SyntheticEvent) { + if (ev.defaultPrevented) return; + + handlers.forEach((handler) => handler(ev)); + }; +} diff --git a/src/shared/utils/create-context/index.test.tsx b/src/shared/utils/create-context/index.test.tsx new file mode 100644 index 0000000..7dc9e4c --- /dev/null +++ b/src/shared/utils/create-context/index.test.tsx @@ -0,0 +1,38 @@ +import React from 'react'; + +import { renderHook, render } from '../../../test/utils'; + +import { createContext } from './index.util'; + +describe('createContext', () => { + it('should confirm that createContent returns a context', () => { + const contextValue = { env: 'test', testTool: 'jest', framework: 'react' }; + const [Provider, useContext, displayName] = createContext('TestContext'); + const wrapper = ({ children }: { children: React.ReactNode }) => ( + {children} + ); + const { result } = renderHook(() => useContext(), { wrapper }); + + expect(result.current).toEqual(contextValue); + expect((displayName as unknown as React.Context).displayName).toBe('TestContext'); + }); + + it('should confirm that createContent throws an error when a component not wrapped with the context provider tries to use the context', () => { + const [, useContext] = createContext<{ name: string | null }>('TestContext'); + + function Component() { + const values = useContext(); + + return

{values.name}

; + } + + try { + render(); + } catch (error) { + expect((error as Error).name).toBe('UseContextError 🚨'); + expect((error as Error).message).toBe( + 'You can not use context outside its Provider component' + ); + } + }); +}); diff --git a/src/shared/utils/create-context/index.util.ts b/src/shared/utils/create-context/index.util.ts new file mode 100644 index 0000000..ee2fe3f --- /dev/null +++ b/src/shared/utils/create-context/index.util.ts @@ -0,0 +1,24 @@ +import React from 'react'; + +import { __DEV__, __TEST__, throwError } from 'shared/utils'; + +export function createContext(displayName: string) { + const Context = React.createContext(null!); + + if (__DEV__ || __TEST__) Context.displayName = displayName; + + function useContext() { + const context = React.useContext(Context); + + if (!context) + throwError( + 'UseContextError', + 'You can not use context outside its Provider component', + useContext + ); + + return context; + } + + return [Context.Provider, useContext, Context.Consumer, Context.displayName] as const; +} diff --git a/src/shared/utils/index.ts b/src/shared/utils/index.ts index 4209fa1..4f9a61b 100644 --- a/src/shared/utils/index.ts +++ b/src/shared/utils/index.ts @@ -1 +1,4 @@ export * from './env/index.util'; +export * from './throw-error/index.util'; +export * from './create-context/index.util'; +export * from './compose-events/index.util'; diff --git a/src/shared/utils/linkedlist-node/index.test.ts b/src/shared/utils/linkedlist-node/index.test.ts new file mode 100644 index 0000000..7d6e9fc --- /dev/null +++ b/src/shared/utils/linkedlist-node/index.test.ts @@ -0,0 +1,16 @@ +import { LinkedlistNode } from './index.util'; + +describe('LinkedlistNode', () => { + it('should create a new node with the given value', () => { + const node = new LinkedlistNode(1); + expect(node.value).toBe(1); + expect(node.next).toBeNull(); + }); + + it('should create a new node with the given value and next node', () => { + const nextNode = new LinkedlistNode(2); + const node = new LinkedlistNode(1, nextNode); + expect(node.value).toBe(1); + expect(node.next).toBe(nextNode); + }); +}); diff --git a/src/shared/utils/linkedlist-node/index.util.ts b/src/shared/utils/linkedlist-node/index.util.ts new file mode 100644 index 0000000..e17366a --- /dev/null +++ b/src/shared/utils/linkedlist-node/index.util.ts @@ -0,0 +1,9 @@ +export class LinkedlistNode { + public value: ValueType; + public next: LinkedlistNode | null; + + constructor(value: ValueType, next: LinkedlistNode | null = null) { + this.value = value; + this.next = next; + } +} diff --git a/src/shared/utils/throw-error/index.test.ts b/src/shared/utils/throw-error/index.test.ts new file mode 100644 index 0000000..53e7ec4 --- /dev/null +++ b/src/shared/utils/throw-error/index.test.ts @@ -0,0 +1,17 @@ +import { throwError } from './index.util'; + +const ERROR_CONSTRUCT = { + name: 'ContextError', + message: 'Sorry, you out of context', + options: { engineer: 'Foo Bar Baz' }, +}; +describe('throwError', () => { + it('should confirm that throwError actually throws an error', () => { + const errorContext = () => + throwError(ERROR_CONSTRUCT.name, ERROR_CONSTRUCT.message, errorContext, { + ...ERROR_CONSTRUCT.options, + }); + + expect(errorContext).toThrowError(ERROR_CONSTRUCT); + }); +}); diff --git a/src/shared/utils/throw-error/index.util.ts b/src/shared/utils/throw-error/index.util.ts new file mode 100644 index 0000000..40f65a3 --- /dev/null +++ b/src/shared/utils/throw-error/index.util.ts @@ -0,0 +1,33 @@ +/** + * @throwError function throws an error with a custom name, message, and optional + * additional options. + * @param {string} name - The name of the error. It is a string that will be concatenated with a " 🚨" + * emoji to make it more noticeable. + * @param {string} message - The `message` parameter is a string that represents the error message that + * will be displayed when the error is thrown. + * @param {Function} [constructor] - The `constructor` parameter is an optional parameter that + * represents the constructor function of the error. It is used to capture the stack trace of the + * error. If provided, the `Error.captureStackTrace` method is called with the error and the + * constructor function as arguments to capture the stack trace. If not provided + * @param [options] - The `options` parameter is an optional object that allows you to pass additional + * information or custom properties to the error object. It is a record type, which means it is a + * key-value pair object where the keys are strings and the values can be of any type. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function throwError( + name: string, + message: string, + // eslint-disable-next-line @typescript-eslint/ban-types + constructor?: Function, + options?: Record +) { + const error = new Error(); + error.name = name.concat(' 🚨'); + error.message = message; + + Error.captureStackTrace?.(error, constructor); + + if (options) Object.assign(error, options); + + throw error; +} diff --git a/src/test/index.ts b/src/test/index.ts index c193f62..cccbe4d 100644 --- a/src/test/index.ts +++ b/src/test/index.ts @@ -1 +1,2 @@ +export * from './utils'; export * from './mocks/server'; diff --git a/src/test/utils/index.test.tsx b/src/test/utils/index.test.tsx new file mode 100644 index 0000000..6bd3f2a --- /dev/null +++ b/src/test/utils/index.test.tsx @@ -0,0 +1,9 @@ +import { renderWithOptions, screen } from '.'; + +describe('renderWithOptions', () => { + it('should render children', () => { + renderWithOptions(

Foo Bar Baz

); + + expect(screen.getByText('Foo Bar Baz')).toBeInTheDocument(); + }); +}); diff --git a/src/test/utils/index.tsx b/src/test/utils/index.tsx new file mode 100644 index 0000000..0f0168f --- /dev/null +++ b/src/test/utils/index.tsx @@ -0,0 +1,38 @@ +import React from 'react'; + +import { ThemeProvider } from 'styled-components'; +import { render, RenderOptions } from '@testing-library/react'; +import { QueryClient, QueryClientProvider } from 'react-query'; + +import { GlobalStore } from 'global-store'; +import { theme, GlobalStyles } from 'design-system'; + +export * from '@testing-library/react'; + +/** + * @Wrapper provides to the children then Style-Theme, Global-state, + * and QueryClient for react-query-lib + */ +function Wrapper({ children }: { children: React.ReactElement }) { + return ( + + + + {children} + + + ); +} + +/** + * @renderWithOptions use this function when you want your test + * component to access our design system + * @param {children:React.ReactNode} + * @returns a transpiled reactNode object + */ +export function renderWithOptions(ui: React.ReactElement, opts?: RenderOptions) { + return render(ui, { + wrapper: Wrapper as React.JSXElementConstructor<{ children: React.ReactElement }>, + ...opts, + }); +} diff --git a/tsconfig.json b/tsconfig.json index 34ef4aa..0cbdd8f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,7 +21,7 @@ "@/*": ["./src/*"] } }, - "include": ["src", "src/@types/global.d.ts"], + "include": ["src", "src/@types/global.d.ts", "src/shared/helpers/.gitkeep"], "exclude": ["node_modules"], "typeRoots": ["./node_modules/@types", "./src/@types"] } diff --git a/yarn.lock b/yarn.lock index 4176531..25cda63 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2059,6 +2059,27 @@ dependencies: "@babel/runtime" "^7.13.10" +"@radix-ui/react-dialog@^1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@radix-ui/react-dialog/-/react-dialog-1.0.5.tgz#71657b1b116de6c7a0b03242d7d43e01062c7300" + integrity sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/primitive" "1.0.1" + "@radix-ui/react-compose-refs" "1.0.1" + "@radix-ui/react-context" "1.0.1" + "@radix-ui/react-dismissable-layer" "1.0.5" + "@radix-ui/react-focus-guards" "1.0.1" + "@radix-ui/react-focus-scope" "1.0.4" + "@radix-ui/react-id" "1.0.1" + "@radix-ui/react-portal" "1.0.4" + "@radix-ui/react-presence" "1.0.1" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-slot" "1.0.2" + "@radix-ui/react-use-controllable-state" "1.0.1" + aria-hidden "^1.1.1" + react-remove-scroll "2.5.5" + "@radix-ui/react-direction@1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@radix-ui/react-direction/-/react-direction-1.0.1.tgz#9cb61bf2ccf568f3421422d182637b7f47596c9b" @@ -2066,6 +2087,40 @@ dependencies: "@babel/runtime" "^7.13.10" +"@radix-ui/react-dismissable-layer@1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.5.tgz#3f98425b82b9068dfbab5db5fff3df6ebf48b9d4" + integrity sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/primitive" "1.0.1" + "@radix-ui/react-compose-refs" "1.0.1" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-use-callback-ref" "1.0.1" + "@radix-ui/react-use-escape-keydown" "1.0.3" + +"@radix-ui/react-focus-guards@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.1.tgz#1ea7e32092216b946397866199d892f71f7f98ad" + integrity sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA== + dependencies: + "@babel/runtime" "^7.13.10" + +"@radix-ui/react-focus-scope@1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.4.tgz#2ac45fce8c5bb33eb18419cdc1905ef4f1906525" + integrity sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-compose-refs" "1.0.1" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-use-callback-ref" "1.0.1" + +"@radix-ui/react-icons@^1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-icons/-/react-icons-1.3.0.tgz#c61af8f323d87682c5ca76b856d60c2312dbcb69" + integrity sha512-jQxj/0LKgp+j9BiTXz3O3sgs26RNet2iLWmsPyRz2SIcR4q/4SbazXfnYwbAr+vLYKSfc7qxzyGQA1HLlYiuNw== + "@radix-ui/react-id@1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@radix-ui/react-id/-/react-id-1.0.1.tgz#73cdc181f650e4df24f0b6a5b7aa426b912c88c0" @@ -2074,6 +2129,14 @@ "@babel/runtime" "^7.13.10" "@radix-ui/react-use-layout-effect" "1.0.1" +"@radix-ui/react-portal@1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.0.4.tgz#df4bfd353db3b1e84e639e9c63a5f2565fb00e15" + integrity sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-presence@1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.0.1.tgz#491990ba913b8e2a5db1b06b203cb24b5cdef9ba" @@ -2114,6 +2177,14 @@ "@babel/runtime" "^7.13.10" "@radix-ui/react-use-callback-ref" "1.0.1" +"@radix-ui/react-use-escape-keydown@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.0.3.tgz#217b840c250541609c66f67ed7bab2b733620755" + integrity sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-use-callback-ref" "1.0.1" + "@radix-ui/react-use-layout-effect@1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz#be8c7bc809b0c8934acf6657b577daf948a75399" @@ -3007,6 +3078,11 @@ "@svgr/plugin-svgo" "^5.5.0" loader-utils "^2.0.0" +"@tanstack/query-core@5.28.4": + version "5.28.4" + resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.28.4.tgz#fa416532f8b33ca8608d40bb9728b60e2e1a38dc" + integrity sha512-uQZqOFqLWUvXNIQZ63XdKzg22NtHzgCBUfDmjDHi3BoF+nUYeBNvMi/xFPtFrMhqRzG2Ir4mYaGsWZzmiEjXpA== + "@tanstack/query-devtools@5.28.3": version "5.28.3" resolved "https://registry.yarnpkg.com/@tanstack/query-devtools/-/query-devtools-5.28.3.tgz#f00f92e261a2d201cc1b982c98fdd38a684c117d" @@ -3019,6 +3095,13 @@ dependencies: "@tanstack/query-devtools" "5.28.3" +"@tanstack/react-query@^5.28.4": + version "5.28.4" + resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.28.4.tgz#32e56ca4fd08513a906fe6323908f0e38ffccbba" + integrity sha512-BErcoB/QQG6YwLSUKnaGxF+lSc270RH2w3kMBpG0i4YzDCsFs2pdxPX1WVknQvFk9bNgukMb158hc2Zb4SdwSA== + dependencies: + "@tanstack/query-core" "5.28.4" + "@testing-library/dom@^8.5.0": version "8.20.1" resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-8.20.1.tgz#2e52a32e46fc88369eef7eef634ac2a192decd9f" @@ -4197,6 +4280,13 @@ argparse@^2.0.1: resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== +aria-hidden@^1.1.1: + version "1.2.3" + resolved "https://registry.yarnpkg.com/aria-hidden/-/aria-hidden-1.2.3.tgz#14aeb7fb692bbb72d69bebfa47279c1fd725e954" + integrity sha512-xcLxITLe2HYa1cnYnwCjkOO1PqUHQpozB8x9AR0OgWN2woOBi5kSDVxKfd0b7sb1hw5qFeJhXm9H1nu3xSfLeQ== + dependencies: + tslib "^2.0.0" + aria-query@5.1.3: version "5.1.3" resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.1.3.tgz#19db27cd101152773631396f7a95a3b58c22c35e" @@ -5698,6 +5788,11 @@ detect-newline@^3.0.0: resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== +detect-node-es@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/detect-node-es/-/detect-node-es-1.1.0.tgz#163acdf643330caa0b4cd7c21e7ee7755d6fa493" + integrity sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ== + detect-node@^2.0.4, detect-node@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1" @@ -7110,6 +7205,11 @@ get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.2, get-intrinsic@ has-symbols "^1.0.3" hasown "^2.0.0" +get-nonce@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/get-nonce/-/get-nonce-1.0.1.tgz#fdf3f0278073820d2ce9426c18f07481b1e0cdf3" + integrity sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q== + get-npm-tarball-url@^2.0.3: version "2.1.0" resolved "https://registry.yarnpkg.com/get-npm-tarball-url/-/get-npm-tarball-url-2.1.0.tgz#cbd6bb25884622bc3191c761466c93ac83343213" @@ -7561,11 +7661,6 @@ human-signals@^5.0.0: resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-5.0.0.tgz#42665a284f9ae0dade3ba41ebc37eb4b852f3a28" integrity sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ== -husky@^9.0.11: - version "9.0.11" - resolved "https://registry.yarnpkg.com/husky/-/husky-9.0.11.tgz#fc91df4c756050de41b3e478b2158b87c1e79af9" - integrity sha512-AB6lFlbwwyIqMdHYhwPe+kjOC3Oc5P3nThEoW/AaO2BX3vJDjWPFxYLxokUZOo6RNX20He3AaT8sESs9NJcmEw== - iconv-lite@0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" @@ -7670,6 +7765,13 @@ internal-slot@^1.0.4, internal-slot@^1.0.5, internal-slot@^1.0.7: hasown "^2.0.0" side-channel "^1.0.4" +invariant@^2.2.4: + version "2.2.4" + resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" + integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== + dependencies: + loose-envify "^1.0.0" + ip@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/ip/-/ip-2.0.1.tgz#e8f3595d33a3ea66490204234b77636965307105" @@ -8999,7 +9101,7 @@ log-symbols@^4.1.0: chalk "^4.1.0" is-unicode-supported "^0.1.0" -loose-envify@^1.1.0, loose-envify@^1.4.0: +loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== @@ -10885,6 +10987,25 @@ react-refresh@^0.11.0: resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.11.0.tgz#77198b944733f0f1f1a90e791de4541f9f074046" integrity sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A== +react-remove-scroll-bar@^2.3.3: + version "2.3.6" + resolved "https://registry.yarnpkg.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.6.tgz#3e585e9d163be84a010180b18721e851ac81a29c" + integrity sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g== + dependencies: + react-style-singleton "^2.2.1" + tslib "^2.0.0" + +react-remove-scroll@2.5.5: + version "2.5.5" + resolved "https://registry.yarnpkg.com/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz#1e31a1260df08887a8a0e46d09271b52b3a37e77" + integrity sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw== + dependencies: + react-remove-scroll-bar "^2.3.3" + react-style-singleton "^2.2.1" + tslib "^2.1.0" + use-callback-ref "^1.3.0" + use-sidecar "^1.1.2" + react-router-dom@^6.22.3: version "6.22.3" resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.22.3.tgz#9781415667fd1361a475146c5826d9f16752a691" @@ -10960,6 +11081,15 @@ react-side-effect@^2.1.0: resolved "https://registry.yarnpkg.com/react-side-effect/-/react-side-effect-2.1.2.tgz#dc6345b9e8f9906dc2eeb68700b615e0b4fe752a" integrity sha512-PVjOcvVOyIILrYoyGEpDN3vmYNLdy1CajSFNt4TDsVQC5KpTijDvWVoR+/7Rz2xT978D8/ZtFceXxzsPwZEDvw== +react-style-singleton@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.1.tgz#f99e420492b2d8f34d38308ff660b60d0b1205b4" + integrity sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g== + dependencies: + get-nonce "^1.0.0" + invariant "^2.2.4" + tslib "^2.0.0" + "react@^16.8.0 || ^17.0.0 || ^18.0.0", react@^18.2.0: version "18.2.0" resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" @@ -12375,7 +12505,7 @@ tslib@^1.13.0, tslib@^1.8.1: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.4.0, tslib@^2.6.2: +tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.4.0, tslib@^2.6.2: version "2.6.2" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== @@ -12673,6 +12803,21 @@ url@^0.11.0: punycode "^1.4.1" qs "^6.11.2" +use-callback-ref@^1.3.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.3.1.tgz#9be64c3902cbd72b07fe55e56408ae3a26036fd0" + integrity sha512-Lg4Vx1XZQauB42Hw3kK7JM6yjVjgFmFC5/Ab797s79aARomD2nEErc4mCgM8EZrARLmmbWpi5DGCadmK50DcAQ== + dependencies: + tslib "^2.0.0" + +use-sidecar@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.1.2.tgz#2f43126ba2d7d7e117aa5855e5d8f0276dfe73c2" + integrity sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw== + dependencies: + detect-node-es "^1.1.0" + tslib "^2.0.0" + util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"