From bdeafa22c2d3a5782fad91b23c192ed552e626b4 Mon Sep 17 00:00:00 2001 From: Shawn Mclean Date: Mon, 20 May 2024 12:50:41 -0500 Subject: [PATCH] Add remix --- .dockerignore | 26 + .eslintignore | 3 + .eslintrc.cjs | 6 + .github/dependabot.yml | 20 + .github/workflows/main.yml | 84 + .gitignore | 21 + .npmrc | 2 + .prettierignore | 7 + .prettierrc | 14 + Dockerfile | 64 + README.md | 44 + app/components/header.tsx | 39 + app/components/logo.tsx | 28 + app/components/misc/client-hints.tsx | 22 + app/components/misc/error-boundary.tsx | 55 + app/components/misc/language-switcher.tsx | 46 + app/components/misc/theme-switcher.tsx | 90 + app/components/navigation.tsx | 272 + app/components/toaster.tsx | 20 + app/components/ui/button.tsx | 54 + app/components/ui/dropdown-menu.tsx | 188 + app/components/ui/input.tsx | 24 + app/components/ui/select.tsx | 145 + app/components/ui/sonner.tsx | 25 + app/components/ui/switch.tsx | 26 + app/entry.client.tsx | 36 + app/entry.server.tsx | 109 + app/modules/auth/auth-session.server.ts | 15 + app/modules/auth/auth.server.ts | 146 + app/modules/email/email.server.ts | 60 + app/modules/email/templates/auth-email.tsx | 118 + .../email/templates/subscription-email.tsx | 131 + app/modules/i18n/i18n.server.ts | 24 + app/modules/i18n/i18n.ts | 16 + app/modules/i18n/locales/en.ts | 5 + app/modules/i18n/locales/es.ts | 5 + app/modules/stripe/plans.ts | 96 + app/modules/stripe/queries.server.ts | 134 + app/modules/stripe/stripe.server.ts | 11 + app/root.css | 138 + app/root.tsx | 163 + app/routes/$.tsx | 52 + app/routes/_home+/_index.tsx | 506 + app/routes/_home+/_layout.tsx | 12 + app/routes/admin+/_index.tsx | 19 + app/routes/admin+/_layout.tsx | 87 + app/routes/api+/webhook.ts | 177 + app/routes/auth+/$provider.callback.tsx | 15 + app/routes/auth+/$provider.tsx | 15 + app/routes/auth+/_layout.tsx | 77 + app/routes/auth+/login.tsx | 173 + app/routes/auth+/logout.tsx | 12 + app/routes/auth+/magic-link.tsx | 14 + app/routes/auth+/verify.tsx | 150 + app/routes/dashboard+/_index.tsx | 79 + app/routes/dashboard+/_layout.tsx | 32 + app/routes/dashboard+/checkout.tsx | 104 + app/routes/dashboard+/settings.billing.tsx | 256 + app/routes/dashboard+/settings.index.tsx | 273 + app/routes/dashboard+/settings.tsx | 72 + app/routes/onboarding+/_layout.tsx | 38 + app/routes/onboarding+/username.tsx | 160 + app/routes/resources+/reset-image.ts | 11 + app/routes/resources+/update-theme.ts | 21 + app/routes/resources+/upload-image.ts | 79 + app/routes/resources+/user-images.$imageId.ts | 24 + app/utils/constants/brand.ts | 15 + app/utils/constants/errors.ts | 18 + app/utils/constants/misc.ts | 11 + app/utils/csrf.server.ts | 29 + app/utils/db.server.ts | 7 + app/utils/env.server.ts | 52 + app/utils/honeypot.server.ts | 20 + app/utils/hooks/use-double-check.ts | 36 + app/utils/hooks/use-hints.ts | 19 + app/utils/hooks/use-interval.ts | 24 + app/utils/hooks/use-nonce.ts | 10 + app/utils/hooks/use-request-info.ts | 12 + app/utils/hooks/use-theme.ts | 79 + app/utils/misc.server.ts | 69 + app/utils/misc.ts | 90 + app/utils/permissions.server.ts | 26 + app/utils/toast.server.ts | 65 + components.json | 17 + docker-entrypoint.js | 32 + docs/README.md | 141 + docs/guide/01-introduction.md | 71 + docs/guide/02-authentication.md | 55 + docs/guide/03-subscriptions.md | 62 + docs/guide/04-internationalization.md | 33 + docs/guide/05-utilities.md | 53 + docs/guide/06-design.md | 39 + docs/guide/07-scripts.md | 36 + docs/guide/08-testing.md | 58 + docs/guide/09-deployment.md | 86 + docs/guide/README.md | 6 + fly.toml | 22 + package-lock.json | 18474 ++++++++++++++++ package.json | 99 + postcss.config.mjs | 9 + prisma/schema.prisma | 119 + prisma/seed.ts | 166 + public/favicon.ico | Bin 0 -> 16958 bytes public/images/shadow.png | Bin 0 -> 1429412 bytes server.mjs | 165 + tailwind.config.ts | 80 + tests/integration/example.test.ts | 5 + tests/setup-test-env.ts | 7 + tsconfig.json | 32 + vite.config.ts | 22 + vitest.config.ts | 19 + 111 files changed, 25480 insertions(+) create mode 100644 .dockerignore create mode 100644 .eslintignore create mode 100644 .eslintrc.cjs create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/main.yml create mode 100644 .gitignore create mode 100644 .npmrc create mode 100644 .prettierignore create mode 100644 .prettierrc create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 app/components/header.tsx create mode 100644 app/components/logo.tsx create mode 100644 app/components/misc/client-hints.tsx create mode 100644 app/components/misc/error-boundary.tsx create mode 100644 app/components/misc/language-switcher.tsx create mode 100644 app/components/misc/theme-switcher.tsx create mode 100644 app/components/navigation.tsx create mode 100644 app/components/toaster.tsx create mode 100644 app/components/ui/button.tsx create mode 100644 app/components/ui/dropdown-menu.tsx create mode 100644 app/components/ui/input.tsx create mode 100644 app/components/ui/select.tsx create mode 100644 app/components/ui/sonner.tsx create mode 100644 app/components/ui/switch.tsx create mode 100644 app/entry.client.tsx create mode 100644 app/entry.server.tsx create mode 100644 app/modules/auth/auth-session.server.ts create mode 100644 app/modules/auth/auth.server.ts create mode 100644 app/modules/email/email.server.ts create mode 100644 app/modules/email/templates/auth-email.tsx create mode 100644 app/modules/email/templates/subscription-email.tsx create mode 100644 app/modules/i18n/i18n.server.ts create mode 100644 app/modules/i18n/i18n.ts create mode 100644 app/modules/i18n/locales/en.ts create mode 100644 app/modules/i18n/locales/es.ts create mode 100644 app/modules/stripe/plans.ts create mode 100644 app/modules/stripe/queries.server.ts create mode 100644 app/modules/stripe/stripe.server.ts create mode 100644 app/root.css create mode 100644 app/root.tsx create mode 100644 app/routes/$.tsx create mode 100644 app/routes/_home+/_index.tsx create mode 100644 app/routes/_home+/_layout.tsx create mode 100644 app/routes/admin+/_index.tsx create mode 100644 app/routes/admin+/_layout.tsx create mode 100644 app/routes/api+/webhook.ts create mode 100644 app/routes/auth+/$provider.callback.tsx create mode 100644 app/routes/auth+/$provider.tsx create mode 100644 app/routes/auth+/_layout.tsx create mode 100644 app/routes/auth+/login.tsx create mode 100644 app/routes/auth+/logout.tsx create mode 100644 app/routes/auth+/magic-link.tsx create mode 100644 app/routes/auth+/verify.tsx create mode 100644 app/routes/dashboard+/_index.tsx create mode 100644 app/routes/dashboard+/_layout.tsx create mode 100644 app/routes/dashboard+/checkout.tsx create mode 100644 app/routes/dashboard+/settings.billing.tsx create mode 100644 app/routes/dashboard+/settings.index.tsx create mode 100644 app/routes/dashboard+/settings.tsx create mode 100644 app/routes/onboarding+/_layout.tsx create mode 100644 app/routes/onboarding+/username.tsx create mode 100644 app/routes/resources+/reset-image.ts create mode 100644 app/routes/resources+/update-theme.ts create mode 100644 app/routes/resources+/upload-image.ts create mode 100644 app/routes/resources+/user-images.$imageId.ts create mode 100644 app/utils/constants/brand.ts create mode 100644 app/utils/constants/errors.ts create mode 100644 app/utils/constants/misc.ts create mode 100644 app/utils/csrf.server.ts create mode 100644 app/utils/db.server.ts create mode 100644 app/utils/env.server.ts create mode 100644 app/utils/honeypot.server.ts create mode 100644 app/utils/hooks/use-double-check.ts create mode 100644 app/utils/hooks/use-hints.ts create mode 100644 app/utils/hooks/use-interval.ts create mode 100644 app/utils/hooks/use-nonce.ts create mode 100644 app/utils/hooks/use-request-info.ts create mode 100644 app/utils/hooks/use-theme.ts create mode 100644 app/utils/misc.server.ts create mode 100644 app/utils/misc.ts create mode 100644 app/utils/permissions.server.ts create mode 100644 app/utils/toast.server.ts create mode 100644 components.json create mode 100644 docker-entrypoint.js create mode 100644 docs/README.md create mode 100644 docs/guide/01-introduction.md create mode 100644 docs/guide/02-authentication.md create mode 100644 docs/guide/03-subscriptions.md create mode 100644 docs/guide/04-internationalization.md create mode 100644 docs/guide/05-utilities.md create mode 100644 docs/guide/06-design.md create mode 100644 docs/guide/07-scripts.md create mode 100644 docs/guide/08-testing.md create mode 100644 docs/guide/09-deployment.md create mode 100644 docs/guide/README.md create mode 100644 fly.toml create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 postcss.config.mjs create mode 100644 prisma/schema.prisma create mode 100644 prisma/seed.ts create mode 100644 public/favicon.ico create mode 100644 public/images/shadow.png create mode 100644 server.mjs create mode 100644 tailwind.config.ts create mode 100644 tests/integration/example.test.ts create mode 100644 tests/setup-test-env.ts create mode 100644 tsconfig.json create mode 100644 vite.config.ts create mode 100644 vitest.config.ts diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..8a3e42c6 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,26 @@ +# Package Managers + +yarn.lock +node_modules + +# Editor Configs + +.idea +.vscode +.DS_Store + +# Miscelaneous + +/.cache +/build +/public/build +.env + +# Prisma + +/prisma/data.db +/prisma/data.db-journal + +# Tests + +/coverage diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 00000000..b73f196f --- /dev/null +++ b/.eslintignore @@ -0,0 +1,3 @@ +/node_modules +/build +/public/build \ No newline at end of file diff --git a/.eslintrc.cjs b/.eslintrc.cjs new file mode 100644 index 00000000..427eacc9 --- /dev/null +++ b/.eslintrc.cjs @@ -0,0 +1,6 @@ +/** + * @type {import('eslint').Linter.Config} + */ +module.exports = { + extends: ['@remix-run/eslint-config', '@remix-run/eslint-config/node', 'prettier'], +} diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..510614ee --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,20 @@ +# Learn about Dependabot: +# https://docs.github.com/en/code-security/dependabot + +version: 2 +updates: + # Enable version updates for npm. + - package-ecosystem: 'npm' + # Look for `package.json` and `lock` files in the `root` directory. + directory: '/' + # Check the npm registry for updates every day. + schedule: + interval: 'weekly' + + # Enable version updates for Github-Actions. + - package-ecosystem: github-actions + # Look in the `root` directory. + directory: / + # Check for updates every day (weekdays) + schedule: + interval: weekly diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 00000000..0e15812a --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,84 @@ +name: 💿 Main +on: + push: + branches: + - main + - dev + pull_request: {} + +permissions: + actions: write + contents: read + +jobs: + lint: + name: ⬣ ESLint + runs-on: ubuntu-latest + steps: + - name: Cancel Previous Runs + uses: styfle/cancel-workflow-action@0.12.1 + + - name: Checkout Repository + uses: actions/checkout@v4.1.5 + + - name: Setup Node + uses: actions/setup-node@v4.0.2 + with: + node-version: 20 + + - name: Install Dependencies + uses: pnpm/action-setup@v4 + with: + version: 8 + run_install: true + + - name: Run Lint + run: pnpm lint + + typecheck: + name: ʦ TypeScript + runs-on: ubuntu-latest + steps: + - name: Cancel Previous Runs + uses: styfle/cancel-workflow-action@0.12.1 + + - name: Checkout Repository + uses: actions/checkout@v4.1.5 + + - name: Setup Node + uses: actions/setup-node@v4.0.2 + with: + node-version: 20 + + - name: Install Dependencies + uses: pnpm/action-setup@v4 + with: + version: 8 + run_install: true + + - name: Run Typechecking + run: pnpm typecheck + + vitest: + name: ⚡ Vitest + runs-on: ubuntu-latest + steps: + - name: Cancel Previous Runs + uses: styfle/cancel-workflow-action@0.12.1 + + - name: Checkout Repository + uses: actions/checkout@v4.1.5 + + - name: Setup Node.js + uses: actions/setup-node@v4.0.2 + with: + node-version: 20 + + - name: Install Dependencies + uses: pnpm/action-setup@v4 + with: + version: 8 + run_install: true + + - name: Run Vitest + run: pnpm test diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..4cf98cdd --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +# Package Managers +yarn.lock +node_modules + +# Editor Configs +.idea +.vscode +.DS_Store + +# Miscelaneous +/.cache +/build +/public/build +.env + +# Prisma +/prisma/data.db +/prisma/data.db-journal + +# Tests +/coverage diff --git a/.npmrc b/.npmrc new file mode 100644 index 00000000..07b96d57 --- /dev/null +++ b/.npmrc @@ -0,0 +1,2 @@ +auto-install-peers=true +registry=https://registry.npmjs.org/ \ No newline at end of file diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..feadd84a --- /dev/null +++ b/.prettierignore @@ -0,0 +1,7 @@ +node_modules +package-lock.json +pnpm-lock.yaml + +/build +/public/build +.env diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..bbb278a7 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,14 @@ +{ + "tabWidth": 2, + "printWidth": 90, + "semi": false, + "useTabs": false, + "bracketSpacing": true, + "bracketSameLine": true, + "singleQuote": true, + "jsxSingleQuote": false, + "singleAttributePerLine": false, + "arrowParens": "always", + "trailingComma": "all", + "plugins": ["prettier-plugin-tailwindcss"] +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..161ce816 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,64 @@ +# syntax = docker/dockerfile:1 + +# Adjust NODE_VERSION as desired +ARG NODE_VERSION=20.11.0 +FROM node:${NODE_VERSION}-slim as base + +LABEL fly_launch_runtime="Remix/Prisma" + +# Remix/Prisma app lives here +WORKDIR /app + +# Set production environment +ENV NODE_ENV="production" + +# Throw-away build stage to reduce size of final image +FROM base as build + +# Install packages needed to build node modules +RUN apt-get update -qq && \ + apt-get install --no-install-recommends -y build-essential node-gyp openssl pkg-config python-is-python3 + +# Install node modules +COPY --link .npmrc package-lock.json package.json ./ +RUN npm ci --include=dev + +# Generate Prisma Client +COPY --link prisma . +RUN npx prisma generate + +# Copy application code +COPY --link . . + +# Build application +RUN npm run build + +# Remove development dependencies +RUN npm prune --omit=dev + +# Final stage for app image +FROM base + +# Install packages needed for deployment +RUN apt-get update -qq && \ + apt-get install --no-install-recommends -y openssl sqlite3 && \ + rm -rf /var/lib/apt/lists /var/cache/apt/archives + +# Copy built application +COPY --from=build /app /app +COPY --from=build /app/node_modules/prisma /app/node_modules/prisma + +# Setup sqlite3 on a separate volume +RUN mkdir -p /data +VOLUME /data + +# add shortcut for connecting to database CLI +RUN echo "#!/bin/sh\nset -x\nsqlite3 \$DATABASE_URL" > /usr/local/bin/database-cli && chmod +x /usr/local/bin/database-cli + +# Entrypoint prepares the database. +ENTRYPOINT [ "/app/docker-entrypoint.js" ] + +# Start the server by default, this can be overwritten at runtime +EXPOSE 3000 +ENV DATABASE_URL="file:///data/sqlite.db" +CMD [ "npm", "run", "start" ] diff --git a/README.md b/README.md new file mode 100644 index 00000000..27798366 --- /dev/null +++ b/README.md @@ -0,0 +1,44 @@ +

+ 🛍️ Remix SaaS +

+ +
+

+ A Lightweight, Feature-Rich, and Production-Ready Remix Stack for your next SaaS application. +

+
+ +
+

+ Live Demo + · + Documentation + · + Twitter +

+
+ +```sh +npx create-remix-saas@latest +``` + +## [Live Demo](https://remix-saas.fly.dev) + +[![Remix SaaS](https://raw.githubusercontent.com/dev-xo/dev-xo/main/remix-saas/intro.png)](https://remix-saas.fly.dev) + +We've created a simple demo that displays all template-provided features. Psst! Give the site a few seconds to load! _(It's running on a free tier!)_ + +> [!NOTE] +> Remix SaaS is an Open Source Template that shares common bits of code with: [Indie Stack](https://github.com/remix-run/indie-stack), [Epic Stack](https://github.com/epicweb-dev/epic-stack), [Supa Stripe Stack](https://github.com/rphlmr/supa-stripe-stack), and some other amazing Open Source Remix resources. Check them out, please! + +## Getting Started + +Please, read the [Getting Started Documentation](https://github.com/dev-xo/remix-saas/tree/main/docs#remix-saas-documentation) to successfully initialize your **Remix SaaS** Template. + +## Support + +If you found **Remix SaaS** helpful, consider supporting it with a ⭐ [Star](https://github.com/dev-xo/remix-saas). It helps the repository grow and provides the required motivation to continue maintaining the project. Thank you! + +## Acknowledgments + +Special thanks to [@mw10013](https://github.com/mw10013) who has been part of the Remix SaaS development. diff --git a/app/components/header.tsx b/app/components/header.tsx new file mode 100644 index 00000000..0f0ebcd4 --- /dev/null +++ b/app/components/header.tsx @@ -0,0 +1,39 @@ +import { useLocation } from '@remix-run/react' +import { ROUTE_PATH as DASHBOARD_PATH } from '#app/routes/dashboard+/_layout' +import { ROUTE_PATH as BILLING_PATH } from '#app/routes/dashboard+/settings.billing' +import { ROUTE_PATH as SETTINGS_PATH } from '#app/routes/dashboard+/settings' +import { ROUTE_PATH as ADMIN_PATH } from '#app/routes/admin+/_layout' + +export function Header() { + const location = useLocation() + const allowedLocations = [DASHBOARD_PATH, BILLING_PATH, SETTINGS_PATH, ADMIN_PATH] + + const headerTitle = () => { + if (location.pathname === DASHBOARD_PATH) return 'Dashboard' + if (location.pathname === BILLING_PATH) return 'Billing' + if (location.pathname === SETTINGS_PATH) return 'Settings' + if (location.pathname === ADMIN_PATH) return 'Admin' + } + const headerDescription = () => { + if (location.pathname === DASHBOARD_PATH) + return 'Manage your Apps and view your usage.' + if (location.pathname === SETTINGS_PATH) return 'Manage your account settings.' + if (location.pathname === BILLING_PATH) + return 'Manage billing and your subscription plan.' + if (location.pathname === ADMIN_PATH) return 'Your admin dashboard.' + } + + if (!allowedLocations.includes(location.pathname as (typeof allowedLocations)[number])) + return null + + return ( +
+
+
+

{headerTitle()}

+

{headerDescription()}

+
+
+
+ ) +} diff --git a/app/components/logo.tsx b/app/components/logo.tsx new file mode 100644 index 00000000..6c3e6812 --- /dev/null +++ b/app/components/logo.tsx @@ -0,0 +1,28 @@ +import { cn } from '#app/utils/misc.js' + +type LogoProps = { + width?: number + height?: number + className?: string + [key: string]: unknown | undefined +} + +export function Logo({ width, height, className, ...args }: LogoProps) { + return ( + + + + ) +} diff --git a/app/components/misc/client-hints.tsx b/app/components/misc/client-hints.tsx new file mode 100644 index 00000000..92a75ede --- /dev/null +++ b/app/components/misc/client-hints.tsx @@ -0,0 +1,22 @@ +import { useEffect } from 'react' +import { useRevalidator } from '@remix-run/react' +import { subscribeToSchemeChange } from '@epic-web/client-hints/color-scheme' +import { hintsUtils } from '#app/utils/hooks/use-hints' + +/** + * Injects an inline script that checks/sets CH Cookies (if not present). + * Reloads the page if any Cookie was set to an inaccurate value. + */ +export function ClientHintCheck({ nonce }: { nonce: string }) { + const { revalidate } = useRevalidator() + useEffect(() => subscribeToSchemeChange(() => revalidate()), [revalidate]) + + return ( +