> {
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 | [](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 |
--------------------------------------------------------------------------------