├── .npmrc ├── app ├── routes │ ├── admin │ │ └── cache │ │ │ ├── sqlite.tsx │ │ │ ├── lru.$cacheKey.ts │ │ │ ├── sqlite.$cacheKey.ts │ │ │ └── sqlite.server.ts │ ├── _marketing │ │ ├── about.tsx │ │ ├── privacy.tsx │ │ ├── support.tsx │ │ ├── tos.tsx │ │ └── +logos │ │ │ ├── stars.jpg │ │ │ ├── testing-library.png │ │ │ ├── shadcn-ui.svg │ │ │ ├── radix.svg │ │ │ ├── tailwind.svg │ │ │ ├── github.svg │ │ │ ├── typescript.svg │ │ │ ├── eslint.svg │ │ │ ├── sentry.svg │ │ │ └── remix.svg │ ├── _auth │ │ ├── logout.tsx │ │ ├── onboarding │ │ │ ├── index.server.ts │ │ │ └── $provider.server.ts │ │ ├── auth.$provider │ │ │ └── index.ts │ │ └── reset-password.server.ts │ ├── _seo │ │ ├── robots[.]txt.ts │ │ └── sitemap[.]xml.ts │ ├── users │ │ └── $username │ │ │ └── notes │ │ │ ├── new.tsx │ │ │ ├── index.tsx │ │ │ └── $noteId_.edit.tsx │ ├── settings │ │ └── profile │ │ │ └── two-factor │ │ │ └── _layout.tsx │ ├── me.tsx │ ├── resources │ │ ├── healthcheck.tsx │ │ └── download-user-data.tsx │ └── $.tsx ├── assets │ └── favicons │ │ ├── apple-touch-icon.png │ │ └── favicon.svg ├── utils │ ├── providers │ │ ├── constants.ts │ │ └── provider.ts │ ├── totp.server.ts │ ├── nonce-provider.ts │ ├── verification.server.ts │ ├── honeypot.server.ts │ ├── redirect-cookie.server.ts │ ├── request-info.ts │ ├── theme.server.ts │ ├── litefs.server.ts │ ├── connections.server.ts │ ├── misc.error-message.test.ts │ ├── monitoring.client.tsx │ ├── headers.server.test.ts │ ├── db.server.ts │ ├── session.server.ts │ ├── permissions.server.ts │ ├── client-hints.tsx │ ├── connections.tsx │ ├── user-validation.ts │ └── toast.server.ts ├── components │ ├── floating-toolbar.tsx │ ├── ui │ │ ├── README.md │ │ ├── label.tsx │ │ ├── textarea.tsx │ │ ├── input.tsx │ │ ├── sonner.tsx │ │ ├── checkbox.tsx │ │ ├── tooltip.tsx │ │ ├── button.tsx │ │ ├── input-otp.tsx │ │ └── status-button.tsx │ ├── toaster.tsx │ ├── spacer.tsx │ ├── error-boundary.tsx │ ├── search-bar.tsx │ └── progress-bar.tsx ├── entry.client.tsx └── routes.ts ├── public ├── favicon.ico ├── img │ └── user.png ├── favicons │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ └── README.md └── site.webmanifest ├── types ├── env.env.d.ts ├── reset.d.ts ├── icon-name.d.ts └── deps.d.ts ├── tests ├── fixtures │ ├── github │ │ └── ghost.jpg │ └── images │ │ ├── user │ │ ├── 0.jpg │ │ ├── 1.jpg │ │ ├── 2.jpg │ │ ├── 3.jpg │ │ ├── 4.jpg │ │ ├── 5.jpg │ │ ├── 6.jpg │ │ ├── 7.jpg │ │ ├── 8.jpg │ │ ├── 9.jpg │ │ ├── kody.png │ │ └── README.md │ │ ├── notes │ │ ├── 0.png │ │ ├── 1.png │ │ ├── 2.png │ │ ├── 3.png │ │ ├── 4.png │ │ ├── 5.png │ │ ├── 6.png │ │ ├── 7.png │ │ ├── 8.png │ │ └── 9.png │ │ └── kody-notes │ │ ├── mountain.png │ │ ├── cute-koala.png │ │ ├── koala-coder.png │ │ ├── koala-cuddle.png │ │ ├── koala-eating.png │ │ ├── koala-mentor.png │ │ └── koala-soccer.png ├── mocks │ ├── pwned-passwords.ts │ ├── README.md │ ├── resend.ts │ ├── index.ts │ └── utils.ts ├── e2e │ ├── error-boundary.test.ts │ └── search.test.ts ├── setup │ ├── db-setup.ts │ ├── global-setup.ts │ └── setup-test-env.ts └── utils.ts ├── prisma ├── migrations │ └── migration_lock.toml └── sql │ └── searchUsers.sql ├── other ├── Dockerfile.dockerignore ├── sly │ ├── sly.json │ └── transform-icon.ts ├── svg-icons │ ├── README.md │ ├── laptop.svg │ ├── plus.svg │ ├── trash.svg │ ├── check.svg │ ├── arrow-right.svg │ ├── arrow-left.svg │ ├── magnifying-glass.svg │ ├── envelope-closed.svg │ ├── lock-open-1.svg │ ├── pencil-1.svg │ ├── reset.svg │ ├── cross-1.svg │ ├── exit.svg │ ├── lock-closed.svg │ ├── dots-horizontal.svg │ ├── clock.svg │ ├── passkey.svg │ ├── file-text.svg │ ├── camera.svg │ ├── download.svg │ ├── avatar.svg │ ├── github-logo.svg │ ├── update.svg │ └── question-mark-circled.svg ├── README.md └── litefs.yml ├── docs ├── decisions │ ├── 000-template.md │ ├── README.md │ ├── 029-remix-auth.md │ ├── 034-source-maps.md │ ├── 022-report-only-csp.md │ ├── 017-resend-email.md │ ├── 027-toasts.md │ ├── 009-region-selection.md │ ├── 011-sitemaps.md │ ├── 008-content-security-policy.md │ ├── 032-csrf.md │ ├── 015-monitoring.md │ ├── 026-path-aliases.md │ ├── 033-honeypot.md │ ├── 006-native-esm.md │ ├── 045-rr-auto-routes.md │ ├── 013-email-code.md │ ├── 001-typescript-only.md │ ├── 046-remove-path-aliases.md │ ├── 037-generated-internal-command.md │ ├── 042-node-sqlite.md │ ├── 020-icons.md │ ├── 035-remove-csrf.md │ ├── 038-remove-cleanup-db.md │ ├── 002-email-service.md │ ├── 010-memory-swap.md │ ├── 041-image-optimization.md │ ├── 023-route-based-dialogs.md │ └── 036-vite.md ├── client-hints.md ├── community.md ├── memory.md ├── email.md ├── apis.md ├── permissions.md ├── secrets.md ├── testing.md ├── image-optimization.md ├── getting-started.md ├── icons.md ├── README.md ├── toasts.md ├── guiding-principles.md ├── seo.md └── timezone.md ├── .prettierignore ├── .vscode ├── extensions.json └── settings.json ├── remix.init ├── index.js ├── package.json └── gitignore ├── eslint.config.js ├── .github └── PULL_REQUEST_TEMPLATE.md ├── components.json ├── tsconfig.json ├── .gitignore ├── index.ts ├── server ├── app.ts └── utils │ └── monitoring.ts ├── react-router.config.ts ├── playwright.config.ts ├── LICENSE.md ├── .env.example ├── fly.toml └── .cursor └── rules └── avoid-use-effect.mdc /.npmrc: -------------------------------------------------------------------------------- 1 | legacy-peer-deps=true 2 | registry=https://registry.npmjs.org/ 3 | -------------------------------------------------------------------------------- /app/routes/admin/cache/sqlite.tsx: -------------------------------------------------------------------------------- 1 | export { action } from './sqlite.server.ts' 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-stack/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/img/user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-stack/HEAD/public/img/user.png -------------------------------------------------------------------------------- /types/env.env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /types/reset.d.ts: -------------------------------------------------------------------------------- 1 | // Do not add any other lines of code to this file! 2 | import '@epic-web/config/reset.d.ts' 3 | -------------------------------------------------------------------------------- /app/routes/_marketing/about.tsx: -------------------------------------------------------------------------------- 1 | export default function AboutRoute() { 2 | return
About page
3 | } 4 | -------------------------------------------------------------------------------- /app/routes/_marketing/privacy.tsx: -------------------------------------------------------------------------------- 1 | export default function PrivacyRoute() { 2 | return
Privacy
3 | } 4 | -------------------------------------------------------------------------------- /app/routes/_marketing/support.tsx: -------------------------------------------------------------------------------- 1 | export default function SupportRoute() { 2 | return
Support
3 | } 4 | -------------------------------------------------------------------------------- /tests/fixtures/github/ghost.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-stack/HEAD/tests/fixtures/github/ghost.jpg -------------------------------------------------------------------------------- /tests/fixtures/images/user/0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-stack/HEAD/tests/fixtures/images/user/0.jpg -------------------------------------------------------------------------------- /tests/fixtures/images/user/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-stack/HEAD/tests/fixtures/images/user/1.jpg -------------------------------------------------------------------------------- /tests/fixtures/images/user/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-stack/HEAD/tests/fixtures/images/user/2.jpg -------------------------------------------------------------------------------- /tests/fixtures/images/user/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-stack/HEAD/tests/fixtures/images/user/3.jpg -------------------------------------------------------------------------------- /tests/fixtures/images/user/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-stack/HEAD/tests/fixtures/images/user/4.jpg -------------------------------------------------------------------------------- /tests/fixtures/images/user/5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-stack/HEAD/tests/fixtures/images/user/5.jpg -------------------------------------------------------------------------------- /tests/fixtures/images/user/6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-stack/HEAD/tests/fixtures/images/user/6.jpg -------------------------------------------------------------------------------- /tests/fixtures/images/user/7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-stack/HEAD/tests/fixtures/images/user/7.jpg -------------------------------------------------------------------------------- /tests/fixtures/images/user/8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-stack/HEAD/tests/fixtures/images/user/8.jpg -------------------------------------------------------------------------------- /tests/fixtures/images/user/9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-stack/HEAD/tests/fixtures/images/user/9.jpg -------------------------------------------------------------------------------- /app/routes/_marketing/tos.tsx: -------------------------------------------------------------------------------- 1 | export default function TermsOfServiceRoute() { 2 | return
Terms of service
3 | } 4 | -------------------------------------------------------------------------------- /tests/fixtures/images/notes/0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-stack/HEAD/tests/fixtures/images/notes/0.png -------------------------------------------------------------------------------- /tests/fixtures/images/notes/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-stack/HEAD/tests/fixtures/images/notes/1.png -------------------------------------------------------------------------------- /tests/fixtures/images/notes/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-stack/HEAD/tests/fixtures/images/notes/2.png -------------------------------------------------------------------------------- /tests/fixtures/images/notes/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-stack/HEAD/tests/fixtures/images/notes/3.png -------------------------------------------------------------------------------- /tests/fixtures/images/notes/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-stack/HEAD/tests/fixtures/images/notes/4.png -------------------------------------------------------------------------------- /tests/fixtures/images/notes/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-stack/HEAD/tests/fixtures/images/notes/5.png -------------------------------------------------------------------------------- /tests/fixtures/images/notes/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-stack/HEAD/tests/fixtures/images/notes/6.png -------------------------------------------------------------------------------- /tests/fixtures/images/notes/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-stack/HEAD/tests/fixtures/images/notes/7.png -------------------------------------------------------------------------------- /tests/fixtures/images/notes/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-stack/HEAD/tests/fixtures/images/notes/8.png -------------------------------------------------------------------------------- /tests/fixtures/images/notes/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-stack/HEAD/tests/fixtures/images/notes/9.png -------------------------------------------------------------------------------- /tests/fixtures/images/user/kody.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-stack/HEAD/tests/fixtures/images/user/kody.png -------------------------------------------------------------------------------- /types/icon-name.d.ts: -------------------------------------------------------------------------------- 1 | // This file is a fallback until you run npm run build:icons 2 | 3 | export type IconName = string 4 | -------------------------------------------------------------------------------- /app/assets/favicons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-stack/HEAD/app/assets/favicons/apple-touch-icon.png -------------------------------------------------------------------------------- /app/routes/_marketing/+logos/stars.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-stack/HEAD/app/routes/_marketing/+logos/stars.jpg -------------------------------------------------------------------------------- /public/favicons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-stack/HEAD/public/favicons/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/favicons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-stack/HEAD/public/favicons/android-chrome-512x512.png -------------------------------------------------------------------------------- /tests/fixtures/images/kody-notes/mountain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-stack/HEAD/tests/fixtures/images/kody-notes/mountain.png -------------------------------------------------------------------------------- /tests/fixtures/images/kody-notes/cute-koala.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-stack/HEAD/tests/fixtures/images/kody-notes/cute-koala.png -------------------------------------------------------------------------------- /app/routes/_marketing/+logos/testing-library.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-stack/HEAD/app/routes/_marketing/+logos/testing-library.png -------------------------------------------------------------------------------- /tests/fixtures/images/kody-notes/koala-coder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-stack/HEAD/tests/fixtures/images/kody-notes/koala-coder.png -------------------------------------------------------------------------------- /tests/fixtures/images/kody-notes/koala-cuddle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-stack/HEAD/tests/fixtures/images/kody-notes/koala-cuddle.png -------------------------------------------------------------------------------- /tests/fixtures/images/kody-notes/koala-eating.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-stack/HEAD/tests/fixtures/images/kody-notes/koala-eating.png -------------------------------------------------------------------------------- /tests/fixtures/images/kody-notes/koala-mentor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-stack/HEAD/tests/fixtures/images/kody-notes/koala-mentor.png -------------------------------------------------------------------------------- /tests/fixtures/images/kody-notes/koala-soccer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-stack/HEAD/tests/fixtures/images/kody-notes/koala-soccer.png -------------------------------------------------------------------------------- /app/utils/providers/constants.ts: -------------------------------------------------------------------------------- 1 | export const MOCK_CODE_GITHUB = 'MOCK_CODE_GITHUB_KODY' 2 | 3 | export const MOCK_CODE_GITHUB_HEADER = 'x-mock-code-github' 4 | -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (e.g., Git) 3 | provider = "sqlite" -------------------------------------------------------------------------------- /tests/fixtures/images/user/README.md: -------------------------------------------------------------------------------- 1 | # User Images 2 | 3 | This is used when creating users with images. If you don't do that, feel free to 4 | delete this directory. 5 | -------------------------------------------------------------------------------- /other/Dockerfile.dockerignore: -------------------------------------------------------------------------------- 1 | # This file is moved to the root directory before building the image 2 | 3 | /node_modules 4 | *.log 5 | .DS_Store 6 | .env 7 | /.cache 8 | /public/build 9 | /build 10 | -------------------------------------------------------------------------------- /app/components/floating-toolbar.tsx: -------------------------------------------------------------------------------- 1 | export const floatingToolbarClassName = 2 | 'absolute bottom-3 inset-x-3 flex items-center gap-2 rounded-lg bg-muted/80 p-4 pl-5 shadow-xl shadow-accent backdrop-blur-xs md:gap-4 md:pl-7 justify-end' 3 | -------------------------------------------------------------------------------- /app/utils/totp.server.ts: -------------------------------------------------------------------------------- 1 | // @epic-web/totp should be used server-side only. It imports `Crypto` which results in Remix 2 | // including a big polyfill. So we put the import in a `.server.ts` file to avoid that 3 | export * from '@epic-web/totp' 4 | -------------------------------------------------------------------------------- /tests/mocks/pwned-passwords.ts: -------------------------------------------------------------------------------- 1 | import { http, HttpResponse } from 'msw' 2 | 3 | export const handlers = [ 4 | http.get('https://api.pwnedpasswords.com/range/:prefix', () => { 5 | return new HttpResponse('', { status: 200 }) 6 | }), 7 | ] 8 | -------------------------------------------------------------------------------- /app/utils/nonce-provider.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | export const NonceContext = React.createContext('') 4 | export const NonceProvider = NonceContext.Provider 5 | export const useNonce = () => React.useContext(NonceContext) 6 | -------------------------------------------------------------------------------- /docs/decisions/000-template.md: -------------------------------------------------------------------------------- 1 | # Title 2 | 3 | Date: YYYY-MM-DD 4 | 5 | Status: proposed | rejected | accepted | deprecated | … | superseded by 6 | [0005](0005-example.md) 7 | 8 | ## Context 9 | 10 | ## Decision 11 | 12 | ## Consequences 13 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | /build 4 | /public/build 5 | .env 6 | 7 | /test-results/ 8 | /playwright-report/ 9 | /playwright/.cache/ 10 | /tests/fixtures/email/*.json 11 | /coverage 12 | /prisma/migrations 13 | 14 | package-lock.json 15 | -------------------------------------------------------------------------------- /other/sly/sly.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://sly-cli.fly.dev/registry/config.json", 3 | "libraries": [ 4 | { 5 | "name": "@radix-ui/icons", 6 | "directory": "./other/svg-icons", 7 | "transformers": ["transform-icon.ts"] 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /types/deps.d.ts: -------------------------------------------------------------------------------- 1 | // This module should contain type definitions for modules which do not have 2 | // their own type definitions and are not available on DefinitelyTyped. 3 | 4 | // declare module 'some-untyped-pkg' { 5 | // export function foo(): void; 6 | // } 7 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "bradlc.vscode-tailwindcss", 4 | "dbaeumer.vscode-eslint", 5 | "esbenp.prettier-vscode", 6 | "prisma.prisma", 7 | "qwtel.sqlite-viewer", 8 | "yoavbls.pretty-ts-errors", 9 | "github.vscode-github-actions" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /remix.init/index.js: -------------------------------------------------------------------------------- 1 | module.exports = async (...args) => { 2 | const { default: main } = await import('./index.mjs') 3 | await main(...args).catch((err) => { 4 | console.error('Oh no! Something went wrong initializing your epic app:') 5 | console.error(err) 6 | process.exit(1) 7 | }) 8 | } 9 | -------------------------------------------------------------------------------- /remix.init/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "remix.init", 3 | "private": true, 4 | "main": "index.js", 5 | "dependencies": { 6 | "@iarna/toml": "^2.2.5", 7 | "execa": "^7.1.1", 8 | "inquirer": "^9.2.6", 9 | "open": "^9.1.0", 10 | "parse-github-url": "^1.0.2" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.preferences.autoImportFileExcludePatterns": [ 3 | "@remix-run/server-runtime", 4 | "express", 5 | "@radix-ui/**", 6 | "@react-email/**", 7 | "node:stream/consumers", 8 | "node:test", 9 | "node:console" 10 | ], 11 | "workbench.editorAssociations": { 12 | "*.db": "sqlite-viewer.view" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/routes/_auth/logout.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from 'react-router' 2 | import { logout } from '#app/utils/auth.server.ts' 3 | import { type Route } from './+types/logout.ts' 4 | 5 | export async function loader() { 6 | return redirect('/') 7 | } 8 | 9 | export async function action({ request }: Route.ActionArgs) { 10 | return logout({ request }) 11 | } 12 | -------------------------------------------------------------------------------- /docs/decisions/README.md: -------------------------------------------------------------------------------- 1 | # Decisions 2 | 3 | This directory contains all the decisions we've made for this starter template 4 | and serves as a record for whenever we wonder why certain decisions were made. 5 | 6 | Decisions in here are never final. But these documents should serve as a good 7 | way for someone to come up to speed on why certain decisions were made. 8 | -------------------------------------------------------------------------------- /tests/e2e/error-boundary.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '#tests/playwright-utils.ts' 2 | 3 | test('Test root error boundary caught', async ({ page, navigate }) => { 4 | const pageUrl = '/does-not-exist' 5 | const res = await navigate(pageUrl as any) 6 | 7 | expect(res?.status()).toBe(404) 8 | await expect(page.getByText(/We can't find this page/i)).toBeVisible() 9 | }) 10 | -------------------------------------------------------------------------------- /app/routes/_seo/robots[.]txt.ts: -------------------------------------------------------------------------------- 1 | import { generateRobotsTxt } from '@nasa-gcn/remix-seo' 2 | import { getDomainUrl } from '#app/utils/misc.tsx' 3 | import { type Route } from './+types/robots[.]txt.ts' 4 | 5 | export function loader({ request }: Route.LoaderArgs) { 6 | return generateRobotsTxt([ 7 | { type: 'sitemap', value: `${getDomainUrl(request)}/sitemap.xml` }, 8 | ]) 9 | } 10 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import { default as defaultConfig } from '@epic-web/config/eslint' 2 | 3 | /** @type {import("eslint").Linter.Config} */ 4 | export default [ 5 | ...defaultConfig, 6 | // add custom config objects here: 7 | { 8 | files: ['**/tests/**/*.ts'], 9 | rules: { 'react-hooks/rules-of-hooks': 'off' }, 10 | }, 11 | { 12 | ignores: ['.react-router/*'], 13 | }, 14 | ] 15 | -------------------------------------------------------------------------------- /app/components/ui/README.md: -------------------------------------------------------------------------------- 1 | # shadcn/ui 2 | 3 | Some components in this directory are downloaded via the 4 | [shadcn/ui](https://ui.shadcn.com) [CLI](https://ui.shadcn.com/docs/cli). Feel 5 | free to customize them to your needs. It's important to know that shadcn/ui is 6 | not a library of components you install, but instead it's a registry of prebuilt 7 | components which you can download and customize. 8 | -------------------------------------------------------------------------------- /app/entry.client.tsx: -------------------------------------------------------------------------------- 1 | import { startTransition } from 'react' 2 | import { hydrateRoot } from 'react-dom/client' 3 | import { HydratedRouter } from 'react-router/dom' 4 | 5 | if (ENV.MODE === 'production' && ENV.SENTRY_DSN) { 6 | void import('./utils/monitoring.client.tsx').then(({ init }) => init()) 7 | } 8 | 9 | startTransition(() => { 10 | hydrateRoot(document, ) 11 | }) 12 | -------------------------------------------------------------------------------- /tests/mocks/README.md: -------------------------------------------------------------------------------- 1 | # Mocks 2 | 3 | Use this to mock any third party HTTP resources that you don't have running 4 | locally and want to have mocked for local development as well as tests. 5 | 6 | Learn more about how to use this at [mswjs.io](https://mswjs.io/) 7 | 8 | For an extensive example, see the 9 | [source code for kentcdodds.com](https://github.com/kentcdodds/kentcdodds.com/blob/main/mocks/index.ts) 10 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Test Plan 4 | 5 | 6 | 7 | ## Checklist 8 | 9 | - [ ] Tests updated 10 | - [ ] Docs updated 11 | 12 | ## Screenshots 13 | 14 | 16 | -------------------------------------------------------------------------------- /app/routes/users/$username/notes/new.tsx: -------------------------------------------------------------------------------- 1 | import { requireUserId } from '#app/utils/auth.server.ts' 2 | import { NoteEditor } from './+shared/note-editor.tsx' 3 | import { type Route } from './+types/new.ts' 4 | 5 | export { action } from './+shared/note-editor.server.tsx' 6 | 7 | export async function loader({ request }: Route.LoaderArgs) { 8 | await requireUserId(request) 9 | return {} 10 | } 11 | 12 | export default NoteEditor 13 | -------------------------------------------------------------------------------- /app/routes/_marketing/+logos/shadcn-ui.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "", 8 | "css": "app/styles/tailwind.css", 9 | "baseColor": "slate", 10 | "cssVariables": true 11 | }, 12 | "aliases": { 13 | "components": "#app/components", 14 | "utils": "#app/utils/misc", 15 | "ui": "#app/components/ui", 16 | "lib": "#app/utils" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/routes/_marketing/+logos/radix.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/components/toaster.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | import { toast as showToast } from 'sonner' 3 | import { type Toast } from '#app/utils/toast.server.ts' 4 | 5 | export function useToast(toast?: Toast | null) { 6 | useEffect(() => { 7 | if (toast) { 8 | setTimeout(() => { 9 | showToast[toast.type](toast.title, { 10 | id: toast.id, 11 | description: toast.description, 12 | }) 13 | }, 0) 14 | } 15 | }, [toast]) 16 | } 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["**/*.ts", "**/*.tsx", ".react-router/types/**/*"], 3 | "extends": ["@epic-web/config/typescript"], 4 | "compilerOptions": { 5 | // TODO: Probably should move this into epic-web/config 6 | "types": ["@react-router/node", "vite/client"], 7 | "rootDirs": [".", "./.react-router/types"], 8 | "paths": { 9 | "@/icon-name": [ 10 | "./app/components/ui/icons/types.ts", 11 | "./types/icon-name.d.ts" 12 | ] 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/utils/verification.server.ts: -------------------------------------------------------------------------------- 1 | import { createCookieSessionStorage } from 'react-router' 2 | 3 | export const verifySessionStorage = createCookieSessionStorage({ 4 | cookie: { 5 | name: 'en_verification', 6 | sameSite: 'lax', // CSRF protection is advised if changing to 'none' 7 | path: '/', 8 | httpOnly: true, 9 | maxAge: 60 * 10, // 10 minutes 10 | secrets: process.env.SESSION_SECRET.split(','), 11 | secure: process.env.NODE_ENV === 'production', 12 | }, 13 | }) 14 | -------------------------------------------------------------------------------- /other/svg-icons/README.md: -------------------------------------------------------------------------------- 1 | # Icons 2 | 3 | These icons were downloaded from https://icons.radix-ui.com/ which is licensed 4 | under MIT: https://github.com/radix-ui/icons/blob/master/LICENSE 5 | 6 | It's important that you only add icons to this directory that the application 7 | actually needs as the `vite-plugin-icons-spritesheet` plugin is configured to 8 | generate a spritesheet with icons from this directory. The plugin re-runs on 9 | every edit/delete/add to this directory. 10 | -------------------------------------------------------------------------------- /public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Epic Notes", 3 | "short_name": "Epic Notes", 4 | "start_url": "/", 5 | "icons": [ 6 | { 7 | "src": "/favicons/android-chrome-192x192.png", 8 | "sizes": "192x192", 9 | "type": "image/png" 10 | }, 11 | { 12 | "src": "/favicons/android-chrome-512x512.png", 13 | "sizes": "512x512", 14 | "type": "image/png" 15 | } 16 | ], 17 | "theme_color": "#A9ADC1", 18 | "background_color": "#1f2028", 19 | "display": "standalone" 20 | } 21 | -------------------------------------------------------------------------------- /prisma/sql/searchUsers.sql: -------------------------------------------------------------------------------- 1 | -- @param {String} $1:like 2 | SELECT 3 | "User".id, 4 | "User".username, 5 | "User".name, 6 | "UserImage".id AS imageId, 7 | "UserImage".objectKey AS imageObjectKey 8 | FROM "User" 9 | LEFT JOIN "UserImage" ON "User".id = "UserImage".userId 10 | WHERE "User".username LIKE :like 11 | OR "User".name LIKE :like 12 | ORDER BY ( 13 | SELECT "Note".updatedAt 14 | FROM "Note" 15 | WHERE "Note".ownerId = "User".id 16 | ORDER BY "Note".updatedAt DESC 17 | LIMIT 1 18 | ) DESC 19 | LIMIT 50 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_store 3 | 4 | /build 5 | .env 6 | .cache 7 | 8 | /prisma/data.db 9 | /prisma/data.db-journal 10 | /tests/prisma 11 | 12 | /test-results/ 13 | /playwright-report/ 14 | /playwright/.cache/ 15 | /tests/fixtures/email/ 16 | /tests/fixtures/uploaded/ 17 | /tests/fixtures/openimg/ 18 | /coverage 19 | 20 | /other/cache.db 21 | 22 | # Easy way to create temporary files/folders that won't accidentally be added to git 23 | *.local.* 24 | 25 | # generated files 26 | /app/components/ui/icons 27 | .react-router/ 28 | -------------------------------------------------------------------------------- /other/sly/transform-icon.ts: -------------------------------------------------------------------------------- 1 | import { type Meta } from '@sly-cli/sly' 2 | 3 | /** 4 | * @type {import('@sly-cli/sly/dist').Transformer} 5 | */ 6 | export default function transformIcon(input: string, meta: Meta) { 7 | input = prependLicenseInfo(input, meta) 8 | 9 | return input 10 | } 11 | 12 | function prependLicenseInfo(input: string, meta: Meta): string { 13 | return [ 14 | ``, 15 | ``, 16 | ``, 17 | input, 18 | ].join('\n') 19 | } 20 | -------------------------------------------------------------------------------- /app/assets/favicons/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /remix.init/gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_store 3 | 4 | /build 5 | .env 6 | .cache 7 | 8 | /prisma/data.db 9 | /prisma/data.db-journal 10 | /tests/prisma 11 | 12 | /test-results/ 13 | /playwright-report/ 14 | /playwright/.cache/ 15 | /tests/fixtures/email/ 16 | /tests/fixtures/uploaded/ 17 | /tests/fixtures/openimg/ 18 | /coverage 19 | 20 | /other/cache.db 21 | 22 | # Easy way to create temporary files/folders that won't accidentally be added to git 23 | *.local.* 24 | 25 | # generated files 26 | /app/components/ui/icons 27 | .react-router/ 28 | -------------------------------------------------------------------------------- /app/utils/honeypot.server.ts: -------------------------------------------------------------------------------- 1 | import { Honeypot, SpamError } from 'remix-utils/honeypot/server' 2 | 3 | export const honeypot = new Honeypot({ 4 | validFromFieldName: process.env.NODE_ENV === 'test' ? null : undefined, 5 | encryptionSeed: process.env.HONEYPOT_SECRET, 6 | }) 7 | 8 | export async function checkHoneypot(formData: FormData) { 9 | try { 10 | await honeypot.check(formData) 11 | } catch (error) { 12 | if (error instanceof SpamError) { 13 | throw new Response('Form not submitted properly', { status: 400 }) 14 | } 15 | throw error 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /other/README.md: -------------------------------------------------------------------------------- 1 | # Other 2 | 3 | The "other" directory is where we put stuff that doesn't really have a place, 4 | but we don't want in the root of the project. In fact, we want to move as much 5 | stuff here from the root as possible. The only things that should stay in the 6 | root directory are those things that have to stay in the root for most editor 7 | and other tool integrations (like most configuration files sadly). Maybe one day 8 | we can convince tools to adopt a new `.config` directory in the future. Until 9 | then, we've got this `./other` directory to keep things cleaner. 10 | -------------------------------------------------------------------------------- /public/favicons/README.md: -------------------------------------------------------------------------------- 1 | # Favicon 2 | 3 | This directory has the icons used for android devices. In some cases, we cannot 4 | reliably detect light/dark mode preference. Hence these icons should not have a 5 | transparent background. These icons are referenced in the `site.webmanifest` 6 | file. 7 | 8 | The icons used by modern browsers and Apple devices are in `app/assets/favicons` 9 | as they can be imported with a fingerprint to bust the browser cache. 10 | 11 | Note, there's also a `favicon.ico` in the root of `/public` which some older 12 | browsers will request automatically. This is a fallback for those browsers. 13 | -------------------------------------------------------------------------------- /app/routes/_seo/sitemap[.]xml.ts: -------------------------------------------------------------------------------- 1 | import { generateSitemap } from '@nasa-gcn/remix-seo' 2 | import { getDomainUrl } from '#app/utils/misc.tsx' 3 | import { type Route } from './+types/sitemap[.]xml.ts' 4 | 5 | export async function loader({ request, context }: Route.LoaderArgs) { 6 | // TODO: This is typeerror is coming up since of the remix-run/server-runtime package. We might need to remove/update that one. 7 | // @ts-expect-error 8 | return generateSitemap(request, context.serverBuild.routes, { 9 | siteUrl: getDomainUrl(request), 10 | headers: { 11 | 'Cache-Control': `public, max-age=${60 * 5}`, 12 | }, 13 | }) 14 | } 15 | -------------------------------------------------------------------------------- /app/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | import * as LabelPrimitive from '@radix-ui/react-label' 2 | import { cva } from 'class-variance-authority' 3 | import * as React from 'react' 4 | 5 | import { cn } from '#app/utils/misc.tsx' 6 | 7 | const labelVariants = cva( 8 | 'text-sm leading-none font-medium peer-disabled:cursor-not-allowed peer-disabled:opacity-70', 9 | ) 10 | 11 | const Label = ({ 12 | className, 13 | ...props 14 | }: React.ComponentProps) => ( 15 | 20 | ) 21 | 22 | export { Label } 23 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config' 2 | import * as fs from 'node:fs' 3 | import sourceMapSupport from 'source-map-support' 4 | 5 | sourceMapSupport.install({ 6 | retrieveSourceMap: function (source) { 7 | // get source file without the `file://` prefix or `?t=...` suffix 8 | const match = source.match(/^file:\/\/(.*)\?t=[.\d]+$/) 9 | if (match) { 10 | return { 11 | url: source, 12 | map: fs.readFileSync(`${match[1]}.map`, 'utf8'), 13 | } 14 | } 15 | return null 16 | }, 17 | }) 18 | 19 | if (process.env.MOCKS === 'true') { 20 | await import('./tests/mocks/index.ts') 21 | } 22 | 23 | await import('./server/index.ts') 24 | -------------------------------------------------------------------------------- /app/routes.ts: -------------------------------------------------------------------------------- 1 | import { type RouteConfig } from '@react-router/dev/routes' 2 | import { autoRoutes } from 'react-router-auto-routes' 3 | 4 | export default autoRoutes({ 5 | ignoredRouteFiles: [ 6 | '.*', 7 | '**/*.css', 8 | '**/*.test.{js,jsx,ts,tsx}', 9 | '**/__*.*', 10 | // This is for server-side utilities you want to colocate 11 | // next to your routes without making an additional 12 | // directory. If you need a route that includes "server" or 13 | // "client" in the filename, use the escape brackets like: 14 | // my-route.[server].tsx 15 | '**/*.server.*', 16 | '**/*.client.*', 17 | ], 18 | }) satisfies RouteConfig 19 | -------------------------------------------------------------------------------- /app/utils/redirect-cookie.server.ts: -------------------------------------------------------------------------------- 1 | import * as cookie from 'cookie' 2 | 3 | const key = 'redirectTo' 4 | export const destroyRedirectToHeader = cookie.serialize(key, '', { maxAge: -1 }) 5 | 6 | export function getRedirectCookieHeader(redirectTo?: string) { 7 | return redirectTo && redirectTo !== '/' 8 | ? cookie.serialize(key, redirectTo, { maxAge: 60 * 10 }) 9 | : null 10 | } 11 | 12 | export function getRedirectCookieValue(request: Request) { 13 | const rawCookie = request.headers.get('cookie') 14 | const parsedCookies = rawCookie ? cookie.parse(rawCookie) : {} 15 | const redirectTo = parsedCookies[key] 16 | return redirectTo || null 17 | } 18 | -------------------------------------------------------------------------------- /app/routes/settings/profile/two-factor/_layout.tsx: -------------------------------------------------------------------------------- 1 | import { type SEOHandle } from '@nasa-gcn/remix-seo' 2 | import { Outlet } from 'react-router' 3 | import { Icon } from '#app/components/ui/icon.tsx' 4 | import { type VerificationTypes } from '#app/routes/_auth/verify.tsx' 5 | import { type BreadcrumbHandle } from '../../profile/_layout.tsx' 6 | 7 | export const handle: BreadcrumbHandle & SEOHandle = { 8 | breadcrumb: 2FA, 9 | getSitemapEntries: () => null, 10 | } 11 | 12 | export const twoFAVerificationType = '2fa' satisfies VerificationTypes 13 | 14 | export default function TwoFactorRoute() { 15 | return 16 | } 17 | -------------------------------------------------------------------------------- /app/utils/request-info.ts: -------------------------------------------------------------------------------- 1 | import { invariant } from '@epic-web/invariant' 2 | import { useRouteLoaderData } from 'react-router' 3 | import { type loader as rootLoader } from '#app/root.tsx' 4 | 5 | /** 6 | * @returns the request info from the root loader (throws an error if it does not exist) 7 | */ 8 | export function useRequestInfo() { 9 | const maybeRequestInfo = useOptionalRequestInfo() 10 | invariant(maybeRequestInfo, 'No requestInfo found in root loader') 11 | 12 | return maybeRequestInfo 13 | } 14 | 15 | export function useOptionalRequestInfo() { 16 | const data = useRouteLoaderData('root') 17 | 18 | return data?.requestInfo 19 | } 20 | -------------------------------------------------------------------------------- /server/app.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-duplicates */ 2 | import 'react-router' 3 | import { createRequestHandler } from '@react-router/express' 4 | import express from 'express' 5 | import { type ServerBuild } from 'react-router' 6 | 7 | declare module 'react-router' { 8 | interface AppLoadContext { 9 | serverBuild: ServerBuild 10 | } 11 | } 12 | 13 | export const app = express() 14 | 15 | app.use( 16 | createRequestHandler({ 17 | mode: process.env.NODE_ENV ?? 'development', 18 | build: () => import('virtual:react-router/server-build'), 19 | getLoadContext: async () => ({ 20 | serverBuild: await import('virtual:react-router/server-build'), 21 | }), 22 | }), 23 | ) 24 | -------------------------------------------------------------------------------- /other/svg-icons/laptop.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 11 | 12 | -------------------------------------------------------------------------------- /docs/decisions/029-remix-auth.md: -------------------------------------------------------------------------------- 1 | # Remix Auth 2 | 3 | Date: 2023-08-14 4 | 5 | Status: accepted 6 | 7 | ## Context 8 | 9 | At the start of Epic Stack, we were using 10 | [remix-auth-form](https://github.com/sergiodxa/remix-auth-form) for our 11 | username/password auth solution. This worked fine, but it really didn't give us 12 | any value over handling the auth song-and-dance ourselves. 13 | 14 | ## Decision 15 | 16 | Instead of relying on remix-auth for handling authenticating the user's login 17 | form submission, we'll manage it ourselves. 18 | 19 | ## Consequences 20 | 21 | This mostly allows us to remove some code. However, we're going to be keeping 22 | remix auth around for GitHub Auth 23 | -------------------------------------------------------------------------------- /app/utils/theme.server.ts: -------------------------------------------------------------------------------- 1 | import * as cookie from 'cookie' 2 | 3 | const cookieName = 'en_theme' 4 | export type Theme = 'light' | 'dark' 5 | 6 | export function getTheme(request: Request): Theme | null { 7 | const cookieHeader = request.headers.get('cookie') 8 | const parsed = cookieHeader ? cookie.parse(cookieHeader)[cookieName] : 'light' 9 | if (parsed === 'light' || parsed === 'dark') return parsed 10 | return null 11 | } 12 | 13 | export function setTheme(theme: Theme | 'system') { 14 | if (theme === 'system') { 15 | return cookie.serialize(cookieName, '', { path: '/', maxAge: -1 }) 16 | } else { 17 | return cookie.serialize(cookieName, theme, { path: '/', maxAge: 31536000 }) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /react-router.config.ts: -------------------------------------------------------------------------------- 1 | import { type Config } from '@react-router/dev/config' 2 | import { sentryOnBuildEnd } from '@sentry/react-router' 3 | 4 | const MODE = process.env.NODE_ENV 5 | 6 | export default { 7 | // Defaults to true. Set to false to enable SPA for all routes. 8 | ssr: true, 9 | 10 | routeDiscovery: { mode: 'initial' }, 11 | 12 | future: { 13 | unstable_optimizeDeps: true, 14 | }, 15 | 16 | buildEnd: async ({ viteConfig, reactRouterConfig, buildManifest }) => { 17 | if (MODE === 'production' && process.env.SENTRY_AUTH_TOKEN) { 18 | await sentryOnBuildEnd({ 19 | viteConfig, 20 | reactRouterConfig, 21 | buildManifest, 22 | }) 23 | } 24 | }, 25 | } satisfies Config 26 | -------------------------------------------------------------------------------- /app/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { cn } from '#app/utils/misc.tsx' 4 | 5 | const Textarea = ({ 6 | className, 7 | ...props 8 | }: React.ComponentProps<'textarea'>) => { 9 | return ( 10 |