├── .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 |
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 |
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 |
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 | [](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 |
--------------------------------------------------------------------------------