├── .eslintrc.json ├── public └── favicon.ico ├── next.config.js ├── next-env.d.ts ├── src ├── createEmotionCache.ts └── theme.ts ├── .gitignore ├── docker-compose.yml ├── tsconfig.json ├── pages ├── api │ └── connection.ts ├── _app.tsx ├── index.tsx └── _document.tsx ├── package.json ├── LICENSE ├── scripts └── generateData.js ├── README.md └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mui/tech-challenge-full-stack/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | module.exports = { 3 | reactStrictMode: true, 4 | }; 5 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/createEmotionCache.ts: -------------------------------------------------------------------------------- 1 | import createCache from '@emotion/cache'; 2 | 3 | // prepend: true moves MUI styles to the top of the so they're loaded first. 4 | // It allows developers to easily override MUI styles with other styling solutions, like CSS modules. 5 | export default function createEmotionCache() { 6 | return createCache({ key: 'css', prepend: true }); 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # It is best to ignoring editor and system files in a local .gitignore configuration file. 2 | # However, in order to prevent issues, they are ignored here. 3 | .DS_STORE 4 | .idea 5 | # IntelliJ IDEA module file 6 | *.iml 7 | .vscode/* 8 | !.vscode/launch.json 9 | *.log 10 | *.tsbuildinfo 11 | /.eslintcache 12 | /.nyc_output 13 | /tmp 14 | .next 15 | build 16 | node_modules 17 | package-lock.json 18 | -------------------------------------------------------------------------------- /src/theme.ts: -------------------------------------------------------------------------------- 1 | import { createTheme } from '@mui/material/styles'; 2 | import { red } from '@mui/material/colors'; 3 | 4 | // Create a theme instance. 5 | const theme = createTheme({ 6 | palette: { 7 | primary: { 8 | main: '#556cd6', 9 | }, 10 | secondary: { 11 | main: '#19857b', 12 | }, 13 | error: { 14 | main: red.A400, 15 | }, 16 | }, 17 | }); 18 | 19 | export default theme; 20 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | services: 3 | postgres: 4 | image: postgres:14.2 5 | restart: always 6 | environment: 7 | - POSTGRES_USER=postgres 8 | - POSTGRES_PASSWORD=postgres 9 | logging: 10 | options: 11 | max-size: 10m 12 | max-file: "3" 13 | ports: 14 | - "5433:5432" 15 | volumes: 16 | - postgres-data:/var/lib/postgresql/data 17 | - ./init.sql:/docker-entrypoint-initdb.d/init.sql 18 | 19 | volumes: 20 | postgres-data: 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "jsxImportSource": "@emotion/react", 17 | "incremental": true 18 | }, 19 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 20 | "exclude": ["node_modules"] 21 | } 22 | -------------------------------------------------------------------------------- /pages/api/connection.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiHandler } from "next"; 2 | import { Pool, DatabaseError } from "pg"; 3 | 4 | const pool = new Pool({ 5 | connectionString: `postgres://postgres:postgres@localhost:5433/postgres`, 6 | }); 7 | 8 | export interface Connection { 9 | error: string | null; 10 | } 11 | const connectionApi: NextApiHandler = async (req, res) => { 12 | try { 13 | await Promise.all([ 14 | pool.query("SELECT count(*) FROM thread"), 15 | pool.query("SELECT count(*) FROM post"), 16 | ]); 17 | res.status(200).json({ error: null }); 18 | } catch (error) { 19 | res.status(200).json({ error: (error as DatabaseError).message }); 20 | } 21 | }; 22 | 23 | export default connectionApi; 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextjs-with-typescript", 3 | "version": "5.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev --port 3002", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "data": "./scripts/generateData.js" 11 | }, 12 | "dependencies": { 13 | "@emotion/cache": "^11.11.0", 14 | "@emotion/react": "^11.11.1", 15 | "@emotion/server": "^11.11.0", 16 | "@emotion/styled": "^11.11.0", 17 | "@mui/icons-material": "^5.14.11", 18 | "@mui/material": "^5.14.11", 19 | "next": "^13.5.3", 20 | "pg": "^8.11.3", 21 | "react": "^18.2.0", 22 | "react-dom": "^18.2.0" 23 | }, 24 | "devDependencies": { 25 | "@faker-js/faker": "^8.1.0", 26 | "@types/node": "^20.7.0", 27 | "@types/pg": "^8.10.3", 28 | "@types/react": "^18.2.23", 29 | "eslint": "^8.50.0", 30 | "eslint-config-next": "^13.5.3", 31 | "typescript": "^5.2.2" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 MUI 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. 22 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Head from 'next/head'; 3 | import { AppProps } from 'next/app'; 4 | import { ThemeProvider } from '@mui/material/styles'; 5 | import CssBaseline from '@mui/material/CssBaseline'; 6 | import { CacheProvider, EmotionCache } from '@emotion/react'; 7 | import theme from '../src/theme'; 8 | import createEmotionCache from '../src/createEmotionCache'; 9 | 10 | // Client-side cache, shared for the whole session of the user in the browser. 11 | const clientSideEmotionCache = createEmotionCache(); 12 | 13 | interface MyAppProps extends AppProps { 14 | emotionCache?: EmotionCache; 15 | } 16 | 17 | export default function MyApp(props: MyAppProps) { 18 | const { Component, emotionCache = clientSideEmotionCache, pageProps } = props; 19 | return ( 20 | 21 | 22 | 23 | 24 | 25 | {/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */} 26 | 27 | 28 | 29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import type { NextPage } from "next"; 3 | import Container from "@mui/material/Container"; 4 | import Typography from "@mui/material/Typography"; 5 | import Box from "@mui/material/Box"; 6 | import type { Connection } from "./api/connection"; 7 | import { CircularProgress } from "@mui/material"; 8 | 9 | const Home: NextPage = () => { 10 | const [connection, setConnection] = React.useState(null); 11 | React.useEffect(() => { 12 | fetch("/api/connection").then(async (response) => { 13 | try { 14 | if (!response.ok) { 15 | throw new Error(`HTTP ${response.status}: ${response.statusText}`); 16 | } 17 | setConnection(await response.json()); 18 | } catch (error: any) { 19 | setConnection({ error: error.message }); 20 | } 21 | }); 22 | }, []); 23 | 24 | return ( 25 | 26 | 35 | 36 | Full stack technical challenge 37 | 38 | 39 | {connection ? ( 40 | `Database connection: ${ 41 | connection.error ? `failed: "${connection.error}"` : "succeeded" 42 | }` 43 | ) : ( 44 | 45 | )} 46 | 47 | 48 | 49 | ); 50 | }; 51 | 52 | export default Home; 53 | -------------------------------------------------------------------------------- /scripts/generateData.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const { faker } = require("@faker-js/faker"); 4 | 5 | const THREAD_COUNT = 100; 6 | const MIN_POSTS_PER_THREAD = 1; 7 | const MAX_POSTS_PER_THREAD = 10; 8 | const START_DATE = new Date("2018-03-5"); 9 | const END_DATE = new Date("2022-03-23"); 10 | 11 | const threads = []; 12 | const posts = []; 13 | let nextPostId = 1; 14 | for (let thread_id = 1; thread_id <= THREAD_COUNT; thread_id++) { 15 | const created_at = faker.date.between(START_DATE, END_DATE); 16 | const postsEndDate = faker.date.between(created_at, END_DATE); 17 | const postCount = faker.datatype.number({ 18 | min: MIN_POSTS_PER_THREAD, 19 | max: MAX_POSTS_PER_THREAD, 20 | }); 21 | const threadPosts = []; 22 | for (let i = 0; i < postCount; i++) { 23 | const id = nextPostId; 24 | nextPostId += 1; 25 | const post = { 26 | id, 27 | thread_id, 28 | created_at: faker.date.between(created_at, postsEndDate), 29 | title: faker.lorem.sentence(), 30 | body: faker.lorem.paragraphs( 31 | faker.datatype.number({ min: 1, max: 5 }), 32 | "\n\n" 33 | ), 34 | }; 35 | threadPosts.push(post); 36 | posts.push(post); 37 | } 38 | threads.push({ 39 | id: thread_id, 40 | created_at, 41 | title: faker.lorem.sentence(), 42 | posts: threadPosts, 43 | }); 44 | } 45 | 46 | const toSqlDateTime = (date) => 47 | date.toISOString().slice(0, 19).replace("T", " "); 48 | 49 | const quote = (string) => `'${string}'`; 50 | 51 | const threadValues = threads 52 | .map( 53 | (thread) => 54 | "(" + 55 | [ 56 | String(thread.id), 57 | quote(toSqlDateTime(thread.created_at)), 58 | quote(thread.title), 59 | ].join(", ") + 60 | ")" 61 | ) 62 | .join(",\n"); 63 | 64 | const postValues = posts 65 | .map( 66 | (post) => 67 | "(" + 68 | [ 69 | String(post.id), 70 | String(post.thread_id), 71 | quote(toSqlDateTime(post.created_at)), 72 | quote(post.title), 73 | quote(post.body), 74 | ].join(", ") + 75 | ")" 76 | ) 77 | .join(",\n"); 78 | 79 | const threadSql = `INSERT INTO thread(id, created_at, title)\nVALUES\n${threadValues};`; 80 | const postSql = `INSERT INTO post(id, thread_id, created_at, title, body)\nVALUES\n${postValues};`; 81 | 82 | console.log(`${threadSql}\n\n${postSql}`); 83 | -------------------------------------------------------------------------------- /pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Document, { Html, Head, Main, NextScript } from 'next/document'; 3 | import createEmotionServer from '@emotion/server/create-instance'; 4 | import theme from '../src/theme'; 5 | import createEmotionCache from '../src/createEmotionCache'; 6 | 7 | export default class MyDocument extends Document { 8 | render() { 9 | return ( 10 | 11 | 12 | {/* PWA primary color */} 13 | 14 | 15 | 19 | {/* Inject MUI styles first to match with the prepend: true configuration. */} 20 | {(this.props as any).emotionStyleTags} 21 | 22 | 23 |
24 | 25 | 26 | 27 | ); 28 | } 29 | } 30 | 31 | // `getInitialProps` belongs to `_document` (instead of `_app`), 32 | // it's compatible with static-site generation (SSG). 33 | MyDocument.getInitialProps = async (ctx) => { 34 | // Resolution order 35 | // 36 | // On the server: 37 | // 1. app.getInitialProps 38 | // 2. page.getInitialProps 39 | // 3. document.getInitialProps 40 | // 4. app.render 41 | // 5. page.render 42 | // 6. document.render 43 | // 44 | // On the server with error: 45 | // 1. document.getInitialProps 46 | // 2. app.render 47 | // 3. page.render 48 | // 4. document.render 49 | // 50 | // On the client 51 | // 1. app.getInitialProps 52 | // 2. page.getInitialProps 53 | // 3. app.render 54 | // 4. page.render 55 | 56 | const originalRenderPage = ctx.renderPage; 57 | 58 | // You can consider sharing the same emotion cache between all the SSR requests to speed up performance. 59 | // However, be aware that it can have global side effects. 60 | const cache = createEmotionCache(); 61 | const { extractCriticalToChunks } = createEmotionServer(cache); 62 | 63 | ctx.renderPage = () => 64 | originalRenderPage({ 65 | enhanceApp: (App: any) => 66 | function EnhanceApp(props) { 67 | return ; 68 | }, 69 | }); 70 | 71 | const initialProps = await Document.getInitialProps(ctx); 72 | // This is important. It prevents emotion to render invalid HTML. 73 | // See https://github.com/mui/material-ui/issues/26561#issuecomment-855286153 74 | const emotionStyles = extractCriticalToChunks(initialProps.html); 75 | const emotionStyleTags = emotionStyles.styles.map((style) => ( 76 |