├── .eslintrc.js ├── .firebaserc ├── .github └── workflows │ └── deploy.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── LICENSE.md ├── README.md ├── app ├── .eslintrc.js ├── __tests__ │ └── routes │ │ └── 404.tsx ├── app.tsx ├── components.tsx ├── entry-browser.tsx ├── entry-server.tsx ├── routes │ ├── 404.d.ts │ ├── 404.js │ ├── 500.d.ts │ ├── 500.js │ ├── index.tsx │ ├── login.tsx │ ├── posts.tsx │ └── posts │ │ ├── $postId.tsx │ │ ├── category.$categoryId.tsx │ │ ├── index.tsx │ │ └── random.tsx ├── tsconfig.json └── types.ts ├── config ├── jest.config.client.js ├── jest.config.common.js ├── jest.config.server.js └── shared-tsconfig.json ├── cypress.json ├── cypress ├── .eslintrc.js ├── e2e │ └── smoke.ts ├── fixtures │ └── example.json ├── plugins │ └── index.ts ├── support │ └── index.ts └── tsconfig.json ├── firebase.json ├── index.js ├── jest.config.js ├── lint-staged.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── prettier.config.js ├── public └── favicon.ico ├── remix.config.js ├── server ├── .eslintrc.js ├── data │ ├── __tests__ │ │ └── global.ts │ ├── global.ts │ └── routes │ │ └── posts │ │ ├── $postId.ts │ │ ├── category.$categoryId.ts │ │ ├── index.ts │ │ └── random.ts ├── start.ts ├── tsconfig.json ├── types.ts └── utils.ts ├── storybook ├── .storybook │ ├── @remix-run │ │ └── react.mock.js │ ├── main.js │ └── preview.js ├── README.md ├── package-lock.json ├── package.json └── stories │ ├── 404.stories.js │ └── index.stories.js ├── styles └── global.css ├── tailwind.config.js ├── tests ├── .eslintrc.js ├── mock-server.ts ├── server-handlers.ts ├── setup-env.js └── tsconfig.json ├── tsconfig.json └── types └── index.d.ts /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: './node_modules/kcd-scripts/eslint.js', 3 | parserOptions: { 4 | tsconfigRootDir: __dirname, 5 | project: './tsconfig.json', 6 | }, 7 | rules: { 8 | 'no-undef': 'off', 9 | 'import/export': 'off', 10 | 'import/no-unresolved': 'off', 11 | 'react/prop-types': 'off', 12 | 'react/no-adjacent-inline-elements': 'off', 13 | 'no-console': 'off', 14 | 15 | // this didn't seem to work 🤔 16 | '@typescript-eslint/restrict-template-expressions': 'off', 17 | // I can't figure these out: 18 | '@typescript-eslint/no-unsafe-call': 'off', 19 | '@typescript-eslint/no-unsafe-assignment': 'off', 20 | '@typescript-eslint/no-unsafe-member-access': 'off', 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "elaborate-56879" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: deploy 2 | on: 3 | push: 4 | branches: 5 | - 'main' 6 | pull_request: 7 | branches: 8 | - 'main' 9 | jobs: 10 | main: 11 | # ignore all-contributors PRs 12 | if: ${{ !contains(github.head_ref, 'all-contributors') }} 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: ⬇️ Checkout repo 16 | uses: actions/checkout@v2 17 | 18 | - name: ⎔ Setup node 19 | uses: actions/setup-node@v1 20 | with: 21 | node-version: 12 22 | 23 | - name: 🔓 Fixup .npmrc 24 | run: 25 | echo "//npm.remix.run/:_authToken=${REMIX_REGISTRY_TOKEN}" >> .npmrc 26 | env: 27 | REMIX_REGISTRY_TOKEN: ${{ secrets.REMIX_REGISTRY_TOKEN }} 28 | 29 | - name: 📥 Download deps 30 | uses: bahmutov/npm-install@v1 31 | 32 | - name: 🔥 Install firebase-tools 33 | # It's used by multiple scripts so we'll just install it globally 34 | run: npm install --global firebase-tools 35 | 36 | - name: ▶️ Run validate script 37 | run: npm run validate 38 | 39 | - name: ⬆️ Upload coverage report 40 | uses: codecov/codecov-action@v1 41 | 42 | - name: 🌳 Cypress run 43 | uses: cypress-io/github-action@v2 44 | with: 45 | install: false 46 | start: npm start -- --token "$FIREBASE_TOKEN" 47 | wait-on: 'http://localhost:5000' 48 | env: 49 | FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }} 50 | 51 | - name: 🚀 Deploy 52 | # only deploy main branch on pushes 53 | if: 54 | ${{ github.ref == 'refs/heads/main' && github.event_name == 'push' }} 55 | run: | 56 | npm run deploy -- --token "$FIREBASE_TOKEN" 57 | env: 58 | FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }} 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | .DS_Store 3 | node_modules/ 4 | *.log 5 | 6 | /.cache 7 | /.firebase 8 | /server-build 9 | /public/build 10 | /coverage 11 | /cypress/videos 12 | /cypress/screenshots 13 | 14 | # styles are in the styles/ directory and compiled to the app directory 15 | app/**/*.css 16 | 17 | storybook/storybook-static 18 | 19 | # these are copied from the root dir 20 | storybook/tailwind.config.js 21 | storybook/postcss.config.js 22 | 23 | /other/google-app-creds.json 24 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | legacy-peer-deps=true 2 | @remix-run:registry=https://npm.remix.run 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | .DS_Store 3 | node_modules/ 4 | 5 | /.cache 6 | /build 7 | /public/build 8 | /coverage 9 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | This material is available for private, non-commercial use under the 2 | [GPL version 3](http://www.gnu.org/licenses/gpl-3.0-standalone.html). If you 3 | would like to use this material to conduct your own workshop, please contact me 4 | at me@kentcdodds.com 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Elaborate 2 | 3 | [![Build Status][build-badge]][build] 4 | [![GPL 3.0 License][license-badge]][license] 5 | 6 | More info coming soon... 7 | 8 | ## Contributing 9 | 10 | If you have a remix license, then make sure you run install with 11 | `REMIX_REGISTRY_TOKEN` env variable set. 12 | 13 | If you do not have a remix license, you will not be able to install dependencies 14 | and run the project locally. However, you _can_ work on the frontend components 15 | in the `storybook` directory. See `storybook/README.md` for more information. 16 | 17 | 18 | [build-badge]: https://img.shields.io/github/workflow/status/kentcdodds/elaborate/deploy/main?logo=github&style=flat-square 19 | [build]: https://github.com/kentcdodds/elaborate/actions?query=workflow%3Adeploy 20 | [license-badge]: https://img.shields.io/badge/license-GPL%203.0%20License-blue.svg?style=flat-square 21 | [license]: https://github.com/kentcdodds/react-fundamentals/blob/main/LICENSE 22 | 23 | -------------------------------------------------------------------------------- /app/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parserOptions: { 3 | tsconfigRootDir: __dirname, 4 | project: './tsconfig.json', 5 | }, 6 | overrides: [ 7 | { 8 | files: ['**/__tests__/**/*.{js,ts,tsx}'], 9 | settings: { 10 | 'import/resolver': { 11 | jest: { 12 | jestConfigFile: './config/jest.config.client.js', 13 | }, 14 | }, 15 | }, 16 | }, 17 | ], 18 | } 19 | -------------------------------------------------------------------------------- /app/__tests__/routes/404.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import {render, screen} from '@testing-library/react' 3 | import FourOhFour from 'routes/404' 4 | 5 | // I never write tests like this. It's basically useless 6 | // it's just here to make sure our testing setup works 7 | test('Renders 404', () => { 8 | render() 9 | expect(screen.getByRole('heading', {name: /404/})).toBeInTheDocument() 10 | }) 11 | -------------------------------------------------------------------------------- /app/app.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import {Meta, Scripts, Styles, Routes} from '@remix-run/react' 3 | 4 | export default function App() { 5 | return ( 6 | 7 | 8 | 9 | 10 | 11 | 12 | {/* tailwind variants need a class name here */} 13 | 14 | 15 | 16 |
17 |

This is a fun little remix project by Kent

18 |
19 | 20 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /app/components.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import {Link} from 'react-router-dom' 3 | import {useRouteData, usePendingLocation} from '@remix-run/react' 4 | import * as Types from 'types' 5 | 6 | function Header() { 7 | const pendingLocation = usePendingLocation() 8 | return ( 9 |
10 |
16 |

Elaborate

17 |
18 |
19 | {`If you don't want to forget what you learned, write it down.`} 20 |
21 |
- Kent
22 |
23 |
24 | 38 |
39 | ) 40 | } 41 | 42 | const formatDate = (date: Date) => 43 | new Intl.DateTimeFormat('en-US', { 44 | day: 'numeric', 45 | month: 'short', 46 | year: 'numeric', 47 | timeZone: 'UTC', 48 | }).format(date) 49 | 50 | function Post({post}: {post: Types.Post}) { 51 | const {users} = useRouteData<{users: Types.User[]}>() 52 | const author = users.find(({id}) => id === post.authorId) ?? {name: 'Unknown'} 53 | return ( 54 |
61 |
62 |

{post.title}

63 |
67 |
68 |
{author.name}
69 | 70 |
71 |
72 | 91 |
92 | ) 93 | } 94 | 95 | export {Header, Post} 96 | -------------------------------------------------------------------------------- /app/entry-browser.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import Remix from '@remix-run/react/browser' 4 | 5 | import App from './app' 6 | 7 | ReactDOM.hydrate( 8 | // @ts-expect-error @types/react-dom says the 2nd argument to ReactDOM.hydrate() must be a 9 | // `Element | DocumentFragment | null` but React 16 allows you to pass the 10 | // `document` object as well. This is a bug in @types/react-dom that we can 11 | // safely ignore for now. 12 | 13 | 14 | , 15 | document, 16 | ) 17 | -------------------------------------------------------------------------------- /app/entry-server.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import ReactDOMServer from 'react-dom/server' 3 | import type {EntryContext} from '@remix-run/core' 4 | import Remix from '@remix-run/react/server' 5 | 6 | import App from './app' 7 | 8 | export default function handleRequest( 9 | request: Request, 10 | responseStatusCode: number, 11 | responseHeaders: Headers, 12 | remixContext: EntryContext, 13 | ) { 14 | const markup = ReactDOMServer.renderToString( 15 | 16 | 17 | , 18 | ) 19 | 20 | return new Response(`${markup}`, { 21 | status: responseStatusCode, 22 | headers: { 23 | ...Object.fromEntries(responseHeaders), 24 | 'Content-Type': 'text/html', 25 | }, 26 | }) 27 | } 28 | -------------------------------------------------------------------------------- /app/routes/404.d.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | export default function FourOhFour() { 4 | return null 5 | } 6 | -------------------------------------------------------------------------------- /app/routes/404.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | export function meta() { 4 | return {title: "Ain't nothing here"} 5 | } 6 | 7 | export default function FourOhFour() { 8 | return ( 9 |
10 |

404

11 |
12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /app/routes/500.d.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | export default function FiveHundred() { 4 | return null 5 | } 6 | -------------------------------------------------------------------------------- /app/routes/500.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | export function meta() { 4 | return {title: 'Shoot...'} 5 | } 6 | 7 | export default function FiveHundred() { 8 | console.error('Check your server terminal output') 9 | 10 | return ( 11 |
12 |

500

13 |
14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /app/routes/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import {Outlet} from 'react-router-dom' 3 | import {Header} from '../components' 4 | 5 | export function meta() { 6 | return { 7 | title: 'Elaborate', 8 | description: 'Alright stop. Elaborate and listen...', 9 | } 10 | } 11 | 12 | function Index() { 13 | return ( 14 |
15 |
16 | 17 |
18 | ) 19 | } 20 | 21 | export default Index 22 | -------------------------------------------------------------------------------- /app/routes/login.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import {Header} from '../components' 3 | 4 | export function meta() { 5 | return { 6 | title: 'Elaborate Login', 7 | description: 'Come on in here yo!', 8 | } 9 | } 10 | 11 | function Index() { 12 | function handleSubmit(event: React.SyntheticEvent) { 13 | event.preventDefault() 14 | const { 15 | elements: { 16 | email: {value: email}, 17 | password: {value: password}, 18 | }, 19 | } = event.target as typeof event.target & { 20 | elements: { 21 | email: {value: string} 22 | password: {value: string} 23 | } 24 | } 25 | console.log({email, password}) 26 | } 27 | return ( 28 |
29 |
30 |
31 |
32 | 33 | 34 |
35 |
36 | 37 | 38 |
39 | 40 |
41 |
42 | ) 43 | } 44 | 45 | export default Index 46 | -------------------------------------------------------------------------------- /app/routes/posts.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import {Outlet} from 'react-router-dom' 3 | import {Header} from '../components' 4 | 5 | export function meta() { 6 | return { 7 | title: 'Elaborate', 8 | description: 'Alright stop. Elaborate and listen...', 9 | } 10 | } 11 | 12 | function Posts() { 13 | return ( 14 |
15 |
16 | 17 |
18 | ) 19 | } 20 | 21 | export default Posts 22 | -------------------------------------------------------------------------------- /app/routes/posts/$postId.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import {useRouteData} from '@remix-run/react' 3 | import * as Types from 'types' 4 | import {Post} from '../../components' 5 | 6 | export function headers({loaderHeaders}: {loaderHeaders: Headers}) { 7 | return loaderHeaders 8 | } 9 | 10 | export function meta({ 11 | data: {users, post}, 12 | }: { 13 | data: {users: Types.User[]; post: Types.Post | null} 14 | params: Record 15 | }) { 16 | const author = users.find(({id}) => id === post?.authorId) 17 | return { 18 | title: `${post?.title ?? 'Unknown post'} | Elaborate`, 19 | description: `Post about ${post?.category ?? 'Unknown'} by ${ 20 | author?.name ?? 'Uknown' 21 | } on Elaborate`, 22 | } 23 | } 24 | 25 | function PostScreen() { 26 | const {post} = useRouteData<{post: Types.Post | null}>() 27 | if (!post) { 28 | return ( 29 |
30 | Oh no... No post found. Super sad. 31 |
32 | ) 33 | } 34 | return ( 35 |
36 | 37 |
38 | ) 39 | } 40 | 41 | export default PostScreen 42 | -------------------------------------------------------------------------------- /app/routes/posts/category.$categoryId.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import {useParams} from 'react-router-dom' 3 | import {useRouteData} from '@remix-run/react' 4 | import * as Types from 'types' 5 | import {Post} from '../../components' 6 | 7 | export function headers({loaderHeaders}: {loaderHeaders: Headers}) { 8 | return loaderHeaders 9 | } 10 | 11 | export function meta({ 12 | data: {users, posts}, 13 | params, 14 | }: { 15 | data: {users: Types.User[]; posts: Types.Post[]} 16 | params: Record 17 | }) { 18 | return { 19 | title: `${params.categoryId} posts | Elaborate`, 20 | description: `${posts.length} posts by ${users.length} users about ${params.categoryId} on Elaborate`, 21 | } 22 | } 23 | 24 | function Category() { 25 | const {categoryId} = useParams() 26 | const {posts} = useRouteData<{posts: Types.Post[]}>() 27 | return ( 28 |
29 |
30 | {categoryId} posts 31 |
32 |
33 | {posts.length 34 | ? posts.map(a => ) 35 | : 'Oh no... there are no matching posts'} 36 |
37 |
38 | ) 39 | } 40 | 41 | export default Category 42 | -------------------------------------------------------------------------------- /app/routes/posts/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import {useRouteData} from '@remix-run/react' 3 | import * as Types from 'types' 4 | import {Post} from '../../components' 5 | 6 | export function meta() { 7 | return { 8 | title: 'Elaborate', 9 | description: 'Alright stop. Elaborate and listen...', 10 | } 11 | } 12 | 13 | export function headers({loaderHeaders}: {loaderHeaders: Headers}) { 14 | return loaderHeaders 15 | } 16 | 17 | function Posts() { 18 | const {posts} = useRouteData<{posts: Types.Post[]}>() 19 | return ( 20 |
21 | {posts.map(a => ( 22 | 23 | ))} 24 |
25 | ) 26 | } 27 | 28 | export default Posts 29 | -------------------------------------------------------------------------------- /app/routes/posts/random.tsx: -------------------------------------------------------------------------------- 1 | import {useRouteData} from '@remix-run/react' 2 | 3 | function Random() { 4 | useRouteData() 5 | // this should never get rendered 6 | return null 7 | } 8 | 9 | export default Random 10 | -------------------------------------------------------------------------------- /app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../config/shared-tsconfig.json", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | // The Remix compiler takes care of compiling everything in the `app` 6 | // directory, so we don't need to emit anything with `tsc`. 7 | "noEmit": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /app/types.ts: -------------------------------------------------------------------------------- 1 | export * from '../types' 2 | -------------------------------------------------------------------------------- /config/jest.config.client.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const config = require('./jest.config.common') 3 | 4 | const fromRoot = d => path.join(__dirname, '..', d) 5 | 6 | module.exports = { 7 | ...config, 8 | displayName: 'client', 9 | roots: [fromRoot('app')], 10 | testEnvironment: 'jsdom', 11 | setupFilesAfterEnv: ['@testing-library/jest-dom'], 12 | moduleDirectories: ['node_modules', fromRoot('app'), fromRoot('tests')], 13 | } 14 | -------------------------------------------------------------------------------- /config/jest.config.common.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const config = require('kcd-scripts/jest') 3 | 4 | module.exports = { 5 | ...config, 6 | rootDir: path.join(__dirname, '..'), 7 | resetMocks: true, 8 | coveragePathIgnorePatterns: [], 9 | collectCoverageFrom: [ 10 | '**/app/**/*.{js,ts,tsx}', 11 | '**/loaders/**/*.{js,ts,tsx}', 12 | ], 13 | coverageThreshold: null, 14 | } 15 | -------------------------------------------------------------------------------- /config/jest.config.server.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const config = require('./jest.config.common') 3 | 4 | const fromRoot = d => path.join(__dirname, '..', d) 5 | 6 | module.exports = { 7 | ...config, 8 | displayName: 'server', 9 | roots: [fromRoot('server')], 10 | testEnvironment: 'node', 11 | moduleDirectories: ['node_modules', fromRoot('server'), fromRoot('tests')], 12 | } 13 | -------------------------------------------------------------------------------- /config/shared-tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "jsx": "react", 5 | "moduleResolution": "node", 6 | "target": "es2019", 7 | "strict": true, 8 | "skipLibCheck": true 9 | }, 10 | "exclude": ["node_modules"] 11 | } 12 | -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /cypress/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parserOptions: { 3 | tsconfigRootDir: __dirname, 4 | project: './tsconfig.json', 5 | }, 6 | rules: { 7 | 'testing-library/prefer-screen-queries': 'off', 8 | }, 9 | } 10 | -------------------------------------------------------------------------------- /cypress/e2e/smoke.ts: -------------------------------------------------------------------------------- 1 | describe('smoke', () => { 2 | it('should allow a typical user flow', () => { 3 | cy.visit('/') 4 | 5 | cy.findByRole('heading', {name: /elaborate/i}) 6 | }) 7 | }) 8 | -------------------------------------------------------------------------------- /cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /cypress/plugins/index.ts: -------------------------------------------------------------------------------- 1 | module.exports = ( 2 | on: Cypress.PluginEvents, 3 | config: Cypress.PluginConfigOptions, 4 | ) => { 5 | config.baseUrl = `http://localhost:5000` 6 | Object.assign(config, { 7 | integrationFolder: 'cypress/e2e', 8 | }) 9 | 10 | return config 11 | } 12 | -------------------------------------------------------------------------------- /cypress/support/index.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/cypress/add-commands' 2 | -------------------------------------------------------------------------------- /cypress/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../config/shared-tsconfig.json", 3 | "exclude": [ 4 | "../node_modules/@types/jest", 5 | "../node_modules/@testing-library/jest-dom" 6 | ], 7 | "include": [ 8 | "./index.ts", 9 | "e2e/**/*", 10 | "plugins/**/*", 11 | "support/**/*", 12 | "../node_modules/cypress", 13 | "../node_modules/@testing-library/cypress" 14 | ], 15 | "compilerOptions": { 16 | "baseUrl": ".", 17 | "noEmit": true, 18 | "types": ["node", "cypress", "@testing-library/cypress"] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "functions": { 3 | "predeploy": "npm --prefix \"$RESOURCE_DIR\" run build", 4 | "source": "." 5 | }, 6 | "hosting": { 7 | "public": "public", 8 | "ignore": ["firebase.json", "**/.*"], 9 | "rewrites": [ 10 | { 11 | "source": "**", 12 | "function": "remixServer" 13 | } 14 | ] 15 | }, 16 | "emulators": { 17 | "functions": { 18 | "port": 5001 19 | }, 20 | "hosting": { 21 | "port": 5000 22 | }, 23 | "ui": { 24 | "enabled": true 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | if (process.env.NODE_ENV === 'production') { 4 | module.exports = require('./server-build/start') 5 | } else { 6 | require('ts-node').register({ 7 | dir: path.resolve('server'), 8 | pretty: true, 9 | transpileOnly: true, 10 | ignore: ['/node_modules/, /__tests__/'], 11 | project: require.resolve('./server/tsconfig.json'), 12 | }) 13 | module.exports = require('./server/start') 14 | } 15 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const config = require('./config/jest.config.common') 2 | 3 | module.exports = { 4 | ...config, 5 | roots: null, 6 | projects: [ 7 | './config/jest.config.client.js', 8 | './config/jest.config.server.js', 9 | ], 10 | } 11 | -------------------------------------------------------------------------------- /lint-staged.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | '*.+(js|jsx|json|yml|yaml|css|less|scss|ts|tsx|md|graphql|mdx|vue)': [ 3 | `kcd-scripts format`, 4 | `kcd-scripts lint`, 5 | `kcd-scripts test --findRelatedTests`, 6 | ], 7 | } 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "elaborate", 3 | "version": "1.0.0", 4 | "engines": { 5 | "node": "12" 6 | }, 7 | "description": "A place to elaborate on what you learned", 8 | "main": "./index.js", 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/kentcdodds/elaborate.git" 12 | }, 13 | "keywords": [], 14 | "author": "Kent C. Dodds (https://kentcdodds.com/)", 15 | "license": "GPL-3.0-only", 16 | "bugs": { 17 | "url": "https://github.com/kentcdodds/elaborate/issues" 18 | }, 19 | "homepage": "https://github.com/kentcdodds/elaborate#readme", 20 | "scripts": { 21 | "prebuild": "npm run css:build", 22 | "build": "concurrently --names \"remix,tsc\" --prefix-colors \"magenta,blue\" \"remix build\" \"tsc --build\"", 23 | "dev": "cross-env NODE_ENV=development concurrently --names \"remix,css,firebase\" --prefix-colors \"magenta,cyan,yellow\" \"remix run > /dev/null\" \"npm run css:watch\" \"firebase emulators:start\"", 24 | "deploy": "firebase deploy", 25 | "start": "firebase emulators:start", 26 | "css:build": "postcss styles --base styles --dir app/ --env production", 27 | "css:watch": "postcss styles --base styles --dir app/ -w", 28 | "cy:run": "cypress run", 29 | "cy:open": "cypress open", 30 | "test:e2e": "start-server-and-test dev http://localhost:500 cy:open", 31 | "test:e2e:run": "start-server-and-test start http://localhost:5000 cy:run", 32 | "test": "kcd-scripts test", 33 | "lint": "kcd-scripts lint", 34 | "format": "kcd-scripts format", 35 | "typecheck": "concurrently --names \"server,app,cypress\" --prefix-colors \"red,green,blue\" \"tsc --project server --noEmit\" \"tsc --project app --noEmit\" \"tsc --project cypress --noEmit\"", 36 | "validate": "kcd-scripts validate", 37 | "setup": "npm install && npm run validate" 38 | }, 39 | "dependencies": { 40 | "@remix-run/cli": "^0.8.0", 41 | "@remix-run/core": "^0.8.0", 42 | "@remix-run/data": "^0.8.0", 43 | "@remix-run/express": "^0.8.0", 44 | "@remix-run/react": "^0.8.0", 45 | "@sindresorhus/slugify": "^1.1.0", 46 | "express": "^4.17.1", 47 | "express-async-errors": "^3.1.1", 48 | "firebase": "^8.1.1", 49 | "firebase-admin": "^9.4.1", 50 | "firebase-functions": "^3.11.0", 51 | "react": "^17.0.1", 52 | "react-dom": "^17.0.1", 53 | "react-router": "^6.0.0-beta.0", 54 | "react-router-dom": "^6.0.0-beta.0", 55 | "rehype-stringify": "^8.0.0", 56 | "remark-parse": "^9.0.0", 57 | "remark-rehype": "^8.0.0", 58 | "tailwindcss": "^2.0.1", 59 | "unified": "^9.2.0", 60 | "wait-on": "^5.2.0" 61 | }, 62 | "devDependencies": { 63 | "@tailwindcss/typography": "^0.3.1", 64 | "@testing-library/cypress": "^7.0.2", 65 | "@testing-library/jest-dom": "^5.11.6", 66 | "@testing-library/react": "^11.2.2", 67 | "@testing-library/user-event": "^12.2.2", 68 | "@types/estree": "0.0.45", 69 | "@types/express": "^4.17.9", 70 | "@types/react": "^17.0.0", 71 | "@types/react-dom": "^17.0.0", 72 | "autoprefixer": "10.0.2", 73 | "concurrently": "^5.3.0", 74 | "cross-env": "^7.0.2", 75 | "cypress": "^6.0.0", 76 | "eslint-import-resolver-jest": "^3.0.0", 77 | "kcd-scripts": "^7.1.0", 78 | "kill-port": "^1.6.1", 79 | "msw": "^0.22.3", 80 | "postcss": "^8.1.10", 81 | "postcss-cli": "8.3.0", 82 | "start-server-and-test": "^1.11.6", 83 | "ts-node": "^9.0.0", 84 | "type-fest": "^0.20.1", 85 | "typescript": "^4.1.2" 86 | }, 87 | "husky": { 88 | "hooks": { 89 | "pre-commit": "kcd-scripts pre-commit" 90 | } 91 | }, 92 | "eslintIgnore": [ 93 | "node_modules", 94 | "coverage", 95 | "build", 96 | "*.d.ts", 97 | "storybook-static" 98 | ] 99 | } 100 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [require('tailwindcss'), require('autoprefixer')], 3 | } 4 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('kcd-scripts/prettier') 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kentcdodds/elaborate/8064b099e5cf78308f2c9bd249a4b76177decace/public/favicon.ico -------------------------------------------------------------------------------- /remix.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | appDirectory: './app', 3 | dataDirectory: 4 | process.env.NODE_ENV === 'production' 5 | ? './server-build/data' 6 | : './server/data', 7 | serverBuildDirectory: './server-build/remix', 8 | browserBuildDirectory: './public/build', 9 | publicPath: '/build/', 10 | devServerPort: 8002, 11 | } 12 | -------------------------------------------------------------------------------- /server/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parserOptions: { 3 | tsconfigRootDir: __dirname, 4 | project: './tsconfig.json', 5 | }, 6 | overrides: [ 7 | { 8 | files: ['**/__tests__/**/*.{js,ts,tsx}'], 9 | settings: { 10 | 'import/resolver': { 11 | jest: { 12 | jestConfigFile: './config/jest.config.server.js', 13 | }, 14 | }, 15 | }, 16 | }, 17 | ], 18 | } 19 | -------------------------------------------------------------------------------- /server/data/__tests__/global.ts: -------------------------------------------------------------------------------- 1 | import {createSession, Request} from '@remix-run/core' 2 | import {loader} from '../global' 3 | 4 | test('sends back the right data', async () => { 5 | const request = new Request() 6 | const session = createSession({}) 7 | const loaderArgs = {params: {}, context: {}, session, request} 8 | expect(await loader(loaderArgs)).toEqual({ 9 | date: expect.any(Date), 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /server/data/global.ts: -------------------------------------------------------------------------------- 1 | import type {Loader} from '@remix-run/data' 2 | 3 | export const loader: Loader = () => { 4 | return { 5 | date: new Date(), 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /server/data/routes/posts/$postId.ts: -------------------------------------------------------------------------------- 1 | import {json, Loader} from '@remix-run/data' 2 | import {getPosts, getUsers} from '../../../utils' 3 | 4 | export const loader: Loader = async ({ 5 | params, 6 | }: { 7 | params: Record 8 | }) => { 9 | // our URLs have the post ID + title as a slug for SEO purposes 10 | // but we only need the ID, so let's grab that 11 | const postId = params.postId.split('-')[0] 12 | const post = (await getPosts()).find(({id}) => id === postId) ?? null 13 | const users = await getUsers() 14 | return json( 15 | {users, post}, 16 | { 17 | headers: { 18 | 'cache-control': 'public, max-age=120', 19 | }, 20 | }, 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /server/data/routes/posts/category.$categoryId.ts: -------------------------------------------------------------------------------- 1 | import {json, Loader} from '@remix-run/data' 2 | import {getPosts, getUsers} from '../../../utils' 3 | 4 | export const loader: Loader = async ({ 5 | params, 6 | }: { 7 | params: Record 8 | }) => { 9 | const posts = (await getPosts()).filter( 10 | ({category}) => category === params.categoryId, 11 | ) 12 | const users = await getUsers() 13 | return json( 14 | {users, posts}, 15 | { 16 | headers: { 17 | 'cache-control': 'public, max-age=60', 18 | }, 19 | }, 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /server/data/routes/posts/index.ts: -------------------------------------------------------------------------------- 1 | import {json, Loader} from '@remix-run/data' 2 | import {getPosts, getUsers} from '../../../utils' 3 | 4 | export const loader: Loader = async () => { 5 | const posts = await getPosts() 6 | const users = await getUsers() 7 | return json( 8 | {posts, users}, 9 | { 10 | headers: { 11 | 'cache-control': 'public, max-age=10', 12 | }, 13 | }, 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /server/data/routes/posts/random.ts: -------------------------------------------------------------------------------- 1 | import {redirect, Loader} from '@remix-run/data' 2 | import {getPosts} from '../../../utils' 3 | 4 | export const loader: Loader = async () => { 5 | const posts = await getPosts() 6 | const randomPost = posts[Math.floor(Math.random() * posts.length)] 7 | return redirect(`/posts/${randomPost.slug}`) 8 | } 9 | -------------------------------------------------------------------------------- /server/start.ts: -------------------------------------------------------------------------------- 1 | import * as functions from 'firebase-functions' 2 | import {createRequestHandler as remix} from '@remix-run/express' 3 | 4 | exports.remixServer = functions.https.onRequest(remix()) 5 | -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../config/shared-tsconfig.json", 3 | "include": ["**/*"], 4 | "compilerOptions": { 5 | "allowSyntheticDefaultImports": true, 6 | "baseUrl": ".", 7 | "outDir": "../server-build", 8 | "module": "commonjs", 9 | "paths": { 10 | "*": ["./*", "../tests/*"] 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /server/types.ts: -------------------------------------------------------------------------------- 1 | export * from '../types' 2 | -------------------------------------------------------------------------------- /server/utils.ts: -------------------------------------------------------------------------------- 1 | import firebase from 'firebase-admin' 2 | import 'firebase/firestore' 3 | import 'firebase/auth' 4 | import {Merge} from 'type-fest' 5 | import slugify from '@sindresorhus/slugify' 6 | import type * as Types from 'types' 7 | 8 | type UserDocumentData = Omit 9 | 10 | type PostDocumentData = Merge< 11 | Omit, 12 | { 13 | author: firebase.firestore.DocumentReference 14 | createdDate: firebase.firestore.Timestamp 15 | } 16 | > 17 | 18 | firebase.initializeApp() 19 | 20 | const firestore = firebase.firestore() 21 | 22 | async function getPosts(): Promise { 23 | const postConverter = { 24 | toFirestore(post: Types.Post) { 25 | const authorId = post.authorId 26 | const {id, slug, ...rest} = post 27 | return { 28 | ...rest, 29 | createdDate: new firebase.firestore.Timestamp(post.createdDate, 0), 30 | author: firestore.doc(`users/${authorId}`), 31 | } 32 | }, 33 | fromFirestore( 34 | snapshot: firebase.firestore.QueryDocumentSnapshot, 35 | ): Types.Post { 36 | const {author, ...data} = snapshot.data() 37 | const authorId = author.id 38 | const createdDate = data.createdDate.toDate().getTime() 39 | const {id} = snapshot 40 | return { 41 | ...data, 42 | id, 43 | slug: `${id}-${slugify(data.title)}`, 44 | authorId, 45 | createdDate, 46 | } 47 | }, 48 | } 49 | 50 | return ( 51 | await firestore 52 | .collection('posts') 53 | .withConverter(postConverter) 54 | .get() 55 | ).docs.map(doc => doc.data()) 56 | } 57 | 58 | async function getUsers(): Promise { 59 | const userConverter = { 60 | toFirestore(user: Types.User) { 61 | return user 62 | }, 63 | fromFirestore( 64 | snapshot: firebase.firestore.QueryDocumentSnapshot, 65 | ) { 66 | return {...snapshot.data(), id: snapshot.id} 67 | }, 68 | } 69 | return ( 70 | await firestore 71 | .collection('users') 72 | .withConverter(userConverter) 73 | .get() 74 | ).docs.map(doc => doc.data()) 75 | } 76 | 77 | export {getPosts, getUsers} 78 | -------------------------------------------------------------------------------- /storybook/.storybook/@remix-run/react.mock.js: -------------------------------------------------------------------------------- 1 | function getCurrentStoryComponent(params) { 2 | // 1️⃣ get story ID and name from URL 3 | let storyInfo = new URLSearchParams(window.location.search) 4 | .get('id') 5 | .split('--') 6 | if (window.parent !== window) { 7 | // if we're in an iframe, then the window.location.search may not 8 | // be updated yet (no idea why), so we'll determine the id and export name 9 | // via the parent's URL. Super weird and annoying, but whatever. 10 | storyInfo = new URLSearchParams(window.parent.location.search) 11 | .get('path') 12 | .slice('/story/'.length) 13 | .split('--') 14 | } 15 | const [id, exportName] = storyInfo 16 | 17 | // 2️⃣ get story module 18 | const mod = require(`../../stories/${id}.stories`) 19 | 20 | // map module exports to the exportName (which is lower-case and kebab-spaced) 21 | const lowerExports = {} 22 | for (const [key, value] of Object.entries(mod)) { 23 | lowerExports[key.toLowerCase()] = value 24 | } 25 | const normalizedExportName = exportName.replace(/-/g, '') 26 | 27 | // 3️⃣ get the component that's being rendered 28 | const comp = lowerExports[normalizedExportName] 29 | if (!comp) { 30 | const info = JSON.stringify( 31 | { 32 | exportName, 33 | normalizedExportName, 34 | exports: Object.keys(mod), 35 | lowerExports: Object.keys(lowerExports), 36 | }, 37 | null, 38 | 2, 39 | ) 40 | throw new Error( 41 | `Unable to determine the story component from the URL. Here's some info:\n\n${info}`, 42 | ) 43 | } 44 | 45 | return comp 46 | } 47 | 48 | function useRouteData() { 49 | const comp = getCurrentStoryComponent() 50 | if (!comp.routeData) { 51 | throw new Error( 52 | `Tried to get route data for a story that uses useRouteData but does not expose routeData: ${comp.name}`, 53 | ) 54 | } 55 | return comp.routeData 56 | } 57 | 58 | function useGlobalData() { 59 | const comp = getCurrentStoryComponent() 60 | if (!comp.globalData) { 61 | throw new Error( 62 | `Tried to get route data for a story that uses useGlobalData but does not expose globalData: ${comp.name}`, 63 | ) 64 | } 65 | return comp.globalData 66 | } 67 | 68 | function usePendingLocation() { 69 | const comp = getCurrentStoryComponent() 70 | if (!comp.pendingLocation) { 71 | throw new Error( 72 | `Tried to get route data for a story that uses usePendingLocation but does not expose pendingLocation: ${comp.name}`, 73 | ) 74 | } 75 | return comp.pendingLocation 76 | } 77 | 78 | export {useGlobalData, useRouteData} 79 | -------------------------------------------------------------------------------- /storybook/.storybook/main.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | 3 | module.exports = { 4 | stories: [ 5 | '../stories/**/*.stories.mdx', 6 | '../stories/**/*.stories.@(js|jsx|ts|tsx)', 7 | ], 8 | addons: ['@storybook/addon-links', '@storybook/addon-essentials'], 9 | 10 | webpackFinal: async config => { 11 | config.plugins.push( 12 | // swap @remix-run/react for our own mock 13 | // this way folks don't have to have remix installed to work 14 | // on components that are built using remix... 15 | // To handle useRouteData, stories must expose a routeData property 16 | new webpack.NormalModuleReplacementPlugin( 17 | /\/@remix-run\/react/, 18 | require.resolve('./@remix-run/react.mock.js'), 19 | ), 20 | ) 21 | return config 22 | }, 23 | } 24 | -------------------------------------------------------------------------------- /storybook/.storybook/preview.js: -------------------------------------------------------------------------------- 1 | import '../../app/global.css' 2 | 3 | const classes = `text-green-900 bg-gray-100 dark:bg-gray-800 dark:text-green-300`.split( 4 | ' ', 5 | ) 6 | for (const c of classes) { 7 | document.body.classList.add(c) 8 | } 9 | 10 | export const parameters = { 11 | actions: {argTypesRegex: '^on[A-Z].*'}, 12 | } 13 | -------------------------------------------------------------------------------- /storybook/README.md: -------------------------------------------------------------------------------- 1 | # [elaborate storybook](https://elaborate.netlify.app) 2 | 3 | [![Netlify Status](https://api.netlify.com/api/v1/badges/0810f882-30ea-4448-b202-7717e680ddce/deploy-status)](https://app.netlify.com/sites/elaborate/deploys) 4 | 5 | To run this storybook, you'll need to install dependencies in this directory. 6 | Then run `npm run dev` to get the storybook running. 7 | 8 | If the story you're working on uses `useRouteData` from `@remix-run/react`, then 9 | you'll have to add `routeData` to your story. For example: 10 | 11 | ```javascript 12 | import * as React from 'react' 13 | // 👇 you must import the css file (if one exists) to get this route's CSS loaded 14 | import '../../app/routes/index.css' 15 | import Index from '../../app/routes/index' 16 | 17 | export default { 18 | title: 'Index', 19 | component: Index, 20 | } 21 | const Template = args => 22 | 23 | export const Home = Template.bind({}) 24 | Home.args = {} 25 | // 👇 this is required if the component uses useRouteData: 26 | Home.routeData = {fake: 'data'} 27 | ``` 28 | 29 | Whatever you set `routeData` to will be the value of `data` returned by 30 | `useRouteData`. This should allow you to easily develop without having to 31 | install remix. 32 | 33 | For `useGlobalData`, it must expose `globalData`. For `usePendingLocation` it's 34 | `pendingLocation`. 35 | 36 | Oh, and another thing, this `package.json` must include all dependencies used by 37 | the stories because none of those dependencies will be installed in the root 38 | directory by people who don't have remix licenses. 39 | -------------------------------------------------------------------------------- /storybook/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "storybook", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "setup": "npm install && npm run setup:css", 8 | "setup:css": "cp ../postcss.config.js postcss.config.js && cp ../tailwind.config.js tailwind.config.js", 9 | "css:build": "postcss --config ./postcss.config.js ../styles --base ../styles --dir ../app/ --env production", 10 | "css:watch": "postcss --config ./postcss.config.js ../styles --base ../styles --dir ../app/ -w", 11 | "dev": "concurrently --names \"css,stories\" --prefix-colors \"cyan,magenta\" \"npm run css:watch\" \"start-storybook -p 6006 --no-dll --no-version-updates --no-release-notes --quiet\"", 12 | "build": "npm run css:build && build-storybook --no-dll --quiet" 13 | }, 14 | "keywords": [], 15 | "author": "Kent C. Dodds (https://kentcdodds.com/)", 16 | "license": "MIT", 17 | "devDependencies": { 18 | "@babel/core": "^7.12.3", 19 | "@storybook/addon-actions": "^6.0.28", 20 | "@storybook/addon-essentials": "^6.0.28", 21 | "@storybook/addon-links": "^6.0.28", 22 | "@storybook/react": "^6.0.28", 23 | "@tailwindcss/typography": "^0.2.0", 24 | "autoprefixer": "^9.8.6", 25 | "babel-loader": "^8.1.0", 26 | "postcss-cli": "^7.1.2", 27 | "react-is": "^17.0.1", 28 | "tailwindcss": "^1.9.6" 29 | }, 30 | "dependencies": { 31 | "concurrently": "^5.3.0", 32 | "react": "^17.0.1", 33 | "react-dom": "^17.0.1" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /storybook/stories/404.stories.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import FourOhFour from '../../app/routes/404' 3 | 4 | export default { 5 | title: '404', 6 | component: FourOhFour, 7 | } 8 | const Template = args => 9 | 10 | export const Main = Template.bind({}) 11 | Main.args = {} 12 | Main.routeData = {fakeFourOhFor: true} 13 | -------------------------------------------------------------------------------- /storybook/stories/index.stories.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import Index from '../../app/routes/index' 3 | 4 | export default { 5 | title: 'Index', 6 | component: Index, 7 | } 8 | const Template = args => 9 | 10 | export const Home = Template.bind({}) 11 | Home.args = {} 12 | Home.routeData = [ 13 | { 14 | id: 'mock-1', 15 | title: 'Mock post 1', 16 | content: '

This is an example.

', 17 | author: 'Mock Author', 18 | createdDate: 1604809560963, 19 | category: 'mock', 20 | }, 21 | ] 22 | -------------------------------------------------------------------------------- /styles/global.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss/base'; 2 | 3 | @import 'tailwindcss/components'; 4 | 5 | @import 'tailwindcss/utilities'; 6 | 7 | :focus:not(:focus-visible) { 8 | outline: none; 9 | } 10 | 11 | body { 12 | font-family: Century Gothic, Futura, sans-serif; 13 | } 14 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | // this file gets copied to the storybook directory 4 | // so to make sure we're pointing to the right files 5 | // we use this to do full path references based on 6 | // the project root. 7 | const dir = __dirname.endsWith('storybook') 8 | ? path.dirname(__dirname) 9 | : __dirname 10 | 11 | const fromRoot = p => path.join(dir, p) 12 | 13 | module.exports = { 14 | darkMode: 'media', 15 | variants: { 16 | opacity: ['responsive', 'hover', 'focus', 'dark'], 17 | boxShadow: ['responsive', 'hover', 'focus', 'dark'], 18 | }, 19 | purge: { 20 | mode: 'layers', 21 | content: [ 22 | fromRoot('./app/**/*.js'), 23 | fromRoot('./app/**/*.ts'), 24 | fromRoot('./app/**/*.tsx'), 25 | fromRoot('./app/**/*.mdx'), 26 | fromRoot('./app/**/*.md'), 27 | fromRoot('./remix.config.js'), 28 | ], 29 | }, 30 | plugins: [require('@tailwindcss/typography')], 31 | } 32 | -------------------------------------------------------------------------------- /tests/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parserOptions: { 3 | tsconfigRootDir: __dirname, 4 | project: './tsconfig.json', 5 | }, 6 | overrides: [ 7 | { 8 | files: ['**/__tests__/**/*.{js,ts,tsx}'], 9 | settings: { 10 | 'import/resolver': { 11 | jest: { 12 | jestConfigFile: './config/jest.config.server.js', 13 | }, 14 | }, 15 | }, 16 | }, 17 | ], 18 | } 19 | -------------------------------------------------------------------------------- /tests/mock-server.ts: -------------------------------------------------------------------------------- 1 | // test/server.js 2 | import {rest} from 'msw' 3 | import {setupServer} from 'msw/node' 4 | import {handlers} from './server-handlers' 5 | 6 | const server = setupServer(...handlers) 7 | 8 | export {server, rest} 9 | -------------------------------------------------------------------------------- /tests/server-handlers.ts: -------------------------------------------------------------------------------- 1 | import {RequestHandlersList} from 'msw/lib/types/setupWorker/glossary' 2 | 3 | const handlers: RequestHandlersList = [] 4 | export {handlers} 5 | -------------------------------------------------------------------------------- /tests/setup-env.js: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom' 2 | 3 | import {server} from './mock-server' 4 | 5 | // enable API mocking in test runs using the same request handlers 6 | // as for the client-side mocking. 7 | beforeAll(() => server.listen()) 8 | afterAll(() => server.close()) 9 | afterEach(() => server.resetHandlers()) 10 | -------------------------------------------------------------------------------- /tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../config/shared-tsconfig.json", 3 | "include": ["**/*"], 4 | "compilerOptions": { 5 | "allowSyntheticDefaultImports": true, 6 | "baseUrl": "." 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [{"path": "app"}, {"path": "server"}, {"path": "cypress"}] 4 | } 5 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | import type * as FT from '@firebase/firestore-types' 2 | import type {firestore} from 'firebase' 3 | 4 | type Post = { 5 | id: string 6 | slug: string 7 | title: string 8 | content: string 9 | authorId: string 10 | createdDate: number 11 | category: string 12 | } 13 | 14 | type User = { 15 | id: string 16 | name: string 17 | } 18 | 19 | export {Post, User} 20 | --------------------------------------------------------------------------------