): Promise
31 | }
32 |
--------------------------------------------------------------------------------
/my-react-project/src/http-client/models/HttpRequestParams.interface.ts:
--------------------------------------------------------------------------------
1 | // file: src/http-client/models/HttpRequestParams.interface.ts
2 |
3 | import { HttpRequestType } from './Constants'
4 |
5 | /**
6 | * @name HttpRequestParamsInterface
7 | * @description
8 | * Interface represents an object we'll use to pass arguments into our HttpClient request method.
9 | * This allow us to specify the type of request we want to execute, the end-point url,
10 | * if the request should include an authentication token, and an optional payload (if POST or PUT for example)
11 | */
12 | export interface HttpRequestParamsInterface
{
13 | requestType: HttpRequestType
14 | endpoint: string
15 | requiresToken: boolean
16 | headers?: { [key: string]: string }
17 | payload?: P
18 | mockDelay?: number
19 | }
20 |
--------------------------------------------------------------------------------
/my-react-project/src/http-client/models/UrlUtils.ts:
--------------------------------------------------------------------------------
1 | // file: src/http-client/models/UrlUtils.ts
2 | export interface UrlUtilsInterface {
3 | getFullUrlWithParams(baseUrl: string, params: { [key: string]: number | string }): string
4 | }
5 |
6 | export const UrlUtils: UrlUtilsInterface = {
7 | /**
8 | * @name getFullUrlWithParams
9 | * @description Returns the full formatted url for an API end-point
10 | * by replacing parameters place holder with the actual values.
11 | * @param baseUrl The base API end-point witht he params placeholders like {projectId}
12 | * @param params The request params object with the key/value entries for each parameter
13 | * @returns The fully formatted API end-point url with the actual parameter values
14 | */
15 | getFullUrlWithParams: (baseUrl: string, params: { [key: string]: number | string }): string => {
16 | const keys: string[] = Object.keys(params || {})
17 | if ((baseUrl || '').indexOf('[') === -1 || keys.length === 0) {
18 | return baseUrl
19 | }
20 | let fullUrl = baseUrl
21 | keys.forEach((key) => {
22 | fullUrl = fullUrl.replace(`[${key}]`, (params[key] || 'null').toString())
23 | })
24 | return fullUrl
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/my-react-project/src/http-client/models/index.ts:
--------------------------------------------------------------------------------
1 | // file: src/http-client/models/index.ts
2 |
3 | export * from './Constants'
4 | export * from './HttpClient.axios'
5 | export * from './HttpClient.fetch'
6 | export * from './HttpClient.interface'
7 | export * from './HttpRequestParams.interface'
8 | export * from './UrlUtils'
9 |
--------------------------------------------------------------------------------
/my-react-project/src/icons/Moon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | export const MoonIcon = () => (
4 |
12 | )
13 |
--------------------------------------------------------------------------------
/my-react-project/src/localization/index.ts:
--------------------------------------------------------------------------------
1 | // file: src/localization/index.ts
2 |
3 | import { useDateTimeFormatters, useNumberFormatters } from '@builtwithjavascript/formatters'
4 |
5 | export * from './useLocalization'
6 | export { useDateTimeFormatters, useNumberFormatters }
7 |
--------------------------------------------------------------------------------
/my-react-project/src/localization/useLocalization.ts:
--------------------------------------------------------------------------------
1 | // file: src/localization/useLocalization.ts
2 |
3 | import { useTranslation } from 'react-i18next'
4 | import i18n from 'i18next'
5 | import { config } from '@/config'
6 |
7 | // import references to our localeStorage helpers:
8 | import { getUserPreferredLocale, setUserPreferredLocale } from './i18n.init'
9 |
10 | // useLocalization hook
11 | export function useLocalization() {
12 | // we have to invoke react-i18next's useTranslation here
13 | const instance = useTranslation('translation')
14 |
15 | return {
16 | t: instance.t, //returna the t translator function from useTranslation
17 | currentLocale: i18n.language, // return the current locale from i18n
18 | changeLocale: (lcid: string) => {
19 | // return helper method changeLocale
20 | i18n.changeLanguage(lcid)
21 | // also save the user preference
22 | setUserPreferredLocale(lcid)
23 | },
24 | locales: config.localization.locales, // retrun vailable locales from our config
25 | getUserPreferredLocale
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/my-react-project/src/main.tsx:
--------------------------------------------------------------------------------
1 | // file: src/main.tsx
2 | import React from 'react'
3 | // import ReactDOM from 'react-dom/client' // before React 18
4 | import { createRoot } from 'react-dom/client' // from React 18
5 | // import tailwind main css file
6 | import './tailwind/app.css'
7 | import App from './App'
8 | // import a reference to our i18n initialization code:
9 | import './localization/i18n.init'
10 |
11 | const container = document.getElementById('root')
12 | // const root = ReactDOM.createRoot(container as Element) // before React 18
13 | const root = createRoot(container as Element) // from React 18
14 | root.render(
15 |
16 |
17 |
18 |
19 |
20 | )
21 |
--------------------------------------------------------------------------------
/my-react-project/src/models/index.ts:
--------------------------------------------------------------------------------
1 | export * from './items'
2 |
--------------------------------------------------------------------------------
/my-react-project/src/models/items/Item.interface.ts:
--------------------------------------------------------------------------------
1 | export interface ItemInterface {
2 | id: number
3 | name: string
4 | selected: boolean
5 | }
6 |
--------------------------------------------------------------------------------
/my-react-project/src/models/items/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Item.interface'
2 |
--------------------------------------------------------------------------------
/my-react-project/src/store/index.ts:
--------------------------------------------------------------------------------
1 | // file: src/store/index.ts
2 |
3 | export * from './root'
4 |
--------------------------------------------------------------------------------
/my-react-project/src/store/items/Items.slice.ts:
--------------------------------------------------------------------------------
1 | // src/store/items/Items.slice.ts
2 |
3 | // import createSlice and PayloadAction from redux toolkit
4 | import { createSlice, PayloadAction } from '@reduxjs/toolkit'
5 |
6 | // import out items state interface, and the item interface
7 | import { ItemsStateInterface } from './models'
8 | import { ItemInterface } from '@/models/items/Item.interface'
9 |
10 | // create an object that represents our initial items state
11 | const initialItemsState: ItemsStateInterface = {
12 | loading: false,
13 | items: []
14 | }
15 |
16 | // create the itemsStoreSlice with createSlice:
17 | export const itemsStoreSlice = createSlice({
18 | name: 'itemsStoreSlice',
19 | initialState: initialItemsState,
20 | reducers: {
21 | // reducers are functions that commit final mutations to the state
22 | // These will commit final mutation/changes to the state
23 |
24 | setLoading: (state, action: PayloadAction) => {
25 | state.loading = action.payload
26 | },
27 |
28 | setItems: (state, action: PayloadAction) => {
29 | // update our state:
30 | // set our items
31 | state.items = action.payload || []
32 | // set loading to false so the loader will be hidden in the UI
33 | state.loading = false
34 | },
35 |
36 | setItemSelected: (state, action: PayloadAction) => {
37 | const item = action.payload
38 | const found = state.items.find((o) => o.id === item.id) as ItemInterface
39 | found.selected = !found.selected
40 | }
41 | }
42 | })
43 |
--------------------------------------------------------------------------------
/my-react-project/src/store/items/Items.store.ts:
--------------------------------------------------------------------------------
1 | // src/store/items/Items.store.ts
2 |
3 | // import hooks useSelector and useDispatch from react-redux
4 | import { useSelector } from 'react-redux'
5 | import { Dispatch } from 'react'
6 |
7 | // import a reference to our RootStateInterface
8 | import { RootStateInterface } from '@/store/root'
9 | // import a reference to our ItemInterface
10 | import { ItemInterface } from '@/models/items/Item.interface'
11 |
12 | // import a refence to our itemsStoreSlice
13 | import { itemsStoreSlice } from './Items.slice'
14 |
15 | // import a reference to our apiClient instance
16 | import { apiClient } from '@/api-client'
17 |
18 | /**
19 | * @name useItemsActions
20 | * @description
21 | * Actions hook that allows us to invoke the Items store actions from our components
22 | */
23 | export function useItemsActions(commit: Dispatch) {
24 | // get a reference to our slice actions (which are really our mutations/commits)
25 | const mutations = itemsStoreSlice.actions
26 |
27 | // our items store actions implementation:
28 | const actions = {
29 | loadItems: async () => {
30 | // set loading to true
31 | commit(mutations.setLoading(true))
32 |
33 | // invoke our API cient fetchItems to load the data from an API end-point
34 | const data = await apiClient.items.fetchItems()
35 |
36 | // commit our mutations by setting state.items to the data loaded
37 | commit(mutations.setItems(data))
38 | },
39 | toggleItemSelected: async (item: ItemInterface) => {
40 | console.log('ItemsStore: action: toggleItemSelected', item)
41 | commit(mutations.setItemSelected(item))
42 | }
43 | }
44 |
45 | // return our store actions
46 | return actions
47 | }
48 |
49 | // hook to allows us to consume read-only state properties from our components
50 | export function useItemsGetters() {
51 | // return our store getters
52 | return {
53 | loading: useSelector((s: RootStateInterface) => s.itemsState.loading),
54 | items: useSelector((s: RootStateInterface) => s.itemsState.items)
55 | }
56 | }
57 |
58 | /**
59 | * @name ItemsStoreInterface
60 | * @description Interface represents our Items store module
61 | */
62 | export interface ItemsStoreInterface {
63 | actions: ReturnType // use TS type inference
64 | getters: ReturnType // use TS type inference
65 | }
66 |
--------------------------------------------------------------------------------
/my-react-project/src/store/items/index.ts:
--------------------------------------------------------------------------------
1 | // file: src/store/items/index.ts
2 |
3 | export * from './Items.slice'
4 | export * from './Items.store'
5 |
--------------------------------------------------------------------------------
/my-react-project/src/store/items/models/ItemsState.interface.ts:
--------------------------------------------------------------------------------
1 | // file: ItemsState.interface.ts
2 |
3 | import { ItemInterface } from '@/models/items/Item.interface'
4 |
5 | /**
6 | * @name ItemsStateInterface
7 | * @description Interface represnets our Items state
8 | */
9 | export interface ItemsStateInterface {
10 | loading: boolean
11 | items: ItemInterface[]
12 | }
13 |
--------------------------------------------------------------------------------
/my-react-project/src/store/items/models/index.ts:
--------------------------------------------------------------------------------
1 | export * from './ItemsState.interface'
2 |
--------------------------------------------------------------------------------
/my-react-project/src/store/root/Root.store.ts:
--------------------------------------------------------------------------------
1 | // file: src/store/root/Root.store.ts
2 |
3 | // import configureStore from redux toolkit
4 | import { configureStore } from '@reduxjs/toolkit'
5 | import { useDispatch } from 'react-redux'
6 |
7 | // import our root store interface
8 | import { RootStoreInterface } from './models'
9 |
10 | // import our modules slices and actions/getters
11 | import { itemsStoreSlice, useItemsActions, useItemsGetters } from '@/store/items/'
12 |
13 | // configure root redux store for the whole app.
14 | // this will be consumed by App.tsx
15 | export const rootStore = configureStore({
16 | reducer: {
17 | // add reducers here
18 | itemsState: itemsStoreSlice.reducer
19 | // keep adding more domain-specific reducers here as needed
20 | }
21 | })
22 |
23 | // Infer the `RootStateInterface` type from the store itself (rootStore.getState)
24 | // thus avoiding to explicitely having to create an additional interface for the
25 | export type RootStateInterface = ReturnType
26 |
27 | // hook that returns our root store instance and will allow us to consume our app store from our components
28 | export function useAppStore(): RootStoreInterface {
29 | // note: we are callin dispatch "commit" here, as it make more sense to call it this way
30 | // feel free to just call it dispatch if you prefer
31 | const commit = useDispatch()
32 |
33 | return {
34 | itemsStore: {
35 | actions: useItemsActions(commit),
36 | getters: useItemsGetters()
37 | }
38 | // additional domain store modules will be added here as needed
39 | }
40 | }
41 |
42 | // infer the type of the entire app state
43 | type IAppState = ReturnType
44 |
45 | /**
46 | * @name getAppState
47 | * @description
48 | * Returns a snapshot of the current app state (non-reactive)
49 | * This will be used mainly across store modules (i.e. items/etc)
50 | * In components we'll usually use getters, not this.
51 | * @returns
52 | */
53 | export function getAppState(): IAppState {
54 | const appState = rootStore.getState()
55 | return {
56 | ...appState
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/my-react-project/src/store/root/index.ts:
--------------------------------------------------------------------------------
1 | // file: src/store/root/index.ts
2 |
3 | export * from './Root.store'
4 |
--------------------------------------------------------------------------------
/my-react-project/src/store/root/models/RootStore.interface.ts:
--------------------------------------------------------------------------------
1 | // file: src/store/root/models/RootStore.interface.ts
2 |
3 | import { ItemsStoreInterface } from '@/store/items'
4 | // additional domain store interfaces will be imported here as needed
5 |
6 | /**
7 | * @name RootStoreInterface
8 | * @description Interface represents our global state manager (store)
9 | */
10 | export interface RootStoreInterface {
11 | itemsStore: ItemsStoreInterface
12 | // additional domain store modules will be added here as needed
13 | }
14 |
--------------------------------------------------------------------------------
/my-react-project/src/store/root/models/index.ts:
--------------------------------------------------------------------------------
1 | // file: src/store/root/models/index.ts
2 |
3 | export * from './RootStore.interface'
4 |
--------------------------------------------------------------------------------
/my-react-project/src/tailwind/app.css:
--------------------------------------------------------------------------------
1 | /* file: src/tailwind/app.css */
2 | @import 'tailwindcss/base';
3 | @import 'tailwindcss/components';
4 | @import 'tailwindcss/utilities';
5 |
6 | @import './other.css';
7 |
--------------------------------------------------------------------------------
/my-react-project/src/tailwind/other.css:
--------------------------------------------------------------------------------
1 | /* file: src/tailwind/other.css */
2 |
3 | ul {
4 | list-style-type: none;
5 | margin-block-start: 0;
6 | margin-block-end: 0;
7 | margin-inline-start: 0px;
8 | margin-inline-end: 0px;
9 | padding-inline-start: 0px;
10 | }
11 | li.item {
12 | padding: 5px;
13 | outline: solid 1px #eee;
14 | display: flex;
15 | align-items: center;
16 | height: 30px;
17 | cursor: pointer;
18 | transition: background-color 0.3s ease;
19 | }
20 | li.item .name {
21 | margin-left: 6px;
22 | }
23 | li.item .selected-indicator {
24 | font-size: 2em;
25 | line-height: 0.5em;
26 | margin: 10px 8px 0 8px;
27 | color: lightgray;
28 | }
29 | li.item.selected .selected-indicator {
30 | color: skyblue;
31 | }
32 | li.item:hover {
33 | background-color: #eee;
34 | }
35 |
36 | /* begin: loader component */
37 | .loader {
38 | display: inline-block;
39 | }
40 | .loader .bounceball {
41 | position: relative;
42 | width: 30px;
43 | }
44 | .loader .bounceball:before {
45 | position: absolute;
46 | content: '';
47 | top: 0;
48 | width: 30px;
49 | height: 30px;
50 | border-radius: 50%;
51 | background-color: #61dafa;
52 | transform-origin: 50%;
53 | animation: bounce 500ms alternate infinite ease;
54 | }
55 | @keyframes bounce {
56 | 0% {
57 | top: 60px;
58 | height: 10px;
59 | border-radius: 60px 60px 20px 20px;
60 | transform: scaleX(2);
61 | }
62 | 25% {
63 | height: 60px;
64 | border-radius: 50%;
65 | transform: scaleX(1);
66 | }
67 | 100% {
68 | top: 0;
69 | }
70 | }
71 | /* end: loader component */
72 |
--------------------------------------------------------------------------------
/my-react-project/src/test-utils/index.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable import/export */
2 | import { render } from '@testing-library/react'
3 |
4 | const customRender = (ui: React.ReactElement, options = {}) =>
5 | render(ui, {
6 | // wrap provider(s) here if needed
7 | wrapper: ({ children }) => children,
8 | ...options
9 | })
10 |
11 | export * from '@testing-library/react'
12 | export { default as userEvent } from '@testing-library/user-event'
13 | // override render export
14 | export { customRender as render }
15 |
--------------------------------------------------------------------------------
/my-react-project/src/tests/unit/config/config-files-map.test.ts:
--------------------------------------------------------------------------------
1 | import { configFilesMap } from '@/config/config-files-map'
2 |
3 | describe('configsMap', () => {
4 | it('instance should have "mock" key', () => {
5 | expect(configFilesMap.has('mock')).to.equal(true)
6 | })
7 |
8 | it('instance should have "jsonserver" key', () => {
9 | expect(configFilesMap.has('jsonserver')).to.equal(true)
10 | })
11 |
12 | it('instance should have "localapis" key', () => {
13 | expect(configFilesMap.has('localapis')).to.equal(true)
14 | })
15 |
16 | it('instance should have "beta" key', () => {
17 | expect(configFilesMap.has('beta')).to.equal(true)
18 | })
19 |
20 | it('instance should have "production" key', () => {
21 | expect(configFilesMap.has('production')).to.equal(true)
22 | })
23 | })
24 |
--------------------------------------------------------------------------------
/my-react-project/src/tests/unit/http-client/UrlUtils.getFullUrlWithParams.test.ts:
--------------------------------------------------------------------------------
1 | // file: src/tests/unit/http-client/UrlUtils.getFullUrlWithParams.test.ts
2 |
3 | import { UrlUtils } from '@/http-client'
4 |
5 | describe('UrlUtils: getFullUrlWithParams', () => {
6 | it('should return fullUrl formatted as expected with one param', () => {
7 | const endpoint = 'https://unit-test-api/v1/domain/[catalogId]/[partId]'
8 | const params = {
9 | catalogId: 5346782,
10 | partId: 'abcde23'
11 | }
12 | const result = UrlUtils.getFullUrlWithParams(endpoint, params)
13 |
14 | expect('https://unit-test-api/v1/domain/5346782/abcde23').toEqual(result)
15 | })
16 |
17 | // test our component click event
18 | it('should return fullUrl formatted as expected with multiple params', () => {
19 | const endpoint = 'https://unit-test-api/v1/domain/[country]/[state]/[cityId]'
20 | const params = {
21 | country: 'USA',
22 | state: 'NY',
23 | cityId: 'gtref345ytr'
24 | }
25 | const result = UrlUtils.getFullUrlWithParams(endpoint, params)
26 |
27 | expect('https://unit-test-api/v1/domain/USA/NY/gtref345ytr').toEqual(result)
28 | })
29 | })
30 |
--------------------------------------------------------------------------------
/my-react-project/src/tests/unit/http-client/axios-client/AxiosClient.request.get.test.ts:
--------------------------------------------------------------------------------
1 | // file: src/tests/unit/http-client/axios-client/AxiosClient.request.get.test.ts
2 |
3 | import axios from 'axios'
4 | import { HttpClientAxios, HttpRequestType, HttpRequestParamsInterface } from '@/http-client'
5 |
6 | let mockRequestParams: HttpRequestParamsInterface = {
7 | requestType: HttpRequestType.get,
8 | endpoint: 'path/to/a/get/api/endpoint',
9 | requiresToken: false
10 | }
11 |
12 | describe('HttpClient: axios-client: request: get', () => {
13 | const httpClient = new HttpClientAxios()
14 |
15 | it('should execute get request succesfully', () => {
16 | vitest
17 | .spyOn(axios, 'get')
18 | .mockImplementation(async () => Promise.resolve({ data: `request completed: ${mockRequestParams.endpoint}` }))
19 |
20 | httpClient
21 | .request(mockRequestParams)
22 | .then((response) => {
23 | //console.debug('response:', response)
24 | expect(response).toEqual(`request completed: ${mockRequestParams.endpoint}`)
25 | })
26 | .catch((error) => {
27 | console.info('AxiosClient.request.get.test.ts: error', error)
28 | })
29 | })
30 |
31 | it('get should throw error on rejection', () => {
32 | vitest
33 | .spyOn(axios, 'get')
34 | .mockImplementation(async () => Promise.reject({ data: `request completed: ${mockRequestParams.endpoint}` }))
35 |
36 | httpClient.request(mockRequestParams).catch((error) => {
37 | expect(error).toBeDefined()
38 | expect(error.toString()).toEqual('Error: HttpClientAxios: exception')
39 | })
40 | })
41 | })
42 |
--------------------------------------------------------------------------------
/my-react-project/src/tests/unit/http-client/axios-client/AxiosClient.request.post.test.ts:
--------------------------------------------------------------------------------
1 | // file: src/tests/unit/http-client/axios-client/AxiosClient.request.post.test.ts
2 |
3 | import axios from 'axios'
4 | import { HttpClientAxios, HttpRequestType, HttpRequestParamsInterface } from '@/http-client'
5 |
6 | let mockRequestParams: HttpRequestParamsInterface = {
7 | requestType: HttpRequestType.post,
8 | endpoint: 'path/to/a/post/api/endpoint',
9 | requiresToken: false,
10 | payload: {}
11 | }
12 |
13 | type P = typeof mockRequestParams.payload
14 |
15 | describe('HttpClient: axios-client: request: post', () => {
16 | const httpClient = new HttpClientAxios()
17 |
18 | it('should execute post request succesfully', () => {
19 | vitest
20 | .spyOn(axios, 'post')
21 | .mockImplementation(async () => Promise.resolve({ data: `request completed: ${mockRequestParams.endpoint}` }))
22 |
23 | httpClient
24 | .request(mockRequestParams)
25 | .then((response) => {
26 | //console.debug('response:', response)
27 | expect(response).toEqual(`request completed: ${mockRequestParams.endpoint}`)
28 | })
29 | .catch((error) => {
30 | console.info('AxiosClient.request.post.test.ts: post error', error)
31 | })
32 | })
33 | })
34 |
--------------------------------------------------------------------------------
/my-react-project/src/tests/unit/http-client/fetch-client/FetchClient.request.get.test.ts:
--------------------------------------------------------------------------------
1 | // file: src/tests/unit/http-client/fetch-client/FetchClient.request.get.test.ts
2 |
3 | import { HttpClientFetch, HttpRequestType, HttpRequestParamsInterface, HttpRequestMethods } from '@/http-client'
4 |
5 | let mockRequestParams: HttpRequestParamsInterface = {
6 | requestType: HttpRequestType.get,
7 | endpoint: 'path/to/a/get/api/endpoint',
8 | requiresToken: false
9 | }
10 |
11 | describe('HttpClient: axios-client: request: get', (done) => {
12 | const httpClient = new HttpClientFetch()
13 |
14 | it('should execute get request succesfully', async () => {
15 | // could not find an easy way to use spyOn for fetch so overriding global.fetch
16 | // save original fetch
17 | const unmockedFetch = global.fetch || (() => {})
18 | global.fetch = unmockedFetch
19 |
20 | const expectedResult = JSON.stringify({
21 | result: `request completed: ${mockRequestParams.endpoint}`
22 | })
23 |
24 | vitest.spyOn(global, 'fetch').mockImplementation(async () =>
25 | Promise.resolve({
26 | redirected: false,
27 | json: () => Promise.resolve(expectedResult)
28 | } as any)
29 | )
30 |
31 | try {
32 | const response = await httpClient.request(mockRequestParams)
33 | expect(response).not.toBeNull()
34 | expect(response).toEqual(expectedResult)
35 | } catch (error) {
36 | console.info('FetchClient.request.get.test.ts: error', error)
37 | }
38 |
39 | // restore globa.fetch
40 | global.fetch = unmockedFetch
41 | })
42 |
43 | it('get should throw error on rejection', () => {
44 | // could not find an easy way to use spyOn for fetch so overriding global.fetch
45 | // save original fetch
46 | const unmockedFetch = global.fetch || (() => {})
47 | global.fetch = unmockedFetch
48 |
49 | vitest.spyOn(global, 'fetch').mockImplementation(async () => Promise.reject())
50 |
51 | httpClient.request(mockRequestParams).catch((error) => {
52 | expect(error).toBeDefined()
53 | expect(error.toString()).toEqual('Error: HttpClientFetch: exception')
54 | })
55 |
56 | // restore globa.fetch
57 | global.fetch = unmockedFetch
58 | })
59 | })
60 |
--------------------------------------------------------------------------------
/my-react-project/src/views/Items.view.tsx:
--------------------------------------------------------------------------------
1 | // file: src/views/Items.view.tsx
2 | import * as React from 'react'
3 |
4 | // import hook useEffect from react
5 | import { useEffect } from 'react'
6 | // import a reference to our ItemInterface
7 | import { ItemInterface } from '@/models/items/Item.interface'
8 | // import a reference to your ItemsList component:
9 | import { ItemsListComponent } from '@/components/items/ItemsList.component'
10 | // import our useAppStore hook from our store
11 | import { useAppStore } from '@/store'
12 |
13 | // ItemsView component:
14 | function ItemsView() {
15 | // get a reference to our itemsStore instanceusing our useAppStore() hook:
16 | const { itemsStore } = useAppStore()
17 |
18 | // get a reference to the items state data through our itemsStore getters:
19 | const { loading, items } = itemsStore.getters
20 |
21 | // item select event handler
22 | const onItemSelect = (item: ItemInterface) => {
23 | itemsStore.actions.toggleItemSelected(item)
24 | }
25 |
26 | // use React useEffect to invoke our itemsStore loadItems action only once after this component is rendered:
27 | useEffect(() => {
28 | itemsStore.actions.loadItems()
29 | }, []) // <-- empty array means 'run once'
30 |
31 | // return our render function containing our ItemslistComponent as we did earlier in the App.tsx file
32 | return (
33 |
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/with-create-react-app/src/http-client/index.ts:
--------------------------------------------------------------------------------
1 | // file: src/http-client/index.ts
2 |
3 | import { HttpClientInterface } from './models/HttpClient.interface'
4 | import { HttpClientAxios } from './models/HttpClient.axios'
5 |
6 | // export instance of HttpClientInterface
7 | export const httpClient: HttpClientInterface = new HttpClientAxios()
8 |
9 | // also export all our interfaces/models/enums
10 | export * from './models'
11 |
--------------------------------------------------------------------------------
/with-create-react-app/src/http-client/models/HttpClient.interface.ts:
--------------------------------------------------------------------------------
1 | // files: src/http-client/models/HttpClient.interface.ts
2 |
3 | import { HttpRequestParamsInterface } from './HttpRequestParams.interface'
4 |
5 | /**
6 | * @name HttpClientInterface
7 | * @description
8 | * Represents our HttpClient.
9 | */
10 | export interface HttpClientInterface {
11 | /**
12 | * @name request
13 | * @description
14 | * A method that executes different types of http requests (i.e. GET/POST/etc)
15 | * based on the parameters argument.
16 | * The type R specify the type of the result returned
17 | * The type P specify the type of payload if any
18 | * @returns A Promise as the implementation of this method will be async.
19 | */
20 | request(parameters: HttpRequestParamsInterface
): Promise
21 | }
22 |
--------------------------------------------------------------------------------
/with-create-react-app/src/http-client/models/HttpRequestParams.interface.ts:
--------------------------------------------------------------------------------
1 | // file: src/http-client/models/HttpRequestParams.interface.ts
2 |
3 | import { HttpRequestType } from './HttpRequestType.enum'
4 |
5 | /**
6 | * @name HttpRequestParamsInterface
7 | * @description
8 | * Interface represents an object we'll use to pass arguments into our HttpClient request method.
9 | * This allow us to specify the type of request we want to execute, the end-point url,
10 | * if the request should include an authentication token, and an optional payload (if POST or PUT for example)
11 | */
12 | export interface HttpRequestParamsInterface
{
13 | requestType: HttpRequestType
14 | url: string
15 | requiresToken: boolean
16 | payload?: P
17 | }
18 |
--------------------------------------------------------------------------------
/with-create-react-app/src/http-client/models/HttpRequestType.enum.ts:
--------------------------------------------------------------------------------
1 | // file: src/http-client/models/HttpRequestType.ts
2 |
3 | /**
4 | * @name HttpRequestType
5 | * @description
6 | * The type of http request we need to execute in our HttpClient request method
7 | */
8 | export const enum HttpRequestType {
9 | get,
10 | post,
11 | put,
12 | delete
13 | }
14 |
--------------------------------------------------------------------------------
/with-create-react-app/src/http-client/models/index.ts:
--------------------------------------------------------------------------------
1 | // file: src/http-client/models/index.ts
2 |
3 | export * from './HttpRequestType.enum'
4 | export * from './HttpRequestParams.interface'
5 | export * from './HttpClient.interface'
6 | export * from './HttpClient.axios'
7 |
--------------------------------------------------------------------------------
/with-create-react-app/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12 | monospace;
13 | }
14 |
--------------------------------------------------------------------------------
/with-create-react-app/src/index.tsx:
--------------------------------------------------------------------------------
1 | // file: index.tsx
2 |
3 | import React from 'react';
4 | import ReactDOM from 'react-dom';
5 | import './index.css';
6 | import App from './App';
7 | import reportWebVitals from './reportWebVitals';
8 |
9 | ReactDOM.render(
10 |
11 |
12 | ,
13 | document.getElementById('root')
14 | );
15 |
16 | // If you want to start measuring performance in your app, pass a function
17 | // to log results (for example: reportWebVitals(console.log))
18 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
19 | reportWebVitals();
20 |
--------------------------------------------------------------------------------
/with-create-react-app/src/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/with-create-react-app/src/models/api-client/ApiClient.interface.ts:
--------------------------------------------------------------------------------
1 | // file: src/models/api-client/ApiClient.interface.ts
2 |
3 | import { ItemsApiClientInterface } from './items'
4 |
5 | /**
6 | * @Name ApiClientInterface
7 | * @description
8 | * Interface wraps all api client modules into one places for keeping code organized.
9 | */
10 | export interface ApiClientInterface {
11 | items: ItemsApiClientInterface
12 | }
13 |
--------------------------------------------------------------------------------
/with-create-react-app/src/models/api-client/items/ItemsApiClient.interface.ts:
--------------------------------------------------------------------------------
1 | // file: src/models/api-client/items/ItemsApiClient.interface.ts
2 |
3 | import { ItemInterface } from '../../items/Item.interface'
4 |
5 | /**
6 | * @Name ItemsApiClientInterface
7 | * @description
8 | * Interface for the Items api client module
9 | */
10 | export interface ItemsApiClientInterface {
11 | fetchItems: () => Promise
12 | }
13 |
--------------------------------------------------------------------------------
/with-create-react-app/src/models/api-client/items/ItemsApiClient.model.ts:
--------------------------------------------------------------------------------
1 | // file: src/models/api-client/items/ItemsApiClient.model.ts
2 |
3 | import { httpClient, HttpRequestParamsInterface, HttpRequestType } from '../../../http-client'
4 |
5 | import { ItemsApiClientUrlsInterface } from './ItemsApiClientUrls.interface'
6 | import { ItemsApiClientInterface } from './ItemsApiClient.interface'
7 | import { ItemInterface } from '../../items/Item.interface'
8 |
9 | /**
10 | * @Name ItemsApiClientModel
11 | * @description
12 | * Implements the ItemsApiClientInterface interface
13 | */
14 | export class ItemsApiClientModel implements ItemsApiClientInterface {
15 | private readonly urls!: ItemsApiClientUrlsInterface
16 | private readonly mockDelay: number = 0
17 |
18 | constructor(options: {
19 | urls: ItemsApiClientUrlsInterface,
20 | mockDelay?: number
21 | }) {
22 | this.urls = options.urls
23 | if (options.mockDelay) {
24 | this.mockDelay = options.mockDelay
25 | }
26 | }
27 |
28 | fetchItems(): Promise {
29 | const requestParameters: HttpRequestParamsInterface = {
30 | requestType: HttpRequestType.get,
31 | url: this.urls.fetchItems,
32 | requiresToken: false,
33 | }
34 |
35 | //return httpClient.request(requestParameters)
36 |
37 | // if you want to keep simulating the artificail delay, use this
38 | if (!this.mockDelay) {
39 | return httpClient.request(requestParameters)
40 | } else {
41 | return new Promise((resolve) => {
42 | httpClient.request(requestParameters)
43 | .then((data) => {
44 | setTimeout(() => {
45 | resolve(data)
46 | }, this.mockDelay)
47 | })
48 | })
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/with-create-react-app/src/models/api-client/items/ItemsApiClientUrls.interface.ts:
--------------------------------------------------------------------------------
1 | // file: src/models/api-client/items/ItemsApiClientUrls.interface.ts
2 |
3 | /**
4 | * @Name ItemsApiClientUrlsInterface
5 | * @description
6 | * Interface for the Items urls used to avoid hard-coded strings
7 | */
8 | export interface ItemsApiClientUrlsInterface {
9 | fetchItems: string
10 | }
11 |
--------------------------------------------------------------------------------
/with-create-react-app/src/models/api-client/items/index.ts:
--------------------------------------------------------------------------------
1 | // file: src/models/api-client/items/index.ts
2 |
3 | export * from './ItemsApiClientUrls.interface'
4 | export * from './ItemsApiClient.interface'
5 | export * from './ItemsApiClient.model'
6 |
--------------------------------------------------------------------------------
/with-create-react-app/src/models/items/Item.interface.ts:
--------------------------------------------------------------------------------
1 | export interface ItemInterface {
2 | id: number
3 | name: string
4 | selected: boolean
5 | }
6 |
--------------------------------------------------------------------------------
/with-create-react-app/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/with-create-react-app/src/reportWebVitals.ts:
--------------------------------------------------------------------------------
1 | import { ReportHandler } from 'web-vitals';
2 |
3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => {
4 | if (onPerfEntry && onPerfEntry instanceof Function) {
5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
6 | getCLS(onPerfEntry);
7 | getFID(onPerfEntry);
8 | getFCP(onPerfEntry);
9 | getLCP(onPerfEntry);
10 | getTTFB(onPerfEntry);
11 | });
12 | }
13 | };
14 |
15 | export default reportWebVitals;
16 |
--------------------------------------------------------------------------------
/with-create-react-app/src/setupTests.ts:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom';
6 |
--------------------------------------------------------------------------------
/with-create-react-app/src/shims-react.d.ts:
--------------------------------------------------------------------------------
1 | // file: src/shims-react.d.ts
2 |
3 | declare interface process {
4 | env: {
5 | REACT_APP_API_CLIENT: string
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/with-create-react-app/src/store/index.ts:
--------------------------------------------------------------------------------
1 | // file: src/store/index.ts
2 |
3 | export * from './root'
4 |
--------------------------------------------------------------------------------
/with-create-react-app/src/store/items/Items.slice.ts:
--------------------------------------------------------------------------------
1 | // src/store/items/Items.slice.ts
2 |
3 | // import createSlice and PayloadAction from redux toolkit
4 | import { createSlice, PayloadAction } from '@reduxjs/toolkit'
5 |
6 | // import out items state interface, and the item interface
7 | import { ItemsStateInterface } from './models'
8 | import { ItemInterface } from '../../models/items/Item.interface'
9 |
10 | // create an object that represents our initial items state
11 | const initialItemsState: ItemsStateInterface = {
12 | loading: false,
13 | items: []
14 | }
15 |
16 | // create the itemsStoreSlice with createSlice:
17 | export const itemsStoreSlice = createSlice({
18 | name: 'itemsStoreSlice',
19 | initialState: initialItemsState,
20 | reducers: {
21 | // reducers are functions that commit final mutations to the state
22 | // These will commit final mutation/changes to the state
23 |
24 | setLoading: (state, action: PayloadAction) => {
25 | state.loading = action.payload
26 | },
27 |
28 | setItems: (state, action: PayloadAction) => {
29 | // update our state:
30 | // set our items
31 | state.items = action.payload || []
32 | // set loading to false so the loader will be hidden in the UI
33 | state.loading = false
34 | },
35 |
36 | setItemSelected: (state, action: PayloadAction) => {
37 | const item = action.payload
38 | const found = state.items.find(o => o.id === item.id) as ItemInterface
39 | found.selected = !found.selected
40 | }
41 | }
42 | })
43 |
--------------------------------------------------------------------------------
/with-create-react-app/src/store/items/Items.store.ts:
--------------------------------------------------------------------------------
1 | // src/store/items/Items.store.ts
2 |
3 | // import hooks useSelector and useDispatch from react-redux
4 | import { useSelector, useDispatch } from 'react-redux'
5 |
6 | // import a refence to our itemsStoreSlice
7 | import { itemsStoreSlice } from './Items.slice'
8 | // import a reference to our RootStateInterface
9 | import { RootStateInterface } from '../root'
10 | // import references to our itesms tore and actions interfaces
11 | import { ItemsStoreInterface, ItemsStoreActionsInterface } from './models'
12 | // import a reference to our ItemInterface
13 | import { ItemInterface } from '../../models/items/Item.interface'
14 |
15 | // import a reference to our apiClient instance
16 | import { apiClient } from '../../api-client'
17 |
18 | // hook to allows us to consume the ItemsStore instance in our components
19 | export function useItemsStore(): ItemsStoreInterface {
20 | // note: we are callin dispatch "commit" here, as it make more sense to call it this way
21 | // feel free to just call it dispatch if you prefer
22 | const commit = useDispatch()
23 |
24 | // get a reference to our slice actions (which are really our mutations/commits)
25 | const mutations = itemsStoreSlice.actions
26 |
27 | // our items store actions implementation:
28 | const actions: ItemsStoreActionsInterface = {
29 | loadItems: async () => {
30 | commit(mutations.setLoading(true))
31 |
32 | // invoke our API cient fetchItems to load the data from an API end-point
33 | const data = await apiClient.items.fetchItems()
34 | commit(mutations.setItems(data))
35 | },
36 | toggleItemSelected: async (item: ItemInterface) => {
37 | console.log('ItemsStore: action: toggleItemSelected', item)
38 | commit(mutations.setItemSelected(item))
39 | }
40 | }
41 |
42 | // return our store intance implementation
43 | return {
44 | getters: {
45 | loading: useSelector((s: RootStateInterface) => s.items.loading),
46 | items: useSelector((s: RootStateInterface) => s.items.items)
47 | },
48 | actions: actions
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/with-create-react-app/src/store/items/index.ts:
--------------------------------------------------------------------------------
1 | // file: src/store/items/index.ts
2 |
3 | export * from './Items.slice'
4 | export * from './Items.store'
5 |
--------------------------------------------------------------------------------
/with-create-react-app/src/store/items/models/ItemsState.interface.ts:
--------------------------------------------------------------------------------
1 | // file: ItemsState.interface.ts
2 |
3 | import { ItemInterface } from '../../../models/items/Item.interface'
4 |
5 | /**
6 | * @name ItemsStateInterface
7 | * @description Interface represnets our Items state
8 | */
9 | export interface ItemsStateInterface {
10 | loading: boolean
11 | items: ItemInterface[]
12 | }
13 |
--------------------------------------------------------------------------------
/with-create-react-app/src/store/items/models/ItemsStore.interface.ts:
--------------------------------------------------------------------------------
1 | // file: ItemsStore.interface.ts
2 |
3 | import { ItemInterface } from '../../../models/items/Item.interface'
4 |
5 | /**
6 | * @name ItemsStoreActionsInterface
7 | * @description Interface represents our Items state actions
8 | */
9 | export interface ItemsStoreActionsInterface {
10 | loadItems (): Promise
11 | toggleItemSelected (item: ItemInterface): Promise
12 | }
13 |
14 | /**
15 | * @name ItemsStoreGettersInterface
16 | * @description Interface represents our store getters
17 | * Getters will be used to consume the data from the store.
18 | */
19 | export interface ItemsStoreGettersInterface {
20 | loading: boolean
21 | items: ItemInterface[]
22 | }
23 |
24 | /**
25 | * @name ItemsStoreInterface
26 | * @description Interface represents our Items store module
27 | */
28 | export interface ItemsStoreInterface {
29 | actions: ItemsStoreActionsInterface
30 | getters: ItemsStoreGettersInterface
31 | }
32 |
--------------------------------------------------------------------------------
/with-create-react-app/src/store/items/models/index.ts:
--------------------------------------------------------------------------------
1 | export * from './ItemsState.interface'
2 | export * from './ItemsStore.interface'
3 |
--------------------------------------------------------------------------------
/with-create-react-app/src/store/root/Root.store.ts:
--------------------------------------------------------------------------------
1 | // file: src/store/root/Root.store.ts
2 |
3 | // import configureStore from redux toolkit
4 | import { configureStore } from '@reduxjs/toolkit'
5 |
6 | // import our root store interface
7 | import { RootStoreInterface } from './models'
8 |
9 | // import our items slice and store
10 | import { itemsStoreSlice, useItemsStore } from '../items/'
11 |
12 | // configure global redux store for the whole app.
13 | // this will be consumed by App.tsx
14 | export const globalStore = configureStore({
15 | reducer: {
16 | //add reducers here
17 | items: itemsStoreSlice.reducer
18 | }
19 | })
20 |
21 | // Infer the `RootStateInterface` type from the store itself (globalStore.getState)
22 | // thus avoiding to explicitely having to create an additional interface for the
23 | export type RootStateInterface = ReturnType
24 | // Infer the dispatch type from globalStore.dispatch itself
25 | //export type AppDispatch = typeof globalStore.dispatch
26 |
27 | // hook that returns our root store instance and will allow us to consume our app store from our components
28 | export function useAppStore(): RootStoreInterface {
29 | return {
30 | itemsStore: useItemsStore(),
31 | // additional domain store modules will be eventually added here
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/with-create-react-app/src/store/root/index.ts:
--------------------------------------------------------------------------------
1 | // file: src/store/root/index.ts
2 |
3 | export * from './Root.store'
4 |
--------------------------------------------------------------------------------
/with-create-react-app/src/store/root/models/RootStore.interface.ts:
--------------------------------------------------------------------------------
1 | // file: RootStore.interface.ts
2 |
3 | import { ItemsStoreInterface } from '../../items/models/ItemsStore.interface'
4 |
5 | /**
6 | * @name RootStateInterface
7 | * @description Interface represents our global state manager
8 | */
9 | export interface RootStoreInterface {
10 | itemsStore: ItemsStoreInterface,
11 | // additional domain store modules will be eventually added here
12 | }
13 |
--------------------------------------------------------------------------------
/with-create-react-app/src/store/root/models/index.ts:
--------------------------------------------------------------------------------
1 | // file: src/store/root/models/index.ts
2 |
3 | export * from './RootStore.interface'
4 |
--------------------------------------------------------------------------------
/with-create-react-app/src/tests/http-client/HttpClient.request.get.test.ts:
--------------------------------------------------------------------------------
1 | // file: src/tests/http-client/HttpClient.request.get.test.ts
2 |
3 | import axios from 'axios'
4 | import { httpClient, HttpRequestType, HttpRequestParamsInterface } from '../../http-client'
5 |
6 |
7 | let mockRequestParams: HttpRequestParamsInterface = {
8 | requestType: HttpRequestType.get,
9 | url: 'path/to/a/get/api/endpoint',
10 | requiresToken: false
11 | }
12 |
13 | // test our component click event
14 | test('httpClient: reqest: should execute get request succesfully', () => {
15 | jest
16 | .spyOn(axios, 'get')
17 | .mockImplementation(async () => Promise.resolve({ data: `request completed: ${ mockRequestParams.url }`}));
18 |
19 | httpClient
20 | .request(mockRequestParams)
21 | .then((response) => {
22 | //console.debug('response:', response)
23 | expect(response).toEqual(`request completed: ${ mockRequestParams.url }`)
24 | })
25 | .catch(error => {
26 | console.info('HttpClient.request.get.test.ts: HttpClient.request(get) error', error)
27 | })
28 | })
29 |
30 | test('httpClient: reqest: get should throw error on rejection', () => {
31 | jest
32 | .spyOn(axios, 'get')
33 | .mockImplementation(async () => Promise.reject({ data: `request completed: ${ mockRequestParams.url }`}));
34 |
35 | httpClient
36 | .request(mockRequestParams)
37 | .catch(error => {
38 | expect(error).toBeDefined()
39 | expect(error.toString()).toEqual('Error: HttpClient exception')
40 | })
41 | })
42 |
--------------------------------------------------------------------------------
/with-create-react-app/src/tests/http-client/HttpClient.request.post.test.ts:
--------------------------------------------------------------------------------
1 | // file: src/tests/http-client/HttpClient.request.post.test.ts
2 |
3 | import axios from 'axios'
4 | import { httpClient, HttpRequestType, HttpRequestParamsInterface } from '../../http-client'
5 |
6 | let mockRequestParams: HttpRequestParamsInterface = {
7 | requestType: HttpRequestType.post,
8 | url: 'path/to/a/post/api/endpoint',
9 | requiresToken: false,
10 | payload: {}
11 | }
12 |
13 | type P = typeof mockRequestParams.payload
14 |
15 | // test our component click event
16 | test('httpClient: reqest: should execute post request succesfully', () => {
17 | jest
18 | .spyOn(axios, 'post')
19 | .mockImplementation(async () => Promise.resolve({ data: `request completed: ${ mockRequestParams.url }`}));
20 |
21 | httpClient
22 | .request(mockRequestParams)
23 | .then((response) => {
24 | //console.debug('response:', response)
25 | expect(response).toEqual(`request completed: ${ mockRequestParams.url }`)
26 | })
27 | .catch(error => {
28 | console.info('HttpClient.request.post.test.ts: HttpClient.request(post) error', error)
29 | })
30 | })
31 |
--------------------------------------------------------------------------------
/with-create-react-app/src/views/Items.view.tsx:
--------------------------------------------------------------------------------
1 | // file: src/views/Items.view.tsx
2 |
3 | // import hook useEffect from react
4 | import { useEffect } from 'react'
5 | // import a reference to our ItemInterface
6 | import { ItemInterface } from '../models/items/Item.interface'
7 | // import a reference to your ItemsList component:
8 | import { ItemsListComponent } from '../components/items/ItemsList.component'
9 | // import our useAppStore hook from our store
10 | import { useAppStore } from '../store'
11 |
12 | // ItemsView component:
13 | function ItemsView() {
14 | // get a reference to our itemsStore instanceusing our useAppStore() hook:
15 | const {
16 | itemsStore
17 | } = useAppStore()
18 |
19 | // get a reference to the items state data through our itemsStore getters:
20 | const {
21 | loading,
22 | items
23 | } = itemsStore.getters
24 |
25 | // item select event handler
26 | const onItemSelect = (item: ItemInterface) => {
27 | console.log('ItemsView: onItemSelect', item)
28 | itemsStore.actions.toggleItemSelected(item)
29 | }
30 |
31 | // use React useEffect to invoke our itemsStore loadItems action only once after this component is rendered:
32 | useEffect(() => {
33 | itemsStore.actions.loadItems()
34 | }, []); // <-- empty array means 'run once'
35 |
36 | // return our render function containing our ItemslistComponent as we did earlier in the App.tsx file
37 | return (
38 |