├── .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 | ![Home Page](assets/home-page.png) 28 | 29 | 2. A Settings page for users to manage their subscription: 30 | 31 | ![Settings Page](assets/settings-page.png) 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 | ![Tech Stack Options](assets/teck-stack-options.png) 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 | ![Simple Multi-Repo](assets/simple-multi-repo.png) 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 | ![Complex Multi-Repo](assets/complex-multi-repo.png) 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 | ![Monorepo](assets/monorepo.png) 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 | ![Client-Side Rendering](assets/csr.png) 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 | ![Server-Side Rendering](assets/ssr.png) 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 | ![Tech Stack Selection](assets/teck-stack-selection.png) 174 | 175 | ## Movie Magic Repo Structure 176 | 177 | ![Repo Structure](assets/repo-structure.png) 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 | 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 |
Movie Magic
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 |
    16 | {planMembers.map((member) => ( 17 |
  • 18 | 23 |
    24 | 25 | {member.name} 26 | 27 | {member.email} 28 |
    29 |
  • 30 | ))} 31 |
32 | 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 |
Movie Magic
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 | 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 |
{children}
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 |
6 |
7 | 8 |
9 |
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 |
    20 | {planMembers.map((member) => ( 21 |
  • 22 | 23 |
    24 | 25 | {member.name} 26 | 27 | {member.email} 28 |
    29 |
  • 30 | ))} 31 |
32 | 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 |
7 |
8 | 9 |
10 |
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 | 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 |
Movie Magic
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 |
    31 | {planMembers.map((member) => ( 32 |
  • 33 | 38 |
    39 | 40 | {member.name} 41 | 42 | {member.email} 43 |
    44 |
  • 45 | ))} 46 |
47 | 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 | 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 | 43 | 46 | 49 | 52 |
53 | 54 |
Outlined Buttons
55 |
56 | 57 | 60 | 63 | 66 |
67 | 68 |
Sizes
69 |
70 | 73 | 81 | 89 |
90 |
91 | 94 | 102 | 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 | 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 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | {movies.map((movie, index) => ( 23 | 24 | 25 | 26 | 27 | 28 | 31 | 32 | ))} 33 | 34 |
RankNameYearRating
{index + 1}{movie.name}{movie.year}{movie.rating.toFixed(1)} 29 | 30 |
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 | --------------------------------------------------------------------------------