├── components ├── Product │ ├── index.ts │ ├── Product.ts │ └── ProductRaw.tsx ├── order │ ├── Continue │ │ ├── index.ts │ │ ├── Continue.ts │ │ └── ContinueRaw.tsx │ ├── Details │ │ ├── index.ts │ │ ├── Details.ts │ │ └── DetailsRaw.tsx │ ├── index.ts │ └── OrderScreens.tsx ├── review │ ├── Continue │ │ ├── index.ts │ │ ├── Continue.ts │ │ └── ContinueRaw.tsx │ ├── Details │ │ ├── index.ts │ │ ├── Details.ts │ │ └── DetailsRaw.tsx │ ├── Reviewer │ │ ├── index.ts │ │ ├── Reviewer.ts │ │ └── ReviewerRaw.tsx │ ├── index.ts │ └── ReviewScreens.tsx ├── JourneySwitch │ ├── index.ts │ ├── JourneySwitch.ts │ └── JourneySwitchRaw.tsx ├── ui-units │ ├── SizePicker │ │ ├── index.ts │ │ ├── ISizePickerProps.tsx │ │ ├── SizePickerRaw.tsx │ │ └── SizePicker.ts │ ├── ColorPicker │ │ ├── index.ts │ │ ├── IColorPickerProps.tsx │ │ ├── ColorPicker.ts │ │ └── ColorPickerRaw.tsx │ ├── StarsPicker │ │ ├── index.ts │ │ ├── IStarsPickerProps.tsx │ │ ├── StarsPicker.ts │ │ └── StarsPickerRaw.tsx │ └── index.ts ├── index.ts └── GlobalStyle.ts ├── debug ├── index.ts └── DebugObserver.tsx ├── .prettierignore ├── proxies ├── index.ts ├── IProduct.ts └── fetchProductsMock.ts ├── states ├── tracking │ ├── index.ts │ └── state-tracking.ts ├── cache │ ├── index.ts │ └── state-cached-products.ts ├── composition │ ├── index.ts │ ├── state-headers.ts │ ├── state-order.ts │ └── state-review.ts ├── root-states │ ├── state-size.ts │ ├── state-count.ts │ ├── state-stars.ts │ ├── state-comment.ts │ ├── state-reviewer.ts │ ├── index.ts │ ├── state-product-id.ts │ └── state-color.ts └── index.ts ├── typings └── styled-components │ └── index.d.ts ├── interfaces ├── IWithClassName.ts ├── IHeader.ts ├── IProductRef.ts ├── Size.ts ├── JourneyType.ts ├── IReview.ts ├── IOrder.ts ├── index.ts └── IRecoilId.ts ├── next-env.d.ts ├── .prettierrc.json ├── routing ├── index.ts ├── IRoutingInfo.ts ├── IFlowRouterProps.ts ├── useRoutingInfo.ts └── useFlowRouter.ts ├── guards ├── index.ts ├── guard-recoil-default-value.ts └── guard-string.ts ├── pages ├── order.tsx ├── index.tsx ├── review.tsx ├── _app.tsx └── _document.tsx ├── .eslintignore ├── .stylelintrc ├── .gitignore ├── tsconfig.json ├── README.md ├── package.json ├── .eslintrc.json └── .vscode └── settings.json /components/Product/index.ts: -------------------------------------------------------------------------------- 1 | export { Product } from './Product'; 2 | -------------------------------------------------------------------------------- /debug/index.ts: -------------------------------------------------------------------------------- 1 | export { DebugObserver } from './DebugObserver'; 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | package-lock.json 2 | .next 3 | node_modules/ 4 | out/ 5 | -------------------------------------------------------------------------------- /components/order/Continue/index.ts: -------------------------------------------------------------------------------- 1 | export { Continue } from './Continue'; 2 | -------------------------------------------------------------------------------- /components/order/Details/index.ts: -------------------------------------------------------------------------------- 1 | export { Details } from './Details'; 2 | -------------------------------------------------------------------------------- /components/order/index.ts: -------------------------------------------------------------------------------- 1 | export { OrderScreens } from './OrderScreens'; 2 | -------------------------------------------------------------------------------- /components/review/Continue/index.ts: -------------------------------------------------------------------------------- 1 | export { Continue } from './Continue'; 2 | -------------------------------------------------------------------------------- /components/review/Details/index.ts: -------------------------------------------------------------------------------- 1 | export { Details } from './Details'; 2 | -------------------------------------------------------------------------------- /components/review/Reviewer/index.ts: -------------------------------------------------------------------------------- 1 | export { Reviewer } from './Reviewer'; 2 | -------------------------------------------------------------------------------- /proxies/index.ts: -------------------------------------------------------------------------------- 1 | export { fetchProductsMock } from './fetchProductsMock'; 2 | -------------------------------------------------------------------------------- /states/tracking/index.ts: -------------------------------------------------------------------------------- 1 | export { stateTracking } from './state-tracking'; 2 | -------------------------------------------------------------------------------- /components/review/index.ts: -------------------------------------------------------------------------------- 1 | export { ReviewScreens } from './ReviewScreens'; 2 | -------------------------------------------------------------------------------- /typings/styled-components/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'react-star-ratings' {} 2 | -------------------------------------------------------------------------------- /components/JourneySwitch/index.ts: -------------------------------------------------------------------------------- 1 | export { JourneySwitch } from './JourneySwitch'; 2 | -------------------------------------------------------------------------------- /components/ui-units/SizePicker/index.ts: -------------------------------------------------------------------------------- 1 | export { SizePicker } from './SizePicker'; 2 | -------------------------------------------------------------------------------- /states/cache/index.ts: -------------------------------------------------------------------------------- 1 | export { stateCachedProducts } from './state-cached-products'; 2 | -------------------------------------------------------------------------------- /components/ui-units/ColorPicker/index.ts: -------------------------------------------------------------------------------- 1 | export { ColorPicker } from './ColorPicker'; 2 | -------------------------------------------------------------------------------- /components/ui-units/StarsPicker/index.ts: -------------------------------------------------------------------------------- 1 | export { StarsPicker } from './StarsPicker'; 2 | -------------------------------------------------------------------------------- /interfaces/IWithClassName.ts: -------------------------------------------------------------------------------- 1 | export interface IWithClassName { 2 | className?: string; 3 | } 4 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /proxies/IProduct.ts: -------------------------------------------------------------------------------- 1 | export interface IProduct { 2 | id: string; 3 | title: string; 4 | } 5 | -------------------------------------------------------------------------------- /interfaces/IHeader.ts: -------------------------------------------------------------------------------- 1 | export interface IHeader { 2 | id: string; 3 | product: string; 4 | } 5 | -------------------------------------------------------------------------------- /interfaces/IProductRef.ts: -------------------------------------------------------------------------------- 1 | export interface IProductRef { 2 | id: string; 3 | productId: string; 4 | } 5 | -------------------------------------------------------------------------------- /interfaces/Size.ts: -------------------------------------------------------------------------------- 1 | export enum Size { 2 | small = 'small', 3 | medium = 'medium', 4 | large = 'large', 5 | } 6 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /routing/index.ts: -------------------------------------------------------------------------------- 1 | export { useFlowRouter } from './useFlowRouter'; 2 | export { useRoutingInfo } from './useRoutingInfo'; 3 | -------------------------------------------------------------------------------- /interfaces/JourneyType.ts: -------------------------------------------------------------------------------- 1 | export enum JourneyType { 2 | unknown = 'unknown', 3 | order = 'order', 4 | review = 'review', 5 | } 6 | -------------------------------------------------------------------------------- /guards/index.ts: -------------------------------------------------------------------------------- 1 | export { guardRecoilDefaultValue } from './guard-recoil-default-value'; 2 | export { guardString } from './guard-string'; 3 | -------------------------------------------------------------------------------- /components/ui-units/index.ts: -------------------------------------------------------------------------------- 1 | export { ColorPicker } from './ColorPicker'; 2 | export { SizePicker } from './SizePicker'; 3 | export { StarsPicker } from './StarsPicker'; 4 | -------------------------------------------------------------------------------- /states/composition/index.ts: -------------------------------------------------------------------------------- 1 | export { stateHeaders } from './state-headers'; 2 | export { stateOrder } from './state-order'; 3 | export { stateReview } from './state-review'; 4 | -------------------------------------------------------------------------------- /pages/order.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { OrderScreens } from '../components'; 3 | 4 | const OrderPage = () => ; 5 | 6 | export default OrderPage; 7 | -------------------------------------------------------------------------------- /interfaces/IReview.ts: -------------------------------------------------------------------------------- 1 | import { IProductRef } from '.'; 2 | 3 | export interface IReview extends IProductRef { 4 | reviewer: string; 5 | comment: string; 6 | stars: number; 7 | } 8 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { JourneySwitch } from '../components'; 3 | 4 | const IndexPage = () => ; 5 | 6 | export default IndexPage; 7 | -------------------------------------------------------------------------------- /pages/review.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ReviewScreens } from '../components'; 3 | 4 | const ReviewPage = () => ; 5 | 6 | export default ReviewPage; 7 | -------------------------------------------------------------------------------- /routing/IRoutingInfo.ts: -------------------------------------------------------------------------------- 1 | import { JourneyType } from '../interfaces'; 2 | 3 | export interface IRoutingInfo { 4 | journey: JourneyType; 5 | id: string; 6 | stageKey?: string; 7 | } 8 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /.next 3 | /out 4 | /storybook-static 5 | server.js 6 | /.vscode 7 | /typings 8 | /package-lock.json 9 | /next.config.js 10 | /**/i18n.js 11 | /**/*.stories.* -------------------------------------------------------------------------------- /interfaces/IOrder.ts: -------------------------------------------------------------------------------- 1 | import { IProductRef } from '.'; 2 | import { Size } from './Size'; 3 | 4 | export interface IOrder extends IProductRef { 5 | color: string; 6 | size: Size; 7 | count: number; 8 | } 9 | -------------------------------------------------------------------------------- /components/index.ts: -------------------------------------------------------------------------------- 1 | export { JourneySwitch } from './JourneySwitch'; 2 | export { OrderScreens } from './order'; 3 | export { ReviewScreens } from './review'; 4 | export { ColorPicker, SizePicker } from './ui-units'; 5 | -------------------------------------------------------------------------------- /routing/IFlowRouterProps.ts: -------------------------------------------------------------------------------- 1 | import { NextRouter } from 'next/dist/next-server/lib/router/router'; 2 | 3 | export interface IFlowRouterProps extends NextRouter { 4 | pushStage: (stageKey: string, isNew?: boolean) => Promise; 5 | } 6 | -------------------------------------------------------------------------------- /components/ui-units/ColorPicker/IColorPickerProps.tsx: -------------------------------------------------------------------------------- 1 | import { RecoilState } from 'recoil'; 2 | import { IWithClassName } from '../../../interfaces'; 3 | 4 | export interface IColorPickerProps extends IWithClassName { 5 | state: RecoilState; 6 | } 7 | -------------------------------------------------------------------------------- /components/ui-units/SizePicker/ISizePickerProps.tsx: -------------------------------------------------------------------------------- 1 | import { RecoilState } from 'recoil'; 2 | import { IWithClassName, Size } from '../../../interfaces'; 3 | 4 | export interface ISizePickerProps extends IWithClassName { 5 | state: RecoilState; 6 | } 7 | -------------------------------------------------------------------------------- /components/ui-units/StarsPicker/IStarsPickerProps.tsx: -------------------------------------------------------------------------------- 1 | import { RecoilState } from 'recoil'; 2 | import { IWithClassName } from '../../../interfaces'; 3 | 4 | export interface IStarsPickerProps extends IWithClassName { 5 | state: RecoilState; 6 | } 7 | -------------------------------------------------------------------------------- /guards/guard-recoil-default-value.ts: -------------------------------------------------------------------------------- 1 | import { DefaultValue } from 'recoil'; 2 | 3 | export const guardRecoilDefaultValue = ( 4 | candidate: any 5 | ): candidate is DefaultValue => { 6 | if (candidate instanceof DefaultValue) return true; 7 | return false; 8 | }; 9 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "processors": [ 3 | "stylelint-processor-styled-components" 4 | ], 5 | "extends": [ 6 | "stylelint-config-recommended", 7 | "stylelint-config-styled-components" 8 | ], 9 | "rules": { 10 | "font-family-no-missing-generic-family-keyword": null 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /components/ui-units/StarsPicker/StarsPicker.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { StarsPickerRaw } from './StarsPickerRaw'; 3 | 4 | export const StarsPicker = styled(StarsPickerRaw)` 5 | display: grid; 6 | font-size: 1.5rem; 7 | align-content: stretch; 8 | justify-content: start; 9 | height: 4rem; 10 | `; 11 | -------------------------------------------------------------------------------- /components/GlobalStyle.ts: -------------------------------------------------------------------------------- 1 | import { createGlobalStyle } from 'styled-components'; 2 | 3 | const GlobalStyle = createGlobalStyle` 4 | body { 5 | margin: 0; 6 | padding: 0; 7 | background: #eee; 8 | font-family: Open-Sans, Helvetica, Sans-Serif; 9 | min-height: 100vh; 10 | } 11 | `; 12 | 13 | export default GlobalStyle; 14 | -------------------------------------------------------------------------------- /states/root-states/state-size.ts: -------------------------------------------------------------------------------- 1 | import { atomFamily } from 'recoil'; 2 | import { IRecoilId, Size } from '../../interfaces'; 3 | 4 | /** 5 | * Represent single size choice of specific product 6 | */ 7 | export const stateSize = atomFamily({ 8 | key: 'state-size', 9 | default: Size.medium, 10 | }); 11 | -------------------------------------------------------------------------------- /states/root-states/state-count.ts: -------------------------------------------------------------------------------- 1 | import { atomFamily } from 'recoil'; 2 | import { IRecoilId } from '../../interfaces'; 3 | 4 | /** 5 | * Represent count of specific product's order 6 | */ 7 | export const stateCount = atomFamily( 8 | { 9 | key: 'state-count', 10 | default: 0, 11 | } 12 | ); 13 | -------------------------------------------------------------------------------- /states/root-states/state-stars.ts: -------------------------------------------------------------------------------- 1 | import { atomFamily } from 'recoil'; 2 | import { IRecoilId } from '../../interfaces'; 3 | 4 | /** 5 | * Represent count of specific review evaluation 6 | */ 7 | export const stateStars = atomFamily( 8 | { 9 | key: 'state-stars', 10 | default: 0, 11 | } 12 | ); 13 | -------------------------------------------------------------------------------- /states/root-states/state-comment.ts: -------------------------------------------------------------------------------- 1 | import { atomFamily } from 'recoil'; 2 | import { IRecoilId } from '../../interfaces'; 3 | 4 | /** 5 | * Represent name choice of specific comment 6 | */ 7 | export const stateComment = atomFamily< 8 | string, 9 | IRecoilId /* recoil family key */ 10 | >({ 11 | key: 'state-comment', 12 | default: '', 13 | }); 14 | -------------------------------------------------------------------------------- /states/index.ts: -------------------------------------------------------------------------------- 1 | export { stateCachedProducts } from './cache'; 2 | export { stateHeaders, stateOrder, stateReview } from './composition'; 3 | export { 4 | stateColor, 5 | stateComment, 6 | stateCount, 7 | stateProductId, 8 | stateReviewer, 9 | stateSize, 10 | stateStars, 11 | } from './root-states'; 12 | export { stateTracking } from './tracking'; 13 | -------------------------------------------------------------------------------- /states/root-states/state-reviewer.ts: -------------------------------------------------------------------------------- 1 | import { atomFamily } from 'recoil'; 2 | import { IRecoilId } from '../../interfaces'; 3 | 4 | /** 5 | * Represent name choice of specific reviewer 6 | */ 7 | export const stateReviewer = atomFamily< 8 | string, 9 | IRecoilId /* recoil family key */ 10 | >({ 11 | key: 'state-reviewer', 12 | default: '', 13 | }); 14 | -------------------------------------------------------------------------------- /states/root-states/index.ts: -------------------------------------------------------------------------------- 1 | export { stateColor } from './state-color'; 2 | export { stateCount } from './state-count'; 3 | export { stateSize } from './state-size'; 4 | export { stateProductId } from './state-product-id'; 5 | export { stateStars } from './state-stars'; 6 | export { stateComment } from './state-comment'; 7 | export { stateReviewer } from './state-reviewer'; 8 | -------------------------------------------------------------------------------- /states/root-states/state-product-id.ts: -------------------------------------------------------------------------------- 1 | import { atomFamily } from 'recoil'; 2 | import { IRecoilId } from '../../interfaces'; 3 | 4 | /** 5 | * Represent name choice of specific product id 6 | */ 7 | export const stateProductId = atomFamily< 8 | string, 9 | IRecoilId /* recoil family key */ 10 | >({ 11 | key: 'state-product-id', 12 | default: '', 13 | }); 14 | -------------------------------------------------------------------------------- /states/cache/state-cached-products.ts: -------------------------------------------------------------------------------- 1 | import { selector } from 'recoil'; 2 | import { fetchProductsMock } from '../../proxies'; 3 | import { IProduct } from '../../proxies/IProduct'; 4 | 5 | /** 6 | * Cache fake service 7 | */ 8 | export const stateCachedProducts = selector({ 9 | key: 'state-cached-products', 10 | get: () => fetchProductsMock(), 11 | }); 12 | -------------------------------------------------------------------------------- /states/root-states/state-color.ts: -------------------------------------------------------------------------------- 1 | import { atomFamily } from 'recoil'; 2 | import { IRecoilId } from '../../interfaces'; 3 | 4 | /** 5 | * Represent single color choice of specific product 6 | */ 7 | export const stateColor = atomFamily< 8 | string /* color */, 9 | IRecoilId /* recoil family key */ 10 | >({ 11 | key: 'state-color', 12 | default: 'black', 13 | }); 14 | -------------------------------------------------------------------------------- /states/tracking/state-tracking.ts: -------------------------------------------------------------------------------- 1 | import { atomFamily } from 'recoil'; 2 | import { JourneyType } from '../../interfaces'; 3 | 4 | /** 5 | * Trace all active ids under specific journey type 6 | */ 7 | export const stateTracking = atomFamily< 8 | string[] /* ids */, 9 | JourneyType /* journey type */ 10 | >({ 11 | key: 'state-product-tracking', 12 | default: [], 13 | }); 14 | -------------------------------------------------------------------------------- /interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export type { IHeader } from './IHeader'; 2 | export type { IOrder } from './IOrder'; 3 | export type { IProductRef } from './IProductRef'; 4 | export type { IRecoilId } from './IRecoilId'; 5 | export type { IReview } from './IReview'; 6 | export type { IWithClassName } from './IWithClassName'; 7 | export { JourneyType } from './JourneyType'; 8 | export { Size } from './Size'; 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .next 3 | 4 | /**/*.log 5 | /**/*.psd 6 | /**/*.icloud 7 | 8 | .idea/* 9 | /.idea/ 10 | 11 | # testing 12 | /coverage 13 | 14 | # next.js 15 | /.next/ 16 | /out/ 17 | /storybook-static/ 18 | .env*.local 19 | 20 | # misc 21 | .DS_Store 22 | #.env* 23 | 24 | # debug 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | /.vs 29 | 30 | 31 | package-lock.json 32 | -------------------------------------------------------------------------------- /interfaces/IRecoilId.ts: -------------------------------------------------------------------------------- 1 | import { SerializableParam } from 'recoil'; 2 | import { JourneyType } from '.'; 3 | 4 | /** 5 | * Recoil key (used within Recoil's atomFamily or selectorFamily) 6 | * 7 | * @description Readonly> is needed to satisfy Recoil family's key 8 | */ 9 | export interface IRecoilId extends Readonly> { 10 | id: string; 11 | journey: JourneyType; 12 | } 13 | -------------------------------------------------------------------------------- /guards/guard-string.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ 3 | /** 4 | * String gard 5 | * @param candidate 6 | * @return true is candidate is a string false otherwise 7 | * @example if(guardString(prm)) 8 | */ 9 | export const guardString = (candidate: any): candidate is string => { 10 | return typeof candidate === 'string' || candidate instanceof String; 11 | }; 12 | -------------------------------------------------------------------------------- /proxies/fetchProductsMock.ts: -------------------------------------------------------------------------------- 1 | import { IProduct } from './IProduct'; 2 | 3 | export const fetchProductsMock = (): IProduct[] => { 4 | const products: IProduct[] = [ 5 | { 6 | id: '1', 7 | title: 'bike', 8 | }, 9 | { 10 | id: '2', 11 | title: 'phone', 12 | }, 13 | { 14 | id: '3', 15 | title: 'donkey', 16 | }, 17 | { 18 | id: '4', 19 | title: 'secret wand', 20 | }, 21 | ]; 22 | 23 | return products; 24 | }; 25 | -------------------------------------------------------------------------------- /components/ui-units/ColorPicker/ColorPicker.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { ColorPickerRaw } from './ColorPickerRaw'; 3 | 4 | export const ColorPicker = styled(ColorPickerRaw)` 5 | display: grid; 6 | justify-content: start; 7 | align-content: center; 8 | grid-template-areas: 'title picker'; 9 | grid-template-columns: 15rem auto; 10 | 11 | .color-title { 12 | grid-area: title; 13 | } 14 | 15 | .picker { 16 | grid-area: picker; 17 | align-self: start; 18 | } 19 | `; 20 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { AppProps } from 'next/dist/next-server/lib/router/router'; 2 | import React from 'react'; 3 | import { RecoilRoot } from 'recoil'; 4 | import GlobalStyle from '../components/GlobalStyle'; 5 | import { DebugObserver } from '../debug'; 6 | 7 | const MyApp = ({ Component, pageProps }: AppProps) => { 8 | return ( 9 | 10 | 11 | 12 | 13 | 14 | ); 15 | }; 16 | 17 | export default MyApp; 18 | -------------------------------------------------------------------------------- /debug/DebugObserver.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { useRecoilSnapshot } from 'recoil'; 3 | 4 | export const DebugObserver = () => { 5 | const snapshot = useRecoilSnapshot(); 6 | useEffect(() => { 7 | const changes = [...snapshot.getNodes_UNSTABLE({ isModified: true })]; 8 | changes.forEach((node) => 9 | // eslint-disable-next-line no-console 10 | console.log(`@ RECOIL: [${node.key}]`, snapshot.getLoadable(node)) 11 | ); 12 | }, [snapshot]); 13 | 14 | return <>; 15 | }; 16 | -------------------------------------------------------------------------------- /components/order/OrderScreens.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useRoutingInfo } from '../../routing'; 3 | import { Product } from '../Product'; 4 | import { Continue } from './Continue'; 5 | import { Details } from './Details'; 6 | 7 | export const OrderScreens = () => { 8 | const { stageKey } = useRoutingInfo(); 9 | return ( 10 | <> 11 | {stageKey === undefined && } 12 | {stageKey === 'select' && } 13 | {stageKey === 'details' &&
} 14 | {stageKey === 'continue' && } 15 | 16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /components/ui-units/StarsPicker/StarsPickerRaw.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactStars from 'react-star-rating-component'; 3 | import { useRecoilState } from 'recoil'; 4 | import { IStarsPickerProps } from './IStarsPickerProps'; 5 | 6 | export const StarsPickerRaw = ({ className, state }: IStarsPickerProps) => { 7 | const [value, setValue] = useRecoilState(state); 8 | 9 | return ( 10 |
11 | setValue(r)} 15 | /> 16 |
17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /routing/useRoutingInfo.ts: -------------------------------------------------------------------------------- 1 | import { useRouter } from 'next/router'; 2 | import { guardString } from '../guards'; 3 | import { JourneyType } from '../interfaces'; 4 | import { IRoutingInfo } from './IRoutingInfo'; 5 | 6 | export const useRoutingInfo = (): IRoutingInfo => { 7 | const router = useRouter(); 8 | 9 | const { id, stageKey } = router.query; 10 | const currentPath = router.pathname; 11 | const journey = currentPath.substring(currentPath.lastIndexOf('/') + 1); 12 | 13 | return { 14 | journey: journey as JourneyType, 15 | id: guardString(id) ? id : '', 16 | stageKey: guardString(stageKey) ? stageKey : undefined, 17 | }; 18 | }; 19 | -------------------------------------------------------------------------------- /components/review/ReviewScreens.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useRoutingInfo } from '../../routing'; 3 | import { Product } from '../Product'; 4 | import { Continue } from './Continue'; 5 | import { Details } from './Details'; 6 | import { Reviewer } from './Reviewer'; 7 | 8 | export const ReviewScreens = () => { 9 | const { stageKey } = useRoutingInfo(); 10 | return ( 11 | <> 12 | {stageKey === undefined && } 13 | {stageKey === 'select' && } 14 | {stageKey === 'reviewer' && } 15 | {stageKey === 'details' &&
} 16 | {stageKey === 'continue' && } 17 | 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /components/ui-units/ColorPicker/ColorPickerRaw.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { SketchPicker } from 'react-color'; 3 | import { useRecoilState } from 'recoil'; 4 | import { IColorPickerProps } from './IColorPickerProps'; 5 | 6 | export const ColorPickerRaw = ({ className, state }: IColorPickerProps) => { 7 | const [value, setValue] = useRecoilState(state); 8 | 9 | return ( 10 |
11 |

Select color:

12 | setValue(color.hex)} 16 | /> 17 |
18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /components/order/Continue/Continue.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { ContinueRaw } from './ContinueRaw'; 3 | 4 | export const Continue = styled(ContinueRaw)` 5 | display: grid; 6 | justify-content: center; 7 | align-content: center; 8 | min-height: 100vh; 9 | grid-auto-rows: max-content; 10 | grid-row-gap: 0.3rem; 11 | width: 100rem; 12 | 13 | .entry { 14 | display: grid; 15 | grid-auto-flow: column; 16 | } 17 | 18 | .action { 19 | font-size: 1.5rem; 20 | padding: 0.2rem 1rem; 21 | margin-bottom: 1rem; 22 | border: solid 0.2rem; 23 | border-radius: 1rem; 24 | cursor: pointer; 25 | justify-self: start; 26 | } 27 | `; 28 | -------------------------------------------------------------------------------- /components/review/Continue/Continue.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { ContinueRaw } from './ContinueRaw'; 3 | 4 | export const Continue = styled(ContinueRaw)` 5 | display: grid; 6 | justify-content: center; 7 | align-content: center; 8 | min-height: 100vh; 9 | grid-auto-rows: max-content; 10 | grid-row-gap: 0.3rem; 11 | width: 100rem; 12 | 13 | .entry { 14 | display: grid; 15 | grid-auto-flow: column; 16 | } 17 | 18 | .action { 19 | font-size: 1.5rem; 20 | padding: 0.2rem 1rem; 21 | margin-bottom: 1rem; 22 | border: solid 0.2rem; 23 | border-radius: 1rem; 24 | cursor: pointer; 25 | justify-self: start; 26 | } 27 | `; 28 | -------------------------------------------------------------------------------- /components/order/Details/Details.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { DetailsRaw } from './DetailsRaw'; 3 | 4 | export const Details = styled(DetailsRaw)` 5 | display: grid; 6 | justify-content: center; 7 | align-content: center; 8 | min-height: 100vh; 9 | grid-auto-rows: max-content; 10 | grid-row-gap: 1.5rem; 11 | width: 100rem; 12 | 13 | .count { 14 | display: grid; 15 | grid-auto-flow: column; 16 | } 17 | 18 | .count-input { 19 | font-size: 1.5rem; 20 | } 21 | 22 | .next { 23 | font-size: 1.5rem; 24 | padding: 0.2rem 1rem; 25 | border: solid 0.2rem; 26 | border-radius: 1rem; 27 | cursor: pointer; 28 | justify-self: start; 29 | } 30 | `; 31 | -------------------------------------------------------------------------------- /components/review/Details/Details.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { DetailsRaw } from './DetailsRaw'; 3 | 4 | export const Details = styled(DetailsRaw)` 5 | display: grid; 6 | justify-content: center; 7 | align-content: center; 8 | min-height: 100vh; 9 | grid-auto-rows: max-content; 10 | grid-row-gap: 1.5rem; 11 | width: 100rem; 12 | 13 | .comment { 14 | display: grid; 15 | grid-auto-flow: column; 16 | } 17 | 18 | .comment-input { 19 | font-size: 1.5rem; 20 | } 21 | 22 | .next { 23 | font-size: 1.5rem; 24 | padding: 0.2rem 1rem; 25 | border: solid 0.2rem; 26 | border-radius: 1rem; 27 | cursor: pointer; 28 | justify-self: start; 29 | } 30 | `; 31 | -------------------------------------------------------------------------------- /components/review/Reviewer/Reviewer.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { ReviewerRaw } from './ReviewerRaw'; 3 | 4 | export const Reviewer = styled(ReviewerRaw)` 5 | display: grid; 6 | justify-content: center; 7 | align-content: center; 8 | min-height: 100vh; 9 | grid-auto-rows: max-content; 10 | grid-row-gap: 1.5rem; 11 | width: 100rem; 12 | 13 | .reviewer { 14 | display: grid; 15 | grid-auto-flow: column; 16 | } 17 | 18 | .reviewer-input { 19 | font-size: 1.5rem; 20 | } 21 | 22 | .next { 23 | font-size: 1.5rem; 24 | padding: 0.2rem 1rem; 25 | border: solid 0.2rem; 26 | border-radius: 1rem; 27 | cursor: pointer; 28 | justify-self: start; 29 | } 30 | `; 31 | -------------------------------------------------------------------------------- /routing/useFlowRouter.ts: -------------------------------------------------------------------------------- 1 | import { useRouter } from 'next/router'; 2 | import { v4 as uuidv4 } from 'uuid'; 3 | import { IFlowRouterProps } from './IFlowRouterProps'; 4 | 5 | export const useFlowRouter = (): IFlowRouterProps => { 6 | const router = useRouter(); 7 | 8 | /** 9 | * customize push 10 | */ 11 | const pushStage = async (stageKey: string, isNew?: boolean) => { 12 | const { id } = router.query; 13 | 14 | let next = `${router.pathname}?stageKey=${stageKey}`; 15 | if (isNew) next = `${next}&id=${uuidv4()}`; 16 | else if (id) next = `${next}&id=${id}`; 17 | 18 | const result = await router.push(next); 19 | 20 | return result; 21 | }; 22 | 23 | return { 24 | ...router, 25 | pushStage, 26 | }; 27 | }; 28 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "alwaysStrict": true, 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "isolatedModules": true, 8 | "jsx": "preserve", 9 | "lib": ["dom", "dom.iterable", "esnext"], 10 | "module": "esnext", 11 | "moduleResolution": "node", 12 | "noEmit": true, 13 | "noFallthroughCasesInSwitch": true, 14 | "noUnusedLocals": true, 15 | "noUnusedParameters": true, 16 | "resolveJsonModule": true, 17 | "skipLibCheck": true, 18 | "strict": true, 19 | "target": "esnext", 20 | "typeRoots": ["./typings", "./node_modules/@types/"] 21 | }, 22 | "exclude": ["node_modules", "typings", "out", "server.js"], 23 | "include": ["**/*.js", "**/*.ts", "**/*.tsx"] 24 | } 25 | -------------------------------------------------------------------------------- /components/ui-units/SizePicker/SizePickerRaw.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useRecoilState } from 'recoil'; 3 | import { Size } from '../../../interfaces'; 4 | import { ISizePickerProps } from './ISizePickerProps'; 5 | 6 | export const SizePickerRaw = ({ className, state }: ISizePickerProps) => { 7 | const [value, setValue] = useRecoilState(state); 8 | 9 | return ( 10 |
11 |

Select size:

12 | 13 |
14 | {Object.keys(Size).map((m) => ( 15 |
setValue(m as Size)} 19 | > 20 | {m} 21 |
22 | ))} 23 |
24 |
25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /components/ui-units/SizePicker/SizePicker.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { SizePickerRaw } from './SizePickerRaw'; 3 | 4 | export const SizePicker = styled(SizePickerRaw)` 5 | display: grid; 6 | justify-content: start; 7 | align-content: center; 8 | grid-template-areas: 'title options'; 9 | grid-template-columns: 15rem 1fr; 10 | 11 | .item { 12 | /* font-size: 3rem; */ 13 | padding: 1rem; 14 | border: solid 0.1rem; 15 | border-radius: 1rem; 16 | cursor: pointer; 17 | } 18 | 19 | .selected { 20 | font-weight: bold; 21 | border: solid 0.2rem; 22 | } 23 | 24 | .title { 25 | grid-area: title; 26 | } 27 | 28 | .options { 29 | grid-area: options; 30 | display: grid; 31 | grid-auto-flow: column; 32 | grid-column-gap: 1rem; 33 | justify-self: stretch; 34 | } 35 | `; 36 | -------------------------------------------------------------------------------- /components/JourneySwitch/JourneySwitch.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { JourneyTypeRaw } from './JourneySwitchRaw'; 3 | 4 | export const JourneySwitch = styled(JourneyTypeRaw)` 5 | display: grid; 6 | grid-template-areas: 'order review'; 7 | justify-content: center; 8 | align-content: center; 9 | grid-column-gap: 4rem; 10 | height: 100vh; 11 | 12 | .order, 13 | .review { 14 | display: grid; 15 | grid-row-gap: 1rem; 16 | grid-auto-flow: row; 17 | } 18 | 19 | .order { 20 | grid-area: order; 21 | } 22 | 23 | .review { 24 | grid-area: review; 25 | } 26 | 27 | .item { 28 | font-size: 2rem; 29 | } 30 | 31 | .btn { 32 | font-size: 3rem; 33 | padding: 1rem; 34 | border: solid 0.2rem; 35 | border-radius: 1rem; 36 | cursor: pointer; 37 | align-self: start; 38 | } 39 | `; 40 | -------------------------------------------------------------------------------- /components/Product/Product.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { ProductRaw } from './ProductRaw'; 3 | 4 | export const Product = styled(ProductRaw)` 5 | display: grid; 6 | justify-content: center; 7 | align-content: center; 8 | grid-row-gap: 1rem; 9 | 10 | .options { 11 | display: grid; 12 | grid-auto-rows: auto; 13 | grid-row-gap: 1rem; 14 | justify-content: start; 15 | 16 | .prod { 17 | font-size: 1.5rem; 18 | cursor: pointer; 19 | 20 | &.selected { 21 | font-weight: bold; 22 | color: blue; 23 | border: solid 0.1rem; 24 | padding: 0.1rem; 25 | } 26 | } 27 | } 28 | 29 | .next { 30 | font-size: 1.5rem; 31 | padding: 0.2rem 1rem; 32 | border: solid 0.2rem; 33 | border-radius: 1rem; 34 | cursor: pointer; 35 | justify-self: start; 36 | 37 | &.disable { 38 | background: #777; 39 | } 40 | } 41 | `; 42 | -------------------------------------------------------------------------------- /components/review/Reviewer/ReviewerRaw.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useRecoilState } from 'recoil'; 3 | import { IRecoilId, IWithClassName } from '../../../interfaces'; 4 | import { useFlowRouter, useRoutingInfo } from '../../../routing'; 5 | import { stateReviewer } from '../../../states'; 6 | 7 | export const ReviewerRaw = ({ className }: IWithClassName) => { 8 | const router = useFlowRouter(); 9 | const { id, journey } = useRoutingInfo(); 10 | const key: IRecoilId = { 11 | id, 12 | journey, 13 | }; 14 | const [reviewer, setReviewer] = useRecoilState(stateReviewer(key)); 15 | 16 | return ( 17 |
18 |
19 |

Reviewer:

20 | setReviewer(e.target.value)} 25 | /> 26 |
27 |
router.pushStage('details')}> 28 | Next 29 |
30 |
31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /components/order/Continue/ContinueRaw.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useRecoilValue } from 'recoil'; 3 | import { IRecoilId, IWithClassName } from '../../../interfaces'; 4 | import { useFlowRouter, useRoutingInfo } from '../../../routing'; 5 | import { stateOrder } from '../../../states/composition/state-order'; 6 | 7 | export const ContinueRaw = ({ className }: IWithClassName) => { 8 | const router = useFlowRouter(); 9 | const { id, journey } = useRoutingInfo(); 10 | const key: IRecoilId = { 11 | id, 12 | journey, 13 | }; 14 | const { count, size, color, productId } = useRecoilValue(stateOrder(key)); 15 | 16 | return ( 17 |
18 |

Continue

19 |
20 |

Product Id: {productId}

21 |
22 |
23 |

Color: {color}

24 |
25 |
26 |

Size: {size}

27 |
28 |
29 |

Count: {count}

30 |
31 |
router.pushStage('select', true)}> 32 | Add 33 |
34 |
router.push('/')}> 35 | Exit 36 |
37 |
38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /components/review/Continue/ContinueRaw.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Rating from 'react-star-rating-component'; 3 | import { useRecoilValue } from 'recoil'; 4 | import { IRecoilId, IWithClassName } from '../../../interfaces'; 5 | import { useFlowRouter, useRoutingInfo } from '../../../routing'; 6 | import { stateReview } from '../../../states/composition/state-review'; 7 | 8 | export const ContinueRaw = ({ className }: IWithClassName) => { 9 | const router = useFlowRouter(); 10 | const { id, journey } = useRoutingInfo(); 11 | const key: IRecoilId = { 12 | id, 13 | journey, 14 | }; 15 | const { reviewer, stars, comment, productId } = useRecoilValue( 16 | stateReview(key) 17 | ); 18 | 19 | return ( 20 |
21 |

Continue

22 |
23 |

Product Id: {productId}

24 |
25 |
26 |

Reviewer: {reviewer}

27 |
28 | 29 |

{comment}

30 |
router.pushStage('select', true)}> 31 | Review other product 32 |
33 |
router.push('/')}> 34 | Exit 35 |
36 |
37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /components/review/Details/DetailsRaw.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useRecoilState } from 'recoil'; 3 | import { IRecoilId, IWithClassName } from '../../../interfaces'; 4 | import { useFlowRouter, useRoutingInfo } from '../../../routing'; 5 | import { stateComment, stateStars } from '../../../states'; 6 | import { StarsPicker } from '../../ui-units'; 7 | 8 | export const DetailsRaw = ({ className }: IWithClassName) => { 9 | const router = useFlowRouter(); 10 | const { id, journey } = useRoutingInfo(); 11 | const key: IRecoilId = { 12 | id, 13 | journey, 14 | }; 15 | // best practice (encapsulate state within component will result with less rendering) 16 | const starState = stateStars(key); 17 | // bad practice (keeping the state at the global level will result in unnecessary rendering) 18 | const [comment, setComment] = useRecoilState(stateComment(key)); 19 | 20 | return ( 21 |
22 |

Details

23 | 24 |
25 |

Comment:

26 | setComment(e.target.value)} 31 | /> 32 |
33 |
router.pushStage('continue')}> 34 | Next 35 |
36 |
37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /components/JourneySwitch/JourneySwitchRaw.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import { useRouter } from 'next/router'; 3 | import React from 'react'; 4 | import { useRecoilValue } from 'recoil'; 5 | import { v4 as uuidv4 } from 'uuid'; 6 | import { IHeader, IWithClassName, JourneyType } from '../../interfaces'; 7 | import { stateHeaders } from '../../states'; 8 | 9 | export const JourneyTypeRaw = ({ className }: IWithClassName) => { 10 | const router = useRouter(); 11 | const orders = useRecoilValue(stateHeaders(JourneyType.order)); 12 | const reviews = useRecoilValue(stateHeaders(JourneyType.review)); 13 | 14 | return ( 15 |
16 |
17 |
router.push(`/order?id=${uuidv4()}`)} 20 | > 21 | Order 22 |
23 | {orders.map((m) => ( 24 |
25 | {m.product} 26 |
27 | ))} 28 |
29 |
30 |
router.push(`/review?id=${uuidv4()}`)} 33 | > 34 | Review 35 |
36 | {reviews.map((m) => ( 37 |
38 | {m.product} 39 |
40 | ))} 41 |
42 |
43 | ); 44 | }; 45 | -------------------------------------------------------------------------------- /states/composition/state-headers.ts: -------------------------------------------------------------------------------- 1 | import { selectorFamily, waitForAll } from 'recoil'; 2 | import { stateCachedProducts, stateProductId } from '..'; 3 | import { IHeader, IRecoilId, JourneyType } from '../../interfaces'; 4 | import { stateTracking } from '../tracking'; 5 | 6 | /** 7 | * Abstract retrieval of orders headers 8 | * 9 | * @description encapsulation of multiple disconnected state into single meaningful entity 10 | */ 11 | export const stateHeaders = selectorFamily< 12 | IHeader[], 13 | JourneyType /* recoil family key */ 14 | >({ 15 | key: 'state-headers', 16 | get: (journey) => ({ get }) => { 17 | const { tracking, products } = get( 18 | waitForAll({ 19 | tracking: stateTracking(journey), 20 | products: stateCachedProducts, 21 | }) 22 | ); 23 | 24 | const pIds = get( 25 | waitForAll( 26 | tracking.map((id) => { 27 | const key: IRecoilId = { journey, id }; 28 | // get the product it out of the atomic state 29 | const pIdState = stateProductId(key); 30 | return pIdState; 31 | }) 32 | ) 33 | ); 34 | 35 | // create index of product id => order / review id 36 | const hash = new Map(tracking.map((m, i) => [pIds[i], m])); 37 | 38 | const headers: IHeader[] = products 39 | .filter((m) => hash.has(m.id)) 40 | .map((m) => { 41 | return { id: hash.get(m.id) ?? '', product: m.title }; 42 | }); 43 | 44 | return headers; 45 | }, 46 | }); 47 | -------------------------------------------------------------------------------- /components/order/Details/DetailsRaw.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useRecoilState } from 'recoil'; 3 | import { ColorPicker, SizePicker } from '../..'; 4 | import { IRecoilId, IWithClassName } from '../../../interfaces'; 5 | import { useFlowRouter, useRoutingInfo } from '../../../routing'; 6 | import { stateColor, stateCount, stateSize } from '../../../states'; 7 | 8 | export const DetailsRaw = ({ className }: IWithClassName) => { 9 | const router = useFlowRouter(); 10 | const { id, journey } = useRoutingInfo(); 11 | const key: IRecoilId = { 12 | id, 13 | journey, 14 | }; 15 | // best practice (encapsulate state within component will result with less rendering) 16 | const colorState = stateColor(key); 17 | const sizeState = stateSize(key); 18 | // bad practice (keeping the state at the global level will result in unnecessary rendering) 19 | const [count, setCount] = useRecoilState(stateCount(key)); 20 | 21 | return ( 22 |
23 |

Details

24 | 25 | 26 |
27 |

Count:

28 | setCount(e.target.valueAsNumber)} 33 | /> 34 |
35 |
router.pushStage('continue')}> 36 | Next 37 |
38 |
39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { AppType } from 'next/dist/next-server/lib/utils'; 2 | import Document, { 3 | DocumentContext, 4 | Head, 5 | Main, 6 | NextScript, 7 | } from 'next/document'; 8 | import React from 'react'; 9 | import { ServerStyleSheet } from 'styled-components'; 10 | 11 | class MyDocument extends Document { 12 | static async getInitialProps(ctx: DocumentContext) { 13 | const sheet = new ServerStyleSheet(); 14 | const originalRenderPage = ctx.renderPage; 15 | 16 | try { 17 | ctx.renderPage = () => 18 | originalRenderPage({ 19 | enhanceApp: (App: AppType) => (props) => 20 | sheet.collectStyles(), 21 | }); 22 | 23 | const initialProps = await Document.getInitialProps(ctx); 24 | return { 25 | ...initialProps, 26 | styles: ( 27 | <> 28 | {initialProps.styles} 29 | {sheet.getStyleElement()} 30 | 31 | ), 32 | }; 33 | } finally { 34 | sheet.seal(); 35 | } 36 | } 37 | 38 | render() { 39 | return ( 40 | <> 41 | 42 | 46 | 47 | 52 | 56 |
57 | 58 | 59 | ); 60 | } 61 | } 62 | 63 | export default MyDocument; 64 | -------------------------------------------------------------------------------- /components/Product/ProductRaw.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; 3 | import { IRecoilId, IWithClassName } from '../../interfaces'; 4 | import { IProduct } from '../../proxies/IProduct'; 5 | import { useFlowRouter, useRoutingInfo } from '../../routing'; 6 | import { 7 | stateCachedProducts, 8 | stateProductId, 9 | stateTracking, 10 | } from '../../states'; 11 | 12 | export const ProductRaw = ({ className }: IWithClassName) => { 13 | const router = useFlowRouter(); 14 | const { id, journey } = useRoutingInfo(); 15 | const key: IRecoilId = { 16 | id, 17 | journey, 18 | }; 19 | const [productId, setProductId] = useRecoilState(stateProductId(key)); 20 | const setTracking = useSetRecoilState(stateTracking(journey)); 21 | const products = useRecoilValue(stateCachedProducts); 22 | 23 | return ( 24 |
25 |

Select Product

26 |
27 | {products.map((p) => ( 28 |
{ 32 | setProductId(p.id); 33 | }} 34 | > 35 | {p.title} 36 |
37 | ))} 38 |
39 |
{ 42 | if (productId === '') return; 43 | setTracking((prev) => (prev.includes(id) ? prev : [id, ...prev])); 44 | 45 | router.pushStage('details'); 46 | }} 47 | > 48 | Next 49 |
50 |
51 | ); 52 | }; 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TypeScript Next.js example 2 | 3 | This is a really simple project that shows the usage of Next.js with TypeScript. 4 | 5 | ## Deploy your own 6 | 7 | Deploy the example using [Vercel](https://vercel.com): 8 | 9 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/import/project?template=https://github.com/vercel/next.js/tree/canary/examples/with-typescript) 10 | 11 | ## How to use it? 12 | 13 | Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init) or [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/) to bootstrap the example: 14 | 15 | ```bash 16 | npx create-next-app --example with-typescript with-typescript-app 17 | # or 18 | yarn create next-app --example with-typescript with-typescript-app 19 | ``` 20 | 21 | Deploy it to the cloud with [Vercel](https://vercel.com/import?filter=next.js&utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/deployment)). 22 | 23 | ## Notes 24 | 25 | This example shows how to integrate the TypeScript type system into Next.js. Since TypeScript is supported out of the box with Next.js, all we have to do is to install TypeScript. 26 | 27 | ``` 28 | npm install --save-dev typescript 29 | ``` 30 | 31 | To enable TypeScript's features, we install the type declarations for React and Node. 32 | 33 | ``` 34 | npm install --save-dev @types/react @types/react-dom @types/node 35 | ``` 36 | 37 | When we run `next dev` the next time, Next.js will start looking for any `.ts` or `.tsx` files in our project and builds it. It even automatically creates a `tsconfig.json` file for our project with the recommended settings. 38 | 39 | Next.js has built-in TypeScript declarations, so we'll get autocompletion for Next.js' modules straight away. 40 | 41 | A `type-check` script is also added to `package.json`, which runs TypeScript's `tsc` CLI in `noEmit` mode to run type-checking separately. You can then include this, for example, in your `test` scripts. 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "weknow", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "dev": "next dev", 6 | "build": "next build", 7 | "start": "next build && next start", 8 | "type-check": "tsc", 9 | "export": "next build && next export && serve out -l 3001", 10 | "publish": "next export", 11 | "lint": "run-s lint:tsx lint:css", 12 | "lint:tsx": "eslint . --ext .ts,.tsx --fix --quiet", 13 | "lint:css": "stylelint **/*.tsx", 14 | "serve": "serve out -l 3001" 15 | }, 16 | "dependencies": { 17 | "next": "10.0.4", 18 | "react": "^17.0.1", 19 | "react-color": "^2.19.3", 20 | "react-dom": "^17.0.1", 21 | "react-rating-stars-component": "^2.2.0", 22 | "react-star-rating-component": "^1.4.1", 23 | "recoil": "^0.1.2", 24 | "styled-components": "^5.2.1", 25 | "uuid": "^8.3.2" 26 | }, 27 | "devDependencies": { 28 | "@babel/core": "^7.12.10", 29 | "@types/node": "^14.14.16", 30 | "@types/react": "^17.0.0", 31 | "@types/react-color": "^3.0.4", 32 | "@types/react-dom": "^17.0.0", 33 | "@types/react-star-rating-component": "^1.4.0", 34 | "@types/styled-components": "^5.1.7", 35 | "@types/uuid": "^8.3.0", 36 | "@typescript-eslint/eslint-plugin": "^4.11.1", 37 | "@typescript-eslint/parser": "^4.11.1", 38 | "babel-loader": "^8.2.2", 39 | "eslint": "^7.16.0", 40 | "eslint-config-airbnb": "^18.2.1", 41 | "eslint-config-prettier": "^7.1.0", 42 | "eslint-import-resolver-typescript": "^2.3.0", 43 | "eslint-plugin-import": "^2.22.1", 44 | "eslint-plugin-json": "^2.1.2", 45 | "eslint-plugin-jsx-a11y": "^6.4.1", 46 | "eslint-plugin-node": "^11.1.0", 47 | "eslint-plugin-prefer-arrow": "^1.2.2", 48 | "eslint-plugin-prettier": "^3.3.0", 49 | "eslint-plugin-react": "^7.22.0", 50 | "next-build-id": "^3.0.0", 51 | "npm-run-all": "^4.1.5", 52 | "prettier": "^2.2.1", 53 | "stylelint": "^13.8.0", 54 | "stylelint-config-recommended": "^3.0.0", 55 | "stylelint-config-styled-components": "^0.1.1", 56 | "stylelint-processor-styled-components": "^1.10.0", 57 | "typescript": "^4.1.3" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /states/composition/state-order.ts: -------------------------------------------------------------------------------- 1 | import { selectorFamily, waitForAll } from 'recoil'; 2 | import { stateColor, stateCount, stateProductId, stateSize } from '..'; 3 | import { guardRecoilDefaultValue } from '../../guards'; 4 | import { IOrder, IRecoilId } from '../../interfaces'; 5 | import { stateTracking } from '../tracking/state-tracking'; 6 | 7 | /** 8 | * Abstract access to structured object of a product's order. 9 | * Useful for load / save 10 | * 11 | * @description encapsulation of multiple disconnected state into single meaningful entity 12 | */ 13 | export const stateOrder = selectorFamily< 14 | IOrder, 15 | IRecoilId /* recoil family key */ 16 | >({ 17 | key: 'state-order', 18 | get: (familyKey) => ({ get }) => { 19 | const { color, size, productId, count } = get( 20 | waitForAll({ 21 | productId: stateProductId(familyKey), 22 | color: stateColor(familyKey), 23 | size: stateSize(familyKey), 24 | count: stateCount(familyKey), 25 | }) 26 | ); 27 | const product: IOrder = { 28 | id: familyKey.id, 29 | count, 30 | size, 31 | color, 32 | productId, 33 | }; 34 | return product; 35 | }, 36 | set: (familyKey) => ({ set, reset }, value) => { 37 | const { journey, id } = familyKey; 38 | 39 | // reset (when recoil's value is empty) 40 | if (guardRecoilDefaultValue(value)) { 41 | reset(stateProductId(familyKey)); 42 | reset(stateColor(familyKey)); 43 | reset(stateSize(familyKey)); 44 | reset(stateCount(familyKey)); 45 | 46 | // remove from tracking 47 | set(stateTracking(journey), (prv) => [...prv.filter((m) => m !== id)]); 48 | 49 | return; 50 | } 51 | // set 52 | set(stateProductId(familyKey), value.productId); 53 | set(stateColor(familyKey), value.color); 54 | set(stateSize(familyKey), value.size); 55 | set(stateCount(familyKey), value.count); 56 | 57 | // track 58 | set(stateTracking(journey), (prv) => { 59 | // already exists 60 | if (prv.includes(id)) return prv; 61 | // add tracking 62 | return [...prv, id]; 63 | }); 64 | }, 65 | }); 66 | -------------------------------------------------------------------------------- /states/composition/state-review.ts: -------------------------------------------------------------------------------- 1 | import { selectorFamily, waitForAll } from 'recoil'; 2 | import { stateComment, stateProductId, stateReviewer, stateStars } from '..'; 3 | import { guardRecoilDefaultValue } from '../../guards'; 4 | import { IRecoilId, IReview } from '../../interfaces'; 5 | import { stateTracking } from '../tracking/state-tracking'; 6 | 7 | /** 8 | * Abstract access to structured object of a product's review. 9 | * Useful for load / save 10 | * 11 | * @description encapsulation of multiple disconnected state into single meaningful entity 12 | */ 13 | export const stateReview = selectorFamily< 14 | IReview, 15 | IRecoilId /* recoil family key */ 16 | >({ 17 | key: 'state-review', 18 | get: (familyKey) => ({ get }) => { 19 | const { comment, stars, reviewer, productId } = get( 20 | waitForAll({ 21 | comment: stateComment(familyKey), 22 | stars: stateStars(familyKey), 23 | reviewer: stateReviewer(familyKey), 24 | productId: stateProductId(familyKey), 25 | }) 26 | ); 27 | const review: IReview = { 28 | id: familyKey.id, 29 | productId, 30 | comment, 31 | reviewer, 32 | stars, 33 | }; 34 | return review; 35 | }, 36 | set: (familyKey) => ({ set, reset }, value) => { 37 | // reset (when recoil's value is empty) 38 | const { journey, id } = familyKey; 39 | if (guardRecoilDefaultValue(value)) { 40 | reset(stateComment(familyKey)); 41 | reset(stateStars(familyKey)); 42 | reset(stateReviewer(familyKey)); 43 | reset(stateProductId(familyKey)); 44 | 45 | // remove from tracking 46 | set(stateTracking(journey), (prv) => [...prv.filter((m) => m !== id)]); 47 | 48 | return; 49 | } 50 | // set 51 | set(stateComment(familyKey), value.comment); 52 | set(stateStars(familyKey), value.stars); 53 | set(stateReviewer(familyKey), value.reviewer); 54 | set(stateProductId(familyKey), value.productId); 55 | 56 | // track 57 | set(stateTracking(journey), (prv) => { 58 | // already exists 59 | if (prv.includes(id)) return prv; 60 | // add tracking 61 | return [...prv, id]; 62 | }); 63 | }, 64 | }); 65 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true 5 | }, 6 | "globals": { 7 | "Atomics": "readonly", 8 | "SharedArrayBuffer": "readonly" 9 | }, 10 | "parser": "@typescript-eslint/parser", 11 | "parserOptions": { 12 | "ecmaFeatures": { 13 | "jsx": true 14 | }, 15 | "ecmaVersion": 2018, 16 | "sourceType": "module" 17 | }, 18 | "plugins": ["react", "@typescript-eslint", "prettier", "prefer-arrow"], 19 | "extends": [ 20 | "plugin:react/recommended", 21 | "airbnb", 22 | "plugin:prettier/recommended", 23 | "plugin:@typescript-eslint/eslint-recommended", 24 | "plugin:@typescript-eslint/recommended", 25 | "prettier", 26 | "prettier/flowtype", 27 | "prettier/react", 28 | "prettier/standard", 29 | "prettier/@typescript-eslint" 30 | ], 31 | "rules": { 32 | "no-shadow": "off", 33 | "react/require-default-props": "off", 34 | 35 | "prefer-arrow/prefer-arrow-functions": [ 36 | "error", 37 | { 38 | "disallowPrototype": true, 39 | "singleReturnOnly": false, 40 | "classPropertiesAllowed": false 41 | } 42 | ], 43 | "no-unused-vars": [ 44 | "error", 45 | { 46 | "argsIgnorePattern": "^[I\\w+|_]", 47 | "varsIgnorePattern": "^[I\\w+|_]" 48 | } 49 | ], 50 | 51 | "@typescript-eslint/explicit-module-boundary-types": "warn", 52 | "@typescript-eslint/naming-convention": [ 53 | "error", 54 | { 55 | "selector": "interface", 56 | "format": ["PascalCase"], 57 | "custom": { 58 | "regex": "^I[A-Z]", 59 | "match": true 60 | } 61 | } 62 | ], 63 | "react/jsx-filename-extension": [ 64 | 2, 65 | { 66 | "extensions": [".tsx"] 67 | } 68 | ], 69 | "prettier/prettier": "off", 70 | "block-scoped-var": "error", 71 | "eqeqeq": "error", 72 | "no-var": "error", 73 | "prefer-const": "error", 74 | "eol-last": "error", 75 | "no-warning-comments": "off", 76 | "react/jsx-props-no-spreading": "off", 77 | "react/prop-types": "off", 78 | "jsx-a11y/anchor-is-valid": "off", 79 | 80 | "react/no-unused-prop-types": "off", 81 | "@typescript-eslint/no-explicit-any": "error", 82 | "@typescript-eslint/no-non-null-assertion": "off", 83 | "no-use-before-define": [0], 84 | "@typescript-eslint/no-use-before-define": [1], 85 | "@typescript-eslint/explicit-function-return-type": "off", 86 | "@typescript-eslint/camelcase": "off", 87 | "node/no-missing-import": "off", 88 | "node/no-unsupported-features/es-syntax": "off", 89 | "node/no-missing-require": "off", 90 | "node/shebang": "off", 91 | "no-dupe-class-members": "off", 92 | "import/no-unresolved": "off", 93 | "no-irregular-whitespace": "off", 94 | "import/prefer-default-export": "off", 95 | "jsx-a11y/click-events-have-key-events": "off", 96 | "jsx-a11y/no-static-element-interactions": "off", 97 | "react/jsx-one-expression-per-line": "off", 98 | "import/extensions": [ 99 | "error", 100 | "never", 101 | { 102 | "ts": "never", 103 | "tsx": "never", 104 | "json": "always" 105 | } 106 | ] 107 | }, 108 | "overrides": [ 109 | { 110 | "files": ["*.tsx"], 111 | "rules": { 112 | "@typescript-eslint/explicit-module-boundary-types": ["off"] 113 | } 114 | } 115 | ] 116 | } 117 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "workbench.colorCustomizations": { 3 | "tab.unfocusedActiveBorder": "#d1962a", 4 | "quickInput.background": "#56565a", 5 | "editor.selectionBackground": "#941c62", 6 | // "editor.selectionHighlightBackground": "#941c6260", 7 | "editor.wordHighlightBackground": "#660269", 8 | "activeTabBackground": "#ff0000", 9 | "activeTabActiveGroupForeground": "#0000ff", 10 | "titleBar.activeBackground": "#1b181ac3", 11 | "titleBar.activeForeground": "#fff6ffc3", 12 | "editor.foldBackground": "#1f202066" 13 | }, 14 | "cSpell.words": [ 15 | "AIIA", 16 | "AMTI", 17 | "APAC", 18 | "BLOS", 19 | "Bnaya", 20 | "CBRM", 21 | "COMMINT", 22 | "DTOs", 23 | "Datalink", 24 | "Deutsch", 25 | "Disptch", 26 | "ELINT", 27 | "GMTI", 28 | "Gilroy", 29 | "ISTAR", 30 | "Know’s", 31 | "Nadav", 32 | "OSINT", 33 | "WAMI", 34 | "WAPS", 35 | "Weknow's", 36 | "alon", 37 | "antd", 38 | "autofit", 39 | "baty", 40 | "brainer", 41 | "callout", 42 | "camelcase", 43 | "checkmark", 44 | "chps", 45 | "clickaway", 46 | "clsx", 47 | "containerheight", 48 | "containerwidth", 49 | "cssprop", 50 | "devtools", 51 | "dropzone", 52 | "eqeqeq", 53 | "esnext", 54 | "evdosin", 55 | "flowtype", 56 | "focusable", 57 | "gtag", 58 | "icofont", 59 | "imgfnc", 60 | "inear", 61 | "jsondiffpatch", 62 | "khtml", 63 | "linkedin", 64 | "logrocket", 65 | "middlewares", 66 | "minmax", 67 | "nadav", 68 | "nextjs", 69 | "nowrap", 70 | "ofir", 71 | "pageview", 72 | "persistor", 73 | "plusplus", 74 | "raban", 75 | "readonly", 76 | "roboto", 77 | "scroller", 78 | "sdks", 79 | "splitted", 80 | "stylelint", 81 | "subcomponents", 82 | "subobjectives", 83 | "swal", 84 | "sweetalert", 85 | "tabindex", 86 | "toastify", 87 | "topbar", 88 | "unfetch", 89 | "ungroup", 90 | "uuidv", 91 | "vivus", 92 | "vmin", 93 | "weknow", 94 | "wifi", 95 | "wsize" 96 | ], 97 | "cSpell.ignorePaths": [ 98 | "**/package-lock.json", 99 | "**/node_modules/**", 100 | "**/vscode-extension/**", 101 | "**/.git/**", 102 | ".vscode", 103 | "typings", 104 | "/public/static/de/**", 105 | "/public/static/he/**" 106 | ], 107 | "typescript.referencesCodeLens.enabled": true, 108 | "typescript.implementationsCodeLens.enabled": true, 109 | "typescript.updateImportsOnFileMove.enabled": "always", 110 | "javascript.updateImportsOnFileMove.enabled": "always", 111 | "[typescript]": { 112 | "editor.defaultFormatter": "esbenp.prettier-vscode", 113 | "editor.formatOnSave": true, 114 | "editor.codeActionsOnSave": { 115 | "source.organizeImports": true 116 | } 117 | }, 118 | "[typescriptreact]": { 119 | "editor.defaultFormatter": "esbenp.prettier-vscode", 120 | "editor.formatOnSave": true, 121 | "editor.codeActionsOnSave": { 122 | "source.organizeImports": true 123 | } 124 | }, 125 | "[javascriptreact]": { 126 | "editor.defaultFormatter": "esbenp.prettier-vscode", 127 | "editor.formatOnSave": true 128 | }, 129 | "[javascript]": { 130 | "editor.defaultFormatter": "esbenp.prettier-vscode", 131 | "editor.formatOnSave": true 132 | }, 133 | "[json]": { 134 | "editor.defaultFormatter": "esbenp.prettier-vscode", 135 | "editor.formatOnSave": true 136 | }, 137 | "[jsonc]": { 138 | "editor.defaultFormatter": "esbenp.prettier-vscode", 139 | "editor.formatOnSave": true 140 | }, 141 | "editor.formatOnSave": true, 142 | "debug.javascript.warnOnLongPrediction": false 143 | } 144 | --------------------------------------------------------------------------------