├── .npmrc ├── app ├── routes │ ├── admin+ │ │ ├── cache_.sqlite.tsx │ │ ├── cache_.lru.$cacheKey.ts │ │ ├── cache_.sqlite.$cacheKey.ts │ │ └── cache_.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 │ │ │ ├── msw.svg │ │ │ └── vitest.svg │ ├── _auth+ │ │ ├── logout.tsx │ │ ├── onboarding.server.ts │ │ ├── onboarding_.$provider.server.ts │ │ ├── auth_.$provider.ts │ │ ├── reset-password.server.ts │ │ └── webauthn+ │ │ │ └── utils.server.ts │ ├── _seo+ │ │ ├── robots[.]txt.ts │ │ └── sitemap[.]xml.ts │ ├── users+ │ │ ├── $username_+ │ │ │ ├── notes.new.tsx │ │ │ ├── notes.index.tsx │ │ │ └── notes.$noteId_.edit.tsx │ │ ├── index.tsx │ │ └── $username.test.tsx │ ├── settings+ │ │ ├── profile.two-factor.tsx │ │ ├── profile.two-factor.disable.tsx │ │ ├── profile.tsx │ │ ├── profile.two-factor.index.tsx │ │ └── profile.change-email.server.tsx │ ├── me.tsx │ ├── mcp+ │ │ ├── index.ts │ │ └── mcp.server.ts │ ├── resources+ │ │ ├── healthcheck.tsx │ │ ├── download-user-data.tsx │ │ └── images.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 │ ├── litefs.server.ts │ ├── honeypot.server.ts │ ├── redirect-cookie.server.ts │ ├── request-info.ts │ ├── theme.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 │ ├── user.ts │ ├── env.server.ts │ ├── email.server.ts │ ├── misc.use-double-check.test.tsx │ ├── timing.server.ts │ └── headers.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 │ │ └── icon.tsx │ ├── toaster.tsx │ ├── spacer.tsx │ ├── error-boundary.tsx │ ├── search-bar.tsx │ ├── progress-bar.tsx │ └── user-dropdown.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 │ │ ├── notes │ │ ├── 0.png │ │ ├── 1.png │ │ ├── 2.png │ │ ├── 3.png │ │ ├── 4.png │ │ ├── 5.png │ │ ├── 6.png │ │ ├── 7.png │ │ ├── 8.png │ │ └── 9.png │ │ ├── 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 │ │ └── 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 │ └── tigris.ts ├── e2e │ ├── error-boundary.test.ts │ └── 2fa.test.ts ├── setup │ ├── db-setup.ts │ ├── global-setup.ts │ └── setup-test-env.ts ├── utils.ts └── db-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 │ ├── sun.svg │ ├── pencil-2.svg │ └── link-2.svg ├── README.md ├── build-server.ts ├── litefs.yml └── Dockerfile ├── .prettierignore ├── eslint.config.js ├── .github └── PULL_REQUEST_TEMPLATE.md ├── components.json ├── tsconfig.json ├── .gitignore ├── server ├── dev-server.js └── utils │ └── monitoring.ts ├── react-router.config.ts ├── index.js ├── playwright.config.ts ├── .env.example ├── fly.toml ├── .cursor └── rules │ └── avoid-use-effect.mdc └── vite.config.ts /.npmrc: -------------------------------------------------------------------------------- 1 | legacy-peer-deps=true 2 | registry=https://registry.npmjs.org/ 3 | -------------------------------------------------------------------------------- /app/routes/admin+/cache_.sqlite.tsx: -------------------------------------------------------------------------------- 1 | export { action } from './cache_.sqlite.server.ts' 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-mcp/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/img/user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-mcp/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-mcp/HEAD/tests/fixtures/github/ghost.jpg -------------------------------------------------------------------------------- /tests/fixtures/images/notes/0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-mcp/HEAD/tests/fixtures/images/notes/0.png -------------------------------------------------------------------------------- /tests/fixtures/images/notes/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-mcp/HEAD/tests/fixtures/images/notes/1.png -------------------------------------------------------------------------------- /tests/fixtures/images/notes/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-mcp/HEAD/tests/fixtures/images/notes/2.png -------------------------------------------------------------------------------- /tests/fixtures/images/notes/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-mcp/HEAD/tests/fixtures/images/notes/3.png -------------------------------------------------------------------------------- /tests/fixtures/images/notes/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-mcp/HEAD/tests/fixtures/images/notes/4.png -------------------------------------------------------------------------------- /tests/fixtures/images/notes/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-mcp/HEAD/tests/fixtures/images/notes/5.png -------------------------------------------------------------------------------- /tests/fixtures/images/notes/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-mcp/HEAD/tests/fixtures/images/notes/6.png -------------------------------------------------------------------------------- /tests/fixtures/images/notes/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-mcp/HEAD/tests/fixtures/images/notes/7.png -------------------------------------------------------------------------------- /tests/fixtures/images/notes/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-mcp/HEAD/tests/fixtures/images/notes/8.png -------------------------------------------------------------------------------- /tests/fixtures/images/notes/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-mcp/HEAD/tests/fixtures/images/notes/9.png -------------------------------------------------------------------------------- /tests/fixtures/images/user/0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-mcp/HEAD/tests/fixtures/images/user/0.jpg -------------------------------------------------------------------------------- /tests/fixtures/images/user/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-mcp/HEAD/tests/fixtures/images/user/1.jpg -------------------------------------------------------------------------------- /tests/fixtures/images/user/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-mcp/HEAD/tests/fixtures/images/user/2.jpg -------------------------------------------------------------------------------- /tests/fixtures/images/user/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-mcp/HEAD/tests/fixtures/images/user/3.jpg -------------------------------------------------------------------------------- /tests/fixtures/images/user/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-mcp/HEAD/tests/fixtures/images/user/4.jpg -------------------------------------------------------------------------------- /tests/fixtures/images/user/5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-mcp/HEAD/tests/fixtures/images/user/5.jpg -------------------------------------------------------------------------------- /tests/fixtures/images/user/6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-mcp/HEAD/tests/fixtures/images/user/6.jpg -------------------------------------------------------------------------------- /tests/fixtures/images/user/7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-mcp/HEAD/tests/fixtures/images/user/7.jpg -------------------------------------------------------------------------------- /tests/fixtures/images/user/8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-mcp/HEAD/tests/fixtures/images/user/8.jpg -------------------------------------------------------------------------------- /tests/fixtures/images/user/9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-mcp/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/user/kody.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-mcp/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/routes/_marketing+/logos/stars.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-mcp/HEAD/app/routes/_marketing+/logos/stars.jpg -------------------------------------------------------------------------------- /app/assets/favicons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-mcp/HEAD/app/assets/favicons/apple-touch-icon.png -------------------------------------------------------------------------------- /public/favicons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-mcp/HEAD/public/favicons/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/favicons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-mcp/HEAD/public/favicons/android-chrome-512x512.png -------------------------------------------------------------------------------- /tests/fixtures/images/kody-notes/mountain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-mcp/HEAD/tests/fixtures/images/kody-notes/mountain.png -------------------------------------------------------------------------------- /app/routes/_marketing+/logos/testing-library.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-mcp/HEAD/app/routes/_marketing+/logos/testing-library.png -------------------------------------------------------------------------------- /tests/fixtures/images/kody-notes/cute-koala.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-mcp/HEAD/tests/fixtures/images/kody-notes/cute-koala.png -------------------------------------------------------------------------------- /tests/fixtures/images/kody-notes/koala-coder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-mcp/HEAD/tests/fixtures/images/kody-notes/koala-coder.png -------------------------------------------------------------------------------- /tests/fixtures/images/kody-notes/koala-cuddle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-mcp/HEAD/tests/fixtures/images/kody-notes/koala-cuddle.png -------------------------------------------------------------------------------- /tests/fixtures/images/kody-notes/koala-eating.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-mcp/HEAD/tests/fixtures/images/kody-notes/koala-eating.png -------------------------------------------------------------------------------- /tests/fixtures/images/kody-notes/koala-mentor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-mcp/HEAD/tests/fixtures/images/kody-notes/koala-mentor.png -------------------------------------------------------------------------------- /tests/fixtures/images/kody-notes/koala-soccer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-mcp/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | /build 4 | /public/build 5 | /server-build 6 | .env 7 | 8 | /test-results/ 9 | /playwright-report/ 10 | /playwright/.cache/ 11 | /tests/fixtures/email/*.json 12 | /coverage 13 | /prisma/migrations 14 | 15 | package-lock.json 16 | -------------------------------------------------------------------------------- /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 | "postinstall": ["npm", "run", "build:icons"], 8 | "transformers": ["transform-icon.ts"] 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 }) => { 4 | const pageUrl = '/does-not-exist' 5 | const res = await page.goto(pageUrl) 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 { type Route } from './+types/notes.new.ts' 3 | import { NoteEditor } from './__note-editor.tsx' 4 | 5 | export { action } from './__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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/utils/litefs.server.ts: -------------------------------------------------------------------------------- 1 | // litefs-js should be used server-side only. It imports `fs` which results in Remix 2 | // including a big polyfill. So we put the import in a `.server.ts` file to avoid that 3 | // polyfill from being included. https://github.com/epicweb-dev/epic-stack/pull/331 4 | export { 5 | getInstanceInfo, 6 | getAllInstances, 7 | getInternalInstanceDomain, 8 | getInstanceInfoSync, 9 | } from 'litefs-js' 10 | export { ensurePrimary, ensureInstance } from 'litefs-js/remix.js' 11 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 there's no "tree-shaking" for sprites. If you wish to manually 8 | split up your SVG sprite into multiple files, you'll need to update the 9 | `build-icons.ts` script to do that. 10 | 11 | Run `npm run build:icons` to update the sprite. 12 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | "#app/*": ["./app/*"], 10 | "#tests/*": ["./tests/*"], 11 | "@/icon-name": [ 12 | "./app/components/ui/icons/name.d.ts", 13 | "./types/icon-name.d.ts" 14 | ] 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/assets/favicons/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_store 3 | 4 | /build 5 | /server-build 6 | .env 7 | .cache 8 | 9 | /prisma/data.db 10 | /prisma/data.db-journal 11 | /tests/prisma 12 | 13 | /test-results/ 14 | /playwright-report/ 15 | /playwright/.cache/ 16 | /tests/fixtures/email/ 17 | /tests/fixtures/uploaded/ 18 | /tests/fixtures/openimg/ 19 | /coverage 20 | 21 | /other/cache.db 22 | 23 | # Easy way to create temporary files/folders that won't accidentally be added to git 24 | *.local.* 25 | 26 | # generated files 27 | /app/components/ui/icons 28 | .react-router/ 29 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /server/dev-server.js: -------------------------------------------------------------------------------- 1 | import { execa } from 'execa' 2 | 3 | if (process.env.NODE_ENV === 'production') { 4 | await import('../server-build/index.js') 5 | } else { 6 | const command = 7 | 'tsx watch --clear-screen=false --ignore ".cache/**" --ignore "app/**" --ignore "vite.config.ts.timestamp-*" --ignore "build/**" --ignore "node_modules/**" --inspect ./index.js' 8 | execa(command, { 9 | stdio: ['ignore', 'inherit', 'inherit'], 10 | shell: true, 11 | env: { 12 | FORCE_COLOR: true, 13 | ...process.env, 14 | }, 15 | // https://github.com/sindresorhus/execa/issues/433 16 | windowsHide: false, 17 | }) 18 | } 19 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/routes/settings+/profile.two-factor.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.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/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 | -------------------------------------------------------------------------------- /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 | future: { 11 | unstable_optimizeDeps: true, 12 | }, 13 | 14 | buildEnd: async ({ viteConfig, reactRouterConfig, buildManifest }) => { 15 | if (MODE === 'production' && process.env.SENTRY_AUTH_TOKEN) { 16 | await sentryOnBuildEnd({ 17 | viteConfig, 18 | reactRouterConfig, 19 | buildManifest, 20 | }) 21 | } 22 | }, 23 | } satisfies Config 24 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /other/svg-icons/laptop.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 11 | 12 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 |