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