├── .gitignore ├── LICENSE ├── README.md ├── apps ├── host │ ├── .env.sample │ ├── .eslintrc.json │ ├── .gitignore │ ├── components.json │ ├── cypress.config.ts │ ├── cypress │ │ ├── e2e │ │ │ └── app.cy.ts │ │ ├── fixtures │ │ │ └── example.json │ │ └── support │ │ │ ├── commands.ts │ │ │ ├── component-index.html │ │ │ ├── component.ts │ │ │ └── e2e.ts │ ├── federated.d.ts │ ├── middleware.ts │ ├── next.config.js │ ├── package.json │ ├── postcss.config.js │ ├── public │ │ └── favicon.ico │ ├── remote-map.ts │ ├── src │ │ ├── assets │ │ │ └── images │ │ │ │ ├── logo.png │ │ │ │ └── winter-photo.webp │ │ ├── components │ │ │ └── ui │ │ │ │ ├── accordion.tsx │ │ │ │ ├── avatar.tsx │ │ │ │ ├── badge.tsx │ │ │ │ ├── button.tsx │ │ │ │ ├── card.tsx │ │ │ │ ├── input.tsx │ │ │ │ ├── label.tsx │ │ │ │ ├── progress.tsx │ │ │ │ ├── select.tsx │ │ │ │ ├── separator.tsx │ │ │ │ └── switch.tsx │ │ ├── lib │ │ │ └── utils.ts │ │ ├── pages │ │ │ ├── 404.tsx │ │ │ ├── 500.tsx │ │ │ ├── _app.tsx │ │ │ ├── _document.tsx │ │ │ ├── api │ │ │ │ └── auth │ │ │ │ │ └── [...nextauth].ts │ │ │ ├── components │ │ │ │ ├── dashboard-layout.tsx │ │ │ │ ├── form-authenticated.tsx │ │ │ │ ├── form-sign-in.tsx │ │ │ │ └── switch-theme.tsx │ │ │ ├── dashboard │ │ │ │ ├── clients │ │ │ │ │ └── index.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── website-analytics │ │ │ │ │ └── index.tsx │ │ │ ├── index.tsx │ │ │ └── login │ │ │ │ └── index.tsx │ │ └── styles │ │ │ └── globals.css │ ├── tailwind.config.js │ └── tsconfig.json └── remote │ ├── .env.sample │ ├── .eslintrc.json │ ├── .gitignore │ ├── components │ └── table │ │ └── table.tsx │ ├── next.config.js │ ├── package.json │ ├── pages │ ├── _app.tsx │ ├── index.tsx │ └── styles.css │ ├── postcss.config.js │ ├── public │ └── favicon.png │ ├── tailwind.config.ts │ └── tsconfig.json ├── docs └── technical-test-react-developer-with-nextjs.pdf └── public ├── screen-1.png ├── screen-2.png ├── screen-3.png └── screen-4.png /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | node_modules 3 | .pnp 4 | .pnp.js 5 | 6 | # testing 7 | coverage 8 | 9 | # next.js 10 | .next/ 11 | out/ 12 | 13 | # production 14 | build 15 | 16 | # misc 17 | .DS_Store 18 | *.pem 19 | 20 | # debug 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | # local env files 26 | .env*.local 27 | 28 | # vercel 29 | .vercel 30 | 31 | # typescript 32 | *.tsbuildinfo 33 | next-env.d.ts -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Mariano Álvarez 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Technical Test: React Developer with Next.js and TailwindCSS 2 | 3 | ## Table of Contents 4 | 5 | - [Technical Test: React Developer with Next.js and TailwindCSS](#technical-test-react-developer-with-nextjs-and-tailwindcss) 6 | - [Table of Contents](#table-of-contents) 7 | - [Description](#description) 8 | - [Screenshots](#screenshots) 9 | - [Requirements](#requirements) 10 | - [Installation / How to run](#installation--how-to-run) 11 | - [Environment Variables](#environment-variables) 12 | - [Pages description](#pages-description) 13 | - [Project Structure](#project-structure) 14 | - [Todo List](#todo-list) 15 | - [Task 1: Advanced Next.js Architecture (30 points)](#task-1-advanced-nextjs-architecture-30-points) 16 | - [Task 2: Advanced Component Development (35 points)](#task-2-advanced-component-development-35-points) 17 | - [Task 3: Large Dataset Handling (25 points)](#task-3-large-dataset-handling-25-points) 18 | - [Task 4: Performance Optimization and Server-Side Rendering (25 points)](#task-4-performance-optimization-and-server-side-rendering-25-points) 19 | - [Task 5: Advanced Features (30 points)](#task-5-advanced-features-30-points) 20 | - [Task 6: Testing and Documentation (15 points)](#task-6-testing-and-documentation-15-points) 21 | - [Deployment](#deployment) 22 | - [Stack, Libraries, and Tools](#stack-libraries-and-tools) 23 | - [Conclusions](#conclusions) 24 | - [Contribute](#contribute) 25 | - [Author of the project](#author-of-the-project) 26 | - [License](#license) 27 | 28 | ## Description 29 | 30 | This project is a technical test for a React Developer position. It is a dashboard application built with Next.js and TailwindCSS. The application is a micro-frontend architecture with Module Federation. The application is a dashboard that displays data from a mock API. The dashboard is a dynamic dashboard with charts and graphs. 31 | 32 | ## Screenshots 33 | 34 | ![Screenshot 1](./public/screen-1.png) 35 | ![Screenshot 2](./public/screen-2.png) 36 | ![Screenshot 3](./public/screen-3.png) 37 | ![Screenshot 4](./public/screen-4.png) 38 | 39 | ## Requirements 40 | 41 | - Node: v20.10.0 42 | - Npm: v10.3.0 43 | - Module-federation/nextjs-mf: v6.7.1 44 | - Next: v13.4.7 45 | - Tailwindcss: v3.0.23 46 | 47 | ## Installation / How to run 48 | 49 | It's a demo, so everything is in the same repository. However, it's a good practice to have each micro-frontend in a different repository and deploy them separately, using run-time integration, for example. 50 | 51 | So to install it you just need to clone the repository and run the following command: 52 | 53 | Install dependencies 54 | 55 | ```bash 56 | npm install 57 | ``` 58 | 59 | To run the samples in development mode, run the following command: 60 | 61 | ```bash 62 | npm run dev 63 | ``` 64 | 65 | To run the samples in production mode, first build the samples using the following command: 66 | 67 | ```bash 68 | npm run build 69 | ``` 70 | 71 | Then, run the following command to serve the samples: 72 | 73 | ```bash 74 | npm run start 75 | ``` 76 | 77 | ## Environment Variables 78 | 79 | The environment variables are located in the `.env` file. The following environment variables are required: 80 | 81 | - `GITHUB_ID`: GitHub OAuth App Client ID. 82 | - `GITHUB_SECRET`: GitHub OAuth App Client Secret. 83 | - `NEXTAUTH_URL`: The URL of the NextAuth.js server. 84 | - `NEXTAUTH_SECRET`: A secret used to encrypt session cookies. 85 | - `ANALYTICS_REMOTE_URL`: The URL of the analytics micro-frontend. 86 | 87 | ## Pages description 88 | 89 | - `/`: The homepage of the application with login and signup buttons. 90 | - `/404`: The 404 page of the application. 91 | - `/dashboard`: The dashboard page of the application. (Protected) 92 | - `/dashboard/website-analytics`: The website analytics page of the application. (Protected) 93 | - `/dashboard/clients`: The clients page of the application. (Protected) 94 | 95 | ## Project Structure 96 | 97 | ```bash 98 | react-nextjs-tailwindcss-microfrontends/ 99 | │ 100 | ├── apps/ 101 | │ │ 102 | │ ├── remote/ 103 | │ │ ├── public/ 104 | │ │ ├── pages/ 105 | │ │ ├── lib/ 106 | │ │ ├── components/ 107 | │ │ ├── styles/ 108 | │ │ ├── ... 109 | │ │ ├── tailwind.config.js 110 | │ │ ├── next.config.js 111 | │ │ └── tsconfig.json 112 | │ │ 113 | │ └── host/ 114 | │ ├── cypress/ 115 | │ ├── public/ 116 | │ └── src/ 117 | │ ├── assets/ 118 | │ ├── components/ 119 | │ ├── pages/ 120 | │ ├── lib/ 121 | │ ├── styles/ 122 | │ ├── ... 123 | │ ├── tailwind.config.js 124 | │ ├── next.config.js 125 | │ └── tsconfig.json 126 | └── docs/ 127 | ``` 128 | 129 | ## Todo List 130 | 131 | ### Task 1: Advanced Next.js Architecture (30 points) 132 | - [X] Implement a micro-frontends architecture using Module Federation. 133 | - [X] Integrate an external analytics module as a federated module. 134 | - [X] Secure routes using NextAuth.js for authentication. 135 | 136 | ### Task 2: Advanced Component Development (35 points) 137 | Animated UI Components (20 points) 138 | - [X] Develop a dynamic dashboard component with charts and graphs to visualize data. 139 | - [X] Use a data visualization library (e.g., D3.js, Chart.js) for rendering charts. 140 | - [X] Implement smooth transitions and interactions within the dashboard. 141 | 142 | Custom Hook (15 points) 143 | 144 | - [X] Create a custom hook for handling complex state management scenarios (e.g., data fetching, caching, and real-time updates). 145 | - [X] Use the hook in at least two different components to showcase its versatility. 146 | 147 | ### Task 3: Large Dataset Handling (25 points) 148 | - [X] Fetch and display a large dataset (e.g., 10,000 records) efficiently on the homepage. 149 | - [X] Implement virtualization or pagination to handle the large dataset without compromising performance. 150 | - [X] Optimize the data fetching mechanism for minimal server load. 151 | 152 | ### Task 4: Performance Optimization and Server-Side Rendering (25 points) 153 | - [X] Optimize the application for mobile performance using Google Lighthouse metrics. 154 | - [X] Implement server-side rendering (SSR) for critical pages to improve the initial loading time. 155 | - [X] Ensure efficient resource loading using strategies like pre-fetching or pre-loading. 156 | 157 | ### Task 5: Advanced Features (30 points) 158 | Real-time Collaboration (15 points) 159 | - [ ] Implement real-time collaborative editing for a shared document. 160 | - [ ] Utilize a technology like Firebase Realtime Database or Socket.io for real-time updates. 161 | 162 | Advanced Styling (15 points) 163 | 164 | - [X] Implement a theming system with multiple theme options. 165 | - [X] Integrate Tailwind CSS JIT for optimizing the styling workflow and reducing the final bundle size. 166 | 167 | ### Task 6: Testing and Documentation (15 points) 168 | - [X] Write end-to-end tests using Cypress for critical user flows. 169 | - [X] Provide comprehensive documentation covering architecture, data models, and instructions for development and deployment. 170 | 171 | Submission Guidelines: 172 | - [X] Share the link to the Git repository containing your code. 173 | - [X] Provide a detailed README with instructions, architecture overview, and any additional information. 174 | - [X] Ensure that your application is deployed, and share the deployment link (e.g., Netlify, Vercel). 175 | 176 | ## Deployment 177 | 178 | - The host/shell application [here](https://nextjs-typescript-module-federation-host.vercel.app/). 179 | - The remote application [here](https://nextjs-typescript-module-federation-remote.vercel.app/). 180 | 181 | ## Stack, Libraries, and Tools 182 | 183 | This project uses the following stack: 184 | 185 | - Library: [React](https://reactjs.org/) 186 | - Framework: [Next.js](https://nextjs.org/) 187 | - Architecture: [Micro-Frontends](https://micro-frontends.org/) 188 | - Concept: [Module Federation](https://webpack.js.org/concepts/module-federation/) 189 | - Language: [TypeScript](https://www.typescriptlang.org/) 190 | - Auth: [NextAuth.js](https://next-auth.js.org/) 191 | - Deployment: [Vercel](https://vercel.com/) 192 | - Styling: [TailwindCSS](https://tailwindcss.com/) 193 | - Components: [MUI](https://mui.com/) and [shadcn/ui](https://ui.shadcn/ui.com/) 194 | - Linting: [ESLint](https://eslint.org/) 195 | - Formatting: [Prettier](https://prettier.io/) 196 | - Version Control: [Git](https://git-scm.com/) 197 | - Repository Hosting: [GitHub](https://github.com/) 198 | - Data fetching: [SWR](https://swr.vercel.app/) 199 | - Testing: [Cypress](https://www.cypress.io/) 200 | 201 | ## Conclusions 202 | 203 | - **Limitation in Next.js App Router:** 204 | 205 | The current version of Next.js App Router does not support Module Federation. 206 | You need to opt for an older version of Next.js and use a plugin designed to integrate Module Federation with Next.js. 207 | 208 | - **Webpack configuration:** 209 | 210 | Configuring Webpack to expose and define remotes is relatively simple. 211 | It is recommended to perform build tests to identify possible incompatibilities with Next.js or other component libraries or Hooks. 212 | 213 | - **Development with Tailwind CSS:** 214 | 215 | Tailwind CSS makes development easier by providing a smooth experience. 216 | When exposing remotes for use in the host/shell application, you face a challenge: class references in the build can break. Two possible solutions are: 217 | Use safelist to maintain the necessary classes. Import the Tailwind CSS module into each component file to generate the necessary classes and allow the host/shell to reference them correctly. 218 | 219 | - **Integration Experience:** 220 | 221 | The combination of Next.js, Module Federation and Tailwind CSS offers an interesting experience during development. 222 | Despite the attractive integration, it is suggested to consider other stacks for projects in production. 223 | 224 | - **Recommendation from the Creator of Module Federation:** 225 | 226 | In conversation with [Zack Jackson](https://github.com/ScriptedAlchemy), creator of Module Federation, he mentions that Next.js presents exceptional difficulties in terms of maintenance due to problems with Vercel in relation to MF. 227 | 228 | The recommendation to carefully consider these difficulties provides perspective on the potential limitations in choosing this technology mix for projects beyond development. 229 | 230 | 231 | ## Contribute 232 | 233 | If you want to contribute to this project, you can do so by following these steps: 234 | 235 | 1. Fork the project. 236 | 2. Create a new branch (`git checkout -b feature/feature-name`). 237 | 3. Commit your changes (`git commit -m 'feat: add new feature'`). 238 | 4. Push the branch (`git push origin feature/feature-name`). 239 | 5. Open a pull request. 240 | 6. Wait for your pull request to be reviewed and accepted. 241 | 7. Start contributing! 242 | 243 | ## Author of the project 244 | 245 | **Mariano Álvarez** 246 | 247 | Frontend Developer with more than 3 years of experience, specialized in creating fluid experiences and always attentive to the latest design trends and cutting-edge technologies. 248 | 249 | - [LinkedIn](https://www.linkedin.com/in/ma-marianoalvarez/) 250 | - [Website](https://marianoalvarez.dev/) 251 | 252 | 253 | ## License 254 | 255 | This project is open source and available under the [MIT License](LICENSE). 256 | -------------------------------------------------------------------------------- /apps/host/.env.sample: -------------------------------------------------------------------------------- 1 | GITHUB_ID= 2 | GITHUB_SECRET= 3 | 4 | NEXTAUTH_URL= 5 | NEXTAUTH_SECRET= 6 | NEXT_PRIVATE_LOCAL_WEBPACK= 7 | 8 | ANALYTICS_REMOTE_URL= -------------------------------------------------------------------------------- /apps/host/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next", "next/core-web-vitals"] 3 | } 4 | -------------------------------------------------------------------------------- /apps/host/.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 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /apps/host/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "./tailwind.config.js", 8 | "css": "./src/styles/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /apps/host/cypress.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "cypress"; 2 | 3 | export default defineConfig({ 4 | e2e: { 5 | setupNodeEvents(on, config) { 6 | // implement node event listeners here 7 | }, 8 | }, 9 | 10 | component: { 11 | devServer: { 12 | framework: "next", 13 | bundler: "webpack", 14 | }, 15 | }, 16 | }); 17 | -------------------------------------------------------------------------------- /apps/host/cypress/e2e/app.cy.ts: -------------------------------------------------------------------------------- 1 | describe("Navigation", () => { 2 | it("should handle incorrect email and display error message", () => { 3 | // Start from the index page 4 | cy.visit("http://localhost:3010/"); 5 | 6 | // Find input type email and type in incorrect email 7 | const incorrectEmail = "example@email.com"; 8 | cy.get('input[type="email"]').clear().type(incorrectEmail); 9 | 10 | // Find button type submit and click it 11 | cy.get('button[type="submit"]').click(); 12 | 13 | // Wait for the error message to be displayed in the small tag 14 | cy.get("small").contains("try again").should("be.visible"); 15 | 16 | // Find input type email and type in correct email 17 | const correctEmail = "example@correo.com"; 18 | cy.get('input[type="email"]').clear().type(correctEmail); 19 | 20 | // Find button type submit and click it again 21 | cy.get('button[type="submit"]').click(); 22 | 23 | // Wait for redirection to the dashboard page 24 | cy.url().should("include", "/dashboard"); 25 | cy.get("small").contains(correctEmail).should("be.visible"); 26 | 27 | // Wait for redirection to the dashboard page 28 | cy.url().should("include", "/dashboard"); 29 | cy.get("small").contains("example@correo.com").should("be.visible"); 30 | 31 | // Scroll down to show the footer 32 | cy.scrollTo("bottom"); 33 | 34 | // Scroll up to show the header 35 | cy.scrollTo("top"); 36 | 37 | // Find button containing "Clients" and click it 38 | cy.get("button").contains("Clients").click(); 39 | 40 | // Wait for the MuiDataGrid div to be loaded in the DOM 41 | cy.get(".MuiDataGrid-root").should("exist").should("be.visible"); 42 | 43 | // Find button containing "Home" and click it 44 | cy.get("button").contains("Home").click(); 45 | 46 | // Wait for redirection to the home page "/" 47 | cy.url().should("include", "/"); 48 | 49 | // Find button containing "Sign out" and click it 50 | cy.get("button").contains("Sign out").click(); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /apps/host/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/host/cypress/support/commands.ts: -------------------------------------------------------------------------------- 1 | /// 2 | // *********************************************** 3 | // This example commands.ts shows you how to 4 | // create various custom commands and overwrite 5 | // existing commands. 6 | // 7 | // For more comprehensive examples of custom 8 | // commands please read more here: 9 | // https://on.cypress.io/custom-commands 10 | // *********************************************** 11 | // 12 | // 13 | // -- This is a parent command -- 14 | // Cypress.Commands.add('login', (email, password) => { ... }) 15 | // 16 | // 17 | // -- This is a child command -- 18 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) 19 | // 20 | // 21 | // -- This is a dual command -- 22 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) 23 | // 24 | // 25 | // -- This will overwrite an existing command -- 26 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) 27 | // 28 | // declare global { 29 | // namespace Cypress { 30 | // interface Chainable { 31 | // login(email: string, password: string): Chainable 32 | // drag(subject: string, options?: Partial): Chainable 33 | // dismiss(subject: string, options?: Partial): Chainable 34 | // visit(originalFn: CommandOriginalFn, url: string, options: Partial): Chainable 35 | // } 36 | // } 37 | // } -------------------------------------------------------------------------------- /apps/host/cypress/support/component-index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Components App 8 | 9 |
10 | 11 | 12 |
13 | 14 | -------------------------------------------------------------------------------- /apps/host/cypress/support/component.ts: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/component.ts 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 | 22 | import { mount } from 'cypress/react18' 23 | 24 | // Augment the Cypress namespace to include type definitions for 25 | // your custom command. 26 | // Alternatively, can be defined in cypress/support/component.d.ts 27 | // with a at the top of your spec. 28 | declare global { 29 | namespace Cypress { 30 | interface Chainable { 31 | mount: typeof mount 32 | } 33 | } 34 | } 35 | 36 | Cypress.Commands.add('mount', mount) 37 | 38 | // Example use: 39 | // cy.mount() -------------------------------------------------------------------------------- /apps/host/cypress/support/e2e.ts: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/e2e.ts 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') -------------------------------------------------------------------------------- /apps/host/federated.d.ts: -------------------------------------------------------------------------------- 1 | declare module "remote/table" { 2 | export * from "@remote/components/table/table"; 3 | import table from "@remote/components/table/table"; 4 | export default table; 5 | } 6 | -------------------------------------------------------------------------------- /apps/host/middleware.ts: -------------------------------------------------------------------------------- 1 | export { default } from "next-auth/middleware"; 2 | export const config = { matcher: ["/dashboard/:path*"] }; 3 | -------------------------------------------------------------------------------- /apps/host/next.config.js: -------------------------------------------------------------------------------- 1 | // host/next.config.js 2 | const { NextFederationPlugin } = require("@module-federation/nextjs-mf"); 3 | 4 | /** @type {import('next').NextConfig} */ 5 | const nextConfig = { 6 | reactStrictMode: true, 7 | webpack(config, options) { 8 | const { isServer } = options; 9 | const remoteDir = isServer ? "ssr" : "chunks"; 10 | config.experiments = { topLevelAwait: true, layers: true }; 11 | 12 | config.plugins.push( 13 | new NextFederationPlugin({ 14 | name: "host", 15 | filename: `static/${remoteDir}/remoteEntry.js`, 16 | extraOptions: {}, 17 | remotes: { 18 | remote: `remote@${process.env.ANALYTICS_REMOTE_URL}/_next/static/${remoteDir}/remoteEntry.js`, 19 | }, 20 | shared: {}, 21 | }), 22 | ); 23 | 24 | return config; 25 | }, 26 | }; 27 | 28 | module.exports = nextConfig; 29 | -------------------------------------------------------------------------------- /apps/host/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "host", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "set NEXT_PRIVATE_LOCAL_WEBPACK=true && next dev -p 3010", 7 | "build": "set NEXT_PRIVATE_LOCAL_WEBPACK=true && next build", 8 | "start": "set NEXT_PRIVATE_LOCAL_WEBPACK=true && next start -p 3010", 9 | "format": "prettier --write .", 10 | "lint": "next lint", 11 | "cypress:open": "cypress open" 12 | }, 13 | "dependencies": { 14 | "@emotion/react": "^11.11.3", 15 | "@emotion/styled": "^11.11.0", 16 | "@module-federation/nextjs-mf": "6.7.1", 17 | "@mui/icons-material": "^5.15.5", 18 | "@mui/material": "^5.15.5", 19 | "@mui/x-data-grid": "^6.18.7", 20 | "@radix-ui/react-accordion": "^1.1.2", 21 | "@radix-ui/react-avatar": "^1.0.4", 22 | "@radix-ui/react-checkbox": "^1.0.4", 23 | "@radix-ui/react-icons": "^1.3.0", 24 | "@radix-ui/react-label": "^2.0.2", 25 | "@radix-ui/react-progress": "^1.0.3", 26 | "@radix-ui/react-select": "^2.0.0", 27 | "@radix-ui/react-separator": "^1.0.3", 28 | "@radix-ui/react-slot": "^1.0.2", 29 | "@radix-ui/react-switch": "^1.0.3", 30 | "@tanstack/react-table": "^8.11.6", 31 | "@tremor/react": "^3.13.1", 32 | "class-variance-authority": "^0.7.0", 33 | "clsx": "^2.1.0", 34 | "framer-motion": "^10.18.0", 35 | "lucide-react": "^0.309.0", 36 | "next": "13.4.7", 37 | "next-auth": "^4.24.5", 38 | "next-themes": "^0.2.1", 39 | "prettier": "^3.2.2", 40 | "react": "^18.2.0", 41 | "react-dom": "^18.2.0", 42 | "react-icons": "^5.0.1", 43 | "swr": "^2.2.4", 44 | "tailwind-merge": "^2.2.0", 45 | "tailwindcss-animate": "^1.0.7", 46 | "webpack": "^5.89.0" 47 | }, 48 | "devDependencies": { 49 | "@faker-js/faker": "^8.3.1", 50 | "@types/node": "latest", 51 | "@types/react": "^18.2.47", 52 | "@types/react-dom": "^18.2.18", 53 | "autoprefixer": "latest", 54 | "cypress": "^13.6.3", 55 | "eslint": "latest", 56 | "eslint-config-next": "latest", 57 | "postcss": "latest", 58 | "tailwindcss": "latest", 59 | "typescript": "^5.3.3" 60 | } 61 | } -------------------------------------------------------------------------------- /apps/host/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /apps/host/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creativoma/nextjs-typescript-module-federation/4d10175ef1d98ceade55279e3f362c4f58e5bc19/apps/host/public/favicon.ico -------------------------------------------------------------------------------- /apps/host/remote-map.ts: -------------------------------------------------------------------------------- 1 | // Here we can define the remote entry points for our remotes 2 | const remoteMap = { 3 | remote: `${process.env.ANALYTICS_REMOTE_URL}/_next/static/__LOCATION__/remoteEntry.js`, 4 | }; 5 | 6 | module.exports = remoteMap; 7 | -------------------------------------------------------------------------------- /apps/host/src/assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creativoma/nextjs-typescript-module-federation/4d10175ef1d98ceade55279e3f362c4f58e5bc19/apps/host/src/assets/images/logo.png -------------------------------------------------------------------------------- /apps/host/src/assets/images/winter-photo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creativoma/nextjs-typescript-module-federation/4d10175ef1d98ceade55279e3f362c4f58e5bc19/apps/host/src/assets/images/winter-photo.webp -------------------------------------------------------------------------------- /apps/host/src/components/ui/accordion.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as AccordionPrimitive from "@radix-ui/react-accordion"; 3 | import { ChevronDown } from "lucide-react"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | 7 | const Accordion = AccordionPrimitive.Root; 8 | 9 | const AccordionItem = React.forwardRef< 10 | React.ElementRef, 11 | React.ComponentPropsWithoutRef 12 | >(({ className, ...props }, ref) => ( 13 | 18 | )); 19 | AccordionItem.displayName = "AccordionItem"; 20 | 21 | const AccordionTrigger = React.forwardRef< 22 | React.ElementRef, 23 | React.ComponentPropsWithoutRef 24 | >(({ className, children, ...props }, ref) => ( 25 | 26 | svg]:rotate-180", 30 | className, 31 | )} 32 | {...props} 33 | > 34 | {children} 35 | 36 | 37 | 38 | )); 39 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName; 40 | 41 | const AccordionContent = React.forwardRef< 42 | React.ElementRef, 43 | React.ComponentPropsWithoutRef 44 | >(({ className, children, ...props }, ref) => ( 45 | 50 |
{children}
51 |
52 | )); 53 | 54 | AccordionContent.displayName = AccordionPrimitive.Content.displayName; 55 | 56 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }; 57 | -------------------------------------------------------------------------------- /apps/host/src/components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as AvatarPrimitive from "@radix-ui/react-avatar"; 3 | 4 | import { cn } from "@/lib/utils"; 5 | 6 | const Avatar = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >(({ className, ...props }, ref) => ( 10 | 18 | )); 19 | Avatar.displayName = AvatarPrimitive.Root.displayName; 20 | 21 | const AvatarImage = React.forwardRef< 22 | React.ElementRef, 23 | React.ComponentPropsWithoutRef 24 | >(({ className, ...props }, ref) => ( 25 | 30 | )); 31 | AvatarImage.displayName = AvatarPrimitive.Image.displayName; 32 | 33 | const AvatarFallback = React.forwardRef< 34 | React.ElementRef, 35 | React.ComponentPropsWithoutRef 36 | >(({ className, ...props }, ref) => ( 37 | 45 | )); 46 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName; 47 | 48 | export { Avatar, AvatarImage, AvatarFallback }; 49 | -------------------------------------------------------------------------------- /apps/host/src/components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { cva, type VariantProps } from "class-variance-authority"; 3 | 4 | import { cn } from "@/lib/utils"; 5 | 6 | const badgeVariants = cva( 7 | "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", 8 | { 9 | variants: { 10 | variant: { 11 | default: 12 | "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", 13 | secondary: 14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", 15 | destructive: 16 | "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", 17 | outline: "text-foreground", 18 | }, 19 | }, 20 | defaultVariants: { 21 | variant: "default", 22 | }, 23 | }, 24 | ); 25 | 26 | export interface BadgeProps 27 | extends React.HTMLAttributes, 28 | VariantProps {} 29 | 30 | function Badge({ className, variant, ...props }: BadgeProps) { 31 | return ( 32 |
33 | ); 34 | } 35 | 36 | export { Badge, badgeVariants }; 37 | -------------------------------------------------------------------------------- /apps/host/src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Slot } from "@radix-ui/react-slot"; 3 | import { cva, type VariantProps } from "class-variance-authority"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 | outline: 16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-primary underline-offset-4 hover:underline", 21 | }, 22 | size: { 23 | default: "h-10 px-4 py-2", 24 | sm: "h-9 rounded-md px-3", 25 | lg: "h-11 rounded-md px-8", 26 | icon: "h-10 w-10", 27 | }, 28 | }, 29 | defaultVariants: { 30 | variant: "default", 31 | size: "default", 32 | }, 33 | }, 34 | ); 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes, 38 | VariantProps { 39 | asChild?: boolean; 40 | } 41 | 42 | const Button = React.forwardRef( 43 | ({ className, variant, size, asChild = false, ...props }, ref) => { 44 | const Comp = asChild ? Slot : "button"; 45 | return ( 46 | 51 | ); 52 | }, 53 | ); 54 | Button.displayName = "Button"; 55 | 56 | export { Button, buttonVariants }; 57 | -------------------------------------------------------------------------------- /apps/host/src/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
17 | )); 18 | Card.displayName = "Card"; 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
29 | )); 30 | CardHeader.displayName = "CardHeader"; 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLParagraphElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |

44 | )); 45 | CardTitle.displayName = "CardTitle"; 46 | 47 | const CardDescription = React.forwardRef< 48 | HTMLParagraphElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |

56 | )); 57 | CardDescription.displayName = "CardDescription"; 58 | 59 | const CardContent = React.forwardRef< 60 | HTMLDivElement, 61 | React.HTMLAttributes 62 | >(({ className, ...props }, ref) => ( 63 |

64 | )); 65 | CardContent.displayName = "CardContent"; 66 | 67 | const CardFooter = React.forwardRef< 68 | HTMLDivElement, 69 | React.HTMLAttributes 70 | >(({ className, ...props }, ref) => ( 71 |
76 | )); 77 | CardFooter.displayName = "CardFooter"; 78 | 79 | export { 80 | Card, 81 | CardHeader, 82 | CardFooter, 83 | CardTitle, 84 | CardDescription, 85 | CardContent, 86 | }; 87 | -------------------------------------------------------------------------------- /apps/host/src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes {} 7 | 8 | const Input = React.forwardRef( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | 20 | ); 21 | }, 22 | ); 23 | Input.displayName = "Input"; 24 | 25 | export { Input }; 26 | -------------------------------------------------------------------------------- /apps/host/src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as LabelPrimitive from "@radix-ui/react-label"; 3 | import { cva, type VariantProps } from "class-variance-authority"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | 7 | const labelVariants = cva( 8 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70", 9 | ); 10 | 11 | const Label = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef & 14 | VariantProps 15 | >(({ className, ...props }, ref) => ( 16 | 21 | )); 22 | Label.displayName = LabelPrimitive.Root.displayName; 23 | 24 | export { Label }; 25 | -------------------------------------------------------------------------------- /apps/host/src/components/ui/progress.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as ProgressPrimitive from "@radix-ui/react-progress"; 3 | 4 | import { cn } from "@/lib/utils"; 5 | 6 | const Progress = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >(({ className, value, ...props }, ref) => ( 10 | 18 | 22 | 23 | )); 24 | Progress.displayName = ProgressPrimitive.Root.displayName; 25 | 26 | export { Progress }; 27 | -------------------------------------------------------------------------------- /apps/host/src/components/ui/select.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as SelectPrimitive from "@radix-ui/react-select"; 3 | import { Check, ChevronDown, ChevronUp } from "lucide-react"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | 7 | const Select = SelectPrimitive.Root; 8 | 9 | const SelectGroup = SelectPrimitive.Group; 10 | 11 | const SelectValue = SelectPrimitive.Value; 12 | 13 | const SelectTrigger = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef 16 | >(({ className, children, ...props }, ref) => ( 17 | span]:line-clamp-1", 21 | className, 22 | )} 23 | {...props} 24 | > 25 | {children} 26 | 27 | 28 | 29 | 30 | )); 31 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName; 32 | 33 | const SelectScrollUpButton = React.forwardRef< 34 | React.ElementRef, 35 | React.ComponentPropsWithoutRef 36 | >(({ className, ...props }, ref) => ( 37 | 45 | 46 | 47 | )); 48 | SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName; 49 | 50 | const SelectScrollDownButton = React.forwardRef< 51 | React.ElementRef, 52 | React.ComponentPropsWithoutRef 53 | >(({ className, ...props }, ref) => ( 54 | 62 | 63 | 64 | )); 65 | SelectScrollDownButton.displayName = 66 | SelectPrimitive.ScrollDownButton.displayName; 67 | 68 | const SelectContent = React.forwardRef< 69 | React.ElementRef, 70 | React.ComponentPropsWithoutRef 71 | >(({ className, children, position = "popper", ...props }, ref) => ( 72 | 73 | 84 | 85 | 92 | {children} 93 | 94 | 95 | 96 | 97 | )); 98 | SelectContent.displayName = SelectPrimitive.Content.displayName; 99 | 100 | const SelectLabel = React.forwardRef< 101 | React.ElementRef, 102 | React.ComponentPropsWithoutRef 103 | >(({ className, ...props }, ref) => ( 104 | 109 | )); 110 | SelectLabel.displayName = SelectPrimitive.Label.displayName; 111 | 112 | const SelectItem = React.forwardRef< 113 | React.ElementRef, 114 | React.ComponentPropsWithoutRef 115 | >(({ className, children, ...props }, ref) => ( 116 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | {children} 131 | 132 | )); 133 | SelectItem.displayName = SelectPrimitive.Item.displayName; 134 | 135 | const SelectSeparator = React.forwardRef< 136 | React.ElementRef, 137 | React.ComponentPropsWithoutRef 138 | >(({ className, ...props }, ref) => ( 139 | 144 | )); 145 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName; 146 | 147 | export { 148 | Select, 149 | SelectGroup, 150 | SelectValue, 151 | SelectTrigger, 152 | SelectContent, 153 | SelectLabel, 154 | SelectItem, 155 | SelectSeparator, 156 | SelectScrollUpButton, 157 | SelectScrollDownButton, 158 | }; 159 | -------------------------------------------------------------------------------- /apps/host/src/components/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as SeparatorPrimitive from "@radix-ui/react-separator"; 3 | 4 | import { cn } from "@/lib/utils"; 5 | 6 | const Separator = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >( 10 | ( 11 | { className, orientation = "horizontal", decorative = true, ...props }, 12 | ref, 13 | ) => ( 14 | 25 | ), 26 | ); 27 | Separator.displayName = SeparatorPrimitive.Root.displayName; 28 | 29 | export { Separator }; 30 | -------------------------------------------------------------------------------- /apps/host/src/components/ui/switch.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as SwitchPrimitives from "@radix-ui/react-switch"; 3 | 4 | import { cn } from "@/lib/utils"; 5 | 6 | const Switch = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >(({ className, ...props }, ref) => ( 10 | 18 | 23 | 24 | )); 25 | Switch.displayName = SwitchPrimitives.Root.displayName; 26 | 27 | export { Switch }; 28 | -------------------------------------------------------------------------------- /apps/host/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /apps/host/src/pages/404.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Link from "next/link"; 3 | import Image from "next/image"; 4 | import imgLogo from "@/assets/images/logo.png"; 5 | import { Button } from "@/components/ui/button"; 6 | import { motion } from "framer-motion"; 7 | import winterPhoto from "@/assets/images/winter-photo.webp"; 8 | import { useRouter } from "next/navigation"; 9 | 10 | const Error404: React.FC = () => { 11 | const router = useRouter(); 12 | 13 | return ( 14 | 21 |
22 | 23 | Logo 32 | 33 |

Error 404

34 |

35 | Page not found or you don't have access. 36 |

37 |

38 | lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer nec 39 | odio. Praesent libero. Sed cursus ante dapibus diam. Sed nisi. 40 |

41 | 44 |
45 |
46 | Winter photo 54 |
55 |
56 | ); 57 | }; 58 | 59 | export default Error404; 60 | -------------------------------------------------------------------------------- /apps/host/src/pages/500.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Link from "next/link"; 3 | import Image from "next/image"; 4 | import imgLogo from "@/assets/images/logo.png"; 5 | import { Button } from "@/components/ui/button"; 6 | import { motion } from "framer-motion"; 7 | import winterPhoto from "@/assets/images/winter-photo.webp"; 8 | import { useRouter } from "next/navigation"; 9 | 10 | const Error404: React.FC = () => { 11 | const router = useRouter(); 12 | 13 | return ( 14 | 21 |
22 | 23 | Logo 31 | 32 |

Error 500

33 |

34 | Server error or you don't have access. 35 |

36 |

37 | lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer nec 38 | odio. Praesent libero. Sed cursus ante dapibus diam. Sed nisi. 39 |

40 | 43 |
44 |
45 | Winter photo 53 |
54 |
55 | ); 56 | }; 57 | 58 | export default Error404; 59 | -------------------------------------------------------------------------------- /apps/host/src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import "@/styles/globals.css"; 2 | import type { AppProps } from "next/app"; 3 | import { SessionProvider } from "next-auth/react"; 4 | import { ThemeProvider as NextThemesProvider } from "next-themes"; 5 | import SwitchTheme from "./components/switch-theme"; 6 | 7 | export default function App({ 8 | Component, 9 | pageProps: { session, pageProps }, 10 | }: AppProps) { 11 | return ( 12 | 13 | 14 | 15 | 16 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /apps/host/src/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { Html, Main, Head, NextScript } from "next/document"; 2 | 3 | export default function Document() { 4 | return ( 5 | 6 | 7 | Technical Test for React.js Developer 8 | 12 | 13 |
14 | 15 | 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /apps/host/src/pages/api/auth/[...nextauth].ts: -------------------------------------------------------------------------------- 1 | import NextAuth from "next-auth"; 2 | import CredentialsProvider from "next-auth/providers/credentials"; 3 | import GithubProvider from "next-auth/providers/github"; 4 | 5 | declare module "next-auth" { 6 | interface User { 7 | id: number; 8 | name: string; 9 | email: string; 10 | } 11 | } 12 | 13 | export const authOptions = { 14 | providers: [ 15 | GithubProvider({ 16 | clientId: process.env.GITHUB_ID as string, 17 | clientSecret: process.env.GITHUB_SECRET as string, 18 | }), 19 | CredentialsProvider({ 20 | name: "Credentials", 21 | credentials: { 22 | email: { 23 | label: "Email", 24 | type: "email", 25 | placeholder: "example@correo.com", 26 | value: "example@correo.com", 27 | }, 28 | }, 29 | async authorize(credentials) { 30 | const user = { 31 | id: 1, 32 | name: "Tony Stark", 33 | email: "example@correo.com", 34 | }; 35 | if (credentials?.email === user.email) { 36 | return user; 37 | } else { 38 | return null; 39 | } 40 | }, 41 | }), 42 | ], 43 | pages: { 44 | signIn: "/", 45 | signOut: "/", 46 | error: "/", 47 | }, 48 | }; 49 | 50 | export default NextAuth(authOptions); 51 | -------------------------------------------------------------------------------- /apps/host/src/pages/components/dashboard-layout.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useSession, signOut } from "next-auth/react"; 3 | import { useRouter } from "next/navigation"; 4 | import { motion } from "framer-motion"; 5 | import { Button } from "@/components/ui/button"; 6 | import { TbLogout } from "react-icons/tb"; 7 | import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; 8 | import { Separator } from "@/components/ui/separator"; 9 | 10 | const DashboardLayout = ({ 11 | title, 12 | children, 13 | }: { 14 | children: React.ReactNode; 15 | title: string; 16 | }) => { 17 | const { data: session, status: sessionStatus } = useSession(); 18 | const router = useRouter(); 19 | 20 | if (sessionStatus === "unauthenticated") { 21 | router.push("/"); 22 | return null; 23 | } 24 | 25 | const name = session?.user?.name; 26 | const nameInitials = name 27 | ? name 28 | .split(" ") 29 | .filter(Boolean) 30 | .map((word) => word.charAt(0)) 31 | .join("") 32 | .toUpperCase() 33 | : ""; 34 | 35 | if (sessionStatus === "authenticated") { 36 | return ( 37 | 44 | 79 |
80 |

{title}

81 | 82 | {children} 83 | 84 | 85 | Lorem ipsum dolor sit amet consectetur adipisicing elit. Quia 86 | voluptatibus, quibusdam, voluptatum, doloribus magni voluptas 87 | consequatur natus quod quos dolorum doloremque ipsa. Quisquam 88 | voluptate, quae quas voluptatibus autem quos. 89 | 90 |
91 |
92 | ); 93 | } 94 | }; 95 | 96 | export default DashboardLayout; 97 | -------------------------------------------------------------------------------- /apps/host/src/pages/components/form-authenticated.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { useSession, signOut } from "next-auth/react"; 3 | import { useRouter } from "next/navigation"; 4 | 5 | import { Button } from "@/components/ui/button"; 6 | import { 7 | Card, 8 | CardContent, 9 | CardDescription, 10 | CardFooter, 11 | CardHeader, 12 | CardTitle, 13 | } from "@/components/ui/card"; 14 | 15 | const FormAuthenticated = () => { 16 | const { data: session } = useSession(); 17 | const router = useRouter(); 18 | 19 | return ( 20 | 21 | 22 | Welcome {session?.user?.name} 23 | 24 | You are now signed in. You can go to the dashboard. 25 | 26 | 27 | 28 | 35 | 36 | 37 |
38 |
39 | 40 |
41 |
42 | 43 | Or Sign Out 44 | 45 |
46 |
47 |
48 | 49 | 56 | 57 | 58 | 59 | By clicking continue, you agree to our Terms of Service and Privacy 60 | Policy. 61 | 62 | 63 |
64 | ); 65 | }; 66 | 67 | export default FormAuthenticated; 68 | -------------------------------------------------------------------------------- /apps/host/src/pages/components/form-sign-in.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { signIn } from "next-auth/react"; 3 | import { FaGithub } from "react-icons/fa"; 4 | import { useRouter } from "next/router"; 5 | 6 | import { Button } from "@/components/ui/button"; 7 | import { 8 | Card, 9 | CardContent, 10 | CardDescription, 11 | CardFooter, 12 | CardHeader, 13 | CardTitle, 14 | } from "@/components/ui/card"; 15 | import { Input } from "@/components/ui/input"; 16 | import { Label } from "@/components/ui/label"; 17 | 18 | import type { 19 | GetServerSidePropsContext, 20 | InferGetServerSidePropsType, 21 | } from "next"; 22 | import { getCsrfToken } from "next-auth/react"; 23 | 24 | const FormSignIn = ({ 25 | csrfToken, 26 | }: InferGetServerSidePropsType) => { 27 | const onSubmit = (event: React.FormEvent) => { 28 | event.preventDefault(); 29 | signIn("credentials", { 30 | email: event.currentTarget.email.value, 31 | callbackUrl: "/dashboard", 32 | }); 33 | }; 34 | 35 | const router = useRouter(); 36 | const errorType = router.query.error as string | undefined; 37 | 38 | let error = ""; 39 | if (errorType === "CredentialsSignin") { 40 | error = 41 | "The email don't is correct, try again with DEMO email: example@correo.com"; 42 | } 43 | 44 | return ( 45 | 46 | 47 | Sign in 48 | 49 | Please use one of the two ways to log in and enter the dashboard 50 | panel. 51 | 52 | 53 | 54 |
55 | 56 |
57 |
58 | 59 | 66 | {error && {error}} 67 | 70 |
71 |
72 |
73 |
74 | 75 |
76 |
77 | 78 |
79 |
80 | 81 | Or continue with 82 | 83 |
84 |
85 |
86 | 87 | 95 | 96 | 97 | 98 | By clicking continue, you agree to our Terms of Service and Privacy 99 | Policy. 100 | 101 | 102 |
103 | ); 104 | }; 105 | 106 | export default FormSignIn; 107 | export async function getServerSideProps(context: GetServerSidePropsContext) { 108 | return { 109 | props: { 110 | csrfToken: await getCsrfToken(context), 111 | }, 112 | }; 113 | } 114 | -------------------------------------------------------------------------------- /apps/host/src/pages/components/switch-theme.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { useTheme } from "next-themes"; 3 | import { TbMoonStars, TbSun } from "react-icons/tb"; 4 | 5 | interface SwitchThemeProps { 6 | clases?: string; 7 | } 8 | 9 | const SwitchTheme: React.FC = ({ ...props }) => { 10 | const { setTheme, theme } = useTheme(); 11 | 12 | const { clases } = props; 13 | 14 | useEffect(() => { 15 | const moonStars = document.getElementById("moonStars"); 16 | const sun = document.getElementById("sun"); 17 | 18 | if (theme === "light") { 19 | moonStars?.classList.add("hidden"); 20 | sun?.classList.remove("hidden"); 21 | } else { 22 | moonStars?.classList.remove("hidden"); 23 | sun?.classList.add("hidden"); 24 | } 25 | }, [theme]); 26 | 27 | return ( 28 |
29 | setTheme("light")} 32 | className="w-6 h-6 cursor-pointer" 33 | /> 34 | setTheme("dark")} 37 | className="w-6 h-6 hidden cursor-pointer" 38 | /> 39 |
40 | ); 41 | }; 42 | 43 | export default SwitchTheme; 44 | -------------------------------------------------------------------------------- /apps/host/src/pages/dashboard/clients/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import DashboardLayout from "@/pages/components/dashboard-layout"; 3 | import dynamic from "next/dynamic"; 4 | 5 | const Table = dynamic(() => import("remote/table"), { 6 | ssr: false, 7 | }); 8 | 9 | const AnalyticsPage: React.FC = () => { 10 | return ( 11 | 12 | 13 | 14 | ); 15 | }; 16 | 17 | export default AnalyticsPage; 18 | -------------------------------------------------------------------------------- /apps/host/src/pages/dashboard/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | Card, 4 | Grid, 5 | Tab, 6 | TabGroup, 7 | TabList, 8 | TabPanel, 9 | TabPanels, 10 | Text, 11 | Metric, 12 | Flex, 13 | ProgressBar, 14 | AreaChart, 15 | Title, 16 | LineChart, 17 | } from "@tremor/react"; 18 | import DashboardLayout from "@/pages/components/dashboard-layout"; 19 | 20 | const chartdata2 = [ 21 | { 22 | date: "Jan 22", 23 | SemiAnalysis: 2890, 24 | "The Pragmatic Engineer": 2338, 25 | }, 26 | { 27 | date: "Feb 22", 28 | SemiAnalysis: 2756, 29 | "The Pragmatic Engineer": 2103, 30 | }, 31 | { 32 | date: "Mar 22", 33 | SemiAnalysis: 3322, 34 | "The Pragmatic Engineer": 2194, 35 | }, 36 | { 37 | date: "Apr 22", 38 | SemiAnalysis: 3470, 39 | "The Pragmatic Engineer": 2108, 40 | }, 41 | { 42 | date: "May 22", 43 | SemiAnalysis: 3475, 44 | "The Pragmatic Engineer": 1812, 45 | }, 46 | { 47 | date: "Jun 22", 48 | SemiAnalysis: 3129, 49 | "The Pragmatic Engineer": 1726, 50 | }, 51 | ]; 52 | 53 | const valueFormatter2 = function (number: number) { 54 | return "$ " + new Intl.NumberFormat("us").format(number).toString(); 55 | }; 56 | 57 | const chartdata = [ 58 | { 59 | year: 1970, 60 | "Export Growth Rate": 2.04, 61 | "Import Growth Rate": 1.53, 62 | }, 63 | { 64 | year: 1971, 65 | "Export Growth Rate": 1.96, 66 | "Import Growth Rate": 1.58, 67 | }, 68 | { 69 | year: 1972, 70 | "Export Growth Rate": 1.96, 71 | "Import Growth Rate": 1.61, 72 | }, 73 | { 74 | year: 1973, 75 | "Export Growth Rate": 1.93, 76 | "Import Growth Rate": 1.61, 77 | }, 78 | { 79 | year: 1974, 80 | "Export Growth Rate": 1.88, 81 | "Import Growth Rate": 1.67, 82 | }, 83 | //... 84 | ]; 85 | 86 | const valueFormatter = (number: number) => 87 | `$ ${new Intl.NumberFormat("us").format(number).toString()}`; 88 | 89 | const DashboardPage: React.FC = () => { 90 | return ( 91 | 92 | {/*
*/} 93 | 94 | 95 | Overview 96 | Detail 97 | 98 | 99 | 100 | 101 | 102 | Sales 103 | $ 71,465 104 | 105 | 32% of annual target 106 | $ 225,000 107 | 108 | 109 | 110 | 111 | Sales 112 | $ 71,465 113 | 114 | 32% of annual target 115 | $ 225,000 116 | 117 | 118 | 119 | 120 | Sales 121 | $ 71,465 122 | 123 | 32% of annual target 124 | $ 225,000 125 | 126 | 127 | 128 | 129 |
130 | 131 | Newsletter revenue over time (USD) 132 | 141 | 142 |
143 | 144 | 145 | Sales 146 | $ 71,465 147 | 148 | 32% of annual target 149 | $ 225,000 150 | 151 | 152 | 153 | 154 | Sales 155 | $ 71,465 156 | 157 | 32% of annual target 158 | $ 225,000 159 | 160 | 161 | 162 | 163 | Sales 164 | $ 71,465 165 | 166 | 32% of annual target 167 | $ 225,000 168 | 169 | 170 | 171 | 172 |
173 | 174 | Export/Import Growth Rates (1970 to 2021) 175 | 184 | 185 |
186 | 187 | 188 | Sales 189 | $ 71,465 190 | 191 | 32% of annual target 192 | $ 225,000 193 | 194 | 195 | 196 | 197 | Sales 198 | $ 71,465 199 | 200 | 32% of annual target 201 | $ 225,000 202 | 203 | 204 | 205 | 206 | Sales 207 | $ 71,465 208 | 209 | 32% of annual target 210 | $ 225,000 211 | 212 | 213 | 214 | 215 |
216 | 217 |
218 | 219 | Newsletter revenue over time (USD) 220 | 229 | 230 |
231 |
232 |
233 |
234 |
235 | ); 236 | }; 237 | 238 | export default DashboardPage; 239 | -------------------------------------------------------------------------------- /apps/host/src/pages/dashboard/website-analytics/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import DashboardLayout from "@/pages/components/dashboard-layout"; 3 | import { 4 | BarList, 5 | BarChart, 6 | Bold, 7 | Card, 8 | Flex, 9 | Text, 10 | Title, 11 | } from "@tremor/react"; 12 | 13 | const chartdata2 = [ 14 | { 15 | name: "Topic 1", 16 | "Group A": 890, 17 | "Group B": 338, 18 | "Group C": 538, 19 | "Group D": 396, 20 | "Group E": 138, 21 | "Group F": 436, 22 | }, 23 | { 24 | name: "Topic 2", 25 | "Group A": 289, 26 | "Group B": 233, 27 | "Group C": 253, 28 | "Group D": 333, 29 | "Group E": 133, 30 | "Group F": 533, 31 | }, 32 | { 33 | name: "Topic 3", 34 | "Group A": 380, 35 | "Group B": 535, 36 | "Group C": 352, 37 | "Group D": 718, 38 | "Group E": 539, 39 | "Group F": 234, 40 | }, 41 | { 42 | name: "Topic 4", 43 | "Group A": 90, 44 | "Group B": 98, 45 | "Group C": 28, 46 | "Group D": 33, 47 | "Group E": 61, 48 | "Group F": 53, 49 | }, 50 | ]; 51 | 52 | const data = [ 53 | { 54 | name: "Twitter", 55 | value: 456, 56 | href: "https://twitter.com/tremorlabs", 57 | icon: function TwitterIcon() { 58 | return ( 59 | 66 | 67 | 68 | 69 | ); 70 | }, 71 | }, 72 | { 73 | name: "Google", 74 | value: 351, 75 | href: "https://google.com", 76 | icon: function GoogleIcon() { 77 | return ( 78 | 85 | 86 | 87 | 88 | ); 89 | }, 90 | }, 91 | { 92 | name: "GitHub", 93 | value: 271, 94 | href: "https://github.com/tremorlabs/tremor", 95 | icon: function GitHubIcon() { 96 | return ( 97 | 104 | 105 | 106 | 107 | ); 108 | }, 109 | }, 110 | { 111 | name: "Reddit", 112 | value: 191, 113 | href: "https://reddit.com", 114 | icon: function RedditIcon() { 115 | return ( 116 | 123 | 124 | 125 | 126 | ); 127 | }, 128 | }, 129 | { 130 | name: "Youtube", 131 | value: 91, 132 | href: "https://www.youtube.com/@tremorlabs3079", 133 | icon: function YouTubeIcon() { 134 | return ( 135 | 142 | 143 | 144 | 145 | ); 146 | }, 147 | }, 148 | { 149 | name: "Facebook", 150 | value: 62, 151 | href: "https://facebook.com", 152 | icon: function FacebookIcon() { 153 | return ( 154 | 161 | 162 | 163 | 164 | ); 165 | }, 166 | }, 167 | ]; 168 | 169 | const valueFormatter = (number: number) => 170 | `$ ${new Intl.NumberFormat("us").format(number).toString()}`; 171 | 172 | const WebsiteAnalytics: React.FC = () => { 173 | return ( 174 | 175 | 176 | Website Analytics 177 | 178 | 179 | Source 180 | 181 | 182 | Visits 183 | 184 | 185 | 186 | 187 | 188 | 204 | 205 | 206 | ); 207 | }; 208 | 209 | export default WebsiteAnalytics; 210 | -------------------------------------------------------------------------------- /apps/host/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Link from "next/link"; 3 | import Image from "next/image"; 4 | import imgLogo from "@/assets/images/logo.png"; 5 | import FormSignIn from "./components/form-sign-in"; 6 | import FormAuthenticated from "./components/form-authenticated"; 7 | import { useSession } from "next-auth/react"; 8 | import { motion } from "framer-motion"; 9 | 10 | const Home: React.FC = () => { 11 | const { data: session } = useSession(); 12 | 13 | return ( 14 | 21 |
22 | 23 | 31 | 32 |

33 | Technical Test for React.js Developer 34 |

35 |

36 | Using Next.js + Typescript + TailwindCSS 37 |

38 |

39 | This project is a technical test for a React Developer position. It is 40 | a dashboard application built with Next.js and TailwindCSS. 41 |

42 |

43 | The application is a micro-frontend architecture with Module 44 | Federation. The application is a dashboard that displays data from a 45 | mock API. The dashboard is a dynamic dashboard with charts and graphs. 46 |

47 |

48 | By{" "} 49 | 54 | Mariano Álvarez 55 | 56 |

57 |
58 |
59 | {session ? : } 60 |
61 |
62 | ); 63 | }; 64 | 65 | export default Home; 66 | -------------------------------------------------------------------------------- /apps/host/src/pages/login/index.tsx: -------------------------------------------------------------------------------- 1 | import type { 2 | GetServerSidePropsContext, 3 | InferGetServerSidePropsType, 4 | } from "next"; 5 | import { getCsrfToken } from "next-auth/react"; 6 | import { signIn } from "next-auth/react"; 7 | 8 | const SignIn = ({ 9 | csrfToken, 10 | }: InferGetServerSidePropsType) => { 11 | const onSubmit = (event: React.FormEvent) => { 12 | event.preventDefault(); 13 | signIn("credentials", { 14 | email: event.currentTarget.name, 15 | callbackUrl: "/dashboard", 16 | }); 17 | }; 18 | 19 | return ( 20 |
21 | 22 | 23 | 24 | 25 | ); 26 | }; 27 | 28 | export default SignIn; 29 | 30 | export async function getServerSideProps(context: GetServerSidePropsContext) { 31 | return { 32 | props: { 33 | csrfToken: await getCsrfToken(context), 34 | }, 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /apps/host/src/styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body { 6 | font-family: "Inter", sans-serif; 7 | background-color: hsl(var(--background)); 8 | } 9 | 10 | @layer base { 11 | :root { 12 | --background: 0 0% 100%; 13 | --foreground: 222.2 84% 4.9%; 14 | --card: 0 0% 100%; 15 | --card-foreground: 222.2 84% 4.9%; 16 | --popover: 0 0% 100%; 17 | --popover-foreground: 222.2 84% 4.9%; 18 | --primary: 221.2 83.2% 53.3%; 19 | --primary-foreground: 210 40% 98%; 20 | --secondary: 210 40% 96.1%; 21 | --secondary-foreground: 222.2 47.4% 11.2%; 22 | --muted: 210 40% 96.1%; 23 | --muted-foreground: 215.4 16.3% 46.9%; 24 | --accent: 210 40% 96.1%; 25 | --accent-foreground: 222.2 47.4% 11.2%; 26 | --destructive: 0 84.2% 60.2%; 27 | --destructive-foreground: 210 40% 98%; 28 | --border: 214.3 31.8% 91.4%; 29 | --input: 214.3 31.8% 91.4%; 30 | --ring: 221.2 83.2% 53.3%; 31 | --radius: 0.3rem; 32 | } 33 | 34 | .dark { 35 | --background: 222.2 84% 4.9%; 36 | --foreground: 210 40% 98%; 37 | --card: 222.2 84% 4.9%; 38 | --card-foreground: 210 40% 98%; 39 | --popover: 222.2 84% 4.9%; 40 | --popover-foreground: 210 40% 98%; 41 | --primary: 217.2 91.2% 59.8%; 42 | --primary-foreground: 222.2 47.4% 11.2%; 43 | --secondary: 217.2 32.6% 17.5%; 44 | --secondary-foreground: 210 40% 98%; 45 | --muted: 217.2 32.6% 17.5%; 46 | --muted-foreground: 215 20.2% 65.1%; 47 | --accent: 217.2 32.6% 17.5%; 48 | --accent-foreground: 210 40% 98%; 49 | --destructive: 0 62.8% 30.6%; 50 | --destructive-foreground: 210 40% 98%; 51 | --border: 217.2 32.6% 17.5%; 52 | --input: 217.2 32.6% 17.5%; 53 | --ring: 224.3 76.3% 48%; 54 | } 55 | } 56 | 57 | @layer base { 58 | * { 59 | @apply border-border; 60 | } 61 | body { 62 | @apply bg-background text-foreground; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /apps/host/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | const colors = require("tailwindcss/colors"); 3 | module.exports = { 4 | mode: "jit", 5 | darkMode: ["class"], 6 | content: [ 7 | "./pages/**/*.{js,ts,jsx,tsx,mdx}", 8 | "./components/**/*.{js,ts,jsx,tsx,mdx}", 9 | "./app/**/*.{js,ts,jsx,tsx,mdx}", 10 | "./src/**/*.{js,ts,jsx,tsx,mdx}", 11 | "./node_modules/@tremor/**/*.{js,ts,jsx,tsx}", 12 | ], 13 | prefix: "", 14 | theme: { 15 | transparent: "transparent", 16 | current: "currentColor", 17 | container: { 18 | center: true, 19 | padding: "2rem", 20 | screens: { 21 | "2xl": "1400px", 22 | }, 23 | }, 24 | extend: { 25 | colors: { 26 | border: "hsl(var(--border))", 27 | input: "hsl(var(--input))", 28 | ring: "hsl(var(--ring))", 29 | background: "hsl(var(--background))", 30 | foreground: "hsl(var(--foreground))", 31 | primary: { 32 | DEFAULT: "hsl(var(--primary))", 33 | foreground: "hsl(var(--primary-foreground))", 34 | }, 35 | secondary: { 36 | DEFAULT: "hsl(var(--secondary))", 37 | foreground: "hsl(var(--secondary-foreground))", 38 | }, 39 | destructive: { 40 | DEFAULT: "hsl(var(--destructive))", 41 | foreground: "hsl(var(--destructive-foreground))", 42 | }, 43 | muted: { 44 | DEFAULT: "hsl(var(--muted))", 45 | foreground: "hsl(var(--muted-foreground))", 46 | }, 47 | accent: { 48 | DEFAULT: "hsl(var(--accent))", 49 | foreground: "hsl(var(--accent-foreground))", 50 | }, 51 | popover: { 52 | DEFAULT: "hsl(var(--popover))", 53 | foreground: "hsl(var(--popover-foreground))", 54 | }, 55 | card: { 56 | DEFAULT: "hsl(var(--card))", 57 | foreground: "hsl(var(--card-foreground))", 58 | }, 59 | tremor: { 60 | brand: { 61 | faint: colors.blue[50], 62 | muted: colors.blue[200], 63 | subtle: colors.blue[400], 64 | DEFAULT: colors.blue[500], 65 | emphasis: colors.blue[700], 66 | inverted: colors.white, 67 | }, 68 | background: { 69 | muted: colors.gray[50], 70 | subtle: colors.gray[100], 71 | DEFAULT: colors.white, 72 | emphasis: colors.gray[700], 73 | }, 74 | border: { 75 | DEFAULT: colors.gray[200], 76 | }, 77 | ring: { 78 | DEFAULT: colors.gray[200], 79 | }, 80 | content: { 81 | subtle: colors.gray[400], 82 | DEFAULT: colors.gray[500], 83 | emphasis: colors.gray[700], 84 | strong: colors.gray[900], 85 | inverted: colors.white, 86 | }, 87 | }, 88 | // dark mode 89 | "dark-tremor": { 90 | brand: { 91 | faint: "#0B1229", 92 | muted: colors.blue[950], 93 | subtle: colors.blue[800], 94 | DEFAULT: colors.blue[500], 95 | emphasis: colors.blue[400], 96 | inverted: colors.blue[950], 97 | }, 98 | background: { 99 | muted: "#131A2B", 100 | subtle: colors.gray[800], 101 | DEFAULT: colors.gray[900], 102 | emphasis: colors.gray[300], 103 | }, 104 | border: { 105 | DEFAULT: colors.gray[700], 106 | }, 107 | ring: { 108 | DEFAULT: colors.gray[800], 109 | }, 110 | content: { 111 | subtle: colors.gray[600], 112 | DEFAULT: colors.gray[500], 113 | emphasis: colors.gray[200], 114 | strong: colors.gray[50], 115 | inverted: colors.gray[950], 116 | }, 117 | }, 118 | }, 119 | borderRadius: { 120 | lg: "var(--radius)", 121 | md: "calc(var(--radius) - 2px)", 122 | sm: "calc(var(--radius) - 4px)", 123 | }, 124 | keyframes: { 125 | "accordion-down": { 126 | from: { height: "0" }, 127 | to: { height: "var(--radix-accordion-content-height)" }, 128 | }, 129 | "accordion-up": { 130 | from: { height: "var(--radix-accordion-content-height)" }, 131 | to: { height: "0" }, 132 | }, 133 | }, 134 | animation: { 135 | "accordion-down": "accordion-down 0.2s ease-out", 136 | "accordion-up": "accordion-up 0.2s ease-out", 137 | }, 138 | boxShadow: { 139 | // light 140 | "tremor-input": "0 1px 2px 0 rgb(0 0 0 / 0.05)", 141 | "tremor-card": 142 | "0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)", 143 | "tremor-dropdown": 144 | "0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)", 145 | // dark 146 | "dark-tremor-input": "0 1px 2px 0 rgb(0 0 0 / 0.05)", 147 | "dark-tremor-card": 148 | "0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)", 149 | "dark-tremor-dropdown": 150 | "0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)", 151 | }, 152 | borderRadius: { 153 | "tremor-small": "0.375rem", 154 | "tremor-default": "0.5rem", 155 | "tremor-full": "9999px", 156 | }, 157 | fontSize: { 158 | "tremor-label": ["0.75rem"], 159 | "tremor-default": ["0.875rem", { lineHeight: "1.25rem" }], 160 | "tremor-title": ["1.125rem", { lineHeight: "1.75rem" }], 161 | "tremor-metric": ["1.875rem", { lineHeight: "2.25rem" }], 162 | }, 163 | }, 164 | }, 165 | safelist: [ 166 | { 167 | pattern: 168 | /^(bg-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/, 169 | variants: ["hover", "ui-selected"], 170 | }, 171 | { 172 | pattern: 173 | /^(text-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/, 174 | variants: ["hover", "ui-selected"], 175 | }, 176 | { 177 | pattern: 178 | /^(border-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/, 179 | variants: ["hover", "ui-selected"], 180 | }, 181 | { 182 | pattern: 183 | /^(ring-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/, 184 | }, 185 | { 186 | pattern: 187 | /^(stroke-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/, 188 | }, 189 | { 190 | pattern: 191 | /^(fill-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/, 192 | }, 193 | ], 194 | plugins: [require("tailwindcss-animate"), require("@headlessui/tailwindcss")], 195 | }; 196 | -------------------------------------------------------------------------------- /apps/host/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "target": "es5", 5 | "lib": ["dom", "dom.iterable", "esnext"], 6 | "allowJs": true, 7 | "skipLibCheck": true, 8 | "strict": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "paths": { 18 | "@/*": ["./src/*"], 19 | }, 20 | "forceConsistentCasingInFileNames": true, 21 | }, 22 | "include": ["next-env.d.ts", "federated.d.ts", "**/*.ts", "**/*.tsx"], 23 | "exclude": ["node_modules"], 24 | } 25 | -------------------------------------------------------------------------------- /apps/remote/.env.sample: -------------------------------------------------------------------------------- 1 | NEXT_PRIVATE_LOCAL_WEBPACK= -------------------------------------------------------------------------------- /apps/remote/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next", "next/core-web-vitals"] 3 | } 4 | -------------------------------------------------------------------------------- /apps/remote/.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 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /apps/remote/components/table/table.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { DataGrid, GridColDef } from "@mui/x-data-grid"; 3 | import useSWRImmutable from "swr/immutable"; 4 | import { faker } from "@faker-js/faker"; 5 | import { motion } from "framer-motion"; 6 | import "tailwindcss/tailwind.css"; 7 | 8 | const fetcher = async () => { 9 | await new Promise((resolve) => setTimeout(resolve, 1000)); 10 | return generateData(15682); 11 | }; 12 | 13 | const generateData = (amount: number) => { 14 | const data = []; 15 | for (let i = 0; i < amount; i++) { 16 | data.push({ 17 | id: i + 1, 18 | col1: faker.person.firstName(), 19 | col2: faker.person.lastName(), 20 | col3: faker.internet.email(), 21 | col4: faker.phone.number(), 22 | col5: faker.location.city(), 23 | }); 24 | } 25 | return data; 26 | }; 27 | 28 | const columns: GridColDef[] = [ 29 | { field: "id", headerName: "ID", width: 50 }, 30 | { field: "col1", headerName: "Name", width: 105 }, 31 | { field: "col2", headerName: "Last Name", width: 105 }, 32 | { field: "col3", headerName: "Email", width: 325 }, 33 | { field: "col4", headerName: "Phone", width: 250 }, 34 | { field: "col5", headerName: "City", width: 150 }, 35 | ]; 36 | 37 | const Table = () => { 38 | const { data: rows, error } = useSWRImmutable("dummy-key", fetcher); 39 | 40 | if (error) return
Error al cargar los datos
; 41 | if (!rows) return
Cargando...
; 42 | 43 | return ( 44 | 50 | 62 | 63 | ); 64 | }; 65 | 66 | export default Table; 67 | -------------------------------------------------------------------------------- /apps/remote/next.config.js: -------------------------------------------------------------------------------- 1 | const { NextFederationPlugin } = require("@module-federation/nextjs-mf"); 2 | 3 | /** @type {import('next').NextConfig} */ 4 | const nextConfig = { 5 | webpack(config, options) { 6 | const { isServer } = options; 7 | const remoteDir = isServer ? "ssr" : "chunks"; 8 | 9 | config.plugins.push( 10 | new NextFederationPlugin({ 11 | name: "remote", 12 | filename: `static/${remoteDir}/remoteEntry.js`, 13 | exposes: { 14 | "./table": "components/table/table", 15 | }, 16 | shared: { 17 | tailwindcss: { 18 | eager: true, 19 | singleton: true, 20 | requiredVersion: false, 21 | }, 22 | }, 23 | }), 24 | ); 25 | 26 | return config; 27 | }, 28 | }; 29 | 30 | module.exports = nextConfig; 31 | -------------------------------------------------------------------------------- /apps/remote/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "remote", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "set NEXT_PRIVATE_LOCAL_WEBPACK=true && next dev -p 3011", 7 | "build": "set NEXT_PRIVATE_LOCAL_WEBPACK=true && next build", 8 | "start": "set NEXT_PRIVATE_LOCAL_WEBPACK=true && next start -p 3011", 9 | "format": "prettier --write .", 10 | "lint": "next lint" 11 | }, 12 | "dependencies": { 13 | "@emotion/react": "^11.11.3", 14 | "@emotion/styled": "^11.11.0", 15 | "@module-federation/nextjs-mf": "6.7.1", 16 | "@mui/icons-material": "^5.15.5", 17 | "@mui/material": "^5.15.5", 18 | "@mui/x-data-grid": "^6.18.7", 19 | "@radix-ui/react-checkbox": "^1.0.4", 20 | "@radix-ui/react-icons": "^1.3.0", 21 | "class-variance-authority": "^0.7.0", 22 | "clsx": "^2.1.0", 23 | "lucide-react": "^0.309.0", 24 | "next": "13.4.7", 25 | "next-auth": "^4.24.5", 26 | "next-themes": "^0.2.1", 27 | "prettier": "^3.2.4", 28 | "react": "^18.2.0", 29 | "react-dom": "^18.2.0", 30 | "swr": "^2.2.4", 31 | "framer-motion": "^10.18.0", 32 | "tailwind-merge": "^2.2.0", 33 | "tailwindcss-animate": "^1.0.7", 34 | "webpack": "^5.89.0" 35 | }, 36 | "devDependencies": { 37 | "@faker-js/faker": "^8.3.1", 38 | "@types/node": "^20.11.5", 39 | "@types/react": "^18.2.47", 40 | "@types/react-dom": "^18.2.18", 41 | "autoprefixer": "latest", 42 | "eslint": "latest", 43 | "eslint-config-next": "latest", 44 | "postcss": "latest", 45 | "tailwindcss": "latest", 46 | "typescript": "^5.3.3" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /apps/remote/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { AppProps } from "next/app"; 2 | import Head from "next/head"; 3 | import "./styles.css"; 4 | 5 | function CustomApp({ Component, pageProps }: AppProps) { 6 | return ( 7 | <> 8 | 9 | Analytics Module - Remote 10 | 11 | 12 | 13 |
14 | 15 |
16 | 17 | ); 18 | } 19 | 20 | export default CustomApp; 21 | -------------------------------------------------------------------------------- /apps/remote/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import Table from "../components/table/table"; 2 | 3 | const HomePage = () => { 4 | return ( 5 |
6 |
7 | 8 | ); 9 | }; 10 | 11 | export default HomePage; 12 | -------------------------------------------------------------------------------- /apps/remote/pages/styles.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 222.2 84% 4.9%; 9 | 10 | --card: 0 0% 100%; 11 | --card-foreground: 222.2 84% 4.9%; 12 | 13 | --popover: 0 0% 100%; 14 | --popover-foreground: 222.2 84% 4.9%; 15 | 16 | --primary: 222.2 47.4% 11.2%; 17 | --primary-foreground: 210 40% 98%; 18 | 19 | --secondary: 210 40% 96.1%; 20 | --secondary-foreground: 222.2 47.4% 11.2%; 21 | 22 | --muted: 210 40% 96.1%; 23 | --muted-foreground: 215.4 16.3% 46.9%; 24 | 25 | --accent: 210 40% 96.1%; 26 | --accent-foreground: 222.2 47.4% 11.2%; 27 | 28 | --destructive: 0 84.2% 60.2%; 29 | --destructive-foreground: 210 40% 98%; 30 | 31 | --border: 214.3 31.8% 91.4%; 32 | --input: 214.3 31.8% 91.4%; 33 | --ring: 222.2 84% 4.9%; 34 | 35 | --radius: 0.5rem; 36 | } 37 | 38 | .dark { 39 | --background: 222.2 84% 4.9%; 40 | --foreground: 210 40% 98%; 41 | 42 | --card: 222.2 84% 4.9%; 43 | --card-foreground: 210 40% 98%; 44 | 45 | --popover: 222.2 84% 4.9%; 46 | --popover-foreground: 210 40% 98%; 47 | 48 | --primary: 210 40% 98%; 49 | --primary-foreground: 222.2 47.4% 11.2%; 50 | 51 | --secondary: 217.2 32.6% 17.5%; 52 | --secondary-foreground: 210 40% 98%; 53 | 54 | --muted: 217.2 32.6% 17.5%; 55 | --muted-foreground: 215 20.2% 65.1%; 56 | 57 | --accent: 217.2 32.6% 17.5%; 58 | --accent-foreground: 210 40% 98%; 59 | 60 | --destructive: 0 62.8% 30.6%; 61 | --destructive-foreground: 210 40% 98%; 62 | 63 | --border: 217.2 32.6% 17.5%; 64 | --input: 217.2 32.6% 17.5%; 65 | --ring: 212.7 26.8% 83.9%; 66 | } 67 | } 68 | 69 | @layer base { 70 | * { 71 | @apply border-border; 72 | } 73 | body { 74 | @apply bg-background text-foreground; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /apps/remote/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /apps/remote/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creativoma/nextjs-typescript-module-federation/4d10175ef1d98ceade55279e3f362c4f58e5bc19/apps/remote/public/favicon.png -------------------------------------------------------------------------------- /apps/remote/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | mode: "jit", 4 | darkMode: ["class"], 5 | content: [ 6 | "./pages/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./components/**/*.{js,ts,jsx,tsx,mdx}", 8 | "./app/**/*.{js,ts,jsx,tsx,mdx}", 9 | "./src/**/*.{js,ts,jsx,tsx,mdx}", 10 | ], 11 | prefix: "", 12 | theme: { 13 | transparent: "transparent", 14 | current: "currentColor", 15 | container: { 16 | center: true, 17 | padding: "2rem", 18 | screens: { 19 | "2xl": "1400px", 20 | }, 21 | }, 22 | extend: { 23 | colors: { 24 | border: "hsl(var(--border))", 25 | input: "hsl(var(--input))", 26 | ring: "hsl(var(--ring))", 27 | background: "hsl(var(--background))", 28 | foreground: "hsl(var(--foreground))", 29 | primary: { 30 | DEFAULT: "hsl(var(--primary))", 31 | foreground: "hsl(var(--primary-foreground))", 32 | }, 33 | secondary: { 34 | DEFAULT: "hsl(var(--secondary))", 35 | foreground: "hsl(var(--secondary-foreground))", 36 | }, 37 | destructive: { 38 | DEFAULT: "hsl(var(--destructive))", 39 | foreground: "hsl(var(--destructive-foreground))", 40 | }, 41 | muted: { 42 | DEFAULT: "hsl(var(--muted))", 43 | foreground: "hsl(var(--muted-foreground))", 44 | }, 45 | accent: { 46 | DEFAULT: "hsl(var(--accent))", 47 | foreground: "hsl(var(--accent-foreground))", 48 | }, 49 | popover: { 50 | DEFAULT: "hsl(var(--popover))", 51 | foreground: "hsl(var(--popover-foreground))", 52 | }, 53 | card: { 54 | DEFAULT: "hsl(var(--card))", 55 | foreground: "hsl(var(--card-foreground))", 56 | }, 57 | }, 58 | borderRadius: { 59 | lg: "var(--radius)", 60 | md: "calc(var(--radius) - 2px)", 61 | sm: "calc(var(--radius) - 4px)", 62 | }, 63 | keyframes: { 64 | "accordion-down": { 65 | from: { height: "0" }, 66 | to: { height: "var(--radix-accordion-content-height)" }, 67 | }, 68 | "accordion-up": { 69 | from: { height: "var(--radix-accordion-content-height)" }, 70 | to: { height: "0" }, 71 | }, 72 | }, 73 | animation: { 74 | "accordion-down": "accordion-down 0.2s ease-out", 75 | "accordion-up": "accordion-up 0.2s ease-out", 76 | }, 77 | }, 78 | }, 79 | plugins: [require("tailwindcss-animate")], 80 | }; 81 | -------------------------------------------------------------------------------- /apps/remote/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": false, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", // Update this line 15 | "incremental": true, 16 | "forceConsistentCasingInFileNames": true, 17 | }, 18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 19 | "exclude": ["node_modules"], 20 | } 21 | -------------------------------------------------------------------------------- /docs/technical-test-react-developer-with-nextjs.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creativoma/nextjs-typescript-module-federation/4d10175ef1d98ceade55279e3f362c4f58e5bc19/docs/technical-test-react-developer-with-nextjs.pdf -------------------------------------------------------------------------------- /public/screen-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creativoma/nextjs-typescript-module-federation/4d10175ef1d98ceade55279e3f362c4f58e5bc19/public/screen-1.png -------------------------------------------------------------------------------- /public/screen-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creativoma/nextjs-typescript-module-federation/4d10175ef1d98ceade55279e3f362c4f58e5bc19/public/screen-2.png -------------------------------------------------------------------------------- /public/screen-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creativoma/nextjs-typescript-module-federation/4d10175ef1d98ceade55279e3f362c4f58e5bc19/public/screen-3.png -------------------------------------------------------------------------------- /public/screen-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creativoma/nextjs-typescript-module-federation/4d10175ef1d98ceade55279e3f362c4f58e5bc19/public/screen-4.png --------------------------------------------------------------------------------