├── public ├── robots.txt ├── favicon.ico ├── img │ └── user.png ├── favicons │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── apple-touch-icon.png │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── mask-icon.svg │ ├── README.md │ └── favicon.svg ├── fonts │ └── nunito-sans │ │ ├── nunito-sans-v12-latin_latin-ext-200.woff │ │ ├── nunito-sans-v12-latin_latin-ext-300.woff │ │ ├── nunito-sans-v12-latin_latin-ext-600.woff │ │ ├── nunito-sans-v12-latin_latin-ext-700.woff │ │ ├── nunito-sans-v12-latin_latin-ext-800.woff │ │ ├── nunito-sans-v12-latin_latin-ext-900.woff │ │ ├── nunito-sans-v12-latin_latin-ext-200.woff2 │ │ ├── nunito-sans-v12-latin_latin-ext-300.woff2 │ │ ├── nunito-sans-v12-latin_latin-ext-600.woff2 │ │ ├── nunito-sans-v12-latin_latin-ext-700.woff2 │ │ ├── nunito-sans-v12-latin_latin-ext-800.woff2 │ │ ├── nunito-sans-v12-latin_latin-ext-900.woff2 │ │ ├── nunito-sans-v12-latin_latin-ext-italic.woff │ │ ├── nunito-sans-v12-latin_latin-ext-italic.woff2 │ │ ├── nunito-sans-v12-latin_latin-ext-regular.woff │ │ ├── nunito-sans-v12-latin_latin-ext-regular.woff2 │ │ ├── nunito-sans-v12-latin_latin-ext-200italic.woff │ │ ├── nunito-sans-v12-latin_latin-ext-200italic.woff2 │ │ ├── nunito-sans-v12-latin_latin-ext-300italic.woff │ │ ├── nunito-sans-v12-latin_latin-ext-300italic.woff2 │ │ ├── nunito-sans-v12-latin_latin-ext-600italic.woff │ │ ├── nunito-sans-v12-latin_latin-ext-600italic.woff2 │ │ ├── nunito-sans-v12-latin_latin-ext-700italic.woff │ │ ├── nunito-sans-v12-latin_latin-ext-700italic.woff2 │ │ ├── nunito-sans-v12-latin_latin-ext-800italic.woff │ │ ├── nunito-sans-v12-latin_latin-ext-800italic.woff2 │ │ ├── nunito-sans-v12-latin_latin-ext-900italic.woff │ │ └── nunito-sans-v12-latin_latin-ext-900italic.woff2 └── site.webmanifest ├── .npmrc ├── app ├── routes │ ├── resources+ │ │ ├── healthcheck.tsx │ │ ├── note-images.$imageId.tsx │ │ ├── user-images.$imageId.tsx │ │ └── download-user-data.tsx │ ├── _marketing+ │ │ ├── about.tsx │ │ ├── privacy.tsx │ │ ├── support.tsx │ │ ├── tos.tsx │ │ ├── logos │ │ │ ├── remix.png │ │ │ ├── stars.jpg │ │ │ ├── docker.png │ │ │ ├── testing-library.png │ │ │ ├── shadcn-ui.svg │ │ │ ├── radix.svg │ │ │ ├── github.svg │ │ │ ├── typescript.svg │ │ │ ├── eslint.svg │ │ │ ├── sentry.svg │ │ │ ├── msw.svg │ │ │ ├── vitest.svg │ │ │ ├── logos.ts │ │ │ └── tailwind.svg │ │ └── index.tsx │ ├── _auth+ │ │ ├── logout.tsx │ │ └── auth.$provider.ts │ ├── users+ │ │ ├── $username_+ │ │ │ ├── notes.new.tsx │ │ │ ├── notes.index.tsx │ │ │ ├── notes.$noteId_.edit.tsx │ │ │ └── notes.tsx │ │ └── $username.test.tsx │ ├── settings+ │ │ ├── profile.two-factor.tsx │ │ ├── profile.tsx │ │ ├── profile.two-factor.disable.tsx │ │ └── profile.two-factor.index.tsx │ ├── me.tsx │ ├── admin+ │ │ ├── cache_.lru.$cacheKey.ts │ │ ├── cache_.sqlite.$cacheKey.ts │ │ └── cache_.sqlite.tsx │ └── $.tsx ├── components │ ├── ui │ │ ├── icons │ │ │ ├── README.md │ │ │ └── name.d.ts │ │ ├── README.md │ │ ├── label.tsx │ │ ├── textarea.tsx │ │ ├── input.tsx │ │ ├── tooltip.tsx │ │ ├── checkbox.tsx │ │ ├── icon.tsx │ │ ├── status-button.tsx │ │ └── button.tsx │ ├── floating-toolbar.tsx │ ├── confetti.tsx │ ├── toaster.tsx │ ├── spinner.tsx │ ├── spacer.tsx │ ├── error-boundary.tsx │ └── search-bar.tsx ├── utils │ ├── nonce-provider.ts │ ├── monitoring.server.ts │ ├── litefs.server.ts │ ├── verification.server.ts │ ├── request-info.ts │ ├── providers │ │ ├── provider.ts │ │ ├── github.server.ts │ │ └── google.server.ts │ ├── redirect-cookie.server.ts │ ├── theme.server.ts │ ├── user.ts │ ├── misc.error-message.test.ts │ ├── monitoring.client.tsx │ ├── db.server.ts │ ├── connections.server.ts │ ├── user-validation.ts │ ├── session.server.ts │ ├── confetti.server.ts │ ├── connections.tsx │ ├── env.server.ts │ ├── toast.server.ts │ ├── email.server.ts │ ├── misc.use-double-check.test.tsx │ ├── permissions.ts │ └── timing.server.ts ├── entry.client.tsx ├── styles │ └── tailwind.css └── entry.server.tsx ├── types ├── reset.d.ts ├── remix.env.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 │ └── google │ │ └── google-avatar.jpg ├── e2e │ ├── error-boundary.test.ts │ └── 2fa.test.ts ├── mocks │ ├── README.md │ ├── resend.ts │ ├── index.ts │ ├── utils.ts │ └── google.ts ├── setup │ ├── global-setup.ts │ ├── setup-test-env.ts │ └── db-setup.ts ├── utils.ts └── db-utils.ts ├── postcss.config.js ├── prisma └── migrations │ └── migration_lock.toml ├── other ├── .dockerignore ├── setup-swap.js ├── sly │ ├── sly.json │ └── transform-icon.ts ├── svg-icons │ ├── README.md │ ├── laptop.svg │ ├── plus.svg │ ├── trash.svg │ ├── google-logo.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 │ ├── 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 │ └── moon.svg ├── README.md ├── sentry-create-release.js ├── patches │ └── remix-utils+6.6.0.patch ├── litefs.yml ├── build-server.ts └── Dockerfile ├── .prettierignore ├── components.json ├── .github └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── COMMUNITY.md ├── server └── dev-server.js ├── vitest.config.ts ├── remix.config.js ├── index.js ├── .env.example ├── .prettierrc.js ├── tailwind.config.ts ├── tsconfig.json ├── playwright.config.ts ├── fly.toml ├── README.md └── .eslintrc.cjs /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Allow: / 3 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | legacy-peer-deps=true 2 | registry=https://registry.npmjs.org/ 3 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-oidc/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/img/user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-oidc/HEAD/public/img/user.png -------------------------------------------------------------------------------- /app/routes/resources+/healthcheck.tsx: -------------------------------------------------------------------------------- 1 | export async function loader() { 2 | return new Response('OK') 3 | } 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /types/reset.d.ts: -------------------------------------------------------------------------------- 1 | // Do not add any other lines of code to this file! 2 | import '@total-typescript/ts-reset/dom' 3 | -------------------------------------------------------------------------------- /public/favicons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-oidc/HEAD/public/favicons/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-oidc/HEAD/public/favicons/favicon-32x32.png -------------------------------------------------------------------------------- /tests/fixtures/github/ghost.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-oidc/HEAD/tests/fixtures/github/ghost.jpg -------------------------------------------------------------------------------- /tests/fixtures/images/notes/0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-oidc/HEAD/tests/fixtures/images/notes/0.png -------------------------------------------------------------------------------- /tests/fixtures/images/notes/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-oidc/HEAD/tests/fixtures/images/notes/1.png -------------------------------------------------------------------------------- /tests/fixtures/images/notes/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-oidc/HEAD/tests/fixtures/images/notes/2.png -------------------------------------------------------------------------------- /tests/fixtures/images/notes/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-oidc/HEAD/tests/fixtures/images/notes/3.png -------------------------------------------------------------------------------- /tests/fixtures/images/notes/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-oidc/HEAD/tests/fixtures/images/notes/4.png -------------------------------------------------------------------------------- /tests/fixtures/images/notes/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-oidc/HEAD/tests/fixtures/images/notes/5.png -------------------------------------------------------------------------------- /tests/fixtures/images/notes/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-oidc/HEAD/tests/fixtures/images/notes/6.png -------------------------------------------------------------------------------- /tests/fixtures/images/notes/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-oidc/HEAD/tests/fixtures/images/notes/7.png -------------------------------------------------------------------------------- /tests/fixtures/images/notes/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-oidc/HEAD/tests/fixtures/images/notes/8.png -------------------------------------------------------------------------------- /tests/fixtures/images/notes/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-oidc/HEAD/tests/fixtures/images/notes/9.png -------------------------------------------------------------------------------- /tests/fixtures/images/user/0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-oidc/HEAD/tests/fixtures/images/user/0.jpg -------------------------------------------------------------------------------- /tests/fixtures/images/user/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-oidc/HEAD/tests/fixtures/images/user/1.jpg -------------------------------------------------------------------------------- /tests/fixtures/images/user/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-oidc/HEAD/tests/fixtures/images/user/2.jpg -------------------------------------------------------------------------------- /tests/fixtures/images/user/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-oidc/HEAD/tests/fixtures/images/user/3.jpg -------------------------------------------------------------------------------- /tests/fixtures/images/user/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-oidc/HEAD/tests/fixtures/images/user/4.jpg -------------------------------------------------------------------------------- /tests/fixtures/images/user/5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-oidc/HEAD/tests/fixtures/images/user/5.jpg -------------------------------------------------------------------------------- /tests/fixtures/images/user/6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-oidc/HEAD/tests/fixtures/images/user/6.jpg -------------------------------------------------------------------------------- /tests/fixtures/images/user/7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-oidc/HEAD/tests/fixtures/images/user/7.jpg -------------------------------------------------------------------------------- /tests/fixtures/images/user/8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-oidc/HEAD/tests/fixtures/images/user/8.jpg -------------------------------------------------------------------------------- /tests/fixtures/images/user/9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-oidc/HEAD/tests/fixtures/images/user/9.jpg -------------------------------------------------------------------------------- /types/remix.env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /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-oidc/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/remix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-oidc/HEAD/app/routes/_marketing+/logos/remix.png -------------------------------------------------------------------------------- /app/routes/_marketing+/logos/stars.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-oidc/HEAD/app/routes/_marketing+/logos/stars.jpg -------------------------------------------------------------------------------- /public/favicons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-oidc/HEAD/public/favicons/apple-touch-icon.png -------------------------------------------------------------------------------- /app/routes/_marketing+/logos/docker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-oidc/HEAD/app/routes/_marketing+/logos/docker.png -------------------------------------------------------------------------------- /tests/fixtures/google/google-avatar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-oidc/HEAD/tests/fixtures/google/google-avatar.jpg -------------------------------------------------------------------------------- /public/favicons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-oidc/HEAD/public/favicons/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/favicons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-oidc/HEAD/public/favicons/android-chrome-512x512.png -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | 'tailwindcss/nesting': {}, 4 | tailwindcss: {}, 5 | autoprefixer: {}, 6 | }, 7 | } 8 | -------------------------------------------------------------------------------- /tests/fixtures/images/kody-notes/mountain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-oidc/HEAD/tests/fixtures/images/kody-notes/mountain.png -------------------------------------------------------------------------------- /app/routes/_marketing+/logos/testing-library.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-oidc/HEAD/app/routes/_marketing+/logos/testing-library.png -------------------------------------------------------------------------------- /tests/fixtures/images/kody-notes/cute-koala.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-oidc/HEAD/tests/fixtures/images/kody-notes/cute-koala.png -------------------------------------------------------------------------------- /tests/fixtures/images/kody-notes/koala-coder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-oidc/HEAD/tests/fixtures/images/kody-notes/koala-coder.png -------------------------------------------------------------------------------- /tests/fixtures/images/kody-notes/koala-cuddle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-oidc/HEAD/tests/fixtures/images/kody-notes/koala-cuddle.png -------------------------------------------------------------------------------- /tests/fixtures/images/kody-notes/koala-eating.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-oidc/HEAD/tests/fixtures/images/kody-notes/koala-eating.png -------------------------------------------------------------------------------- /tests/fixtures/images/kody-notes/koala-mentor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-oidc/HEAD/tests/fixtures/images/kody-notes/koala-mentor.png -------------------------------------------------------------------------------- /tests/fixtures/images/kody-notes/koala-soccer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-oidc/HEAD/tests/fixtures/images/kody-notes/koala-soccer.png -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. 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 | -------------------------------------------------------------------------------- /public/fonts/nunito-sans/nunito-sans-v12-latin_latin-ext-200.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-oidc/HEAD/public/fonts/nunito-sans/nunito-sans-v12-latin_latin-ext-200.woff -------------------------------------------------------------------------------- /public/fonts/nunito-sans/nunito-sans-v12-latin_latin-ext-300.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-oidc/HEAD/public/fonts/nunito-sans/nunito-sans-v12-latin_latin-ext-300.woff -------------------------------------------------------------------------------- /public/fonts/nunito-sans/nunito-sans-v12-latin_latin-ext-600.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-oidc/HEAD/public/fonts/nunito-sans/nunito-sans-v12-latin_latin-ext-600.woff -------------------------------------------------------------------------------- /public/fonts/nunito-sans/nunito-sans-v12-latin_latin-ext-700.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-oidc/HEAD/public/fonts/nunito-sans/nunito-sans-v12-latin_latin-ext-700.woff -------------------------------------------------------------------------------- /public/fonts/nunito-sans/nunito-sans-v12-latin_latin-ext-800.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-oidc/HEAD/public/fonts/nunito-sans/nunito-sans-v12-latin_latin-ext-800.woff -------------------------------------------------------------------------------- /public/fonts/nunito-sans/nunito-sans-v12-latin_latin-ext-900.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-oidc/HEAD/public/fonts/nunito-sans/nunito-sans-v12-latin_latin-ext-900.woff -------------------------------------------------------------------------------- /app/components/ui/icons/README.md: -------------------------------------------------------------------------------- 1 | # Icons 2 | 3 | This directory contains SVG icons that are used by the app. 4 | 5 | Everything in this directory is generated by `npm run build:icons`. 6 | -------------------------------------------------------------------------------- /public/fonts/nunito-sans/nunito-sans-v12-latin_latin-ext-200.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-oidc/HEAD/public/fonts/nunito-sans/nunito-sans-v12-latin_latin-ext-200.woff2 -------------------------------------------------------------------------------- /public/fonts/nunito-sans/nunito-sans-v12-latin_latin-ext-300.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-oidc/HEAD/public/fonts/nunito-sans/nunito-sans-v12-latin_latin-ext-300.woff2 -------------------------------------------------------------------------------- /public/fonts/nunito-sans/nunito-sans-v12-latin_latin-ext-600.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-oidc/HEAD/public/fonts/nunito-sans/nunito-sans-v12-latin_latin-ext-600.woff2 -------------------------------------------------------------------------------- /public/fonts/nunito-sans/nunito-sans-v12-latin_latin-ext-700.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-oidc/HEAD/public/fonts/nunito-sans/nunito-sans-v12-latin_latin-ext-700.woff2 -------------------------------------------------------------------------------- /public/fonts/nunito-sans/nunito-sans-v12-latin_latin-ext-800.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-oidc/HEAD/public/fonts/nunito-sans/nunito-sans-v12-latin_latin-ext-800.woff2 -------------------------------------------------------------------------------- /public/fonts/nunito-sans/nunito-sans-v12-latin_latin-ext-900.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-oidc/HEAD/public/fonts/nunito-sans/nunito-sans-v12-latin_latin-ext-900.woff2 -------------------------------------------------------------------------------- /public/fonts/nunito-sans/nunito-sans-v12-latin_latin-ext-italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-oidc/HEAD/public/fonts/nunito-sans/nunito-sans-v12-latin_latin-ext-italic.woff -------------------------------------------------------------------------------- /public/fonts/nunito-sans/nunito-sans-v12-latin_latin-ext-italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-oidc/HEAD/public/fonts/nunito-sans/nunito-sans-v12-latin_latin-ext-italic.woff2 -------------------------------------------------------------------------------- /public/fonts/nunito-sans/nunito-sans-v12-latin_latin-ext-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-oidc/HEAD/public/fonts/nunito-sans/nunito-sans-v12-latin_latin-ext-regular.woff -------------------------------------------------------------------------------- /public/fonts/nunito-sans/nunito-sans-v12-latin_latin-ext-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-oidc/HEAD/public/fonts/nunito-sans/nunito-sans-v12-latin_latin-ext-regular.woff2 -------------------------------------------------------------------------------- /other/.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 | -------------------------------------------------------------------------------- /public/fonts/nunito-sans/nunito-sans-v12-latin_latin-ext-200italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-oidc/HEAD/public/fonts/nunito-sans/nunito-sans-v12-latin_latin-ext-200italic.woff -------------------------------------------------------------------------------- /public/fonts/nunito-sans/nunito-sans-v12-latin_latin-ext-200italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-oidc/HEAD/public/fonts/nunito-sans/nunito-sans-v12-latin_latin-ext-200italic.woff2 -------------------------------------------------------------------------------- /public/fonts/nunito-sans/nunito-sans-v12-latin_latin-ext-300italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-oidc/HEAD/public/fonts/nunito-sans/nunito-sans-v12-latin_latin-ext-300italic.woff -------------------------------------------------------------------------------- /public/fonts/nunito-sans/nunito-sans-v12-latin_latin-ext-300italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-oidc/HEAD/public/fonts/nunito-sans/nunito-sans-v12-latin_latin-ext-300italic.woff2 -------------------------------------------------------------------------------- /public/fonts/nunito-sans/nunito-sans-v12-latin_latin-ext-600italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-oidc/HEAD/public/fonts/nunito-sans/nunito-sans-v12-latin_latin-ext-600italic.woff -------------------------------------------------------------------------------- /public/fonts/nunito-sans/nunito-sans-v12-latin_latin-ext-600italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-oidc/HEAD/public/fonts/nunito-sans/nunito-sans-v12-latin_latin-ext-600italic.woff2 -------------------------------------------------------------------------------- /public/fonts/nunito-sans/nunito-sans-v12-latin_latin-ext-700italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-oidc/HEAD/public/fonts/nunito-sans/nunito-sans-v12-latin_latin-ext-700italic.woff -------------------------------------------------------------------------------- /public/fonts/nunito-sans/nunito-sans-v12-latin_latin-ext-700italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-oidc/HEAD/public/fonts/nunito-sans/nunito-sans-v12-latin_latin-ext-700italic.woff2 -------------------------------------------------------------------------------- /public/fonts/nunito-sans/nunito-sans-v12-latin_latin-ext-800italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-oidc/HEAD/public/fonts/nunito-sans/nunito-sans-v12-latin_latin-ext-800italic.woff -------------------------------------------------------------------------------- /public/fonts/nunito-sans/nunito-sans-v12-latin_latin-ext-800italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-oidc/HEAD/public/fonts/nunito-sans/nunito-sans-v12-latin_latin-ext-800italic.woff2 -------------------------------------------------------------------------------- /public/fonts/nunito-sans/nunito-sans-v12-latin_latin-ext-900italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-oidc/HEAD/public/fonts/nunito-sans/nunito-sans-v12-latin_latin-ext-900italic.woff -------------------------------------------------------------------------------- /public/fonts/nunito-sans/nunito-sans-v12-latin_latin-ext-900italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/epic-oidc/HEAD/public/fonts/nunito-sans/nunito-sans-v12-latin_latin-ext-900italic.woff2 -------------------------------------------------------------------------------- /app/components/floating-toolbar.tsx: -------------------------------------------------------------------------------- 1 | export const floatingToolbarClassName = 2 | 'absolute bottom-3 left-3 right-3 flex items-center gap-2 rounded-lg bg-muted/80 p-4 pl-5 shadow-xl shadow-accent backdrop-blur-sm md:gap-4 md:pl-7 justify-end' 3 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 'tailwindcss-animate' { 5 | declare const _default: { 6 | handler: () => void 7 | } 8 | export = _default 9 | } 10 | -------------------------------------------------------------------------------- /app/utils/monitoring.server.ts: -------------------------------------------------------------------------------- 1 | import * as Sentry from '@sentry/remix' 2 | 3 | export function init() { 4 | Sentry.init({ 5 | dsn: ENV.SENTRY_DSN, 6 | tracesSampleRate: 1, 7 | // TODO: Make this work with Prisma 8 | // integrations: [new Sentry.Integrations.Prisma({ client: prisma })], 9 | }) 10 | } 11 | -------------------------------------------------------------------------------- /app/routes/_auth+/logout.tsx: -------------------------------------------------------------------------------- 1 | import { redirect, type DataFunctionArgs } from '@remix-run/node' 2 | import { logout } from '#app/utils/auth.server.ts' 3 | 4 | export async function loader() { 5 | return redirect('/') 6 | } 7 | 8 | export async function action({ request }: DataFunctionArgs) { 9 | return logout({ request }) 10 | } 11 | -------------------------------------------------------------------------------- /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 | await page.goto('/does-not-exist') 5 | 6 | await expect(page.getByText(/We can't find this page/i)).toBeVisible() 7 | // TODO: figure out how to assert the 404 status code 8 | }) 9 | -------------------------------------------------------------------------------- /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 * from 'litefs-js' 5 | export * from 'litefs-js/remix.js' 6 | -------------------------------------------------------------------------------- /public/favicons/mask-icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/entry.client.tsx: -------------------------------------------------------------------------------- 1 | import { RemixBrowser } from '@remix-run/react' 2 | import { startTransition } from 'react' 3 | import { hydrateRoot } from 'react-dom/client' 4 | 5 | if (ENV.MODE === 'production' && ENV.SENTRY_DSN) { 6 | import('./utils/monitoring.client.tsx').then(({ init }) => init()) 7 | } 8 | 9 | startTransition(() => { 10 | hydrateRoot(document, ) 11 | }) 12 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": false, 5 | "tailwind": { 6 | "config": "tailwind.config.ts", 7 | "css": "app/styles/global.css", 8 | "baseColor": "slate", 9 | "cssVariables": true 10 | }, 11 | "aliases": { 12 | "components": "#app/components", 13 | "utils": "#app/utils/misc.tsx" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_store 3 | 4 | /build 5 | /public/build 6 | /server-build 7 | .env 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 | /coverage 18 | 19 | /other/cache.db 20 | 21 | # Easy way to create temporary files/folders that won't accidentally be added to git 22 | *.local.* 23 | -------------------------------------------------------------------------------- /COMMUNITY.md: -------------------------------------------------------------------------------- 1 | # Community 2 | 3 | Here you can find useful learning resources and tools built and maintained by 4 | the community, such as libraries, examples, articles, and videos. 5 | 6 | ## Learning resources 7 | 8 | ### Videos 9 | 10 | - **Dark Mode Toggling using Client-preference cookies** by 11 | [@rajeshdavidbabu](https://github.com/rajeshdavidbabu) - Youtube 12 | [link](https://www.youtube.com/watch?v=UND-kib_iw4) 13 | -------------------------------------------------------------------------------- /app/utils/verification.server.ts: -------------------------------------------------------------------------------- 1 | import { createCookieSessionStorage } from '@remix-run/node' 2 | 3 | export const verifySessionStorage = createCookieSessionStorage({ 4 | cookie: { 5 | name: 'en_verification', 6 | sameSite: 'lax', 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 | -------------------------------------------------------------------------------- /app/routes/_marketing+/logos/shadcn-ui.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/routes/_marketing+/logos/radix.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /other/setup-swap.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { writeFile } from 'node:fs/promises' 4 | import { $ } from 'execa' 5 | 6 | console.log('setting up swapfile...') 7 | await $`fallocate -l 512M /swapfile` 8 | await $`chmod 0600 /swapfile` 9 | await $`mkswap /swapfile` 10 | await writeFile('/proc/sys/vm/swappiness', '10') 11 | await $`swapon /swapfile` 12 | await writeFile('/proc/sys/vm/overcommit_memory', '1') 13 | console.log('swapfile setup complete') 14 | -------------------------------------------------------------------------------- /app/routes/users+/$username_+/notes.new.tsx: -------------------------------------------------------------------------------- 1 | import { json } from '@remix-run/router' 2 | import { type DataFunctionArgs } from '@remix-run/server-runtime' 3 | import { requireUserId } from '#app/utils/auth.server.ts' 4 | import { NoteEditor, action } from './__note-editor.tsx' 5 | 6 | export async function loader({ request }: DataFunctionArgs) { 7 | await requireUserId(request) 8 | return json({}) 9 | } 10 | 11 | export { action } 12 | export default NoteEditor 13 | -------------------------------------------------------------------------------- /app/routes/settings+/profile.two-factor.tsx: -------------------------------------------------------------------------------- 1 | import { Outlet } from '@remix-run/react' 2 | import { Icon } from '#app/components/ui/icon.tsx' 3 | import { type VerificationTypes } from '#app/routes/_auth+/verify.tsx' 4 | 5 | export const handle = { 6 | breadcrumb: 2FA, 7 | } 8 | 9 | export const twoFAVerificationType = '2fa' satisfies VerificationTypes 10 | 11 | export default function TwoFactorRoute() { 12 | return 13 | } 14 | -------------------------------------------------------------------------------- /app/utils/request-info.ts: -------------------------------------------------------------------------------- 1 | import { useRouteLoaderData } from '@remix-run/react' 2 | import { type loader as rootLoader } from '#app/root.tsx' 3 | import { invariant } from './misc.tsx' 4 | 5 | /** 6 | * @returns the request info from the root loader 7 | */ 8 | export function useRequestInfo() { 9 | const data = useRouteLoaderData('root') 10 | invariant(data?.requestInfo, 'No requestInfo found in root loader') 11 | 12 | return data.requestInfo 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 | -------------------------------------------------------------------------------- /public/favicons/README.md: -------------------------------------------------------------------------------- 1 | # Favicon 2 | 3 | This directory has a few versions of icons to account for different devices. In 4 | some cases, we cannot reliably detect light/dark mode preference. Hence some of 5 | the icons in here should not have a transparent background. These icons are 6 | referenced in the `site.webmanifest` file. 7 | 8 | Note, there's also a `favicon.ico` in the root of `/public` which some older 9 | browsers will request automatically. This is a fallback for those browsers. 10 | -------------------------------------------------------------------------------- /app/components/confetti.tsx: -------------------------------------------------------------------------------- 1 | import { Index as ConfettiShower } from 'confetti-react' 2 | import { ClientOnly } from 'remix-utils' 3 | 4 | export function Confetti({ id }: { id?: string | null }) { 5 | if (!id) return null 6 | 7 | return ( 8 | 9 | {() => ( 10 | 18 | )} 19 | 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /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 | "name": "simple-icons", 12 | "directory": "./other/svg-icons", 13 | "postinstall": ["npm", "run", "build:icons"], 14 | "transformers": ["transform-icon.ts"] 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/utils/providers/provider.ts: -------------------------------------------------------------------------------- 1 | import { type Strategy } from 'remix-auth' 2 | 3 | // Define a user type for cleaner typing 4 | export type ProviderUser = { 5 | id: string 6 | email: string 7 | username?: string 8 | name?: string 9 | imageUrl?: string 10 | } 11 | 12 | export interface AuthProvider { 13 | getAuthStrategy(): Strategy 14 | handleMockAction(request: Request): Promise 15 | resolveConnectionData(providerId: string): Promise<{ 16 | displayName: string 17 | link?: string | null 18 | }> 19 | } 20 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /public/favicons/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /server/dev-server.js: -------------------------------------------------------------------------------- 1 | import { execa } from 'execa' 2 | 3 | if (process.env.NODE_ENV === 'production') { 4 | await import('./index.js') 5 | } else { 6 | const command = 7 | 'tsx watch --clear-screen=false --ignore "app/**" --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 | MOCKS: true, 14 | ...process.env, 15 | }, 16 | // https://github.com/sindresorhus/execa/issues/433 17 | windowsHide: false, 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import react from '@vitejs/plugin-react' 4 | import { defineConfig } from 'vite' 5 | 6 | export default defineConfig({ 7 | // @ts-expect-error their types are wrong 8 | plugins: [react()], 9 | css: { postcss: { plugins: [] } }, 10 | test: { 11 | include: ['./app/**/*.test.{ts,tsx}'], 12 | setupFiles: ['./tests/setup/setup-test-env.ts'], 13 | globalSetup: ['./tests/setup/global-setup.ts'], 14 | restoreMocks: true, 15 | coverage: { 16 | include: ['app/**/*.{ts,tsx}'], 17 | all: true, 18 | }, 19 | }, 20 | }) 21 | -------------------------------------------------------------------------------- /other/sentry-create-release.js: -------------------------------------------------------------------------------- 1 | import { createRelease } from '@sentry/remix/scripts/createRelease.js' 2 | import 'dotenv/config' 3 | 4 | const DEFAULT_URL_PREFIX = '#build/' 5 | const DEFAULT_BUILD_PATH = 'public/build' 6 | 7 | // exit with non-zero code if we have everything for Sentry 8 | if ( 9 | process.env.SENTRY_DSN && 10 | process.env.SENTRY_ORG && 11 | process.env.SENTRY_PROJECT && 12 | process.env.SENTRY_AUTH_TOKEN 13 | ) { 14 | createRelease({}, DEFAULT_URL_PREFIX, DEFAULT_BUILD_PATH) 15 | } else { 16 | console.log( 17 | 'Missing Sentry environment variables, skipping sourcemap upload.', 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /remix.config.js: -------------------------------------------------------------------------------- 1 | import { flatRoutes } from 'remix-flat-routes' 2 | 3 | /** 4 | * @type {import('@remix-run/dev').AppConfig} 5 | */ 6 | export default { 7 | cacheDirectory: './node_modules/.cache/remix', 8 | ignoredRouteFiles: ['**/*'], 9 | serverModuleFormat: 'esm', 10 | serverPlatform: 'node', 11 | tailwind: true, 12 | postcss: true, 13 | watchPaths: ['./tailwind.config.ts'], 14 | routes: async defineRoutes => { 15 | return flatRoutes('routes', defineRoutes, { 16 | ignoredRouteFiles: [ 17 | '.*', 18 | '**/*.css', 19 | '**/*.test.{js,jsx,ts,tsx}', 20 | '**/__*.*', 21 | ], 22 | }) 23 | }, 24 | } 25 | -------------------------------------------------------------------------------- /tests/setup/global-setup.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | import { execaCommand } from 'execa' 3 | import fsExtra from 'fs-extra' 4 | 5 | export const BASE_DATABASE_PATH = path.join( 6 | process.cwd(), 7 | `./tests/prisma/base.db`, 8 | ) 9 | 10 | export async function setup() { 11 | const databaseExists = await fsExtra.pathExists(BASE_DATABASE_PATH) 12 | if (databaseExists) return 13 | 14 | await execaCommand('prisma migrate reset --force --skip-generate', { 15 | stdio: 'inherit', 16 | env: { 17 | ...process.env, 18 | MINIMAL_SEED: 'true', 19 | DATABASE_URL: `file:${BASE_DATABASE_PATH}`, 20 | }, 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /app/components/ui/icons/name.d.ts: -------------------------------------------------------------------------------- 1 | // This file is generated by npm run build:icons 2 | 3 | export type IconName = 4 | | 'arrow-left' 5 | | 'arrow-right' 6 | | 'avatar' 7 | | 'camera' 8 | | 'check' 9 | | 'clock' 10 | | 'cross-1' 11 | | 'dots-horizontal' 12 | | 'download' 13 | | 'envelope-closed' 14 | | 'exit' 15 | | 'file-text' 16 | | 'github-logo' 17 | | 'google-logo' 18 | | 'laptop' 19 | | 'link-2' 20 | | 'lock-closed' 21 | | 'lock-open-1' 22 | | 'magnifying-glass' 23 | | 'moon' 24 | | 'pencil-1' 25 | | 'pencil-2' 26 | | 'plus' 27 | | 'question-mark-circled' 28 | | 'reset' 29 | | 'sun' 30 | | 'trash' 31 | | 'update' 32 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import 'dotenv/config' 2 | import 'source-map-support/register.js' 3 | import { installGlobals } from '@remix-run/node' 4 | import chalk from 'chalk' 5 | import closeWithGrace from 'close-with-grace' 6 | 7 | installGlobals() 8 | 9 | closeWithGrace(async ({ err }) => { 10 | if (err) { 11 | console.error(chalk.red(err)) 12 | console.error(chalk.red(err.stack)) 13 | process.exit(1) 14 | } 15 | }) 16 | 17 | if (process.env.MOCKS === 'true') { 18 | await import('./tests/mocks/index.ts') 19 | } 20 | 21 | if (process.env.NODE_ENV === 'production') { 22 | await import('./server-build/index.js') 23 | } else { 24 | await import('./server/index.ts') 25 | } 26 | -------------------------------------------------------------------------------- /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: '/' }) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/components/toaster.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | import { Toaster, toast as showToast } from 'sonner' 3 | import { type Toast } from '#app/utils/toast.server.ts' 4 | 5 | export function EpicToaster({ toast }: { toast?: Toast | null }) { 6 | return ( 7 | <> 8 | 9 | {toast ? : null} 10 | 11 | ) 12 | } 13 | 14 | function ShowToast({ toast }: { toast: Toast }) { 15 | const { id, type, title, description } = toast 16 | useEffect(() => { 17 | setTimeout(() => { 18 | showToast[type](title, { id, description }) 19 | }, 0) 20 | }, [description, id, title, type]) 21 | return null 22 | } 23 | -------------------------------------------------------------------------------- /other/svg-icons/laptop.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 11 | 12 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | LITEFS_DIR="/litefs/data" 2 | DATABASE_PATH="./prisma/data.db" 3 | DATABASE_URL="file:./data.db?connection_limit=1" 4 | CACHE_DATABASE_PATH="./other/cache.db" 5 | SESSION_SECRET="super-duper-s3cret" 6 | INTERNAL_COMMAND_TOKEN="some-made-up-token" 7 | RESEND_API_KEY="re_blAh_blaHBlaHblahBLAhBlAh" 8 | SENTRY_DSN="your-dsn" 9 | 10 | # the mocks and some code rely on these two being prefixed with "MOCK_" 11 | # if they aren't then the real github api will be attempted 12 | GITHUB_CLIENT_ID="MOCK_GITHUB_CLIENT_ID" 13 | GITHUB_CLIENT_SECRET="MOCK_GITHUB_CLIENT_SECRET" 14 | GITHUB_TOKEN="MOCK_GITHUB_TOKEN" 15 | 16 | GOOGLE_CLIENT_ID="MOCK_GOOGLE_CLIENT_ID" 17 | GOOGLE_CLIENT_SECRET="MOCK_GOOGLE_CLIENT_SECRET" 18 | -------------------------------------------------------------------------------- /tests/mocks/resend.ts: -------------------------------------------------------------------------------- 1 | import { faker } from '@faker-js/faker' 2 | import { HttpResponse, http, type HttpHandler } from 'msw' 3 | import { requireHeader, writeEmail } from './utils.ts' 4 | 5 | const { json } = HttpResponse 6 | 7 | export const handlers: Array = [ 8 | http.post(`https://api.resend.com/emails`, async ({ request }) => { 9 | requireHeader(request.headers, 'Authorization') 10 | const body = await request.json() 11 | console.info('🔶 mocked email contents:', body) 12 | 13 | const email = await writeEmail(body) 14 | 15 | return json({ 16 | id: faker.string.uuid(), 17 | from: email.from, 18 | to: email.to, 19 | created_at: new Date().toISOString(), 20 | }) 21 | }), 22 | ] 23 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | /** @type {import("prettier").Options} */ 2 | export default { 3 | arrowParens: 'avoid', 4 | bracketSameLine: false, 5 | bracketSpacing: true, 6 | embeddedLanguageFormatting: 'auto', 7 | endOfLine: 'lf', 8 | htmlWhitespaceSensitivity: 'css', 9 | insertPragma: false, 10 | jsxSingleQuote: false, 11 | printWidth: 80, 12 | proseWrap: 'always', 13 | quoteProps: 'as-needed', 14 | requirePragma: false, 15 | semi: false, 16 | singleAttributePerLine: false, 17 | singleQuote: true, 18 | tabWidth: 2, 19 | trailingComma: 'all', 20 | useTabs: true, 21 | overrides: [ 22 | { 23 | files: ['**/*.json'], 24 | options: { 25 | useTabs: false, 26 | }, 27 | }, 28 | ], 29 | plugins: ['prettier-plugin-tailwindcss'], 30 | } 31 | -------------------------------------------------------------------------------- /other/svg-icons/plus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 11 | 12 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import { type Config } from 'tailwindcss' 2 | import defaultTheme from 'tailwindcss/defaultTheme.js' 3 | import animatePlugin from 'tailwindcss-animate' 4 | import radixPlugin from 'tailwindcss-radix' 5 | import { extendedTheme } from './app/utils/extended-theme.ts' 6 | 7 | export default { 8 | content: ['./app/**/*.{ts,tsx,jsx,js}'], 9 | darkMode: 'class', 10 | theme: { 11 | container: { 12 | center: true, 13 | padding: '2rem', 14 | screens: { 15 | '2xl': '1400px', 16 | }, 17 | }, 18 | extend: { 19 | ...extendedTheme, 20 | fontFamily: { 21 | sans: ['var(--font-sans)', ...defaultTheme.fontFamily.sans], 22 | }, 23 | }, 24 | }, 25 | plugins: [animatePlugin, radixPlugin], 26 | } satisfies Config 27 | -------------------------------------------------------------------------------- /other/svg-icons/trash.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 11 | 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["**/*.ts", "**/*.tsx"], 3 | "compilerOptions": { 4 | "lib": ["DOM", "DOM.Iterable", "ES2022"], 5 | "isolatedModules": true, 6 | "esModuleInterop": true, 7 | "jsx": "react-jsx", 8 | "module": "nodenext", 9 | "moduleResolution": "nodenext", 10 | "resolveJsonModule": true, 11 | "target": "ES2022", 12 | "strict": true, 13 | "noImplicitAny": true, 14 | "allowJs": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "paths": { 17 | "#*": ["./*"], 18 | "@/icon-name": [ 19 | "./app/components/ui/icons/name.d.ts", 20 | "./types/icon-name.d.ts" 21 | ] 22 | }, 23 | "skipLibCheck": true, 24 | "allowImportingTsExtensions": true, 25 | "noEmit": true 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/routes/me.tsx: -------------------------------------------------------------------------------- 1 | import { redirect, type DataFunctionArgs } from '@remix-run/node' 2 | import { authenticator, requireUserId } from '#app/utils/auth.server.ts' 3 | import { prisma } from '#app/utils/db.server.ts' 4 | 5 | export async function loader({ request }: DataFunctionArgs) { 6 | const userId = await requireUserId(request) 7 | const user = await prisma.user.findUnique({ where: { id: userId } }) 8 | if (!user) { 9 | const requestUrl = new URL(request.url) 10 | const loginParams = new URLSearchParams([ 11 | ['redirectTo', `${requestUrl.pathname}${requestUrl.search}`], 12 | ]) 13 | const redirectTo = `/login?${loginParams}` 14 | await authenticator.logout(request, { redirectTo }) 15 | return redirect(redirectTo) 16 | } 17 | return redirect(`/users/${user.username}`) 18 | } 19 | -------------------------------------------------------------------------------- /other/svg-icons/google-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Google -------------------------------------------------------------------------------- /other/svg-icons/check.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 11 | 12 | -------------------------------------------------------------------------------- /app/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | import * as LabelPrimitive from '@radix-ui/react-label' 2 | import { cva, type VariantProps } 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 font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70', 9 | ) 10 | 11 | const Label = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef & 14 | VariantProps 15 | >(({ className, ...props }, ref) => ( 16 | 21 | )) 22 | Label.displayName = LabelPrimitive.Root.displayName 23 | 24 | export { Label } 25 | -------------------------------------------------------------------------------- /other/svg-icons/arrow-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 11 | 12 | -------------------------------------------------------------------------------- /other/svg-icons/arrow-left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 11 | 12 | -------------------------------------------------------------------------------- /app/utils/user.ts: -------------------------------------------------------------------------------- 1 | import { type SerializeFrom } from '@remix-run/node' 2 | import { useRouteLoaderData } from '@remix-run/react' 3 | import { type loader as rootLoader } from '#app/root.tsx' 4 | 5 | function isUser(user: any): user is SerializeFrom['user'] { 6 | return user && typeof user === 'object' && typeof user.id === 'string' 7 | } 8 | 9 | export function useOptionalUser() { 10 | const data = useRouteLoaderData('root') 11 | if (!data || !isUser(data.user)) { 12 | return undefined 13 | } 14 | return data.user 15 | } 16 | 17 | export function useUser() { 18 | const maybeUser = useOptionalUser() 19 | if (!maybeUser) { 20 | throw new Error( 21 | 'No user found in root loader, but user is required by useUser. If user is optional, try useOptionalUser instead.', 22 | ) 23 | } 24 | return maybeUser 25 | } 26 | -------------------------------------------------------------------------------- /tests/mocks/index.ts: -------------------------------------------------------------------------------- 1 | import closeWithGrace from 'close-with-grace' 2 | import { passthrough, http } from 'msw' 3 | import { setupServer } from 'msw/node' 4 | import { handlers as githubHandlers } from './github.ts' 5 | import { handlers as googleHandlers } from './google.ts' 6 | import { handlers as resendHandlers } from './resend.ts' 7 | 8 | const miscHandlers = [ 9 | process.env.REMIX_DEV_ORIGIN 10 | ? http.post(`${process.env.REMIX_DEV_ORIGIN}ping`, passthrough) 11 | : null, 12 | ].filter(Boolean) 13 | 14 | export const server = setupServer( 15 | ...miscHandlers, 16 | ...resendHandlers, 17 | ...googleHandlers, 18 | ...githubHandlers, 19 | ) 20 | 21 | server.listen({ onUnhandledRequest: 'warn' }) 22 | 23 | if (process.env.NODE_ENV !== 'test') { 24 | console.info('🔶 Mock server installed') 25 | 26 | closeWithGrace(() => { 27 | server.close() 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /other/svg-icons/magnifying-glass.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 11 | 12 | -------------------------------------------------------------------------------- /app/utils/misc.error-message.test.ts: -------------------------------------------------------------------------------- 1 | import { faker } from '@faker-js/faker' 2 | import { expect, test } from 'vitest' 3 | import { consoleError } from '#tests/setup/setup-test-env.ts' 4 | import { getErrorMessage } from './misc.tsx' 5 | 6 | test('Error object returns message', () => { 7 | const message = faker.lorem.words(2) 8 | expect(getErrorMessage(new Error(message))).toBe(message) 9 | }) 10 | 11 | test('String returns itself', () => { 12 | const message = faker.lorem.words(2) 13 | expect(getErrorMessage(message)).toBe(message) 14 | }) 15 | 16 | test('undefined falls back to Unknown', () => { 17 | consoleError.mockImplementation(() => {}) 18 | expect(getErrorMessage(undefined)).toBe('Unknown Error') 19 | expect(consoleError).toHaveBeenCalledWith( 20 | 'Unable to get error message for error', 21 | undefined, 22 | ) 23 | expect(consoleError).toHaveBeenCalledTimes(1) 24 | }) 25 | -------------------------------------------------------------------------------- /app/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { cn } from '#app/utils/misc.tsx' 4 | 5 | export interface TextareaProps 6 | extends React.TextareaHTMLAttributes {} 7 | 8 | const Textarea = React.forwardRef( 9 | ({ className, ...props }, ref) => { 10 | return ( 11 |