diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..91077d062 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +/node_modules +*.log +.DS_Store +.env +/.cache +/public/build +/build diff --git a/.env.example b/.env.example new file mode 100644 index 000000000..3b9a0dc00 --- /dev/null +++ b/.env.example @@ -0,0 +1,7 @@ +LITEFS_DIR=/litefs/data +DATABASE_PATH="./prisma/data.db" +DATABASE_URL="file:./data.db?connection_limit=1" +SESSION_SECRET="super-duper-s3cret" +ENCRYPTION_SECRET="something-fake" +MAILGUN_DOMAIN="mg.example.com" +MAILGUN_SENDING_KEY="some-api-token-with-dashes" diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 000000000..6ec0c3260 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,30 @@ +/** @type {import('@types/eslint').Linter.BaseConfig} */ +module.exports = { + extends: [ + '@remix-run/eslint-config', + '@remix-run/eslint-config/node', + '@remix-run/eslint-config/jest-testing-library', + 'prettier', + ], + rules: { + '@typescript-eslint/consistent-type-imports': [ + 'warn', + { + prefer: 'type-imports', + disallowTypeAnnotations: true, + fixStyle: 'inline-type-imports', + }, + ], + 'testing-library/no-await-sync-events': 'off', + 'jest-dom/prefer-in-document': 'off', + '@typescript-eslint/no-duplicate-imports': 'warn', + }, + // we're using vitest which has a very similar API to jest + // (so the linting plugins work nicely), but it means we have to explicitly + // set the jest version. + settings: { + jest: { + version: 28, + }, + }, +} diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 000000000..5d113cc26 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,165 @@ +name: πŸš€ Deploy +on: + push: + branches: + - main + - dev + pull_request: {} + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + actions: write + contents: read + +jobs: + lint: + name: ⬣ ESLint + runs-on: ubuntu-latest + steps: + - name: ⬇️ Checkout repo + uses: actions/checkout@v3 + + - name: βŽ” Setup node + uses: actions/setup-node@v3 + with: + node-version: 18 + + - name: πŸ“₯ Download deps + uses: bahmutov/npm-install@v1 + with: + useLockFile: false + + - name: πŸ”¬ Lint + run: npm run lint + + typecheck: + name: Κ¦ TypeScript + runs-on: ubuntu-latest + steps: + - name: ⬇️ Checkout repo + uses: actions/checkout@v3 + + - name: βŽ” Setup node + uses: actions/setup-node@v3 + with: + node-version: 18 + + - name: πŸ“₯ Download deps + uses: bahmutov/npm-install@v1 + with: + useLockFile: false + + - name: πŸ”Ž Type check + run: npm run typecheck --if-present + + vitest: + name: ⚑ Vitest + runs-on: ubuntu-latest + steps: + - name: ⬇️ Checkout repo + uses: actions/checkout@v3 + + - name: βŽ” Setup node + uses: actions/setup-node@v3 + with: + node-version: 18 + + - name: πŸ“₯ Download deps + uses: bahmutov/npm-install@v1 + with: + useLockFile: false + + - name: πŸ„ Copy test env vars + run: cp .env.example .env + + - name: ⚑ Run vitest + run: npm run test -- --coverage + + playwright: + name: 🎭 Playwright + runs-on: ubuntu-latest + timeout-minutes: 60 + steps: + - name: ⬇️ Checkout repo + uses: actions/checkout@v3 + + - name: πŸ„ Copy test env vars + run: cp .env.example .env + + - name: βŽ” Setup node + uses: actions/setup-node@v3 + with: + node-version: 18 + + - name: πŸ“₯ Download deps + uses: bahmutov/npm-install@v1 + with: + useLockFile: false + + - name: πŸ“₯ Install Playwright Browsers + run: npx playwright install --with-deps + + - name: πŸ›  Setup Database + run: npx prisma migrate deploy + + - name: 🏦 Cache Database + id: db-cache + uses: actions/cache@v3 + with: + path: prisma/sqlite.db + key: db-cache + + - name: πŸ— Build + run: npm run build + + - name: 🎭 Playwright tests + run: npx playwright test + + - name: πŸ“Š Upload report + uses: actions/upload-artifact@v3 + if: always() + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 + + deploy: + name: πŸš€ Deploy + runs-on: ubuntu-latest + needs: [lint, typecheck, vitest, playwright] + # only build/deploy main branch on pushes + if: + ${{ (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev') && + github.event_name == 'push' }} + + steps: + - name: ⬇️ Checkout repo + uses: actions/checkout@v3 + + - name: πŸ‘€ Read app name + uses: SebRollen/toml-action@v1.0.2 + id: app_name + with: + file: 'fly.toml' + field: 'app' + + - name: πŸš€ Deploy Staging + if: ${{ github.ref == 'refs/heads/dev' }} + uses: superfly/flyctl-actions@1.3 + with: + args: + 'deploy --remote-only --build-arg COMMIT_SHA=${{ github.sha }} --app + ${{ steps.app_name.outputs.value }}-staging' + env: + FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} + + - name: πŸš€ Deploy Production + if: ${{ github.ref == 'refs/heads/main' }} + uses: superfly/flyctl-actions@1.3 + with: + args: 'deploy --remote-only --build-arg COMMIT_SHA=${{ github.sha }}' + env: + FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..8ed42b40f --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +node_modules + +/build +/public/build +.env + +/prisma/data.db +/prisma/data.db-journal +/prisma/test + +*.local.* +/test-results/ +/playwright-report/ +/playwright/.cache/ +/mocks/fixtures/email/*.json +/coverage +/prisma/data.db.bkp + +prisma/test +other/image.db + +server-build \ No newline at end of file diff --git a/.npmrc b/.npmrc new file mode 100644 index 000000000..668efa17f --- /dev/null +++ b/.npmrc @@ -0,0 +1,2 @@ +legacy-peer-deps=true +registry=https://registry.npmjs.org/ diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 000000000..aa45326fb --- /dev/null +++ b/.prettierignore @@ -0,0 +1,8 @@ +node_modules + +/build +/public/build +.env + +/app/styles/tailwind.css +package.json diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..9c1cb8d26 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,70 @@ +# Contributing + +Thanks for your willingness to contribute! Please make sure to check with me +before doing a bunch of work on something. + +## Project setup + +If you do need to set the project up locally yourself, feel free to follow these +instructions: + +### System Requirements + +- [Node.js](https://nodejs.org/) >= 18.0.0 +- [npm](https://npmjs.com/) >= 8.18.0 +- [git](https://git-scm.com/) >= 2.38.0 + +### Setup steps + +1. Fork and clone the repo +2. Copy `.env.example` into `.env` +3. Run `npm run setup -s` to install dependencies and run validation +4. Create a branch for your PR with `git checkout -b pr/your-branch-name` + +> Tip: Keep your `main` branch pointing at the original repository and make pull +> requests from branches on your fork. To do this, run: +> +> ``` +> git remote add upstream https://github.com/epicweb-dev/rocket-rental.git +> git fetch upstream +> git branch --set-upstream-to=upstream/main main +> ``` +> +> This will add the original repository as a "remote" called "upstream," Then +> fetch the git information from that remote, then set your local `main` branch +> to use the upstream main branch whenever you run `git pull`. Then you can make +> all of your pull request branches based on this `main` branch. Whenever you +> want to update your version of `main`, do a regular `git pull`. + +If the setup script doesn't work, you can try to run the commands manually: + +```sh +git clone +cd ./rocket-rental + +# copy the .env.example to .env +# everything's mocked out during development so you shouldn't need to +# change any of these values unless you want to hit real environments. +cp .env.example .env + +# Install deps +npm install + +# setup database +prisma migrate reset --force + +# Install playwright browsers +npm run test:e2e:install + +# run build, typecheck, linting +npm run validate +``` + +If that all worked without trouble, you should be able to start development +with: + +```sh +npm run dev +``` + +And open up `http://localhost:3000` and rock! diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..02893da07 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,74 @@ +# base node image +FROM node:16-bullseye-slim as base + +# set for base and all layer that inherit from it +ENV NODE_ENV production + +# Install openssl for Prisma +RUN apt-get update && apt-get install -y fuse3 openssl sqlite3 ca-certificates + +# Install all node_modules, including dev dependencies +FROM base as deps + +WORKDIR /myapp + +ADD package.json package-lock.json .npmrc ./ +RUN npm install --production=false + +# Setup production node_modules +FROM base as production-deps + +WORKDIR /myapp + +COPY --from=deps /myapp/node_modules /myapp/node_modules +ADD package.json package-lock.json .npmrc ./ +RUN npm prune --production + +# Build the app +FROM base as build + +WORKDIR /myapp + +COPY --from=deps /myapp/node_modules /myapp/node_modules + +ADD prisma . +RUN npx prisma generate + +ADD . . +RUN npm run build + +# Finally, build the production image with minimal footprint +FROM base + +ENV FLY="true" +ENV LITEFS_DIR="/litefs/data" +ENV DATABASE_FILENAME="sqlite.db" +ENV DATABASE_PATH="$LITEFS_DIR/$DATABASE_FILENAME" +ENV DATABASE_URL="file:$DATABASE_PATH" +ENV INTERNAL_PORT="8080" +ENV PORT="8081" +ENV NODE_ENV="production" + +# add shortcut for connecting to database CLI +RUN echo "#!/bin/sh\nset -x\nsqlite3 \$DATABASE_URL" > /usr/local/bin/database-cli && chmod +x /usr/local/bin/database-cli + +WORKDIR /myapp + +COPY --from=production-deps /myapp/node_modules /myapp/node_modules +COPY --from=build /myapp/node_modules/.prisma /myapp/node_modules/.prisma + +COPY --from=build /myapp/server-build /myapp/server-build +COPY --from=build /myapp/build /myapp/build +COPY --from=build /myapp/public /myapp/public +COPY --from=build /myapp/package.json /myapp/package.json +COPY --from=build /myapp/other/start.js /myapp/other/start.js +COPY --from=build /myapp/prisma /myapp/prisma + +# prepare for litefs +COPY --from=flyio/litefs:0.4.0 /usr/local/bin/litefs /usr/local/bin/litefs +ADD other/litefs.yml /etc/litefs.yml +RUN mkdir -p /data ${LITEFS_DIR} + +ADD . . + +CMD ["litefs", "mount", "--", "node", "./other/start.js"] diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 000000000..1e7c64747 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,7 @@ +Copyright Β© 2023 Kent C. Dodds + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the β€œSoftware”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED β€œAS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 000000000..3b2615215 --- /dev/null +++ b/README.md @@ -0,0 +1,224 @@ +
+

The Epic Stack πŸš€

+ + A starter project for building Epic web applications + +

+ This is a Remix Stack with the basic + things setup and configured for you to hit the ground running on your next + Epic idea. +

+
+ +![The Epic Stack](https://user-images.githubusercontent.com/1500684/236281980-66d33a85-f5fc-4094-ab73-e8994acc27a3.jpg) + +
+ + +[![Build Status][build-badge]][build] +[![MIT License][license-badge]][license] +[![Code of Conduct][coc-badge]][coc] + + +Learn more about [Remix Stacks](https://remix.run/stacks). + +```sh +npx create-remix@latest --template remix-run/indie-stack +``` + +## What's in the stack + +- [Fly app deployment](https://fly.io) with [Docker](https://www.docker.com/) +- Multi-region, distributed, production-ready + [SQLite Database](https://sqlite.org) with + [LiteFS](https://fly.io/docs/litefs/). +- Healthcheck endpoint for + [Fly backups region fallbacks](https://fly.io/docs/reference/configuration/#services-http_checks) +- [GitHub Actions](https://github.com/features/actions) for deploy on merge to + production and staging environments +- Email/Password Authentication with + [cookie-based sessions](https://remix.run/utils/sessions#md-createcookiesessionstorage) +- Database ORM with [Prisma](https://prisma.io) +- Styling with [Tailwind](https://tailwindcss.com/) +- End-to-end testing with [Playwright](https://playwright.dev/) +- Local third party request mocking with [MSW](https://mswjs.io) +- Unit testing with [Vitest](https://vitest.dev) and + [Testing Library](https://testing-library.com) +- Code formatting with [Prettier](https://prettier.io) +- Linting with [ESLint](https://eslint.org) +- Static Types with [TypeScript](https://typescriptlang.org) +- Runtime schema validation with [zod](https://zod.dev) + +Not a fan of bits of the stack? Fork it, change it, and use +`npx create-remix --template your/repo`! Make it your own. + +## Development + +- Initial setup: + + ```sh + npm run setup + ``` + +- Start dev server: + + ```sh + npm run dev + ``` + +This starts your app in development mode, rebuilding assets on file changes. + +The database seed script creates a new user with some data you can use to get +started: + +- Email: `kody@epicweb.dev` +- Password: `kodylovesyou` + +### Relevant code + +This is a pretty simple note-taking app, but it's a good example of how you can +build a full stack app with Prisma and Remix. The main functionality is creating +users, logging in and out, and creating and deleting notes. + +## Deployment + +The Epic Stack comes with a GitHub Action that handles automatically deploying +your app to production and staging environments. + +Prior to your first deployment, you'll need to do a few things: + +- [Install Fly](https://fly.io/docs/getting-started/installing-flyctl/) + +- Sign up and log in to Fly + + ```sh + fly auth signup + ``` + + > **Note:** If you have more than one Fly account, ensure that you are signed + > into the same account in the Fly CLI as you are in the browser. In your + > terminal, run `fly auth whoami` and ensure the email matches the Fly account + > signed into the browser. + +- Create two apps on Fly, one for staging and one for production: + + ```sh + fly apps create epic-stack-template + fly apps create epic-stack-template-staging + ``` + + > **Note:** Make sure this name matches the `app` set in your `fly.toml` file. + > Otherwise, you will not be able to deploy. + + - Initialize Git. + + ```sh + git init + ``` + +- Create a new [GitHub Repository](https://repo.new), and then add it as the + remote for your project. **Do not push your app yet!** + + ```sh + git remote add origin + ``` + +- Add a `FLY_API_TOKEN` to your GitHub repo. To do this, go to your user + settings on Fly and create a new + [token](https://web.fly.io/user/personal_access_tokens/new), then add it to + [your repo secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets) + with the name `FLY_API_TOKEN`. + +- Add a `SESSION_SECRET` to your fly app secrets, to do this you can run the + following commands: + + ```sh + fly secrets set SESSION_SECRET=$(openssl rand -hex 32) --app epic-stack-template + fly secrets set SESSION_SECRET=$(openssl rand -hex 32) --app epic-stack-template-staging + ``` + + If you don't have openssl installed, you can also use + [1Password](https://1password.com/password-generator) to generate a random + secret, just replace `$(openssl rand -hex 32)` with the generated secret. + +- Create a persistent volume for the sqlite database for both your staging and + production environments. Run the following: + + ```sh + fly volumes create data --size 1 --app epic-stack-template + fly volumes create data --size 1 --app epic-stack-template-staging + ``` + +Now that everything is set up you can commit and push your changes to your repo. +Every commit to your `main` branch will trigger a deployment to your production +environment, and every commit to your `dev` branch will trigger a deployment to +your staging environment. + +### Connecting to your database + +The sqlite database lives at `/data/sqlite.db` in the deployed application. You +can connect to the live database by running `fly ssh console -C database-cli`. + +## GitHub Actions + +We use GitHub Actions for continuous integration and deployment. Anything that +gets into the `main` branch will be deployed to production after running +tests/build/etc. Anything in the `dev` branch will be deployed to staging. + +## Testing + +### Playwright + +We use Playwright for our End-to-End tests in this project. You'll find those in +the `tests` directory. As you make changes, add to an existing file or create a +new file in the `tests` directory to test your changes. + +To run these tests in development, run `npm run test:e2e:dev` which will start +the dev server for the app and run Playwright on it. + +We have a fixture for testing authenticated features without having to go +through the login flow: + +```ts +test('my test', async ({ page, login }) => { + const user = await login() + // you are now logged in +}) +``` + +We also auto-delete the user at the end of your test. That way, we can keep your +local db clean and keep your tests isolated from one another. + +### Vitest + +For lower level tests of utilities and individual components, we use `vitest`. +We have DOM-specific assertion helpers via +[`@testing-library/jest-dom`](https://testing-library.com/jest-dom). + +### Type Checking + +This project uses TypeScript. It's recommended to get TypeScript set up for your +editor to get a really great in-editor experience with type checking and +auto-complete. To run type checking across the whole project, run +`npm run typecheck`. + +### Linting + +This project uses ESLint for linting. That is configured in `.eslintrc.js`. + +### Formatting + +We use [Prettier](https://prettier.io/) for auto-formatting in this project. +It's recommended to install an editor plugin (like the +[VSCode Prettier plugin](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode)) +to get auto-formatting on save. There's also a `npm run format` script you can +run to format all files in the project. + + +[build-badge]: https://img.shields.io/github/actions/workflow/status/epicweb-dev/epic-stack/validate.yml?branch=main&logo=github&style=flat-square +[build]: https://github.com/epicweb-dev/epic-stack/actions?query=workflow%3Avalidate +[license-badge]: https://img.shields.io/badge/license-MIT%20License-blue.svg?style=flat-square +[license]: https://github.com/epicweb-dev/epic-stack/blob/main/LICENSE +[coc-badge]: https://img.shields.io/badge/code%20of-conduct-ff69b4.svg?style=flat-square +[coc]: https://kentcdodds.com/conduct + diff --git a/app/components/error-boundary.tsx b/app/components/error-boundary.tsx new file mode 100644 index 000000000..0d2f9ac51 --- /dev/null +++ b/app/components/error-boundary.tsx @@ -0,0 +1,44 @@ +import { + isRouteErrorResponse, + useParams, + useRouteError, +} from '@remix-run/react' +import { type ErrorResponse } from '@remix-run/router' +import { getErrorMessage } from '~/utils/misc' + +type StatusHandler = (info: { + error: ErrorResponse + params: Record +}) => JSX.Element | null + +export function GeneralErrorBoundary({ + defaultStatusHandler = ({ error }) => ( +

+ {error.status} {error.data} +

+ ), + statusHandlers, + unexpectedErrorHandler = error =>

{getErrorMessage(error)}

, +}: { + defaultStatusHandler?: StatusHandler + statusHandlers?: Record + unexpectedErrorHandler?: (error: unknown) => JSX.Element | null +}) { + const error = useRouteError() + const params = useParams() + + if (typeof document !== 'undefined') { + console.error(error) + } + + return ( +
+ {isRouteErrorResponse(error) + ? (statusHandlers?.[error.status] ?? defaultStatusHandler)({ + error, + params, + }) + : unexpectedErrorHandler(error)} +
+ ) +} diff --git a/app/components/spacer.tsx b/app/components/spacer.tsx new file mode 100644 index 000000000..8a8e5372d --- /dev/null +++ b/app/components/spacer.tsx @@ -0,0 +1,57 @@ +export function Spacer({ + size, +}: { + /** + * The size of the space + * + * 4xs: h-4 (16px) + * + * 3xs: h-8 (32px) + * + * 2xs: h-12 (48px) + * + * xs: h-16 (64px) + * + * sm: h-20 (80px) + * + * md: h-24 (96px) + * + * lg: h-28 (112px) + * + * xl: h-32 (128px) + * + * 2xl: h-36 (144px) + * + * 3xl: h-40 (160px) + * + * 4xl: h-44 (176px) + */ + size: + | '4xs' + | '3xs' + | '2xs' + | 'xs' + | 'sm' + | 'md' + | 'lg' + | 'xl' + | '2xl' + | '3xl' + | '4xl' +}) { + const options: Record = { + '4xs': 'h-4', + '3xs': 'h-8', + '2xs': 'h-12', + xs: 'h-16', + sm: 'h-20', + md: 'h-24', + lg: 'h-28', + xl: 'h-32', + '2xl': 'h-36', + '3xl': 'h-40', + '4xl': 'h-44', + } + const className = options[size] + return
+} diff --git a/app/components/spinner.tsx b/app/components/spinner.tsx new file mode 100644 index 000000000..c8b0d7b5d --- /dev/null +++ b/app/components/spinner.tsx @@ -0,0 +1,33 @@ +export function Spinner({ showSpinner }: { showSpinner: boolean }) { + return ( +
+ + Loading + + + +
+ ) +} diff --git a/app/entry.client.tsx b/app/entry.client.tsx new file mode 100644 index 000000000..562888e43 --- /dev/null +++ b/app/entry.client.tsx @@ -0,0 +1,10 @@ +import { RemixBrowser } from '@remix-run/react' +import { startTransition } from 'react' +import { hydrateRoot } from 'react-dom/client' + +if (ENV.MODE === 'development') { + import('~/utils/devtools').then(({ init }) => init()) +} +startTransition(() => { + hydrateRoot(document, ) +}) diff --git a/app/entry.server.tsx b/app/entry.server.tsx new file mode 100644 index 000000000..49809e0ad --- /dev/null +++ b/app/entry.server.tsx @@ -0,0 +1,74 @@ +import 'dotenv/config' +import { PassThrough } from 'stream' +import { type EntryContext, Response } from '@remix-run/node' +import { RemixServer } from '@remix-run/react' +import isbot from 'isbot' +import { renderToPipeableStream } from 'react-dom/server' +import { init, getEnv } from './utils/env.server' +import { getInstanceInfo } from 'litefs-js' + +const ABORT_DELAY = 5000 + +init() +global.ENV = getEnv() + +export default async function handleRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext, +) { + const { currentInstance, primaryInstance } = await getInstanceInfo() + + responseHeaders.set('fly-region', process.env.FLY_REGION ?? 'unknown') + responseHeaders.set('fly-app', process.env.FLY_APP_NAME ?? 'unknown') + responseHeaders.set('fly-primary-instance', primaryInstance) + responseHeaders.set('fly-instance', currentInstance) + + const callbackName = isbot(request.headers.get('user-agent')) + ? 'onAllReady' + : 'onShellReady' + + return new Promise((resolve, reject) => { + let didError = false + + const { pipe, abort } = renderToPipeableStream( + , + { + [callbackName]: () => { + const body = new PassThrough() + + responseHeaders.set('Content-Type', 'text/html') + resolve( + new Response(body, { + headers: responseHeaders, + status: didError ? 500 : responseStatusCode, + }), + ) + + pipe(body) + }, + onShellError: (err: unknown) => { + reject(err) + }, + onError: (error: unknown) => { + didError = true + + console.error(error) + }, + }, + ) + + setTimeout(abort, ABORT_DELAY) + }) +} + +export async function handleDataRequest(response: Response) { + const { currentInstance, primaryInstance } = await getInstanceInfo() + response.headers.set('fly-region', process.env.FLY_REGION ?? 'unknown') + response.headers.set('fly-app', process.env.FLY_APP_NAME ?? 'unknown') + response.headers.set('fly-primary-instance', primaryInstance) + response.headers.set('fly-instance', currentInstance) + + return response +} diff --git a/app/root.tsx b/app/root.tsx new file mode 100644 index 000000000..e5a715f3a --- /dev/null +++ b/app/root.tsx @@ -0,0 +1,270 @@ +import * as Checkbox from '@radix-ui/react-checkbox' +import * as DropdownMenu from '@radix-ui/react-dropdown-menu' +import { cssBundleHref } from '@remix-run/css-bundle' +import { + json, + type DataFunctionArgs, + type LinksFunction, + type V2_MetaFunction, +} from '@remix-run/node' +import { + Form, + Link, + Links, + LiveReload, + Meta, + Outlet, + Scripts, + ScrollRestoration, + useFetcher, + useLoaderData, + useSubmit, +} from '@remix-run/react' +import clsx from 'clsx' +import { useState } from 'react' +import tailwindStylesheetUrl from './styles/tailwind.css' +import { authenticator } from './utils/auth.server' +import { prisma } from './utils/db.server' +import { getEnv } from './utils/env.server' +import { ButtonLink } from './utils/forms' +import { getUserImgSrc, typedBoolean } from './utils/misc' +import { useUser } from './utils/user' + +export const links: LinksFunction = () => { + return [ + { + rel: 'apple-touch-icon', + sizes: '180x180', + href: '/favicons/apple-touch-icon.png', + }, + { + rel: 'icon', + type: 'image/png', + sizes: '32x32', + href: '/favicons/favicon-32x32.png', + }, + { + rel: 'icon', + type: 'image/png', + sizes: '16x16', + href: '/favicons/favicon-16x16.png', + }, + { rel: 'manifest', href: '/site.webmanifest' }, + { rel: 'icon', href: '/favicon.ico' }, + { rel: 'stylesheet', href: '/fonts/nunito-sans/font.css' }, + { rel: 'stylesheet', href: tailwindStylesheetUrl }, + cssBundleHref ? { rel: 'stylesheet', href: cssBundleHref } : null, + ].filter(typedBoolean) +} + +export const meta: V2_MetaFunction = () => { + return [ + { title: 'Rocket Rental' }, + { name: 'description', content: 'Find yourself in outer space' }, + ] +} + +export async function loader({ request }: DataFunctionArgs) { + const userId = await authenticator.isAuthenticated(request) + + const user = userId + ? await prisma.user.findUnique({ + where: { id: userId }, + select: { id: true, name: true, username: true, imageId: true }, + }) + : null + if (userId && !user) { + console.info('something weird happened') + // something weird happened... The user is authenticated but we can't find + // them in the database. Maybe they were deleted? Let's log them out. + await authenticator.logout(request, { redirectTo: '/' }) + } + + return json({ user, ENV: getEnv() }) +} + +export default function App() { + const data = useLoaderData() + const { user } = data + return ( + + + + + + + + +
+ +
+ +
+ +
+ +
+ +
epic
+
notes
+ + +
+
+ + +