├── .gitignore
├── .prettierignore
├── .prettierrc.js
├── README.md
├── apps
├── movie-magic-api
│ ├── .eslintrc.js
│ ├── babel.config.js
│ ├── package.json
│ ├── src
│ │ ├── createApp.ts
│ │ ├── index.ts
│ │ └── routes
│ │ │ ├── data
│ │ │ ├── plan-members.json
│ │ │ └── top-10-movies.json
│ │ │ ├── index.ts
│ │ │ ├── moviesRouter.ts
│ │ │ └── planMembersRouter.ts
│ └── tsconfig.json
├── movie-magic-nextjs
│ ├── .env.development
│ ├── .eslintrc.json
│ ├── .gitignore
│ ├── next-env.d.ts
│ ├── next.config.js
│ ├── package.json
│ ├── public
│ │ └── favicon.ico
│ ├── src
│ │ ├── components
│ │ │ ├── Header
│ │ │ │ ├── Header.tsx
│ │ │ │ └── index.ts
│ │ │ └── PageLayout
│ │ │ │ ├── PageLayout.tsx
│ │ │ │ └── index.ts
│ │ └── pages
│ │ │ ├── _app.tsx
│ │ │ ├── _document.tsx
│ │ │ ├── index.tsx
│ │ │ └── settings.tsx
│ └── tsconfig.json
├── movie-magic-react
│ ├── .env
│ ├── .eslintrc.js
│ ├── .gitignore
│ ├── cypress.json
│ ├── cypress
│ │ ├── fixtures
│ │ │ ├── example.json
│ │ │ ├── profile.json
│ │ │ └── users.json
│ │ ├── integration
│ │ │ └── home.spec.ts
│ │ ├── plugins
│ │ │ └── index.js
│ │ ├── screenshots
│ │ │ └── All Integration Specs
│ │ │ │ └── .gitkeep
│ │ ├── support
│ │ │ ├── commands.ts
│ │ │ └── index.ts
│ │ └── tsconfig.json
│ ├── index.html
│ ├── package.json
│ ├── public
│ │ └── mockServiceWorker.js
│ ├── src
│ │ ├── App.tsx
│ │ ├── components
│ │ │ ├── Header
│ │ │ │ ├── Header.stories.tsx
│ │ │ │ ├── Header.tsx
│ │ │ │ └── index.ts
│ │ │ ├── PageLayout
│ │ │ │ ├── PageLayout.tsx
│ │ │ │ └── index.ts
│ │ │ └── index.ts
│ │ ├── favicon.ico
│ │ ├── main.tsx
│ │ ├── mocks
│ │ │ ├── browser.ts
│ │ │ ├── constants.ts
│ │ │ ├── handlers.ts
│ │ │ ├── mockMovies.ts
│ │ │ ├── mockPlanMembers.ts
│ │ │ └── server.ts
│ │ ├── pages
│ │ │ ├── HomePage
│ │ │ │ ├── HomePage.tsx
│ │ │ │ ├── MovieListContainer.tsx
│ │ │ │ ├── index.ts
│ │ │ │ └── useMovies.ts
│ │ │ ├── NotFoundPage
│ │ │ │ ├── NotFoundPage.tsx
│ │ │ │ └── index.ts
│ │ │ ├── SettingsPage
│ │ │ │ ├── SettingsContainer.tsx
│ │ │ │ ├── SettingsPage.tsx
│ │ │ │ ├── index.ts
│ │ │ │ └── usePlanMembers.ts
│ │ │ └── index.ts
│ │ └── vite-env.d.ts
│ ├── tsconfig.json
│ ├── tsconfig.node.json
│ └── vite.config.ts
└── movie-magic-remix
│ ├── .env
│ ├── .eslintrc
│ ├── .gitignore
│ ├── app
│ ├── components
│ │ ├── Header
│ │ │ ├── Header.tsx
│ │ │ └── index.ts
│ │ └── index.ts
│ ├── entry.client.tsx
│ ├── entry.server.tsx
│ ├── root.tsx
│ └── routes
│ │ ├── __layout.tsx
│ │ └── __layout
│ │ ├── index.tsx
│ │ └── settings.tsx
│ ├── package.json
│ ├── public
│ ├── favicon.ico
│ └── fonts
│ │ ├── inter-v7-latin-500.woff
│ │ ├── inter-v7-latin-500.woff2
│ │ ├── inter-v7-latin-600.woff
│ │ ├── inter-v7-latin-600.woff2
│ │ ├── inter-v7-latin-regular.woff
│ │ └── inter-v7-latin-regular.woff2
│ ├── remix.config.js
│ ├── remix.env.d.ts
│ ├── styles
│ └── styles.css
│ └── tsconfig.json
├── assets
├── complex-multi-repo.png
├── csr.png
├── home-page.png
├── monorepo.png
├── repo-structure.png
├── settings-page.png
├── simple-multi-repo.png
├── ssr.png
├── teck-stack-options.png
└── teck-stack-selection.png
├── package-lock.json
├── package.json
├── packages
├── eslint-config-custom
│ ├── index.js
│ └── package.json
├── movie-models
│ ├── .eslintrc.js
│ ├── index.ts
│ ├── package.json
│ ├── src
│ │ └── models
│ │ │ ├── Movie.ts
│ │ │ └── PlanMember.ts
│ └── tsconfig.json
├── tsconfig
│ ├── README.md
│ ├── base.json
│ ├── nextjs.json
│ ├── package.json
│ ├── react-library.json
│ ├── remix.json
│ └── vite.json
└── ui-lib
│ ├── .eslintrc.js
│ ├── jest.config.js
│ ├── package.json
│ ├── src
│ ├── components
│ │ ├── Button
│ │ │ ├── Button.stories.tsx
│ │ │ ├── Button.tsx
│ │ │ └── index.ts
│ │ └── MovieList
│ │ │ ├── MovieList.stories.tsx
│ │ │ ├── MovieList.test.tsx
│ │ │ ├── MovieList.tsx
│ │ │ └── index.ts
│ ├── index.ts
│ ├── styles
│ │ ├── Typography.stories.tsx
│ │ └── main.css
│ └── test
│ │ └── test-utils.tsx
│ └── tsconfig.json
├── storybook
├── .babelrc.json
├── .storybook
│ ├── main.ts
│ ├── preview-head.html
│ └── preview.tsx
├── package-lock.json
└── package.json
└── turbo.json
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | node_modules
5 | .pnp
6 | .pnp.js
7 |
8 | # testing
9 | coverage
10 |
11 | # next.js
12 | .next/
13 | out/
14 | build
15 |
16 | # vite
17 | dist
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 | .pnpm-debug.log*
28 |
29 | # local env files
30 | .env.local
31 | .env.development.local
32 | .env.test.local
33 | .env.production.local
34 |
35 | # turbo
36 | .turbo
37 |
38 | # IDEs
39 | .idea
40 |
41 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | package.json
2 | package-lock.json
3 | src/graphql/schema.json
4 | *.snap
5 | build
6 | dist
7 |
--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | singleQuote: true,
3 | proseWrap: 'always',
4 | };
5 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Custom React Stack
2 |
3 | React has a very rich ecosystem. For anything you want to do, there is probably
4 | a library or a framework available for it. That's great, but having too many
5 | options can also be very confusing. Do you want to start out simple or go for
6 | the ultimate because your app demands it? What's the right set of options for
7 | your specific use case?
8 |
9 | This guide will help you build your own custom React stack, explaining key
10 | options and tradeoffs at each step.
11 |
12 | Another advantage of this approach is that you will know exactly what's in your
13 | stack. When that new shiny technology comes along, you will be in a better
14 | position to slot it in.
15 |
16 | ## Our Sample App: Movie Magic
17 |
18 | For the purpose of this discussion, imagine that you want to write a movie
19 | streaming app - _Movie Magic_. The app should present the available movie titles
20 | and help users make a choice. It should also allow them to manage their
21 | subscriptions.
22 |
23 | Here's a very humble beginning, just two pages:
24 |
25 | 1. A Home page showing the list of top 10 movies:
26 |
27 | 
28 |
29 | 2. A Settings page for users to manage their subscription:
30 |
31 | 
32 |
33 | Click [here](https://custom-react-stack.vercel.app/) to test drive the final
34 | application. As you can see, it doesn't do much yet. However, this is good
35 | enough for the purpose of our discussion. Let's start by discussing our
36 | architecture choices.
37 |
38 | ## Tech Stack Options
39 |
40 | The diagram below shows the key items that make up our tech stack, along with
41 | some options. Subsequent sections will discuss the pros & cons of each option.
42 | Note that the diagram is meant to be read bottom up - imagine that we are
43 | building a stack.
44 |
45 | 
46 |
47 | ## Monorepo vs. Multi-repo
48 |
49 | For a really simple app, you can just create a single Git repo and call it a
50 | day. But what if you want to separate out reusable components into a library?
51 | Well, you can create a separate repo for it and put the compiled output in a
52 | binary repository (such as npm or Artifactory). See below. The app can pick up
53 | the library from the binary repository by adding a dependency to it. This is
54 | called a multi-repo set up (two repos in this case).
55 |
56 | 
57 |
58 | Continuing this scenario, what happens when you have multiple applications and
59 | multiple libraries with complex dependencies on each other? Let's say we start
60 | adding a new repo for each application and each library. This is now starting to
61 | look ugly.
62 |
63 | 
64 |
65 | Here are some issues with the multi-repo approach:
66 |
67 | - It is cumbersome to add new repos. You have to set up new tooling, new CI/CD
68 | pipelines, add committers, and the list goes on.
69 | - You start to see duplicate code in your repos because people are reluctant to
70 | put in the effort to create a new repo just to reuse code. It is much easier
71 | to copy code from another repo.
72 | - It is difficult to maintain consistent tooling across repos.
73 |
74 | A solution that works better in such use cases is a monorepo.
75 |
76 | > Monorepo is a single repository containing **multiple distinct projects**,
77 | > with **well-defined relationships**.
78 |
79 | Here's an example of taking the multiple repos in the above diagram and
80 | replacing them with a single repo:
81 |
82 | 
83 |
84 | Here are the advantages of a monorepo:
85 |
86 | - No overhead to create new projects
87 | - Easy to refactor code for reuse
88 | - Consistent way of building every project
89 | - Bug fixes are available immediately to all dependent projects and can be
90 | tested exhaustively before committing
91 | - Developers can confidently contribute to any project
92 |
93 | You can build a monorepo manually by putting all your projects into one repo and
94 | figuring out a way to organize and build them. However, people generally use an
95 | off-the-shelf monorepo platform such as Lerna, Yarn Workspaces, npm Workspaces,
96 | Turborepo or Nx.
97 |
98 | Turborepo and Nx provide some advanced features:
99 |
100 | - Caching of build artifacts for faster builds
101 | - Parallelize builds based on dependencies
102 | - Build only the affected projects
103 | - Visualize dependency graph (this helps in avoiding cyclic dependencies)
104 |
105 | Turborepo is my favorite because it is less opinionated - it builds on top of
106 | npm or Yarn Workspaces. This approach allows me to integrate existing templates
107 | more easily into my monorepo. Each project has its own `package.json` file,
108 | making it easy to track why a particular dependency was added.
109 |
110 | Nx, on the other hand, keeps the dependencies for all projects in a single giant
111 | `package.json` sitting at the root. This helps maintain a _single version
112 | policy_. However, this has its own pros & cons (you can google for opinions).
113 | Note that
114 | [Nx can be used in a bare-bones way without using Nx plugins](https://nx.dev/getting-started/nx-core).
115 | In this mode, Nx simply builds on top of npm or Yarn Workspaces and becomes
116 | equivalent to Turborepo.
117 |
118 | ## Language
119 |
120 | JavaScript is one of the most popular programming languages in the world. It has
121 | a huge community behind it and plenty of learning resources. For simple
122 | applications, it works really well. However, as the complexity increases it is
123 | easier to introduce bugs due to its weak type checking. If your application
124 | deals with complex data structures, it is better to switch over to TypeScript
125 | because of its static type checking. The TypeScript compiler will catch many
126 | mistakes that will simply slip by JavaScript. TypeScript is also great for
127 | defining conceptual models that are shared between backend and frontend teams.
128 |
129 | ## CSR vs. SSR
130 |
131 | ### Client-Side Rendering (CSR)
132 |
133 | 
134 |
135 | For simple applications (say with 10-20 pages) you can probably get away with a
136 | Single Page App (SPA) where all the JavaScript is loaded in one shot and
137 | thereafter all rendering takes place client-side. The client makes HTTP requests
138 | to an API server to get any data it needs to render pages. This is called
139 | Client-Side Rendering (CSR). This is also how classic React works.
140 |
141 | CSR is simple to develop and simple to deploy (only a static bundle needs to be
142 | deployed to your web server). It's perfect for simple applications. However, the
143 | downside is that the initial render is slow because the entire JavaScript has to
144 | be downloaded before rendering can happen. In technical terms, we have a slow
145 | First Contentful Paint (FCP) and a slow Time to Interactive (TTI). If you are
146 | building a marketing or eCommerce site your users may not have the patience to
147 | wait for the app to load. If that's the case, you should consider SSR.
148 |
149 | ### Server-Side Rendering (SSR)
150 |
151 | 
152 |
153 | In Server-Side Rendering, as the user navigates through the site, the browser
154 | makes requests to the web server to render individual pages. The web server must
155 | connect to the API server and get the data it needs to render pages. It needs to
156 | be smart to compose pages really fast and send them to the browser (unlike CSR,
157 | where the server simply sends a pre-built static bundle to the browser). This
158 | puts a lot more load on the web server. However, the advantage is that the
159 | browser sees its First Contentful Paint (FCP) really fast. The Time to
160 | Interactive (TTI) is still slow because interactions cannot start until the
161 | JavaScript is also sent to the browser (a process called hydration). That's the
162 | tradeoff.
163 |
164 | Next.js and Remix are a two of the most popular Server-Side Rendering frameworks
165 | in the React ecosystem.
166 |
167 | ## Movie Magic Tech Stack
168 |
169 | Given the options discussed above, I decided to build Movie Magic using three
170 | different stacks to illustrate the differences: Classic React, Next.js & Remix.
171 | The chart below shows the decisions made within each stack.
172 |
173 | 
174 |
175 | ## Movie Magic Repo Structure
176 |
177 | 
178 |
179 | ## Building Movie Magic
180 |
181 | ### Development Build
182 |
183 | ```bash
184 | npm install
185 | npm run dev
186 | ```
187 |
188 | Open browser windows at each of the following URLs to see the respective demo
189 | apps:
190 |
191 | 1. http://localhost:3000/: Movie Magic | React
192 | 2. http://localhost:3001/: Movie Magic | Next.js
193 | 3. http://localhost:3002/: Movie Magic | Remix
194 |
195 | Note that the React app fetches mock data from MSW, whereas the other two apps
196 | fetch real data from the movie-magic-api.
197 |
198 | > Note: Do not run `npm install` in any of the subdirectories. It will break the
199 | > build. There should be only one `package-lock.json` file in the entire repo
200 | > (at the root).
201 |
202 | ### Production Build
203 |
204 | To build all apps and packages, run the following command:
205 |
206 | ```bash
207 | npm install
208 | npm run build
209 | ```
210 |
211 | ### Clean Build
212 |
213 | Removes all build artifacts and performs a clean build.
214 |
215 | ```bash
216 | npm run clean
217 | npm install
218 | npm run dev
219 | ```
220 |
221 | For an "aggressive" clean build, add one more step as shown below. This wil
222 | build the lock file from scratch.
223 |
224 | ```bash
225 | npm run clean
226 | rm package-lock.json
227 | npm install
228 | npm run dev
229 | ```
230 |
231 | ## Running Storybook
232 |
233 | ```bash
234 | cd storybook
235 | npm install
236 | npm run storybook # you can also run it from the root directory
237 | ```
238 |
239 | ## Running Unit Tests
240 |
241 | ```bash
242 | npm run test
243 | ```
244 |
245 | ## Running End-to-End Tests
246 |
247 | ```sh
248 | npm run dev # starts a local server hosting the react app
249 |
250 | # run cypress in a different shell
251 | npm run cypress
252 | ```
253 |
254 | ## Code Formatting
255 |
256 | ```sh
257 | npm run format
258 | ```
259 |
--------------------------------------------------------------------------------
/apps/movie-magic-api/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | extends: ['custom'],
4 | };
5 |
--------------------------------------------------------------------------------
/apps/movie-magic-api/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | ['@babel/preset-env', { targets: { node: 'current' } }],
4 | '@babel/preset-typescript',
5 | ],
6 | };
7 |
--------------------------------------------------------------------------------
/apps/movie-magic-api/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "movie-magic-api",
3 | "description": "Movie Magic API",
4 | "version": "1.0.0",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "ts-node src/index.ts",
8 | "start:debug": "ndb ts-node src/index.ts",
9 | "start:prod": "node dist",
10 | "dev": "nodemon --watch src -e ts --exec npm start",
11 | "build": "tsc",
12 | "lint": "eslint src/**/*.ts*"
13 | },
14 | "dependencies": {
15 | "body-parser": "^1.20.0",
16 | "cors": "^2.8.5",
17 | "express": "^4.17.3",
18 | "morgan": "^1.10.0"
19 | },
20 | "devDependencies": {
21 | "@babel/core": "^7.17.9",
22 | "@babel/preset-env": "^7.16.11",
23 | "@babel/preset-typescript": "^7.16.7",
24 | "@types/cors": "^2.8.12",
25 | "@types/express": "^4.17.13",
26 | "@types/morgan": "^1.9.3",
27 | "@types/node": "^17.0.25",
28 | "eslint": "^8.15.0",
29 | "eslint-config-custom": "*",
30 | "nodemon": "^2.0.15",
31 | "ts-node": "^10.7.0",
32 | "typescript": "^4.6.3"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/apps/movie-magic-api/src/createApp.ts:
--------------------------------------------------------------------------------
1 | import bodyParser from 'body-parser';
2 | import cors from 'cors';
3 | import express, { Request, Response, NextFunction } from 'express';
4 | import morgan from 'morgan';
5 | import { rootRouter } from './routes';
6 |
7 | export function createApp() {
8 | // Create Express App
9 | const app = express();
10 |
11 | // Add middleware to log requests
12 | app.use(morgan('combined'));
13 |
14 | // Add middleware to enable CORS
15 | app.use(cors());
16 |
17 | // Add middleware to parse the POST data of the body
18 | app.use(bodyParser.urlencoded({ extended: true }));
19 |
20 | // Add middleware to parse application/json
21 | app.use(bodyParser.json());
22 |
23 | // Add routes
24 | app.use(rootRouter);
25 |
26 | // Add application error handler
27 | app.use(appErrorHandler);
28 |
29 | return app;
30 | }
31 |
32 | function appErrorHandler(
33 | err: Error,
34 | req: Request,
35 | res: Response,
36 | next: NextFunction
37 | ) {
38 | if (err.message) {
39 | res.status(500).send({ error: err.message });
40 | } else {
41 | next(err);
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/apps/movie-magic-api/src/index.ts:
--------------------------------------------------------------------------------
1 | import { createServer } from 'http';
2 | import { createApp } from './createApp';
3 |
4 | // -----------------------------------------------------------------------------
5 | // Start the HTTP Server using the Express App
6 | // -----------------------------------------------------------------------------
7 | const port = process.env.PORT || 8080;
8 | const app = createApp();
9 | const server = createServer(app);
10 | server.listen(port, () => console.log('Listening on port ' + port));
11 |
12 | // -----------------------------------------------------------------------------
13 | // When SIGINT is received (i.e. Ctrl-C is pressed), shutdown services
14 | // -----------------------------------------------------------------------------
15 | process.on('SIGINT', () => {
16 | console.log('SIGINT received ...');
17 |
18 | console.log('Shutting down the server');
19 | server.close(() => {
20 | console.log('Server stopped ...');
21 | console.log('Exiting process ...');
22 | process.exit(0);
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/apps/movie-magic-api/src/routes/data/plan-members.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "name": "Paul Silva",
4 | "email": "paul.silva@gmail.com",
5 | "photo": "https://images.unsplash.com/photo-1568585105565-e372998a195d?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80"
6 | },
7 | {
8 | "name": "Danielle Silva",
9 | "email": "danielle.silva@gmail.com",
10 | "photo": "https://images.unsplash.com/photo-1517841905240-472988babdf9?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80"
11 | },
12 | {
13 | "name": "Trevor Silva",
14 | "email": "trevor.silva@gmail.com",
15 | "photo": "https://images.unsplash.com/photo-1531427186611-ecfd6d936c79?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80"
16 | }
17 | ]
18 |
--------------------------------------------------------------------------------
/apps/movie-magic-api/src/routes/data/top-10-movies.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "name": "The Shawshank Redemption",
4 | "year": 1994,
5 | "rating": 9.3
6 | },
7 | {
8 | "name": "The Godfather",
9 | "year": 1972,
10 | "rating": 9.2
11 | },
12 | {
13 | "name": "The Godfather: Part II",
14 | "year": 1974,
15 | "rating": 9.0
16 | },
17 | {
18 | "name": "The Dark Knight",
19 | "year": 2008,
20 | "rating": 9.0
21 | },
22 | {
23 | "name": "12 Angry Men",
24 | "year": 1957,
25 | "rating": 8.9
26 | },
27 | {
28 | "name": "Schindler's List",
29 | "year": 1993,
30 | "rating": 8.9
31 | },
32 | {
33 | "name": "The Lord Of The Rings: The Return Of The King",
34 | "year": 2003,
35 | "rating": 8.9
36 | },
37 | {
38 | "name": "Pulp Fiction",
39 | "year": 1994,
40 | "rating": 8.9
41 | },
42 | {
43 | "name": "The Good, The Bad And The Ugly",
44 | "year": 1966,
45 | "rating": 8.8
46 | },
47 | {
48 | "name": "The Lord Of The Rings: The Fellowship Of The Rings",
49 | "year": 2001,
50 | "rating": 8.8
51 | }
52 | ]
53 |
--------------------------------------------------------------------------------
/apps/movie-magic-api/src/routes/index.ts:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import { moviesRouter } from './moviesRouter';
3 | import { planMembersRouter } from './planMembersRouter';
4 |
5 | export const rootRouter = express.Router();
6 | rootRouter.use('/top-10-movies', moviesRouter);
7 | rootRouter.use('/plan-members', planMembersRouter);
8 |
--------------------------------------------------------------------------------
/apps/movie-magic-api/src/routes/moviesRouter.ts:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import movies from './data/top-10-movies.json';
3 |
4 | export const moviesRouter = express.Router();
5 |
6 | /** get top 10 movies */
7 | moviesRouter.get('/', (req, res) => {
8 | res.send(movies);
9 | });
10 |
--------------------------------------------------------------------------------
/apps/movie-magic-api/src/routes/planMembersRouter.ts:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import planMembers from './data/plan-members.json';
3 |
4 | export const planMembersRouter = express.Router();
5 |
6 | planMembersRouter.get('/', (req, res) => {
7 | res.send(planMembers);
8 | });
9 |
--------------------------------------------------------------------------------
/apps/movie-magic-api/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "emitDecoratorMetadata": true,
4 | "esModuleInterop": true,
5 | "experimentalDecorators": true,
6 | "forceConsistentCasingInFileNames": true,
7 | "lib": ["ES2019", "esnext.asynciterable"],
8 | "module": "commonjs",
9 | "moduleResolution": "node",
10 | "outDir": "./dist",
11 | "resolveJsonModule": true,
12 | "sourceMap": true,
13 | "strict": true,
14 | "target": "ES2019"
15 | },
16 | "include": [
17 | "src"
18 | ]
19 | }
20 |
--------------------------------------------------------------------------------
/apps/movie-magic-nextjs/.env.development:
--------------------------------------------------------------------------------
1 | API_URL=http://localhost:8080
2 |
--------------------------------------------------------------------------------
/apps/movie-magic-nextjs/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/apps/movie-magic-nextjs/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 | .pnpm-debug.log*
27 |
28 | # local env files
29 | .env*.local
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 |
--------------------------------------------------------------------------------
/apps/movie-magic-nextjs/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/basic-features/typescript for more information.
6 |
--------------------------------------------------------------------------------
/apps/movie-magic-nextjs/next.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | reactStrictMode: true,
3 | };
4 |
--------------------------------------------------------------------------------
/apps/movie-magic-nextjs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "movie-magic-nextjs",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev --port 3001",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint",
10 | "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf .next"
11 | },
12 | "dependencies": {
13 | "@heroicons/react": "^1.0.6",
14 | "movie-models": "*",
15 | "next": "^12.1.5",
16 | "react": "^18.0.0",
17 | "react-dom": "^18.0.0",
18 | "ui-lib": "*"
19 | },
20 | "devDependencies": {
21 | "@types/node": "^17.0.25",
22 | "@types/react": "^18.0.6",
23 | "@types/react-dom": "^18.0.2",
24 | "eslint": "^8.15.0",
25 | "eslint-config-next": "^12.1.5",
26 | "tsconfig": "*",
27 | "typescript": "^4.6.3"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/apps/movie-magic-nextjs/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nareshbhatia/custom-react-stack/54b84db60c34d5630b2799a8a0328c7ae9010d24/apps/movie-magic-nextjs/public/favicon.ico
--------------------------------------------------------------------------------
/apps/movie-magic-nextjs/src/components/Header/Header.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { CogIcon } from '@heroicons/react/outline';
3 | import { useRouter } from 'next/router';
4 |
5 | interface HeaderProps {
6 | children?: React.ReactNode;
7 | }
8 |
9 | export function Header({ children }: HeaderProps) {
10 | const router = useRouter();
11 |
12 | const navigateToHome = () => {
13 | router.push('/');
14 | };
15 |
16 | const navigateToSettings = () => {
17 | router.push('/settings');
18 | };
19 |
20 | return (
21 |
22 |
28 | {children}
29 |
30 |
31 |
39 |
40 |
41 |
42 |
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/apps/movie-magic-nextjs/src/components/Header/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Header';
2 |
--------------------------------------------------------------------------------
/apps/movie-magic-nextjs/src/components/PageLayout/PageLayout.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Header } from '../Header';
3 |
4 | interface PageLayoutProps {
5 | children?: React.ReactNode;
6 | }
7 |
8 | export function PageLayout({ children }: PageLayoutProps) {
9 | return (
10 |
11 |
12 | {children}
13 |
14 | );
15 | }
16 |
--------------------------------------------------------------------------------
/apps/movie-magic-nextjs/src/components/PageLayout/index.ts:
--------------------------------------------------------------------------------
1 | export * from './PageLayout';
2 |
--------------------------------------------------------------------------------
/apps/movie-magic-nextjs/src/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import type { ReactElement, ReactNode } from 'react';
2 | import type { NextPage } from 'next';
3 | import type { AppProps } from 'next/app';
4 | import '../../../../packages/ui-lib/src/styles/main.css';
5 |
6 | type NextPageWithLayout = NextPage & {
7 | getLayout?: (page: ReactElement) => ReactNode;
8 | };
9 |
10 | type AppPropsWithLayout = AppProps & {
11 | Component: NextPageWithLayout;
12 | };
13 |
14 | export default function MyApp({ Component, pageProps }: AppPropsWithLayout) {
15 | // Use the layout defined at the page level, if available
16 | const getLayout = Component.getLayout ?? ((page) => page);
17 |
18 | return getLayout( );
19 | }
20 |
--------------------------------------------------------------------------------
/apps/movie-magic-nextjs/src/pages/_document.tsx:
--------------------------------------------------------------------------------
1 | import { Html, Head, Main, NextScript } from 'next/document';
2 |
3 | export default function Document() {
4 | return (
5 |
6 |
7 |
8 |
9 | Movie Magic | Next.js
10 |
11 |
16 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | );
27 | }
28 |
--------------------------------------------------------------------------------
/apps/movie-magic-nextjs/src/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import type { ReactElement } from 'react';
2 | import { Movie } from 'movie-models';
3 | import { MovieList } from 'ui-lib';
4 | import { PageLayout } from '../components/PageLayout';
5 |
6 | interface HomePageProps {
7 | movies: Array;
8 | }
9 |
10 | const HomePage = ({ movies }: HomePageProps) => {
11 | return (
12 |
13 |
14 |
Top 10 Movies Of All Time
15 |
16 |
17 |
18 | );
19 | };
20 |
21 | HomePage.getLayout = function getLayout(page: ReactElement) {
22 | return {page} ;
23 | };
24 |
25 | export async function getServerSideProps() {
26 | const API_URL = process.env.API_URL;
27 | const resMovies = await fetch(`${API_URL}/top-10-movies`);
28 | const movies = await resMovies.json();
29 |
30 | return {
31 | props: {
32 | movies,
33 | },
34 | };
35 | }
36 |
37 | export default HomePage;
38 |
--------------------------------------------------------------------------------
/apps/movie-magic-nextjs/src/pages/settings.tsx:
--------------------------------------------------------------------------------
1 | import type { ReactElement } from 'react';
2 | import { PlanMember } from 'movie-models';
3 | import { PageLayout } from '../components/PageLayout';
4 | import { Button } from 'ui-lib';
5 |
6 | interface SettingsPageProps {
7 | planMembers: Array;
8 | }
9 |
10 | const SettingsPage = ({ planMembers }: SettingsPageProps) => {
11 | return (
12 |
13 |
14 |
Manage your Family Plan
15 |
32 |
33 | ADD TO FAMILY PLAN
34 |
35 |
36 |
37 | );
38 | };
39 |
40 | SettingsPage.getLayout = function getLayout(page: ReactElement) {
41 | return {page} ;
42 | };
43 |
44 | export async function getServerSideProps() {
45 | const apiUrl = process.env.API_URL;
46 | const resPlanMembers = await fetch(`${apiUrl}/plan-members`);
47 | const planMembers = await resPlanMembers.json();
48 |
49 | return {
50 | props: {
51 | planMembers,
52 | },
53 | };
54 | }
55 |
56 | export default SettingsPage;
57 |
--------------------------------------------------------------------------------
/apps/movie-magic-nextjs/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "tsconfig/nextjs.json",
3 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
4 | "exclude": ["node_modules"]
5 | }
6 |
--------------------------------------------------------------------------------
/apps/movie-magic-react/.env:
--------------------------------------------------------------------------------
1 | VITE_API_URL=http://localhost:8080
2 |
--------------------------------------------------------------------------------
/apps/movie-magic-react/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | extends: ['custom'],
4 | };
5 |
--------------------------------------------------------------------------------
/apps/movie-magic-react/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/apps/movie-magic-react/cypress.json:
--------------------------------------------------------------------------------
1 | {
2 | "baseUrl": "http://localhost:3000"
3 | }
4 |
--------------------------------------------------------------------------------
/apps/movie-magic-react/cypress/fixtures/example.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Using fixtures to represent data",
3 | "email": "hello@cypress.io",
4 | "body": "Fixtures are a great way to mock data for responses to routes"
5 | }
6 |
--------------------------------------------------------------------------------
/apps/movie-magic-react/cypress/fixtures/profile.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": 8739,
3 | "name": "Jane",
4 | "email": "jane@example.com"
5 | }
--------------------------------------------------------------------------------
/apps/movie-magic-react/cypress/fixtures/users.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "id": 1,
4 | "name": "Leanne Graham",
5 | "username": "Bret",
6 | "email": "Sincere@april.biz",
7 | "address": {
8 | "street": "Kulas Light",
9 | "suite": "Apt. 556",
10 | "city": "Gwenborough",
11 | "zipcode": "92998-3874",
12 | "geo": {
13 | "lat": "-37.3159",
14 | "lng": "81.1496"
15 | }
16 | },
17 | "phone": "1-770-736-8031 x56442",
18 | "website": "hildegard.org",
19 | "company": {
20 | "name": "Romaguera-Crona",
21 | "catchPhrase": "Multi-layered client-server neural-net",
22 | "bs": "harness real-time e-markets"
23 | }
24 | },
25 | {
26 | "id": 2,
27 | "name": "Ervin Howell",
28 | "username": "Antonette",
29 | "email": "Shanna@melissa.tv",
30 | "address": {
31 | "street": "Victor Plains",
32 | "suite": "Suite 879",
33 | "city": "Wisokyburgh",
34 | "zipcode": "90566-7771",
35 | "geo": {
36 | "lat": "-43.9509",
37 | "lng": "-34.4618"
38 | }
39 | },
40 | "phone": "010-692-6593 x09125",
41 | "website": "anastasia.net",
42 | "company": {
43 | "name": "Deckow-Crist",
44 | "catchPhrase": "Proactive didactic contingency",
45 | "bs": "synergize scalable supply-chains"
46 | }
47 | },
48 | {
49 | "id": 3,
50 | "name": "Clementine Bauch",
51 | "username": "Samantha",
52 | "email": "Nathan@yesenia.net",
53 | "address": {
54 | "street": "Douglas Extension",
55 | "suite": "Suite 847",
56 | "city": "McKenziehaven",
57 | "zipcode": "59590-4157",
58 | "geo": {
59 | "lat": "-68.6102",
60 | "lng": "-47.0653"
61 | }
62 | },
63 | "phone": "1-463-123-4447",
64 | "website": "ramiro.info",
65 | "company": {
66 | "name": "Romaguera-Jacobson",
67 | "catchPhrase": "Face to face bifurcated interface",
68 | "bs": "e-enable strategic applications"
69 | }
70 | },
71 | {
72 | "id": 4,
73 | "name": "Patricia Lebsack",
74 | "username": "Karianne",
75 | "email": "Julianne.OConner@kory.org",
76 | "address": {
77 | "street": "Hoeger Mall",
78 | "suite": "Apt. 692",
79 | "city": "South Elvis",
80 | "zipcode": "53919-4257",
81 | "geo": {
82 | "lat": "29.4572",
83 | "lng": "-164.2990"
84 | }
85 | },
86 | "phone": "493-170-9623 x156",
87 | "website": "kale.biz",
88 | "company": {
89 | "name": "Robel-Corkery",
90 | "catchPhrase": "Multi-tiered zero tolerance productivity",
91 | "bs": "transition cutting-edge web services"
92 | }
93 | },
94 | {
95 | "id": 5,
96 | "name": "Chelsey Dietrich",
97 | "username": "Kamren",
98 | "email": "Lucio_Hettinger@annie.ca",
99 | "address": {
100 | "street": "Skiles Walks",
101 | "suite": "Suite 351",
102 | "city": "Roscoeview",
103 | "zipcode": "33263",
104 | "geo": {
105 | "lat": "-31.8129",
106 | "lng": "62.5342"
107 | }
108 | },
109 | "phone": "(254)954-1289",
110 | "website": "demarco.info",
111 | "company": {
112 | "name": "Keebler LLC",
113 | "catchPhrase": "User-centric fault-tolerant solution",
114 | "bs": "revolutionize end-to-end systems"
115 | }
116 | },
117 | {
118 | "id": 6,
119 | "name": "Mrs. Dennis Schulist",
120 | "username": "Leopoldo_Corkery",
121 | "email": "Karley_Dach@jasper.info",
122 | "address": {
123 | "street": "Norberto Crossing",
124 | "suite": "Apt. 950",
125 | "city": "South Christy",
126 | "zipcode": "23505-1337",
127 | "geo": {
128 | "lat": "-71.4197",
129 | "lng": "71.7478"
130 | }
131 | },
132 | "phone": "1-477-935-8478 x6430",
133 | "website": "ola.org",
134 | "company": {
135 | "name": "Considine-Lockman",
136 | "catchPhrase": "Synchronised bottom-line interface",
137 | "bs": "e-enable innovative applications"
138 | }
139 | },
140 | {
141 | "id": 7,
142 | "name": "Kurtis Weissnat",
143 | "username": "Elwyn.Skiles",
144 | "email": "Telly.Hoeger@billy.biz",
145 | "address": {
146 | "street": "Rex Trail",
147 | "suite": "Suite 280",
148 | "city": "Howemouth",
149 | "zipcode": "58804-1099",
150 | "geo": {
151 | "lat": "24.8918",
152 | "lng": "21.8984"
153 | }
154 | },
155 | "phone": "210.067.6132",
156 | "website": "elvis.io",
157 | "company": {
158 | "name": "Johns Group",
159 | "catchPhrase": "Configurable multimedia task-force",
160 | "bs": "generate enterprise e-tailers"
161 | }
162 | },
163 | {
164 | "id": 8,
165 | "name": "Nicholas Runolfsdottir V",
166 | "username": "Maxime_Nienow",
167 | "email": "Sherwood@rosamond.me",
168 | "address": {
169 | "street": "Ellsworth Summit",
170 | "suite": "Suite 729",
171 | "city": "Aliyaview",
172 | "zipcode": "45169",
173 | "geo": {
174 | "lat": "-14.3990",
175 | "lng": "-120.7677"
176 | }
177 | },
178 | "phone": "586.493.6943 x140",
179 | "website": "jacynthe.com",
180 | "company": {
181 | "name": "Abernathy Group",
182 | "catchPhrase": "Implemented secondary concept",
183 | "bs": "e-enable extensible e-tailers"
184 | }
185 | },
186 | {
187 | "id": 9,
188 | "name": "Glenna Reichert",
189 | "username": "Delphine",
190 | "email": "Chaim_McDermott@dana.io",
191 | "address": {
192 | "street": "Dayna Park",
193 | "suite": "Suite 449",
194 | "city": "Bartholomebury",
195 | "zipcode": "76495-3109",
196 | "geo": {
197 | "lat": "24.6463",
198 | "lng": "-168.8889"
199 | }
200 | },
201 | "phone": "(775)976-6794 x41206",
202 | "website": "conrad.com",
203 | "company": {
204 | "name": "Yost and Sons",
205 | "catchPhrase": "Switchable contextually-based project",
206 | "bs": "aggregate real-time technologies"
207 | }
208 | },
209 | {
210 | "id": 10,
211 | "name": "Clementina DuBuque",
212 | "username": "Moriah.Stanton",
213 | "email": "Rey.Padberg@karina.biz",
214 | "address": {
215 | "street": "Kattie Turnpike",
216 | "suite": "Suite 198",
217 | "city": "Lebsackbury",
218 | "zipcode": "31428-2261",
219 | "geo": {
220 | "lat": "-38.2386",
221 | "lng": "57.2232"
222 | }
223 | },
224 | "phone": "024-648-3804",
225 | "website": "ambrose.net",
226 | "company": {
227 | "name": "Hoeger LLC",
228 | "catchPhrase": "Centralized empowering task-force",
229 | "bs": "target end-to-end models"
230 | }
231 | }
232 | ]
--------------------------------------------------------------------------------
/apps/movie-magic-react/cypress/integration/home.spec.ts:
--------------------------------------------------------------------------------
1 | describe('Home page', function () {
2 | it('renders correctly', function () {
3 | // Go to home page
4 | cy.visit('/');
5 |
6 | // Verify that the correct title is rendered
7 | cy.contains('Top 10 Movies Of All Time');
8 |
9 | // Verify that 10 movies are rendered
10 | const movies = cy.get('[data-testid="movie-table"] > tbody > tr');
11 | movies.should('have.length', 10);
12 |
13 | // Go to Settings Page
14 | cy.get('[aria-label="Settings"]').click();
15 | cy.contains('Manage your Family Plan');
16 |
17 | // Go back to Home Page
18 | cy.get('[aria-label="Home"]').click();
19 | cy.contains('Top 10 Movies Of All Time');
20 | });
21 | });
22 |
--------------------------------------------------------------------------------
/apps/movie-magic-react/cypress/plugins/index.js:
--------------------------------------------------------------------------------
1 | ///
2 | // ***********************************************************
3 | // This example plugins/index.js can be used to load plugins
4 | //
5 | // You can change the location of this file or turn off loading
6 | // the plugins file with the 'pluginsFile' configuration option.
7 | //
8 | // You can read more here:
9 | // https://on.cypress.io/plugins-guide
10 | // ***********************************************************
11 |
12 | // This function is called when a project is opened or re-opened (e.g. due to
13 | // the project's config changing)
14 |
15 | /**
16 | * @type {Cypress.PluginConfig}
17 | */
18 | // eslint-disable-next-line no-unused-vars
19 | module.exports = (on, config) => {
20 | // `on` is used to hook into various events Cypress emits
21 | // `config` is the resolved Cypress config
22 | }
23 |
--------------------------------------------------------------------------------
/apps/movie-magic-react/cypress/screenshots/All Integration Specs/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nareshbhatia/custom-react-stack/54b84db60c34d5630b2799a8a0328c7ae9010d24/apps/movie-magic-react/cypress/screenshots/All Integration Specs/.gitkeep
--------------------------------------------------------------------------------
/apps/movie-magic-react/cypress/support/commands.ts:
--------------------------------------------------------------------------------
1 | // ***********************************************
2 | // This example commands.js shows you how to
3 | // create various custom commands and overwrite
4 | // existing commands.
5 | //
6 | // For more comprehensive examples of custom
7 | // commands please read more here:
8 | // https://on.cypress.io/custom-commands
9 | // ***********************************************
10 | //
11 | //
12 | // -- This is a parent command --
13 | // Cypress.Commands.add('login', (email, password) => { ... })
14 | //
15 | //
16 | // -- This is a child command --
17 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
18 | //
19 | //
20 | // -- This is a dual command --
21 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
22 | //
23 | //
24 | // -- This will overwrite an existing command --
25 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
26 |
--------------------------------------------------------------------------------
/apps/movie-magic-react/cypress/support/index.ts:
--------------------------------------------------------------------------------
1 | // ***********************************************************
2 | // This example support/index.js is processed and
3 | // loaded automatically before your test files.
4 | //
5 | // This is a great place to put global configuration and
6 | // behavior that modifies Cypress.
7 | //
8 | // You can change the location of this file or turn off
9 | // automatically serving support files with the
10 | // 'supportFile' configuration option.
11 | //
12 | // You can read more here:
13 | // https://on.cypress.io/configuration
14 | // ***********************************************************
15 |
16 | // Import commands.js using ES2015 syntax:
17 | import './commands'
18 |
19 | // Alternatively you can use CommonJS syntax:
20 | // require('./commands')
21 |
--------------------------------------------------------------------------------
/apps/movie-magic-react/cypress/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["es5", "dom"],
5 | "types": ["cypress"]
6 | },
7 | "include": ["**/*.ts"]
8 | }
9 |
--------------------------------------------------------------------------------
/apps/movie-magic-react/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Movie Magic | React
7 |
8 |
9 |
10 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/apps/movie-magic-react/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "movie-magic-react",
3 | "private": true,
4 | "version": "0.0.0",
5 | "scripts": {
6 | "dev": "vite",
7 | "build": "tsc && vite build",
8 | "preview": "vite preview",
9 | "lint": "eslint src/**/*.ts*",
10 | "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist",
11 | "cypress": "cypress open"
12 | },
13 | "dependencies": {
14 | "@heroicons/react": "^1.0.6",
15 | "movie-models": "*",
16 | "react": "^18.0.0",
17 | "react-dom": "^18.0.0",
18 | "react-router-dom": "^6.3.0",
19 | "ui-lib": "*"
20 | },
21 | "devDependencies": {
22 | "@storybook/react": "next",
23 | "@types/react": "^18.0.6",
24 | "@types/react-dom": "^18.0.2",
25 | "@vitejs/plugin-react": "^1.3.1",
26 | "cypress": "^9.5.4",
27 | "msw": "^0.39.2",
28 | "eslint": "^8.15.0",
29 | "eslint-config-custom": "*",
30 | "tsconfig": "*",
31 | "typescript": "^4.6.3",
32 | "vite": "^2.9.5"
33 | },
34 | "msw": {
35 | "workerDirectory": "public"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/apps/movie-magic-react/public/mockServiceWorker.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | /* tslint:disable */
3 |
4 | /**
5 | * Mock Service Worker (0.39.2).
6 | * @see https://github.com/mswjs/msw
7 | * - Please do NOT modify this file.
8 | * - Please do NOT serve this file on production.
9 | */
10 |
11 | const INTEGRITY_CHECKSUM = '02f4ad4a2797f85668baf196e553d929';
12 | const bypassHeaderName = 'x-msw-bypass';
13 | const activeClientIds = new Set();
14 |
15 | self.addEventListener('install', function () {
16 | return self.skipWaiting();
17 | });
18 |
19 | self.addEventListener('activate', async function (event) {
20 | return self.clients.claim();
21 | });
22 |
23 | self.addEventListener('message', async function (event) {
24 | const clientId = event.source.id;
25 |
26 | if (!clientId || !self.clients) {
27 | return;
28 | }
29 |
30 | const client = await self.clients.get(clientId);
31 |
32 | if (!client) {
33 | return;
34 | }
35 |
36 | const allClients = await self.clients.matchAll();
37 |
38 | switch (event.data) {
39 | case 'KEEPALIVE_REQUEST': {
40 | sendToClient(client, {
41 | type: 'KEEPALIVE_RESPONSE',
42 | });
43 | break;
44 | }
45 |
46 | case 'INTEGRITY_CHECK_REQUEST': {
47 | sendToClient(client, {
48 | type: 'INTEGRITY_CHECK_RESPONSE',
49 | payload: INTEGRITY_CHECKSUM,
50 | });
51 | break;
52 | }
53 |
54 | case 'MOCK_ACTIVATE': {
55 | activeClientIds.add(clientId);
56 |
57 | sendToClient(client, {
58 | type: 'MOCKING_ENABLED',
59 | payload: true,
60 | });
61 | break;
62 | }
63 |
64 | case 'MOCK_DEACTIVATE': {
65 | activeClientIds.delete(clientId);
66 | break;
67 | }
68 |
69 | case 'CLIENT_CLOSED': {
70 | activeClientIds.delete(clientId);
71 |
72 | const remainingClients = allClients.filter((client) => {
73 | return client.id !== clientId;
74 | });
75 |
76 | // Unregister itself when there are no more clients
77 | if (remainingClients.length === 0) {
78 | self.registration.unregister();
79 | }
80 |
81 | break;
82 | }
83 | }
84 | });
85 |
86 | // Resolve the "main" client for the given event.
87 | // Client that issues a request doesn't necessarily equal the client
88 | // that registered the worker. It's with the latter the worker should
89 | // communicate with during the response resolving phase.
90 | async function resolveMainClient(event) {
91 | const client = await self.clients.get(event.clientId);
92 |
93 | if (client.frameType === 'top-level') {
94 | return client;
95 | }
96 |
97 | const allClients = await self.clients.matchAll();
98 |
99 | return allClients
100 | .filter((client) => {
101 | // Get only those clients that are currently visible.
102 | return client.visibilityState === 'visible';
103 | })
104 | .find((client) => {
105 | // Find the client ID that's recorded in the
106 | // set of clients that have registered the worker.
107 | return activeClientIds.has(client.id);
108 | });
109 | }
110 |
111 | async function handleRequest(event, requestId) {
112 | const client = await resolveMainClient(event);
113 | const response = await getResponse(event, client, requestId);
114 |
115 | // Send back the response clone for the "response:*" life-cycle events.
116 | // Ensure MSW is active and ready to handle the message, otherwise
117 | // this message will pend indefinitely.
118 | if (client && activeClientIds.has(client.id)) {
119 | (async function () {
120 | const clonedResponse = response.clone();
121 | sendToClient(client, {
122 | type: 'RESPONSE',
123 | payload: {
124 | requestId,
125 | type: clonedResponse.type,
126 | ok: clonedResponse.ok,
127 | status: clonedResponse.status,
128 | statusText: clonedResponse.statusText,
129 | body:
130 | clonedResponse.body === null ? null : await clonedResponse.text(),
131 | headers: serializeHeaders(clonedResponse.headers),
132 | redirected: clonedResponse.redirected,
133 | },
134 | });
135 | })();
136 | }
137 |
138 | return response;
139 | }
140 |
141 | async function getResponse(event, client, requestId) {
142 | const { request } = event;
143 | const requestClone = request.clone();
144 | const getOriginalResponse = () => fetch(requestClone);
145 |
146 | // Bypass mocking when the request client is not active.
147 | if (!client) {
148 | return getOriginalResponse();
149 | }
150 |
151 | // Bypass initial page load requests (i.e. static assets).
152 | // The absence of the immediate/parent client in the map of the active clients
153 | // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet
154 | // and is not ready to handle requests.
155 | if (!activeClientIds.has(client.id)) {
156 | return await getOriginalResponse();
157 | }
158 |
159 | // Bypass requests with the explicit bypass header
160 | if (requestClone.headers.get(bypassHeaderName) === 'true') {
161 | const cleanRequestHeaders = serializeHeaders(requestClone.headers);
162 |
163 | // Remove the bypass header to comply with the CORS preflight check.
164 | delete cleanRequestHeaders[bypassHeaderName];
165 |
166 | const originalRequest = new Request(requestClone, {
167 | headers: new Headers(cleanRequestHeaders),
168 | });
169 |
170 | return fetch(originalRequest);
171 | }
172 |
173 | // Send the request to the client-side MSW.
174 | const reqHeaders = serializeHeaders(request.headers);
175 | const body = await request.text();
176 |
177 | const clientMessage = await sendToClient(client, {
178 | type: 'REQUEST',
179 | payload: {
180 | id: requestId,
181 | url: request.url,
182 | method: request.method,
183 | headers: reqHeaders,
184 | cache: request.cache,
185 | mode: request.mode,
186 | credentials: request.credentials,
187 | destination: request.destination,
188 | integrity: request.integrity,
189 | redirect: request.redirect,
190 | referrer: request.referrer,
191 | referrerPolicy: request.referrerPolicy,
192 | body,
193 | bodyUsed: request.bodyUsed,
194 | keepalive: request.keepalive,
195 | },
196 | });
197 |
198 | switch (clientMessage.type) {
199 | case 'MOCK_SUCCESS': {
200 | return delayPromise(
201 | () => respondWithMock(clientMessage),
202 | clientMessage.payload.delay
203 | );
204 | }
205 |
206 | case 'MOCK_NOT_FOUND': {
207 | return getOriginalResponse();
208 | }
209 |
210 | case 'NETWORK_ERROR': {
211 | const { name, message } = clientMessage.payload;
212 | const networkError = new Error(message);
213 | networkError.name = name;
214 |
215 | // Rejecting a request Promise emulates a network error.
216 | throw networkError;
217 | }
218 |
219 | case 'INTERNAL_ERROR': {
220 | const parsedBody = JSON.parse(clientMessage.payload.body);
221 |
222 | console.error(
223 | `\
224 | [MSW] Uncaught exception in the request handler for "%s %s":
225 |
226 | ${parsedBody.location}
227 |
228 | This exception has been gracefully handled as a 500 response, however, it's strongly recommended to resolve this error, as it indicates a mistake in your code. If you wish to mock an error response, please see this guide: https://mswjs.io/docs/recipes/mocking-error-responses\
229 | `,
230 | request.method,
231 | request.url
232 | );
233 |
234 | return respondWithMock(clientMessage);
235 | }
236 | }
237 |
238 | return getOriginalResponse();
239 | }
240 |
241 | self.addEventListener('fetch', function (event) {
242 | const { request } = event;
243 | const accept = request.headers.get('accept') || '';
244 |
245 | // Bypass server-sent events.
246 | if (accept.includes('text/event-stream')) {
247 | return;
248 | }
249 |
250 | // Bypass navigation requests.
251 | if (request.mode === 'navigate') {
252 | return;
253 | }
254 |
255 | // Opening the DevTools triggers the "only-if-cached" request
256 | // that cannot be handled by the worker. Bypass such requests.
257 | if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') {
258 | return;
259 | }
260 |
261 | // Bypass all requests when there are no active clients.
262 | // Prevents the self-unregistered worked from handling requests
263 | // after it's been deleted (still remains active until the next reload).
264 | if (activeClientIds.size === 0) {
265 | return;
266 | }
267 |
268 | const requestId = uuidv4();
269 |
270 | return event.respondWith(
271 | handleRequest(event, requestId).catch((error) => {
272 | if (error.name === 'NetworkError') {
273 | console.warn(
274 | '[MSW] Successfully emulated a network error for the "%s %s" request.',
275 | request.method,
276 | request.url
277 | );
278 | return;
279 | }
280 |
281 | // At this point, any exception indicates an issue with the original request/response.
282 | console.error(
283 | `\
284 | [MSW] Caught an exception from the "%s %s" request (%s). This is probably not a problem with Mock Service Worker. There is likely an additional logging output above.`,
285 | request.method,
286 | request.url,
287 | `${error.name}: ${error.message}`
288 | );
289 | })
290 | );
291 | });
292 |
293 | function serializeHeaders(headers) {
294 | const reqHeaders = {};
295 | headers.forEach((value, name) => {
296 | reqHeaders[name] = reqHeaders[name]
297 | ? [].concat(reqHeaders[name]).concat(value)
298 | : value;
299 | });
300 | return reqHeaders;
301 | }
302 |
303 | function sendToClient(client, message) {
304 | return new Promise((resolve, reject) => {
305 | const channel = new MessageChannel();
306 |
307 | channel.port1.onmessage = (event) => {
308 | if (event.data && event.data.error) {
309 | return reject(event.data.error);
310 | }
311 |
312 | resolve(event.data);
313 | };
314 |
315 | client.postMessage(JSON.stringify(message), [channel.port2]);
316 | });
317 | }
318 |
319 | function delayPromise(cb, duration) {
320 | return new Promise((resolve) => {
321 | setTimeout(() => resolve(cb()), duration);
322 | });
323 | }
324 |
325 | function respondWithMock(clientMessage) {
326 | return new Response(clientMessage.payload.body, {
327 | ...clientMessage.payload,
328 | headers: clientMessage.payload.headers,
329 | });
330 | }
331 |
332 | function uuidv4() {
333 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
334 | const r = (Math.random() * 16) | 0;
335 | const v = c == 'x' ? r : (r & 0x3) | 0x8;
336 | return v.toString(16);
337 | });
338 | }
339 |
--------------------------------------------------------------------------------
/apps/movie-magic-react/src/App.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { Routes, Route } from 'react-router-dom';
3 | import { PageLayout } from './components';
4 | import { HomePage, NotFoundPage, SettingsPage } from './pages';
5 |
6 | export function App() {
7 | return (
8 |
9 | Movie Magic}>
10 | } />
11 | } />
12 |
13 | } />
14 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/apps/movie-magic-react/src/components/Header/Header.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Meta } from '@storybook/react';
3 | import { BrowserRouter as Router } from 'react-router-dom';
4 | import { Header } from './Header';
5 |
6 | export default {
7 | title: 'Components/Header',
8 | component: Header,
9 | parameters: {
10 | layout: 'fullscreen',
11 | },
12 | } as Meta;
13 |
14 | // ----- Note -----
15 | // If we were not mixing stacks, then the Router below would go as a decorator
16 | // in .storybook/preview.tsx and will not be needed here.
17 | export const HeaderStory = () => {
18 | return (
19 |
20 |
21 |
22 | );
23 | };
24 | HeaderStory.storyName = 'Header';
25 |
--------------------------------------------------------------------------------
/apps/movie-magic-react/src/components/Header/Header.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { CogIcon } from '@heroicons/react/outline';
3 | import { useNavigate } from 'react-router-dom';
4 |
5 | interface HeaderProps {
6 | children?: React.ReactNode;
7 | }
8 |
9 | export function Header({ children }: HeaderProps) {
10 | const navigate = useNavigate();
11 |
12 | const navigateToHome = () => {
13 | navigate('/');
14 | };
15 |
16 | const navigateToSettings = () => {
17 | navigate('/settings');
18 | };
19 |
20 | return (
21 |
22 |
28 | {children}
29 |
30 |
31 |
39 |
40 |
41 |
42 |
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/apps/movie-magic-react/src/components/Header/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Header';
2 |
--------------------------------------------------------------------------------
/apps/movie-magic-react/src/components/PageLayout/PageLayout.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Outlet } from 'react-router-dom';
3 | import { Header } from '../Header';
4 |
5 | interface PageLayoutProps {
6 | children?: React.ReactNode;
7 | }
8 |
9 | export function PageLayout({ children }: PageLayoutProps) {
10 | return (
11 |
12 |
13 |
14 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/apps/movie-magic-react/src/components/PageLayout/index.ts:
--------------------------------------------------------------------------------
1 | export * from './PageLayout';
2 |
--------------------------------------------------------------------------------
/apps/movie-magic-react/src/components/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Header';
2 | export * from './PageLayout';
3 |
--------------------------------------------------------------------------------
/apps/movie-magic-react/src/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nareshbhatia/custom-react-stack/54b84db60c34d5630b2799a8a0328c7ae9010d24/apps/movie-magic-react/src/favicon.ico
--------------------------------------------------------------------------------
/apps/movie-magic-react/src/main.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 | import { BrowserRouter as Router } from 'react-router-dom';
4 | import { App } from './App';
5 | import '../../../packages/ui-lib/src/styles/main.css';
6 |
7 | // Start mock service worker in dev environment
8 | async function startMockServiceWorker() {
9 | if (import.meta.env.DEV) {
10 | const { worker } = await import('./mocks/browser');
11 | await worker.start();
12 | worker.printHandlers();
13 | }
14 | }
15 |
16 | startMockServiceWorker().then(() => {
17 | const root = ReactDOM.createRoot(
18 | document.getElementById('root') as HTMLElement
19 | );
20 |
21 | root.render(
22 |
23 |
24 |
25 |
26 |
27 | );
28 | });
29 |
--------------------------------------------------------------------------------
/apps/movie-magic-react/src/mocks/browser.ts:
--------------------------------------------------------------------------------
1 | import { setupWorker } from 'msw';
2 | import { handlers } from './handlers';
3 |
4 | // This configures a Service Worker with the given request handlers.
5 | export const worker = setupWorker(...handlers);
6 |
--------------------------------------------------------------------------------
/apps/movie-magic-react/src/mocks/constants.ts:
--------------------------------------------------------------------------------
1 | export const MOCK_API_URL = 'http://localhost:8080';
2 |
--------------------------------------------------------------------------------
/apps/movie-magic-react/src/mocks/handlers.ts:
--------------------------------------------------------------------------------
1 | import { rest } from 'msw';
2 | import { MOCK_API_URL } from './constants';
3 | import { mockMovies } from './mockMovies';
4 | import { mockPlanMembers } from './mockPlanMembers';
5 |
6 | export const handlers = [
7 | rest.get(`${MOCK_API_URL}/top-10-movies`, (req, res, ctx) => {
8 | return res(ctx.status(200), ctx.json(mockMovies));
9 | }),
10 |
11 | rest.get(`${MOCK_API_URL}/plan-members`, (req, res, ctx) => {
12 | return res(ctx.status(200), ctx.json(mockPlanMembers));
13 | }),
14 | ];
15 |
--------------------------------------------------------------------------------
/apps/movie-magic-react/src/mocks/mockMovies.ts:
--------------------------------------------------------------------------------
1 | import { Movie } from 'movie-models';
2 |
3 | export const mockMovies: Array = [
4 | {
5 | name: 'The Shawshank Redemption [mock]',
6 | year: 1994,
7 | rating: 9.3,
8 | },
9 | {
10 | name: 'The Godfather [mock]',
11 | year: 1972,
12 | rating: 9.2,
13 | },
14 | {
15 | name: 'The Godfather: Part II [mock]',
16 | year: 1974,
17 | rating: 9.0,
18 | },
19 | {
20 | name: 'The Dark Knight [mock]',
21 | year: 2008,
22 | rating: 9.0,
23 | },
24 | {
25 | name: '12 Angry Men [mock]',
26 | year: 1957,
27 | rating: 8.9,
28 | },
29 | {
30 | name: "Schindler's List [mock]",
31 | year: 1993,
32 | rating: 8.9,
33 | },
34 | {
35 | name: 'The Lord Of The Rings: The Return Of The King [mock]',
36 | year: 2003,
37 | rating: 8.9,
38 | },
39 | {
40 | name: 'Pulp Fiction [mock]',
41 | year: 1994,
42 | rating: 8.9,
43 | },
44 | {
45 | name: 'The Good, The Bad And The Ugly [mock]',
46 | year: 1966,
47 | rating: 8.8,
48 | },
49 | {
50 | name: 'The Lord Of The Rings: The Fellowship Of The Rings [mock]',
51 | year: 2001,
52 | rating: 8.8,
53 | },
54 | ];
55 |
--------------------------------------------------------------------------------
/apps/movie-magic-react/src/mocks/mockPlanMembers.ts:
--------------------------------------------------------------------------------
1 | import { PlanMember } from 'movie-models';
2 |
3 | export const mockPlanMembers: Array = [
4 | {
5 | name: 'Paul Silva',
6 | email: 'paul.silva@mock.com',
7 | photo:
8 | 'https://images.unsplash.com/photo-1568585105565-e372998a195d?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80',
9 | },
10 | {
11 | name: 'Danielle Silva',
12 | email: 'danielle.silva@mock.com',
13 | photo:
14 | 'https://images.unsplash.com/photo-1517841905240-472988babdf9?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80',
15 | },
16 | {
17 | name: 'Trevor Silva',
18 | email: 'trevor.silva@mock.com',
19 | photo:
20 | 'https://images.unsplash.com/photo-1531427186611-ecfd6d936c79?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80',
21 | },
22 | ];
23 |
--------------------------------------------------------------------------------
/apps/movie-magic-react/src/mocks/server.ts:
--------------------------------------------------------------------------------
1 | import { setupServer } from 'msw/node';
2 | import { handlers } from './handlers';
3 |
4 | // This configures a request mocking server with the given request handlers.
5 | export const server = setupServer(...handlers);
6 |
--------------------------------------------------------------------------------
/apps/movie-magic-react/src/pages/HomePage/HomePage.tsx:
--------------------------------------------------------------------------------
1 | import { MovieListContainer } from './MovieListContainer';
2 |
3 | export function HomePage() {
4 | return (
5 |
10 | );
11 | }
12 |
--------------------------------------------------------------------------------
/apps/movie-magic-react/src/pages/HomePage/MovieListContainer.tsx:
--------------------------------------------------------------------------------
1 | import { Fragment } from 'react';
2 | import { MovieList } from 'ui-lib';
3 | import { useMovies } from './useMovies';
4 |
5 | export function MovieListContainer() {
6 | const { isLoading, isError, error, movies } = useMovies();
7 |
8 | if (isLoading) {
9 | return Loading...
;
10 | }
11 |
12 | if (isError) {
13 | return {(error as any).message} ;
14 | }
15 |
16 | return (
17 |
18 | Top 10 Movies Of All Time
19 |
20 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/apps/movie-magic-react/src/pages/HomePage/index.ts:
--------------------------------------------------------------------------------
1 | export * from './HomePage';
2 |
--------------------------------------------------------------------------------
/apps/movie-magic-react/src/pages/HomePage/useMovies.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import { Movie } from 'movie-models';
3 |
4 | /**
5 | * Hook to fetch movies
6 | */
7 | export const useMovies = () => {
8 | const apiUrl = import.meta.env.VITE_API_URL;
9 |
10 | const [isLoading, setIsLoading] = useState(false);
11 | const [isError, setIsError] = useState(false);
12 | const [error, setError] = useState();
13 | const [movies, setMovies] = useState>([]);
14 |
15 | useEffect(() => {
16 | const fetchMovies = async () => {
17 | try {
18 | setIsLoading(true);
19 | const response = await fetch(`${apiUrl}/top-10-movies`);
20 |
21 | if (!response.ok) {
22 | const message = `Error: ${response.status}`;
23 | throw new Error(message);
24 | }
25 |
26 | const movies = await response.json();
27 | setMovies(movies);
28 | setIsLoading(false);
29 | } catch (error) {
30 | setIsError(true);
31 | setError(error);
32 | setIsLoading(false);
33 | }
34 | };
35 |
36 | fetchMovies();
37 | }, [apiUrl]);
38 | return { isLoading, isError, error, movies };
39 | };
40 |
--------------------------------------------------------------------------------
/apps/movie-magic-react/src/pages/NotFoundPage/NotFoundPage.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export const NotFoundPage = () => {
4 | return (
5 |
6 |
Page Not Found
7 |
8 | );
9 | };
10 |
--------------------------------------------------------------------------------
/apps/movie-magic-react/src/pages/NotFoundPage/index.ts:
--------------------------------------------------------------------------------
1 | export * from './NotFoundPage';
2 |
--------------------------------------------------------------------------------
/apps/movie-magic-react/src/pages/SettingsPage/SettingsContainer.tsx:
--------------------------------------------------------------------------------
1 | import { Fragment } from 'react';
2 | import { Button } from 'ui-lib';
3 | import { usePlanMembers } from './usePlanMembers';
4 |
5 | export const SettingsContainer = () => {
6 | const { isLoading, isError, error, planMembers } = usePlanMembers();
7 |
8 | if (isLoading) {
9 | return Loading...
;
10 | }
11 |
12 | if (isError) {
13 | return {(error as any).message} ;
14 | }
15 |
16 | return (
17 |
18 | Manage your Family Plan
19 |
32 |
33 | ADD TO FAMILY PLAN
34 |
35 |
36 | );
37 | };
38 |
--------------------------------------------------------------------------------
/apps/movie-magic-react/src/pages/SettingsPage/SettingsPage.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { SettingsContainer } from './SettingsContainer';
3 |
4 | export const SettingsPage = () => {
5 | return (
6 |
11 | );
12 | };
13 |
--------------------------------------------------------------------------------
/apps/movie-magic-react/src/pages/SettingsPage/index.ts:
--------------------------------------------------------------------------------
1 | export * from './SettingsPage';
2 |
--------------------------------------------------------------------------------
/apps/movie-magic-react/src/pages/SettingsPage/usePlanMembers.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import { PlanMember } from 'movie-models';
3 |
4 | /**
5 | * Hook to fetch plan members
6 | */
7 | export const usePlanMembers = () => {
8 | const apiUrl = import.meta.env.VITE_API_URL;
9 |
10 | const [isLoading, setIsLoading] = useState(false);
11 | const [isError, setIsError] = useState(false);
12 | const [error, setError] = useState();
13 | const [planMembers, setPlanMembers] = useState>([]);
14 |
15 | useEffect(() => {
16 | const fetchPlanMembers = async () => {
17 | try {
18 | setIsLoading(true);
19 | const response = await fetch(`${apiUrl}/plan-members`);
20 |
21 | if (!response.ok) {
22 | const message = `Error: ${response.status}`;
23 | throw new Error(message);
24 | }
25 |
26 | const planMembers = await response.json();
27 | setPlanMembers(planMembers);
28 | setIsLoading(false);
29 | } catch (error) {
30 | setIsError(true);
31 | setError(error);
32 | setIsLoading(false);
33 | }
34 | };
35 |
36 | fetchPlanMembers();
37 | }, [apiUrl]);
38 | return { isLoading, isError, error, planMembers };
39 | };
40 |
--------------------------------------------------------------------------------
/apps/movie-magic-react/src/pages/index.ts:
--------------------------------------------------------------------------------
1 | export * from './HomePage';
2 | export * from './NotFoundPage';
3 | export * from './SettingsPage';
4 |
--------------------------------------------------------------------------------
/apps/movie-magic-react/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/apps/movie-magic-react/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "tsconfig/vite.json",
3 | "include": ["src"],
4 | "references": [{ "path": "./tsconfig.node.json" }]
5 | }
6 |
--------------------------------------------------------------------------------
/apps/movie-magic-react/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "module": "esnext",
5 | "moduleResolution": "node"
6 | },
7 | "include": ["vite.config.ts"]
8 | }
9 |
--------------------------------------------------------------------------------
/apps/movie-magic-react/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite';
2 | import react from '@vitejs/plugin-react';
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [react()],
7 | server: {
8 | port: 3000,
9 | },
10 | });
11 |
--------------------------------------------------------------------------------
/apps/movie-magic-remix/.env:
--------------------------------------------------------------------------------
1 | API_URL=http://localhost:8080
2 | PORT=3002
3 |
--------------------------------------------------------------------------------
/apps/movie-magic-remix/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["@remix-run/eslint-config", "@remix-run/eslint-config/node"]
3 | }
4 |
--------------------------------------------------------------------------------
/apps/movie-magic-remix/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
3 | /.cache
4 | /build
5 | /public/build
6 |
--------------------------------------------------------------------------------
/apps/movie-magic-remix/app/components/Header/Header.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { CogIcon } from '@heroicons/react/outline';
3 | import { useNavigate } from '@remix-run/react';
4 |
5 | interface HeaderProps {
6 | children?: React.ReactNode;
7 | }
8 |
9 | export function Header({ children }: HeaderProps) {
10 | const navigate = useNavigate();
11 |
12 | const navigateToHome = () => {
13 | navigate('/');
14 | };
15 |
16 | const navigateToSettings = () => {
17 | navigate('/settings');
18 | };
19 |
20 | return (
21 |
22 |
28 | {children}
29 |
30 |
31 |
39 |
40 |
41 |
42 |
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/apps/movie-magic-remix/app/components/Header/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Header';
2 |
--------------------------------------------------------------------------------
/apps/movie-magic-remix/app/components/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Header';
2 |
--------------------------------------------------------------------------------
/apps/movie-magic-remix/app/entry.client.tsx:
--------------------------------------------------------------------------------
1 | import { RemixBrowser } from '@remix-run/react';
2 | import { hydrate } from 'react-dom';
3 |
4 | hydrate( , document);
5 |
--------------------------------------------------------------------------------
/apps/movie-magic-remix/app/entry.server.tsx:
--------------------------------------------------------------------------------
1 | import type { EntryContext } from '@remix-run/node';
2 | import { RemixServer } from '@remix-run/react';
3 | import { renderToString } from 'react-dom/server';
4 |
5 | export default function handleRequest(
6 | request: Request,
7 | responseStatusCode: number,
8 | responseHeaders: Headers,
9 | remixContext: EntryContext
10 | ) {
11 | let markup = renderToString(
12 |
13 | );
14 |
15 | responseHeaders.set('Content-Type', 'text/html');
16 |
17 | return new Response('' + markup, {
18 | status: responseStatusCode,
19 | headers: responseHeaders,
20 | });
21 | }
22 |
--------------------------------------------------------------------------------
/apps/movie-magic-remix/app/root.tsx:
--------------------------------------------------------------------------------
1 | import type { LinksFunction, MetaFunction } from '@remix-run/node';
2 | import {
3 | Links,
4 | LiveReload,
5 | Meta,
6 | Outlet,
7 | Scripts,
8 | ScrollRestoration,
9 | } from '@remix-run/react';
10 | import mainStylesHref from '../../../packages/ui-lib/src/styles/main.css';
11 | import stylesHref from '../styles/styles.css';
12 |
13 | export const meta: MetaFunction = () => ({
14 | charset: 'utf-8',
15 | title: 'Movie Magic | Remix',
16 | viewport: 'width=device-width,initial-scale=1',
17 | });
18 |
19 | export const links: LinksFunction = () => {
20 | return [
21 | // preload Inter font for performance (font weights 400, 500, 600)
22 | {
23 | rel: 'preload',
24 | as: 'font',
25 | href: '/fonts/inter-v7-latin-regular.woff2',
26 | type: 'font/woff2',
27 | crossOrigin: 'anonymous',
28 | },
29 | {
30 | rel: 'preload',
31 | as: 'font',
32 | href: '/fonts/inter-v7-latin-500.woff2',
33 | type: 'font/woff2',
34 | crossOrigin: 'anonymous',
35 | },
36 | {
37 | rel: 'preload',
38 | as: 'font',
39 | href: '/fonts/inter-v7-latin-600.woff2',
40 | type: 'font/woff2',
41 | crossOrigin: 'anonymous',
42 | },
43 |
44 | // load local styles
45 | { rel: 'stylesheet', href: mainStylesHref },
46 | { rel: 'stylesheet', href: stylesHref },
47 | ];
48 | };
49 |
50 | export default function App() {
51 | return (
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 | );
65 | }
66 |
--------------------------------------------------------------------------------
/apps/movie-magic-remix/app/routes/__layout.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Outlet } from '@remix-run/react';
3 | import { Header } from '../components';
4 |
5 | export default function Layout() {
6 | return (
7 |
8 |
9 |
10 |
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/apps/movie-magic-remix/app/routes/__layout/index.tsx:
--------------------------------------------------------------------------------
1 | import type { LoaderFunction } from '@remix-run/node';
2 | import { json } from '@remix-run/node';
3 | import { useLoaderData } from '@remix-run/react';
4 | import { Movie } from 'movie-models';
5 | import { MovieList } from 'ui-lib';
6 |
7 | type HomePageData = {
8 | movies: Array;
9 | };
10 |
11 | export let loader: LoaderFunction = async () => {
12 | const API_URL = process.env.API_URL;
13 | const resMovies = await fetch(`${API_URL}/top-10-movies`);
14 | const movies = await resMovies.json();
15 |
16 | let data: HomePageData = {
17 | movies,
18 | };
19 |
20 | return json(data);
21 | };
22 |
23 | export default function HomePage() {
24 | const { movies } = useLoaderData();
25 |
26 | return (
27 |
28 |
29 |
Top 10 Movies Of All Time
30 |
31 |
32 |
33 | );
34 | }
35 |
--------------------------------------------------------------------------------
/apps/movie-magic-remix/app/routes/__layout/settings.tsx:
--------------------------------------------------------------------------------
1 | import type { LoaderFunction } from '@remix-run/node';
2 | import { json } from '@remix-run/node';
3 | import { useLoaderData } from '@remix-run/react';
4 | import { PlanMember } from 'movie-models';
5 | import { Button, MovieList } from 'ui-lib';
6 |
7 | type SettingsPageData = {
8 | planMembers: Array;
9 | };
10 |
11 | export let loader: LoaderFunction = async () => {
12 | const API_URL = process.env.API_URL;
13 | const resPlanMembers = await fetch(`${API_URL}/plan-members`);
14 | const planMembers = await resPlanMembers.json();
15 |
16 | let data: SettingsPageData = {
17 | planMembers,
18 | };
19 |
20 | return json(data);
21 | };
22 |
23 | export default function SettingsPage() {
24 | const { planMembers } = useLoaderData();
25 |
26 | return (
27 |
28 |
29 |
Manage your Family Plan
30 |
47 |
48 | ADD TO FAMILY PLAN
49 |
50 |
51 |
52 | );
53 | }
54 |
--------------------------------------------------------------------------------
/apps/movie-magic-remix/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "movie-magic-remix",
3 | "private": true,
4 | "version": "0.0.0",
5 | "description": "",
6 | "license": "",
7 | "sideEffects": false,
8 | "scripts": {
9 | "build": "remix build",
10 | "dev": "remix dev",
11 | "start": "remix-serve build",
12 | "lint": "eslint app/**/*.ts*",
13 | "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf build public/build"
14 | },
15 | "dependencies": {
16 | "@heroicons/react": "^1.0.6",
17 | "@remix-run/node": "1.3.5",
18 | "@remix-run/react": "1.3.5",
19 | "@remix-run/serve": "1.3.5",
20 | "movie-models": "*",
21 | "react": "^18.0.0",
22 | "react-dom": "^18.0.0",
23 | "ui-lib": "*"
24 | },
25 | "devDependencies": {
26 | "@remix-run/dev": "1.3.5",
27 | "@remix-run/eslint-config": "1.3.5",
28 | "@types/react": "^18.0.6",
29 | "@types/react-dom": "^18.0.2",
30 | "eslint": "^8.15.0",
31 | "tsconfig": "*",
32 | "typescript": "^4.6.3"
33 | },
34 | "engines": {
35 | "node": ">=14"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/apps/movie-magic-remix/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nareshbhatia/custom-react-stack/54b84db60c34d5630b2799a8a0328c7ae9010d24/apps/movie-magic-remix/public/favicon.ico
--------------------------------------------------------------------------------
/apps/movie-magic-remix/public/fonts/inter-v7-latin-500.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nareshbhatia/custom-react-stack/54b84db60c34d5630b2799a8a0328c7ae9010d24/apps/movie-magic-remix/public/fonts/inter-v7-latin-500.woff
--------------------------------------------------------------------------------
/apps/movie-magic-remix/public/fonts/inter-v7-latin-500.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nareshbhatia/custom-react-stack/54b84db60c34d5630b2799a8a0328c7ae9010d24/apps/movie-magic-remix/public/fonts/inter-v7-latin-500.woff2
--------------------------------------------------------------------------------
/apps/movie-magic-remix/public/fonts/inter-v7-latin-600.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nareshbhatia/custom-react-stack/54b84db60c34d5630b2799a8a0328c7ae9010d24/apps/movie-magic-remix/public/fonts/inter-v7-latin-600.woff
--------------------------------------------------------------------------------
/apps/movie-magic-remix/public/fonts/inter-v7-latin-600.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nareshbhatia/custom-react-stack/54b84db60c34d5630b2799a8a0328c7ae9010d24/apps/movie-magic-remix/public/fonts/inter-v7-latin-600.woff2
--------------------------------------------------------------------------------
/apps/movie-magic-remix/public/fonts/inter-v7-latin-regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nareshbhatia/custom-react-stack/54b84db60c34d5630b2799a8a0328c7ae9010d24/apps/movie-magic-remix/public/fonts/inter-v7-latin-regular.woff
--------------------------------------------------------------------------------
/apps/movie-magic-remix/public/fonts/inter-v7-latin-regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nareshbhatia/custom-react-stack/54b84db60c34d5630b2799a8a0328c7ae9010d24/apps/movie-magic-remix/public/fonts/inter-v7-latin-regular.woff2
--------------------------------------------------------------------------------
/apps/movie-magic-remix/remix.config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @type {import('@remix-run/dev').AppConfig}
3 | */
4 | module.exports = {
5 | ignoredRouteFiles: [".*"],
6 | // appDirectory: "app",
7 | // assetsBuildDirectory: "public/build",
8 | // serverBuildPath: "build/index.js",
9 | // publicPath: "/build/",
10 | };
11 |
--------------------------------------------------------------------------------
/apps/movie-magic-remix/remix.env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
--------------------------------------------------------------------------------
/apps/movie-magic-remix/styles/styles.css:
--------------------------------------------------------------------------------
1 | /* inter-regular - latin */
2 | @font-face {
3 | font-family: 'Inter';
4 | font-style: normal;
5 | font-weight: 400;
6 |
7 | /* avoid showing invisible text - see https://web.dev/font-display/ */
8 | font-display: swap;
9 |
10 | src: url('/fonts/inter-v7-latin-regular.woff2') format('woff2'),
11 | url('/fonts/inter-v7-latin-regular.woff') format('woff');
12 | }
13 |
14 | /* inter-500 - latin */
15 | @font-face {
16 | font-family: 'Inter';
17 | font-style: normal;
18 | font-weight: 500;
19 |
20 | /* avoid showing invisible text - see https://web.dev/font-display/ */
21 | font-display: swap;
22 |
23 | src: url('/fonts/inter-v7-latin-500.woff2') format('woff2'),
24 | url('/fonts/inter-v7-latin-500.woff') format('woff');
25 | }
26 |
27 | /* inter-600 - latin */
28 | @font-face {
29 | font-family: 'Inter';
30 | font-style: normal;
31 | font-weight: 600;
32 |
33 | /* avoid showing invisible text - see https://web.dev/font-display/ */
34 | font-display: swap;
35 |
36 | src: url('/fonts/inter-v7-latin-600.woff2') format('woff2'),
37 | url('/fonts/inter-v7-latin-600.woff') format('woff');
38 | }
39 |
--------------------------------------------------------------------------------
/apps/movie-magic-remix/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "tsconfig/remix.json",
3 | "include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"],
4 | }
5 |
--------------------------------------------------------------------------------
/assets/complex-multi-repo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nareshbhatia/custom-react-stack/54b84db60c34d5630b2799a8a0328c7ae9010d24/assets/complex-multi-repo.png
--------------------------------------------------------------------------------
/assets/csr.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nareshbhatia/custom-react-stack/54b84db60c34d5630b2799a8a0328c7ae9010d24/assets/csr.png
--------------------------------------------------------------------------------
/assets/home-page.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nareshbhatia/custom-react-stack/54b84db60c34d5630b2799a8a0328c7ae9010d24/assets/home-page.png
--------------------------------------------------------------------------------
/assets/monorepo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nareshbhatia/custom-react-stack/54b84db60c34d5630b2799a8a0328c7ae9010d24/assets/monorepo.png
--------------------------------------------------------------------------------
/assets/repo-structure.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nareshbhatia/custom-react-stack/54b84db60c34d5630b2799a8a0328c7ae9010d24/assets/repo-structure.png
--------------------------------------------------------------------------------
/assets/settings-page.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nareshbhatia/custom-react-stack/54b84db60c34d5630b2799a8a0328c7ae9010d24/assets/settings-page.png
--------------------------------------------------------------------------------
/assets/simple-multi-repo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nareshbhatia/custom-react-stack/54b84db60c34d5630b2799a8a0328c7ae9010d24/assets/simple-multi-repo.png
--------------------------------------------------------------------------------
/assets/ssr.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nareshbhatia/custom-react-stack/54b84db60c34d5630b2799a8a0328c7ae9010d24/assets/ssr.png
--------------------------------------------------------------------------------
/assets/teck-stack-options.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nareshbhatia/custom-react-stack/54b84db60c34d5630b2799a8a0328c7ae9010d24/assets/teck-stack-options.png
--------------------------------------------------------------------------------
/assets/teck-stack-selection.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nareshbhatia/custom-react-stack/54b84db60c34d5630b2799a8a0328c7ae9010d24/assets/teck-stack-selection.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "custom-react-stack",
3 | "description": "A guide to building your own React stack, explaining options and tradeoffs along the way",
4 | "version": "0.0.0",
5 | "private": true,
6 | "author": "Naresh Bhatia",
7 | "license": "MIT",
8 | "workspaces": [
9 | "apps/*",
10 | "packages/*"
11 | ],
12 | "scripts": {
13 | "build": "turbo run build",
14 | "dev": "turbo run dev --parallel",
15 | "graph": "turbo run build --graph",
16 | "lint": "turbo run lint",
17 | "test": "turbo run test",
18 | "clean": "turbo run clean && rm -rf node_modules",
19 | "format": "prettier --write README.md \"**/{src,app,public}/**/*.{js,jsx,ts,tsx,json,md}\" \"**/cypress/integration/**/*\"",
20 | "storybook": "cd storybook && npm run storybook",
21 | "cypress": "cd apps/movie-magic-react && npm run cypress"
22 | },
23 | "devDependencies": {
24 | "prettier": "^2.6.2",
25 | "turbo": "latest"
26 | },
27 | "overrides": {
28 | "react": "^18.0.0",
29 | "react-dom": "^18.0.0"
30 | },
31 | "engines": {
32 | "npm": ">=7.0.0",
33 | "node": ">=14.0.0"
34 | },
35 | "packageManager": "npm@8.4.0"
36 | }
37 |
--------------------------------------------------------------------------------
/packages/eslint-config-custom/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | parser: '@typescript-eslint/parser',
3 | plugins: ['@typescript-eslint'],
4 | extends: [
5 | 'eslint:recommended',
6 | 'plugin:@typescript-eslint/recommended',
7 |
8 | // https://github.com/benmosher/eslint-plugin-import
9 | 'plugin:import/recommended',
10 | 'plugin:import/typescript',
11 |
12 | // https://prettier.io/docs/en/integrating-with-linters.html
13 | // https://github.com/prettier/eslint-config-prettier
14 | 'prettier',
15 | ],
16 | settings: {
17 | 'import/resolver': {
18 | node: {
19 | extensions: ['.js', '.jsx', '.ts', '.tsx'],
20 | },
21 | },
22 | },
23 | rules: {
24 | '@typescript-eslint/ban-ts-comment': 'off',
25 | },
26 | };
27 |
--------------------------------------------------------------------------------
/packages/eslint-config-custom/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "eslint-config-custom",
3 | "version": "0.0.1",
4 | "main": "index.js",
5 | "license": "MIT",
6 | "dependencies": {
7 | "eslint-config-prettier": "^8.5.0"
8 | },
9 | "devDependencies": {
10 | "@typescript-eslint/eslint-plugin": "^5.22.0",
11 | "@typescript-eslint/parser": "^5.22.0",
12 | "eslint-import-resolver-typescript": "^2.7.1",
13 | "eslint-plugin-import": "^2.26.0"
14 | },
15 | "peerDependencies": {
16 | "eslint": ">= 8"
17 | },
18 | "publishConfig": {
19 | "access": "public"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/packages/movie-models/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | extends: ['custom'],
4 | };
5 |
--------------------------------------------------------------------------------
/packages/movie-models/index.ts:
--------------------------------------------------------------------------------
1 | export * from './src/models/Movie';
2 | export * from './src/models/PlanMember';
3 |
--------------------------------------------------------------------------------
/packages/movie-models/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "movie-models",
3 | "version": "0.0.0",
4 | "main": "./index.ts",
5 | "license": "MIT",
6 | "scripts": {
7 | "lint": "eslint src/**/*.ts*",
8 | "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist"
9 | },
10 | "devDependencies": {
11 | "eslint": "^8.15.0",
12 | "eslint-config-custom": "*"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/packages/movie-models/src/models/Movie.ts:
--------------------------------------------------------------------------------
1 | export interface Movie {
2 | name: string;
3 | year: number;
4 | rating: number;
5 | }
6 |
--------------------------------------------------------------------------------
/packages/movie-models/src/models/PlanMember.ts:
--------------------------------------------------------------------------------
1 | export interface PlanMember {
2 | name: string;
3 | email: string;
4 | photo: string;
5 | }
6 |
--------------------------------------------------------------------------------
/packages/movie-models/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "emitDecoratorMetadata": true,
4 | "esModuleInterop": true,
5 | "experimentalDecorators": true,
6 | "forceConsistentCasingInFileNames": true,
7 | "lib": ["ES2019", "esnext.asynciterable"],
8 | "module": "commonjs",
9 | "moduleResolution": "node",
10 | "outDir": "./dist",
11 | "resolveJsonModule": true,
12 | "sourceMap": true,
13 | "strict": true,
14 | "target": "ES2019"
15 | },
16 | "include": [
17 | "src"
18 | ]
19 | }
20 |
--------------------------------------------------------------------------------
/packages/tsconfig/README.md:
--------------------------------------------------------------------------------
1 | # `tsconfig`
2 |
3 | These are base shared `tsconfig.json`s from which all other `tsconfig.json`'s
4 | inherit from.
5 |
--------------------------------------------------------------------------------
/packages/tsconfig/base.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "display": "Default",
4 | "compilerOptions": {
5 | "composite": false,
6 | "declaration": true,
7 | "declarationMap": true,
8 | "esModuleInterop": true,
9 | "forceConsistentCasingInFileNames": true,
10 | "inlineSources": false,
11 | "isolatedModules": true,
12 | "moduleResolution": "node",
13 | "noUnusedLocals": false,
14 | "noUnusedParameters": false,
15 | "preserveWatchOutput": true,
16 | "skipLibCheck": true,
17 | "strict": true
18 | },
19 | "exclude": ["node_modules"]
20 | }
21 |
--------------------------------------------------------------------------------
/packages/tsconfig/nextjs.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "display": "Next.js",
4 | "extends": "./base.json",
5 | "compilerOptions": {
6 | "target": "es5",
7 | "lib": ["dom", "dom.iterable", "esnext"],
8 | "allowJs": true,
9 | "skipLibCheck": true,
10 | "strict": true,
11 | "forceConsistentCasingInFileNames": true,
12 | "noEmit": true,
13 | "esModuleInterop": true,
14 | "module": "esnext",
15 | "moduleResolution": "node",
16 | "resolveJsonModule": true,
17 | "isolatedModules": true,
18 | "jsx": "preserve",
19 | "incremental": true
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/packages/tsconfig/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tsconfig",
3 | "version": "0.0.0",
4 | "private": true,
5 | "main": "index.js",
6 | "files": [
7 | "base.json",
8 | "nextjs.json",
9 | "react-library.json"
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/packages/tsconfig/react-library.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "display": "React Library",
4 | "extends": "./base.json",
5 | "compilerOptions": {
6 | "lib": ["ES2015", "dom"],
7 | "module": "ESNext",
8 | "target": "ES6",
9 | "jsx": "react-jsx"
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/packages/tsconfig/remix.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "display": "Remix",
4 | "extends": "./base.json",
5 | "compilerOptions": {
6 | "lib": ["DOM", "DOM.Iterable", "ES2019"],
7 | "isolatedModules": true,
8 | "esModuleInterop": true,
9 | "jsx": "react-jsx",
10 | "moduleResolution": "node",
11 | "resolveJsonModule": true,
12 | "target": "ES2019",
13 | "strict": true,
14 | "baseUrl": ".",
15 | "noEmit": true,
16 | "paths": {
17 | "~/*": ["./app/*"]
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/packages/tsconfig/vite.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "extends": "./base.json",
4 | "Display": "Vite",
5 | "compilerOptions": {
6 | "target": "ESNext",
7 | "useDefineForClassFields": true,
8 | "lib": ["DOM", "DOM.Iterable", "ESNext"],
9 | "allowJs": false,
10 | "skipLibCheck": false,
11 | "esModuleInterop": false,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "module": "ESNext",
16 | "moduleResolution": "Node",
17 | "resolveJsonModule": true,
18 | "isolatedModules": true,
19 | "noEmit": true,
20 | "jsx": "react-jsx"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/packages/ui-lib/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | extends: ['custom'],
4 | };
5 |
--------------------------------------------------------------------------------
/packages/ui-lib/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | verbose: true,
3 | testEnvironment: 'jsdom',
4 | roots: ['/src'],
5 | transform: {
6 | '^.+\\.tsx?$': 'ts-jest',
7 | },
8 | testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$',
9 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
10 | collectCoverageFrom: [
11 | 'src/**/*.{js,jsx,ts,tsx}',
12 | '!src/**/*.stories.{js,jsx,ts,tsx}',
13 | '!src/test/**',
14 | '!src/index.tsx',
15 | ],
16 | coverageThreshold: {
17 | global: {
18 | branches: 80,
19 | functions: 80,
20 | lines: 80,
21 | statements: 80,
22 | },
23 | },
24 | };
25 |
--------------------------------------------------------------------------------
/packages/ui-lib/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ui-lib",
3 | "version": "0.0.0",
4 | "main": "./dist/index.js",
5 | "module": "./dist/index.mjs",
6 | "types": "./dist/index.d.ts",
7 | "sideEffects": false,
8 | "license": "MIT",
9 | "files": [
10 | "dist/**"
11 | ],
12 | "scripts": {
13 | "test": "jest --coverage",
14 | "build": "tsup src/index.ts --format esm,cjs --dts --external react",
15 | "dev": "tsup src/index.ts --format esm,cjs --watch --dts --external react",
16 | "lint": "eslint src/**/*.ts*",
17 | "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist"
18 | },
19 | "devDependencies": {
20 | "@storybook/react": "next",
21 | "@testing-library/dom": "^8.13.0",
22 | "@testing-library/jest-dom": "^5.16.4",
23 | "@testing-library/react": "^13.1.1",
24 | "@testing-library/user-event": "^14.1.1",
25 | "@types/jest": "^27.4.1",
26 | "@types/node": "^17.0.25",
27 | "@types/react": "^18.0.6",
28 | "@types/react-dom": "^18.0.2",
29 | "eslint": "^8.15.0",
30 | "eslint-config-custom": "*",
31 | "jest": "^27.5.1",
32 | "movie-models": "*",
33 | "react": "^18.0.0",
34 | "ts-jest": "^27.1.4",
35 | "tsconfig": "*",
36 | "tsup": "^5.12.5",
37 | "typescript": "^4.6.3"
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/packages/ui-lib/src/components/Button/Button.stories.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { Story, Meta } from '@storybook/react';
3 | import { Button } from './Button';
4 |
5 | export default {
6 | title: 'Components/Button',
7 | component: Button,
8 | argTypes: { onClick: { action: 'clicked' } },
9 | } as Meta;
10 |
11 | const Template: Story = (args) => (
12 |
20 | {args.text}
21 |
22 | );
23 |
24 | export const ButtonStory = Template.bind({});
25 | ButtonStory.storyName = 'Button';
26 | ButtonStory.args = {
27 | text: 'Button',
28 | rootClass: '',
29 | color: 'primary',
30 | disabled: false,
31 | size: 'medium',
32 | variant: 'contained',
33 | };
34 |
35 | export const ButtonVariationsStory = () => {
36 | return (
37 |
38 |
Buttons
39 |
40 |
Contained Buttons
41 |
42 | Default
43 |
44 | Primary
45 |
46 |
47 | Secondary
48 |
49 |
50 | Disabled
51 |
52 |
53 |
54 |
Outlined Buttons
55 |
56 | Default
57 |
58 | Primary
59 |
60 |
61 | Secondary
62 |
63 |
64 | Disabled
65 |
66 |
67 |
68 |
Sizes
69 |
70 |
71 | Small
72 |
73 |
79 | Medium
80 |
81 |
87 | Large
88 |
89 |
90 |
91 |
92 | Small
93 |
94 |
100 | Medium
101 |
102 |
108 | Large
109 |
110 |
111 |
112 | );
113 | };
114 | ButtonVariationsStory.storyName = 'Button Variations';
115 |
--------------------------------------------------------------------------------
/packages/ui-lib/src/components/Button/Button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | interface ButtonProps {
4 | rootClass?: string;
5 | color?: 'default' | 'primary' | 'secondary';
6 | disabled?: boolean;
7 | size?: 'small' | 'medium' | 'large';
8 | variant?: 'contained' | 'outlined';
9 | children?: React.ReactNode;
10 | onClick?: React.MouseEventHandler;
11 | }
12 |
13 | export const Button = ({
14 | rootClass,
15 | color = 'default',
16 | disabled = false,
17 | size = 'medium',
18 | variant = 'contained',
19 | children,
20 | onClick,
21 | }: ButtonProps) => {
22 | const classes = [];
23 |
24 | if (rootClass) {
25 | classes.push(rootClass);
26 | }
27 |
28 | classes.push('button');
29 |
30 | switch (variant) {
31 | case 'contained':
32 | classes.push('button--contained');
33 | if (disabled) {
34 | classes.push('button--disabled');
35 | classes.push('button--disabled-contained');
36 | } else {
37 | switch (color) {
38 | case 'default': {
39 | classes.push('button--contained-default');
40 | break;
41 | }
42 | case 'primary': {
43 | classes.push('button--contained-primary');
44 | break;
45 | }
46 | case 'secondary': {
47 | classes.push('button--contained-secondary');
48 | break;
49 | }
50 | }
51 | }
52 | break;
53 | case 'outlined':
54 | classes.push('button--outlined');
55 | if (disabled) {
56 | classes.push('button--disabled');
57 | classes.push('button--disabled-outlined');
58 | } else {
59 | switch (color) {
60 | case 'default': {
61 | classes.push('button--outlined-default');
62 | break;
63 | }
64 | case 'primary': {
65 | classes.push('button--outlined-primary');
66 | break;
67 | }
68 | case 'secondary': {
69 | classes.push('button--outlined-secondary');
70 | break;
71 | }
72 | }
73 | }
74 | break;
75 | }
76 |
77 | switch (size) {
78 | case 'small': {
79 | classes.push('button--small');
80 | break;
81 | }
82 | case 'medium': {
83 | classes.push('button--medium');
84 | break;
85 | }
86 | case 'large': {
87 | classes.push('button--large');
88 | break;
89 | }
90 | }
91 |
92 | return (
93 |
94 | {children}
95 |
96 | );
97 | };
98 |
--------------------------------------------------------------------------------
/packages/ui-lib/src/components/Button/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Button';
2 |
--------------------------------------------------------------------------------
/packages/ui-lib/src/components/MovieList/MovieList.stories.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { Story, Meta } from '@storybook/react';
3 | import { MovieList } from './MovieList';
4 |
5 | export default {
6 | title: 'Components/MovieList',
7 | component: MovieList,
8 | } as Meta;
9 |
10 | const Template: Story = (args) => (
11 |
12 |
13 |
14 | );
15 |
16 | export const MovieListStory = Template.bind({});
17 | MovieListStory.storyName = 'MovieList';
18 | MovieListStory.args = {
19 | movies: [
20 | {
21 | name: 'The Shawshank Redemption',
22 | year: 1994,
23 | rating: 9.3,
24 | },
25 | {
26 | name: 'The Godfather',
27 | year: 1972,
28 | rating: 9.2,
29 | },
30 | {
31 | name: 'The Godfather: Part II',
32 | year: 1974,
33 | rating: 9.0,
34 | },
35 | ],
36 | };
37 |
--------------------------------------------------------------------------------
/packages/ui-lib/src/components/MovieList/MovieList.test.tsx:
--------------------------------------------------------------------------------
1 | import { render, screen } from '../../test/test-utils';
2 | import { MovieList } from './MovieList';
3 |
4 | const movies = [
5 | {
6 | name: 'The Shawshank Redemption',
7 | year: 1994,
8 | rating: 9.3,
9 | },
10 | {
11 | name: 'The Godfather',
12 | year: 1972,
13 | rating: 9.2,
14 | },
15 | {
16 | name: 'The Godfather: Part II',
17 | year: 1974,
18 | rating: 9.0,
19 | },
20 | ];
21 |
22 | describe(' ', () => {
23 | test('renders correctly', async () => {
24 | render( );
25 |
26 | // expect 10 movies
27 | const movieTable = await screen.findByTestId('movie-table');
28 | const movieRows = movieTable.querySelectorAll('tbody tr');
29 | expect(movieRows.length).toBe(movies.length);
30 | });
31 | });
32 |
--------------------------------------------------------------------------------
/packages/ui-lib/src/components/MovieList/MovieList.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { Movie } from 'movie-models';
3 | import { Button } from '../Button';
4 |
5 | interface MovieListProps {
6 | movies: Array;
7 | }
8 |
9 | export const MovieList = ({ movies }: MovieListProps) => {
10 | return (
11 |
12 |
13 |
14 | Rank
15 | Name
16 | Year
17 | Rating
18 |
19 |
20 |
21 |
22 | {movies.map((movie, index) => (
23 |
24 | {index + 1}
25 | {movie.name}
26 | {movie.year}
27 | {movie.rating.toFixed(1)}
28 |
29 | Watch
30 |
31 |
32 | ))}
33 |
34 |
35 | );
36 | };
37 |
--------------------------------------------------------------------------------
/packages/ui-lib/src/components/MovieList/index.ts:
--------------------------------------------------------------------------------
1 | export * from './MovieList';
2 |
--------------------------------------------------------------------------------
/packages/ui-lib/src/index.ts:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | export * from './components/Button';
3 | export * from './components/MovieList';
4 |
--------------------------------------------------------------------------------
/packages/ui-lib/src/styles/Typography.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Meta } from '@storybook/react';
3 |
4 | export default {
5 | title: 'Style Guide/Typography',
6 | } as Meta;
7 |
8 | export const TypographyStory = () => (
9 |
10 |
h1. Heading 1
11 |
h2. Heading 2
12 |
h3. Heading 3
13 |
h4. Heading 4
14 |
h5. Heading 5
15 |
h6. Heading 6
16 |
h6sm. Heading 6sm
17 |
h6xs. Heading 6xs
18 |
19 | body1. Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quos
20 | blanditiis tenetur unde suscipit, quam beatae rerum inventore consectetur,
21 | neque doloribus, cupiditate numquam dignissimos laborum fugiat deleniti?
22 | Eum quasi quidem quibusdam.
23 |
24 |
25 | body2. Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quos
26 | blanditiis tenetur unde suscipit, quam beatae rerum inventore consectetur,
27 | neque doloribus, cupiditate numquam dignissimos laborum fugiat deleniti?
28 | Eum quasi quidem quibusdam.
29 |
30 |
31 | );
32 | TypographyStory.storyName = 'Typography';
33 |
--------------------------------------------------------------------------------
/packages/ui-lib/src/styles/main.css:
--------------------------------------------------------------------------------
1 | /*
2 | This is a sample stylesheet to demonstrate our components without
3 | creating dependencies on external styling libraries. It is inspired
4 | by tailwindcss.
5 |
6 | Please replace this stylesheet with your choice of styling mechanism.
7 | */
8 |
9 | html {
10 | box-sizing: border-box;
11 | }
12 |
13 | *,
14 | *:before,
15 | *:after {
16 | box-sizing: inherit;
17 | }
18 |
19 | body {
20 | color: var(--semantic-color-text-primary);
21 | background-color: var(--semantic-color-background-default);
22 | margin: 0;
23 | font-family: var(--semantic-font-family-sans);
24 | font-size: 1rem;
25 | font-weight: 400;
26 | line-height: 1.6;
27 | -webkit-font-smoothing: antialiased;
28 | -moz-osx-font-smoothing: grayscale;
29 | }
30 |
31 | h1,
32 | h2,
33 | h3,
34 | h4,
35 | h5,
36 | h6,
37 | p {
38 | margin: 0;
39 | }
40 |
41 | ol, ul {
42 | list-style: none;
43 | margin: 0;
44 | padding: 0;
45 | }
46 |
47 | /* ----- Interactivity ----- */
48 |
49 | table {
50 | width: 100%;
51 | border: none;
52 | border-collapse: collapse;
53 | border-spacing: 0;
54 | text-align: left;
55 | }
56 |
57 | table tr:nth-of-type(2n) {
58 | background-color: var(--color-blue-grey-50);
59 | }
60 |
61 | td,
62 | th {
63 | vertical-align: middle;
64 | padding: var(--px-12) var(--px-4);
65 | }
66 |
67 | thead {
68 | border-bottom: var(--px-2) solid var(--color-blue-grey-100);
69 | }
70 |
71 | .text-center {
72 | text-align: center;
73 | }
74 |
75 | /* ----- Interactivity ----- */
76 |
77 | .cursor-pointer {
78 | cursor: pointer;
79 | }
80 |
81 | /* ----- Flexbox ----- */
82 |
83 | .flex {
84 | display: flex;
85 | }
86 |
87 | .flex-row {
88 | flex-direction: row;
89 | }
90 |
91 | .flex-col {
92 | flex-direction: column;
93 | }
94 |
95 | .flex-1 {
96 | flex: 1;
97 | }
98 |
99 | .flex-2 {
100 | flex: 2;
101 | }
102 |
103 | .flex-grow-1 {
104 | flex-grow: 1;
105 | }
106 |
107 | .flex-grow-2 {
108 | flex-grow: 2;
109 | }
110 |
111 | .justify-start {
112 | justify-content: flex-start;
113 | }
114 |
115 | .justify-end {
116 | justify-content: flex-end;
117 | }
118 |
119 | .justify-center {
120 | justify-content: center;
121 | }
122 |
123 | .justify-between {
124 | justify-content: space-between;
125 | }
126 |
127 | .justify-around {
128 | justify-content: space-around;
129 | }
130 |
131 | .justify-evenly {
132 | justify-content: space-evenly;
133 | }
134 |
135 | .items-start {
136 | align-items: flex-start;
137 | }
138 |
139 | .items-end {
140 | align-items: flex-end;
141 | }
142 |
143 | .items-center {
144 | align-items: center;
145 | }
146 |
147 | .items-baseline {
148 | align-items: baseline;
149 | }
150 |
151 | .items-stretch {
152 | align-items: stretch;
153 | }
154 |
155 | /* ----- Margin ----- */
156 |
157 | .m-0 {
158 | margin: 0;
159 | }
160 |
161 | .m-1 {
162 | margin: var(--sp-1);
163 | }
164 |
165 | .m-2 {
166 | margin: var(--sp-2);
167 | }
168 |
169 | .m-3 {
170 | margin: var(--sp-3);
171 | }
172 |
173 | .m-4 {
174 | margin: var(--sp-4);
175 | }
176 |
177 | .mt-0 {
178 | margin-top: 0;
179 | }
180 |
181 | .mt-1 {
182 | margin-top: var(--sp-1);
183 | }
184 |
185 | .mt-2 {
186 | margin-top: var(--sp-2);
187 | }
188 |
189 | .mt-3 {
190 | margin-top: var(--sp-3);
191 | }
192 |
193 | .mt-4 {
194 | margin-top: var(--sp-4);
195 | }
196 |
197 | .mb-0 {
198 | margin-bottom: 0;
199 | }
200 |
201 | .mb-1 {
202 | margin-bottom: var(--sp-1);
203 | }
204 |
205 | .mb-2 {
206 | margin-bottom: var(--sp-2);
207 | }
208 |
209 | .mb-3 {
210 | margin-bottom: var(--sp-3);
211 | }
212 |
213 | .mb-4 {
214 | margin-bottom: var(--sp-4);
215 | }
216 |
217 | .ml-0 {
218 | margin-left: 0;
219 | }
220 |
221 | .ml-1 {
222 | margin-left: var(--sp-1);
223 | }
224 |
225 | .ml-2 {
226 | margin-left: var(--sp-2);
227 | }
228 |
229 | .ml-3 {
230 | margin-left: var(--sp-3);
231 | }
232 |
233 | .ml-4 {
234 | margin-left: var(--sp-4);
235 | }
236 |
237 | .mr-0 {
238 | margin-right: 0;
239 | }
240 |
241 | .mr-1 {
242 | margin-right: var(--sp-1);
243 | }
244 |
245 | .mr-2 {
246 | margin-right: var(--sp-2);
247 | }
248 |
249 | .mr-3 {
250 | margin-right: var(--sp-3);
251 | }
252 |
253 | .mr-4 {
254 | margin-right: var(--sp-4);
255 | }
256 |
257 | /* ----- Padding ----- */
258 |
259 | .p-0 {
260 | padding: 0;
261 | }
262 |
263 | .p-1 {
264 | padding: var(--sp-1);
265 | }
266 |
267 | .p-2 {
268 | padding: var(--sp-2);
269 | }
270 |
271 | .p-3 {
272 | padding: var(--sp-3);
273 | }
274 |
275 | .p-4 {
276 | padding: var(--sp-4);
277 | }
278 |
279 | .py-1 {
280 | padding-top: var(--space-pt-4);
281 | padding-bottom: var(--space-pb-4);
282 | }
283 |
284 | .py-2 {
285 | padding-top: var(--space-pt-8);
286 | padding-bottom: var(--space-pb-8);
287 | }
288 |
289 | .py-3 {
290 | padding-top: var(--space-pt-12);
291 | padding-bottom: var(--space-pb-12);
292 | }
293 |
294 | .py-4 {
295 | padding-top: var(--space-pt-16);
296 | padding-bottom: var(--space-pb-16);
297 | }
298 |
299 | /* ----- Border ----- */
300 |
301 | .border-none {
302 | border-width: var(--semantic-border-width-none);
303 | }
304 |
305 | .border-bottom {
306 | border-bottom: var(--px-1) solid var(--color-blue-grey-100);
307 | }
308 |
309 | .rounded-full {
310 | border-radius: var(--border-radius-full);
311 | }
312 |
313 | /* ----- Typography ----- */
314 |
315 | .font-sans {
316 | font-family: var(--semantic-font-family-sans);
317 | }
318 |
319 | .text-xs {
320 | font-size: var(--font-size-xs);
321 | }
322 |
323 | .text-sm {
324 | font-size: var(--font-size-sm);
325 | }
326 |
327 | .text-base {
328 | font-size: var(--font-size-base);
329 | }
330 |
331 | .font-light {
332 | font-weight: var(--font-weight-light);
333 | }
334 |
335 | .font-regular {
336 | font-weight: var(--font-weight-regular);
337 | }
338 |
339 | .font-medium {
340 | font-weight: var(--font-weight-medium);
341 | }
342 |
343 | .line-height-none {
344 | line-height: var(--line-height-none) !important;
345 | }
346 |
347 | .h1 {
348 | font-family: var(--semantic-font-family-sans);
349 | font-size: var(--semantic-font-size-h1);
350 | font-weight: var(--font-weight-light);
351 | line-height: var(--semantic-line-height-h1);
352 | letter-spacing: var(--semantic-letter-spacing-h1);
353 | }
354 |
355 | .h2 {
356 | font-family: var(--semantic-font-family-sans);
357 | font-size: var(--semantic-font-size-h2);
358 | font-weight: var(--font-weight-light);
359 | line-height: var(--semantic-line-height-h2);
360 | letter-spacing: var(--semantic-letter-spacing-h2);
361 | }
362 |
363 | .h3 {
364 | font-family: var(--semantic-font-family-sans);
365 | font-size: var(--semantic-font-size-h3);
366 | font-weight: var(--font-weight-regular);
367 | line-height: var(--semantic-line-height-h3);
368 | letter-spacing: var(--letter-spacing-none);
369 | }
370 |
371 | .h4 {
372 | font-family: var(--semantic-font-family-sans);
373 | font-size: var(--semantic-font-size-h4);
374 | font-weight: var(--font-weight-regular);
375 | line-height: var(--semantic-line-height-h4);
376 | letter-spacing: var(--semantic-letter-spacing-h4);
377 | }
378 |
379 | .h5 {
380 | font-family: var(--semantic-font-family-sans);
381 | font-size: var(--semantic-font-size-h5);
382 | font-weight: var(--font-weight-regular);
383 | line-height: var(--semantic-line-height-h5);
384 | letter-spacing: var(--letter-spacing-none);
385 | }
386 |
387 | .h6 {
388 | font-family: var(--semantic-font-family-sans);
389 | font-size: var(--semantic-font-size-h6);
390 | font-weight: var(--font-weight-medium);
391 | line-height: var(--semantic-line-height-h6);
392 | letter-spacing: var(--semantic-letter-spacing-h6);
393 | }
394 |
395 | .h6sm {
396 | font-family: var(--semantic-font-family-sans);
397 | font-size: var(--semantic-font-size-h6sm);
398 | font-weight: var(--font-weight-medium);
399 | line-height: var(--semantic-line-height-h6);
400 | letter-spacing: var(--semantic-letter-spacing-h6);
401 | }
402 |
403 | .h6xs {
404 | font-family: var(--semantic-font-family-sans);
405 | font-size: var(--semantic-font-size-h6xs);
406 | font-weight: var(--font-weight-medium);
407 | line-height: var(--semantic-line-height-h6);
408 | letter-spacing: var(--semantic-letter-spacing-h6);
409 | }
410 |
411 | .body1 {
412 | font-family: var(--semantic-font-family-sans);
413 | font-size: var(--semantic-font-size-body1);
414 | font-weight: var(--font-weight-regular);
415 | line-height: var(--semantic-line-height-body1);
416 | letter-spacing: var(--semantic-letter-spacing-body1);
417 | }
418 |
419 | .body2 {
420 | font-family: var(--semantic-font-family-sans);
421 | font-size: var(--semantic-font-size-body2);
422 | font-weight: var(--font-weight-regular);
423 | line-height: var(--semantic-line-height-body2);
424 | letter-spacing: var(--semantic-letter-spacing-body2);
425 | }
426 |
427 | /* ----- Backgrounds ----- */
428 |
429 | .bg-default {
430 | background-color: var(--semantic-color-background-default);
431 | }
432 |
433 | .bg-paper {
434 | background-color: var(--semantic-color-background-paper);
435 | }
436 |
437 | .bg-primary-light {
438 | background-color: var(--semantic-color-primary-light);
439 | }
440 |
441 | .bg-primary-main {
442 | background-color: var(--semantic-color-primary-main);
443 | }
444 |
445 | .bg-primary-dark {
446 | background-color: var(--semantic-color-primary-dark);
447 | }
448 |
449 | .bg-secondary-light {
450 | background-color: var(--semantic-color-secondary-light);
451 | }
452 |
453 | .bg-secondary-main {
454 | background-color: var(--semantic-color-secondary-main);
455 | }
456 |
457 | .bg-secondary-dark {
458 | background-color: var(--semantic-color-secondary-dark);
459 | }
460 |
461 | /* ----- Text Colors ----- */
462 |
463 | .text-primary {
464 | color: var(--semantic-color-text-primary);
465 | }
466 |
467 | .text-secondary {
468 | color: var(--semantic-color-text-secondary);
469 | }
470 |
471 | .text-primary-contrast {
472 | color: var(--semantic-color-primary-contrast-text);
473 | }
474 |
475 | .text-secondary-contrast {
476 | color: var(--semantic-color-secondary-contrast-text);
477 | }
478 |
479 | /* ----- Icon Colors ----- */
480 |
481 | .primary-icon-muted {
482 | color: var(--semantic-color-primary-icon-muted);
483 | }
484 |
485 | .primary-icon-bright {
486 | color: var(--semantic-color-primary-icon-bright);
487 | }
488 |
489 | /* ----- Components ----- */
490 |
491 | .appbar {
492 | display: flex;
493 | flex-direction: row;
494 | align-items: center;
495 | padding: 0 var(--sp-3);
496 | min-height: var(--px-48);
497 | background-color: var(--semantic-color-primary-main);
498 | color: var(--semantic-color-primary-contrast-text);
499 | box-shadow: var(--box-shadow-2);
500 | }
501 |
502 | .card {
503 | background-color: var(--semantic-color-background-paper);
504 | border: var(--semantic-border-width-default) solid var(--card-border-color);
505 | border-radius: var(--card-border-radius);
506 | box-shadow: var(--box-shadow-1);
507 | }
508 |
509 | .button {
510 | font-family: inherit;
511 | font-weight: var(--font-weight-medium);
512 | cursor: pointer;
513 | border-radius: var(--button-border-radius);
514 | }
515 |
516 | .button--small {
517 | font-size: var(--font-size-xs);
518 | line-height: var(--line-height-16);
519 | padding: var(--space-pt-6) var(--space-pr-10) var(--space-pb-6)
520 | var(--space-pl-10);
521 | }
522 |
523 | .button--medium {
524 | font-size: var(--font-size-sm);
525 | line-height: var(--line-height-16);
526 | padding: var(--space-pt-8) var(--space-pr-12) var(--space-pb-8)
527 | var(--space-pl-12);
528 | }
529 |
530 | .button--large {
531 | font-size: var(--font-size-sm);
532 | line-height: var(--line-height-20);
533 | padding: var(--space-pt-8) var(--space-pr-16) var(--space-pb-8)
534 | var(--space-pl-16);
535 | }
536 |
537 | .button--contained {
538 | border-width: var(--semantic-border-width-default);
539 | border-color: var(--color-transparent);
540 | }
541 |
542 | .button--contained-default {
543 | color: var(--button-contained-default-text-color);
544 | background-color: var(--button-contained-default-background-color);
545 | }
546 |
547 | .button--contained-default:hover {
548 | background-color: var(--button-contained-default-state-hover);
549 | }
550 |
551 | .button--contained-primary {
552 | color: var(--semantic-color-primary-contrast-text);
553 | background-color: var(--semantic-color-primary-main);
554 | }
555 |
556 | .button--contained-primary:hover {
557 | background-color: var(--button-contained-primary-state-hover);
558 | }
559 |
560 | .button--contained-secondary {
561 | color: var(--semantic-color-secondary-contrast-text);
562 | background-color: var(--semantic-color-secondary-main);
563 | }
564 |
565 | .button--contained-secondary:hover {
566 | background-color: var(--button-contained-secondary-state-hover);
567 | }
568 |
569 | .button--outlined {
570 | background-color: var(--color-transparent);
571 | border-width: var(--semantic-border-width-default);
572 | border-style: solid;
573 | }
574 |
575 | .button--outlined-default {
576 | color: var(--semantic-color-text-primary);
577 | border-color: var(--button-outlined-default-border-color);
578 | }
579 |
580 | .button--outlined-default:hover {
581 | background-color: var(--button-outlined-default-state-hover);
582 | }
583 |
584 | .button--outlined-primary {
585 | color: var(--semantic-color-primary-main);
586 | border-color: var(--semantic-color-primary-main);
587 | }
588 |
589 | .button--outlined-primary:hover {
590 | background-color: var(--button-outlined-primary-state-hover);
591 | }
592 |
593 | .button--outlined-secondary {
594 | color: var(--semantic-color-secondary-dark);
595 | border-color: var(--semantic-color-secondary-dark);
596 | }
597 |
598 | .button--outlined-secondary:hover {
599 | background-color: var(--button-outlined-secondary-state-hover);
600 | }
601 |
602 | .button--disabled {
603 | cursor: default;
604 | pointer-events: none;
605 | }
606 |
607 | .button--disabled-contained {
608 | color: var(--semantic-color-state-disabled-text);
609 | background-color: var(--semantic-color-state-disabled);
610 | }
611 |
612 | .button--disabled-outlined {
613 | color: var(--semantic-color-state-disabled-text);
614 | border-color: var(--semantic-color-state-disabled);
615 | }
616 |
617 | /* ----- Layouts ----- */
618 |
619 | .h-24 {
620 | height: 24px;
621 | }
622 |
623 | .h-40 {
624 | height: 40px;
625 | }
626 |
627 | .h-86 {
628 | height: 86px;
629 | }
630 |
631 | .h-114 {
632 | height: 114px;
633 | }
634 |
635 | .h-400 {
636 | height: 400px;
637 | }
638 |
639 | .w-24 {
640 | width: 24px;
641 | }
642 |
643 | .w-40 {
644 | width: 40px;
645 | }
646 |
647 | .max-w-375 {
648 | max-width: 375px;
649 | }
650 |
651 | .max-w-800 {
652 | max-width: 800px;
653 | }
654 |
655 | .grid-2-col {
656 | display: grid;
657 | grid-template-columns: 1fr 1fr;
658 | -moz-column-gap: 16px;
659 | column-gap: 16px;
660 | row-gap: 16px;
661 | }
662 |
663 | /* ----- CSS Variables ----- */
664 |
665 | :root {
666 | /* ----- pixel to rem conversion based on 16px browser default size ----- */
667 | --px-1: 0.0625rem;
668 | --px-2: 0.125rem;
669 | --px-4: 0.25rem;
670 | --px-6: 0.375rem;
671 | --px-8: 0.5rem;
672 | --px-10: 0.625rem;
673 | --px-12: 0.75rem;
674 | --px-14: 0.875rem;
675 | --px-16: 1rem;
676 | --px-18: 1.125rem;
677 | --px-20: 1.25rem;
678 | --px-22: 1.375rem;
679 | --px-24: 1.5rem;
680 | --px-26: 1.625rem;
681 | --px-28: 1.75rem;
682 | --px-30: 1.875rem;
683 | --px-32: 2rem;
684 | --px-34: 2.125rem;
685 | --px-36: 2.25rem;
686 | --px-38: 2.375rem;
687 | --px-40: 2.5rem;
688 | --px-48: 3rem;
689 |
690 | /* ----- Spacing ----- */
691 | --sp-1: var(--px-8);
692 | --sp-2: var(--px-16);
693 | --sp-3: var(--px-24);
694 | --sp-4: var(--px-32);
695 | --sp-5: var(--px-40);
696 | --sp-6: var(--px-48);
697 |
698 | --space-pt-0: 0rem;
699 | --space-pt-1: 0.0625rem;
700 | --space-pt-2: 0.125rem;
701 | --space-pt-4: 0.25rem;
702 | --space-pt-6: 0.375rem;
703 | --space-pt-8: 0.5rem;
704 | --space-pt-10: 0.625rem;
705 | --space-pt-12: 0.75rem;
706 | --space-pt-14: 0.875rem;
707 | --space-pt-16: 1rem;
708 | --space-pl-0: 0rem;
709 | --space-pl-1: 0.0625rem;
710 | --space-pl-2: 0.125rem;
711 | --space-pl-4: 0.25rem;
712 | --space-pl-6: 0.375rem;
713 | --space-pl-8: 0.5rem;
714 | --space-pl-10: 0.625rem;
715 | --space-pl-12: 0.75rem;
716 | --space-pl-14: 0.875rem;
717 | --space-pl-16: 1rem;
718 | --space-pb-0: 0rem;
719 | --space-pb-1: 0.0625rem;
720 | --space-pb-2: 0.125rem;
721 | --space-pb-4: 0.25rem;
722 | --space-pb-6: 0.375rem;
723 | --space-pb-8: 0.5rem;
724 | --space-pb-10: 0.625rem;
725 | --space-pb-12: 0.75rem;
726 | --space-pb-14: 0.875rem;
727 | --space-pb-16: 1rem;
728 | --space-pr-0: 0rem;
729 | --space-pr-1: 0.0625rem;
730 | --space-pr-2: 0.125rem;
731 | --space-pr-4: 0.25rem;
732 | --space-pr-6: 0.375rem;
733 | --space-pr-8: 0.5rem;
734 | --space-pr-10: 0.625rem;
735 | --space-pr-12: 0.75rem;
736 | --space-pr-14: 0.875rem;
737 | --space-pr-16: 1rem;
738 |
739 | /* ----- Box Shadows ----- */
740 | --box-shadow-1: 0px 11px 4px 0px #00000003, 0px 6px 4px 0px #0000000d,
741 | 0px 3px 3px 0px #00000014, 0px 1px 1px 0px #00000017,
742 | 0px 0px 0px 0px #0000001a;
743 | --box-shadow-2: 0px 2px 4px -1px #00000033, 0px 4px 5px 0px #00000024,
744 | 0px 1px 10px 0px #0000001f;
745 |
746 | /* ----- Borders ----- */
747 | --border-radius-2: 2px;
748 | --border-radius-4: 4px;
749 | --border-radius-6: 6px;
750 | --border-radius-8: 8px;
751 | --border-radius-full: 9999px;
752 | --border-radius-none: 0px;
753 |
754 | --border-width-0: 0px;
755 | --border-width-1: 1px;
756 | --border-width-2: 2px;
757 | --border-width-4: 4px;
758 | --border-width-8: 8px;
759 |
760 | /* ----- Color Palette ----- */
761 | --color-black: #000000;
762 | --color-white: #ffffff;
763 | --color-transparent: #00000000;
764 |
765 | --color-grey-50: #fafafa;
766 | --color-grey-100: #f5f5f5;
767 | --color-grey-200: #eeeeee;
768 | --color-grey-300: #e0e0e0;
769 | --color-grey-400: #bdbdbd;
770 | --color-grey-500: #9e9e9e;
771 | --color-grey-600: #757575;
772 | --color-grey-700: #616161;
773 | --color-grey-800: #424242;
774 | --color-grey-900: #212121;
775 |
776 | --color-grey-custom-50: #f1f2f6;
777 | --color-grey-custom-500: #4b535f;
778 | --color-grey-custom-600: #1f2937;
779 | --color-grey-custom-700: #1e1e1e;
780 | --color-grey-custom-800: #151c26;
781 | --color-grey-custom-900: #121212;
782 |
783 | --color-blue-50: #dceefb;
784 | --color-blue-100: #b6e0fe;
785 | --color-blue-200: #84c5f4;
786 | --color-blue-300: #62b0e8;
787 | --color-blue-400: #4098d7;
788 | --color-blue-500: #2680c2;
789 | --color-blue-600: #186faf;
790 | --color-blue-700: #0f609b;
791 | --color-blue-800: #0a558c;
792 | --color-blue-900: #003e6b;
793 |
794 | --color-blue-grey-50: #f0f4f8;
795 | --color-blue-grey-100: #d9e2ec;
796 | --color-blue-grey-200: #bcccdc;
797 | --color-blue-grey-300: #9fb3c8;
798 | --color-blue-grey-400: #829ab1;
799 | --color-blue-grey-500: #627d98;
800 | --color-blue-grey-600: #486581;
801 | --color-blue-grey-700: #334e68;
802 | --color-blue-grey-800: #243b53;
803 | --color-blue-grey-900: #102a43;
804 |
805 | --color-yellow-50: #fffbea;
806 | --color-yellow-100: #fff3c4;
807 | --color-yellow-200: #fce588;
808 | --color-yellow-300: #fadb5f;
809 | --color-yellow-400: #f7c948;
810 | --color-yellow-500: #f0b429;
811 | --color-yellow-600: #de911d;
812 | --color-yellow-700: #cb6e17;
813 | --color-yellow-800: #b44d12;
814 | --color-yellow-900: #8d2b0b;
815 |
816 | /* ----- Typography ----- */
817 | --font-family-fira-sans: Fira Sans;
818 | --font-family-inter: Inter;
819 |
820 | --letter-spacing-none: 0em;
821 |
822 | --line-height-12: 0.75rem;
823 | --line-height-16: 1rem;
824 | --line-height-20: 1.25rem;
825 | --line-height-24: 1.5rem;
826 | --line-height-28: 1.75rem;
827 | --line-height-32: 2rem;
828 | --line-height-36: 2.25rem;
829 | --line-height-40: 2.5rem;
830 | --line-height-none: 1;
831 |
832 | --font-size-xs: 0.75rem;
833 | --font-size-sm: 0.875rem;
834 | --font-size-base: 1rem;
835 | --font-size-lg: 1.125rem;
836 | --font-size-xl: 1.25rem;
837 | --font-size-2xl: 1.5rem;
838 | --font-size-3xl: 1.875rem;
839 | --font-size-4xl: 2.25rem;
840 | --font-size-5xl: 3rem;
841 | --font-size-6xl: 3.75rem;
842 | --font-size-7xl: 4.5rem;
843 | --font-size-8xl: 6rem;
844 | --font-size-9xl: 8rem;
845 |
846 | --font-weight-light: 300;
847 | --font-weight-regular: 400;
848 | --font-weight-medium: 500;
849 |
850 | --text-case-none: none;
851 | --text-decoration-none: none;
852 |
853 | /* ----- Semantic Layer ----- */
854 | --semantic-border-width-none: var(--border-width-0);
855 | --semantic-border-width-default: var(--border-width-1);
856 |
857 | --semantic-font-family-sans: var(--font-family-inter);
858 |
859 | --semantic-line-height-h1: 1.167;
860 | --semantic-line-height-h2: 1.2;
861 | --semantic-line-height-h3: 1.167;
862 | --semantic-line-height-h4: 1.235;
863 | --semantic-line-height-h5: 1.334;
864 | --semantic-line-height-h6: 1.6;
865 | --semantic-line-height-body1: 1.5;
866 | --semantic-line-height-body2: 1.43;
867 |
868 | --semantic-font-size-h1: var(--font-size-8xl);
869 | --semantic-font-size-h2: var(--font-size-6xl);
870 | --semantic-font-size-h3: var(--font-size-5xl);
871 | --semantic-font-size-h4: var(--font-size-4xl);
872 | --semantic-font-size-h5: var(--font-size-2xl);
873 | --semantic-font-size-h6: var(--font-size-xl);
874 | --semantic-font-size-h6sm: var(--font-size-lg);
875 | --semantic-font-size-h6xs: var(--font-size-base);
876 | --semantic-font-size-body1: var(--font-size-base);
877 | --semantic-font-size-body2: var(--font-size-sm);
878 |
879 | --semantic-letter-spacing-h1: -0.01562em;
880 | --semantic-letter-spacing-h2: -0.00833em;
881 | --semantic-letter-spacing-h4: 0.00735em;
882 | --semantic-letter-spacing-h6: 0.0075em;
883 | --semantic-letter-spacing-body1: 0.00938em;
884 | --semantic-letter-spacing-body2: 0.01073em;
885 |
886 | --semantic-color-primary-light: var(--color-blue-500);
887 | --semantic-color-primary-main: var(--color-blue-800);
888 | --semantic-color-primary-dark: var(--color-blue-900);
889 | --semantic-color-primary-contrast-text: var(--color-white);
890 | --semantic-color-primary-icon-muted: var(--color-blue-grey-300);
891 | --semantic-color-primary-icon-bright: var(--color-blue-grey-50);
892 | --semantic-color-secondary-light: var(--color-yellow-200);
893 | --semantic-color-secondary-main: var(--color-yellow-400);
894 | --semantic-color-secondary-dark: var(--color-yellow-600);
895 | --semantic-color-secondary-contrast-text: var(--color-blue-grey-900);
896 | --semantic-color-secondary-icon-muted: var(--color-blue-grey-500);
897 | --semantic-color-secondary-icon-bright: var(--color-blue-grey-900);
898 | --semantic-color-background-default: var(--color-grey-custom-50);
899 | --semantic-color-background-paper: var(--color-white);
900 | --semantic-color-text-primary: var(--color-blue-grey-900);
901 | --semantic-color-text-secondary: var(--color-blue-grey-600);
902 | --semantic-color-icon-muted: var(--color-blue-grey-300);
903 | --semantic-color-icon-bright: var(--color-blue-grey-800);
904 | --semantic-color-state-disabled: #0000001f;
905 | --semantic-color-state-disabled-text: #00000042;
906 |
907 | /* ----- Component Layer ----- */
908 | --button-border-radius: var(--border-radius-4);
909 | --button-contained-default-background-color: var(--color-grey-300);
910 | --button-contained-default-text-color: #000000de;
911 | --button-contained-default-state-hover: var(--color-grey-400);
912 | --button-contained-primary-state-hover: var(--color-blue-900);
913 | --button-contained-secondary-state-hover: var(--color-yellow-500);
914 | --button-outlined-default-border-color: var(--color-grey-300);
915 | --button-outlined-default-state-hover: var(--color-grey-300);
916 | --button-outlined-primary-state-hover: var(--color-blue-50);
917 | --button-outlined-secondary-state-hover: var(--color-yellow-100);
918 |
919 | --card-border-color: var(--color-grey-300);
920 | --card-border-radius: var(--border-radius-8);
921 | }
922 |
--------------------------------------------------------------------------------
/packages/ui-lib/src/test/test-utils.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { render, RenderOptions } from '@testing-library/react';
3 | import userEvent from '@testing-library/user-event';
4 | import { BrowserRouter as Router } from 'react-router-dom';
5 |
6 | // -----------------------------------------------------------------------------
7 | // This file re-exports everything from React Testing Library and then overrides
8 | // its render method. In tests that require global context providers, import
9 | // this file instead of React Testing Library.
10 | //
11 | // For further details, see:
12 | // https://testing-library.com/docs/react-testing-library/setup/#custom-render
13 | // -----------------------------------------------------------------------------
14 |
15 | interface AllProvidersProps {
16 | children?: React.ReactNode;
17 | }
18 |
19 | function AllProviders({ children }: AllProvidersProps) {
20 | return {children} ;
21 | }
22 |
23 | /**
24 | * Custom render method that includes global context providers
25 | */
26 | type CustomRenderOptions = {
27 | initialRoute?: string;
28 | renderOptions?: Omit;
29 | };
30 |
31 | function customRender(ui: React.ReactElement, options?: CustomRenderOptions) {
32 | const opts = options || {};
33 | const { initialRoute, renderOptions } = opts;
34 |
35 | if (initialRoute) {
36 | window.history.pushState({}, 'Initial Route', initialRoute);
37 | }
38 |
39 | return render(ui, { wrapper: AllProviders, ...renderOptions });
40 | }
41 |
42 | export * from '@testing-library/react'; // eslint-disable-line
43 | export { customRender as render, userEvent }; // eslint-disable-line
44 |
--------------------------------------------------------------------------------
/packages/ui-lib/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "tsconfig/react-library.json",
3 | "include": ["."],
4 | "exclude": ["dist", "build", "node_modules", "**/*.test.ts"]
5 | }
6 |
--------------------------------------------------------------------------------
/storybook/.babelrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "sourceType": "unambiguous",
3 | "presets": [
4 | [
5 | "@babel/preset-env",
6 | {
7 | "shippedProposals": true,
8 | "loose": true
9 | }
10 | ],
11 | "@babel/preset-typescript"
12 | ],
13 | "plugins": [
14 | "@babel/plugin-transform-shorthand-properties",
15 | "@babel/plugin-transform-block-scoping",
16 | [
17 | "@babel/plugin-proposal-decorators",
18 | {
19 | "legacy": true
20 | }
21 | ],
22 | [
23 | "@babel/plugin-proposal-class-properties",
24 | {
25 | "loose": true
26 | }
27 | ],
28 | [
29 | "@babel/plugin-proposal-private-methods",
30 | {
31 | "loose": true
32 | }
33 | ],
34 | "@babel/plugin-proposal-export-default-from",
35 | "@babel/plugin-syntax-dynamic-import",
36 | [
37 | "@babel/plugin-proposal-object-rest-spread",
38 | {
39 | "loose": true,
40 | "useBuiltIns": true
41 | }
42 | ],
43 | "@babel/plugin-transform-classes",
44 | "@babel/plugin-transform-arrow-functions",
45 | "@babel/plugin-transform-parameters",
46 | "@babel/plugin-transform-destructuring",
47 | "@babel/plugin-transform-spread",
48 | "@babel/plugin-transform-for-of",
49 | "babel-plugin-macros",
50 | "@babel/plugin-proposal-optional-chaining",
51 | "@babel/plugin-proposal-nullish-coalescing-operator",
52 | [
53 | "babel-plugin-polyfill-corejs3",
54 | {
55 | "method": "usage-global",
56 | "absoluteImports": "core-js",
57 | "version": "3.21.1"
58 | }
59 | ]
60 | ]
61 | }
--------------------------------------------------------------------------------
/storybook/.storybook/main.ts:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | stories: [
3 | '../../packages/*/src/**/*.stories.mdx',
4 | '../../packages/*/src/**/*.stories.@(js|jsx|ts|tsx)',
5 | '../../apps/*/src/**/*.stories.mdx',
6 | '../../apps/*/src/**/*.stories.@(js|jsx|ts|tsx)',
7 | ],
8 | addons: [
9 | '@storybook/addon-links',
10 | '@storybook/addon-essentials',
11 | '@storybook/addon-interactions',
12 | ],
13 | framework: '@storybook/react',
14 | };
15 |
--------------------------------------------------------------------------------
/storybook/.storybook/preview-head.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
--------------------------------------------------------------------------------
/storybook/.storybook/preview.tsx:
--------------------------------------------------------------------------------
1 | import '../../packages/ui-lib/src/styles/main.css';
2 |
3 | export const parameters = {
4 | // Show calls to "on*" arguments (based on user actions) in the Actions panel
5 | actions: { argTypesRegex: '^on[A-Z].*' },
6 |
7 | controls: {
8 | matchers: {
9 | color: /(background|color)$/i,
10 | date: /Date$/,
11 | },
12 | },
13 |
14 | options: {
15 | storySort: {
16 | order: ['Style Guide', 'Components', 'Pages'],
17 | },
18 | },
19 | };
20 |
--------------------------------------------------------------------------------
/storybook/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "storybook",
3 | "version": "1.0.0",
4 | "main": "index.js",
5 | "author": "Naresh Bhatia",
6 | "license": "MIT",
7 | "scripts": {
8 | "storybook": "start-storybook -p 6006",
9 | "build-storybook": "build-storybook"
10 | },
11 | "dependencies": {},
12 | "devDependencies": {
13 | "@babel/core": "^7.17.9",
14 | "@babel/plugin-proposal-class-properties": "^7.16.7",
15 | "@babel/plugin-proposal-decorators": "^7.17.9",
16 | "@babel/plugin-proposal-export-default-from": "^7.16.7",
17 | "@babel/plugin-proposal-nullish-coalescing-operator": "^7.16.7",
18 | "@babel/plugin-proposal-object-rest-spread": "^7.17.3",
19 | "@babel/plugin-proposal-optional-chaining": "^7.16.7",
20 | "@babel/plugin-proposal-private-methods": "^7.16.11",
21 | "@babel/plugin-syntax-dynamic-import": "^7.8.3",
22 | "@babel/plugin-transform-arrow-functions": "^7.16.7",
23 | "@babel/plugin-transform-block-scoping": "^7.16.7",
24 | "@babel/plugin-transform-classes": "^7.16.7",
25 | "@babel/plugin-transform-destructuring": "^7.17.7",
26 | "@babel/plugin-transform-for-of": "^7.16.7",
27 | "@babel/plugin-transform-parameters": "^7.16.7",
28 | "@babel/plugin-transform-shorthand-properties": "^7.16.7",
29 | "@babel/plugin-transform-spread": "^7.16.7",
30 | "@babel/preset-env": "^7.16.11",
31 | "@babel/preset-typescript": "^7.16.7",
32 | "@storybook/addon-actions": "^6.4.22",
33 | "@storybook/addon-essentials": "^6.4.22",
34 | "@storybook/addon-interactions": "^6.4.22",
35 | "@storybook/addon-links": "^6.4.22",
36 | "@storybook/react": "next",
37 | "@storybook/testing-library": "^0.0.11",
38 | "babel-loader": "^8.2.5",
39 | "babel-plugin-macros": "^3.1.0",
40 | "babel-plugin-polyfill-corejs3": "^0.5.2",
41 | "core-js": "^3.22.2"
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/turbo.json:
--------------------------------------------------------------------------------
1 | {
2 | "pipeline": {
3 | "build": {
4 | "dependsOn": ["^build"],
5 | "outputs": ["dist/**", ".next/**"]
6 | },
7 | "lint": {
8 | "dependsOn": ["^build"],
9 | "outputs": []
10 | },
11 | "test": {
12 | "dependsOn": ["^lint"],
13 | "outputs": ["coverage/**"]
14 | },
15 | "dev": {
16 | "cache": false
17 | },
18 | "clean": {
19 | "cache": false
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------