From 58648f1ca05013764ec67a166604dfb0794712fa Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Tue, 6 Jan 2026 17:19:38 -0500 Subject: [PATCH 1/6] Add test apps for react router --- apps/react-router/react-router-v7-project | 1 + apps/react-router/rrv7-starter | 1 + .../.cursor/commands/better-writer.md | 53 + .../.cursor/commands/brainstorm.md | 35 + .../saas-template/.cursor/commands/commit.md | 15 + .../saas-template/.cursor/commands/debug.md | 22 + .../.cursor/commands/documentation.md | 34 + .../saas-template/.cursor/commands/log.md | 64 + .../saas-template/.cursor/commands/name.md | 48 + .../saas-template/.cursor/commands/plan.md | 36 + .../.cursor/commands/svg-to-react.md | 114 + .../.cursor/commands/unit-tests.md | 43 + .../saas-template/.cursor/commands/write.md | 135 + .../saas-template/.cursor/rules/facades.mdc | 23 + .../saas-template/.cursor/rules/js-and-ts.mdc | 77 + .../.cursor/rules/jsx-and-tsx.mdc | 75 + apps/react-router/saas-template/.dockerignore | 4 + apps/react-router/saas-template/.env.example | 32 + .../saas-template/.github/workflows/ci.yml | 172 + apps/react-router/saas-template/.gitignore | 30 + .../saas-template/.husky/commit-msg | 1 + .../saas-template/.husky/pre-commit | 1 + .../saas-template/.vscode/settings.json | 26 + .../saas-template/CONTRIBUTING.md | 186 + apps/react-router/saas-template/Dockerfile | 22 + apps/react-router/saas-template/LICENSE | 10 + apps/react-router/saas-template/README.md | 1068 ++ apps/react-router/saas-template/app/app.css | 445 + .../app/components/avatar-upload.tsx | 156 + .../app/components/disableable-link.test.tsx | 42 + .../app/components/disableable-link.tsx | 23 + .../app/components/general-error-boundary.tsx | 71 + .../app/components/magicui/iphone-15-pro.tsx | 129 + .../app/components/magicui/marquee.tsx | 73 + .../app/components/not-found.test.tsx | 34 + .../app/components/not-found.tsx | 40 + .../app/components/svgs/google-icon.tsx | 30 + .../app/components/ui/accordion.tsx | 78 + .../saas-template/app/components/ui/alert.tsx | 77 + .../app/components/ui/avatar.tsx | 109 + .../saas-template/app/components/ui/badge.tsx | 53 + .../app/components/ui/breadcrumb.tsx | 128 + .../app/components/ui/button.tsx | 59 + .../saas-template/app/components/ui/card.tsx | 103 + .../app/components/ui/checkbox.tsx | 26 + .../app/components/ui/collapsible.tsx | 19 + .../app/components/ui/command.tsx | 143 + .../app/components/ui/dialog.tsx | 154 + .../app/components/ui/dropdown-menu.tsx | 262 + .../saas-template/app/components/ui/field.tsx | 245 + .../saas-template/app/components/ui/globe.tsx | 142 + .../app/components/ui/hover-card.tsx | 49 + .../app/components/ui/input-group.tsx | 170 + .../app/components/ui/input-otp.tsx | 90 + .../saas-template/app/components/ui/input.tsx | 25 + .../saas-template/app/components/ui/item.tsx | 203 + .../saas-template/app/components/ui/label.tsx | 19 + .../app/components/ui/light-rays.tsx | 171 + .../app/components/ui/navigation-menu.tsx | 169 + .../app/components/ui/popover.tsx | 88 + .../app/components/ui/radio-group.tsx | 37 + .../app/components/ui/select.tsx | 216 + .../app/components/ui/separator.tsx | 23 + .../saas-template/app/components/ui/sheet.tsx | 132 + .../app/components/ui/sidebar.tsx | 745 + .../app/components/ui/skeleton.tsx | 13 + .../app/components/ui/sonner.tsx | 45 + .../app/components/ui/spinner.tsx | 22 + .../app/components/ui/switch.tsx | 30 + .../saas-template/app/components/ui/table.tsx | 119 + .../saas-template/app/components/ui/tabs.tsx | 81 + .../app/components/ui/textarea.tsx | 18 + .../app/components/ui/tooltip.tsx | 68 + .../saas-template/app/entry.client.tsx | 33 + .../saas-template/app/entry.server.tsx | 159 + .../features/billing/billing-action.server.ts | 291 + .../app/features/billing/billing-constants.ts | 46 + .../billing/billing-factories.server.ts | 426 + .../billing/billing-helpers.server.test.ts | 396 + .../billing/billing-helpers.server.ts | 191 + .../features/billing/billing-helpers.test.ts | 30 + .../app/features/billing/billing-helpers.ts | 31 + .../features/billing/billing-page.test.tsx | 481 + .../app/features/billing/billing-page.tsx | 654 + .../app/features/billing/billing-schemas.ts | 53 + .../billing/billing-sidebar-card.test.tsx | 107 + .../features/billing/billing-sidebar-card.tsx | 126 + ...modify-subscription-modal-content.test.tsx | 298 + ...l-or-modify-subscription-modal-content.tsx | 530 + .../contact-sales-action.server.ts | 39 + .../contact-sales/contact-sales-constants.ts | 1 + .../contact-sales-factories.server.ts | 23 + ...tact-sales-form-submission-model.server.ts | 56 + .../contact-sales/contact-sales-schemas.ts | 67 + .../contact-sales/contact-sales-team.test.tsx | 44 + .../contact-sales/contact-sales-team.tsx | 168 + ...create-subscription-modal-content.test.tsx | 189 + .../create-subscription-modal-content.tsx | 499 + .../app/features/billing/description-list.tsx | 32 + .../edit-billing-email-modal-content.tsx | 94 + .../app/features/billing/pricing.tsx | 117 + .../features/billing/stripe-admin.server.ts | 36 + .../billing/stripe-event-factories.server.ts | 201 + .../billing/stripe-event-handlers.server.ts | 327 + .../billing/stripe-factories.server.ts | 683 + .../features/billing/stripe-helpers.server.ts | 347 + .../billing/stripe-prices-model.server.ts | 102 + .../billing/stripe-product-model.server.ts | 111 + .../stripe-subscription-model.server.ts | 180 + ...ripe-subscription-schedule-model.server.ts | 180 + .../color-scheme/color-scheme-constants.ts | 10 + .../color-scheme/color-scheme.server.ts | 46 + .../color-scheme/theme-toggle.test.tsx | 68 + .../features/color-scheme/theme-toggle.tsx | 104 + .../features/color-scheme/use-color-scheme.ts | 18 + .../app/features/landing/bento-grid.tsx | 67 + .../app/features/landing/cta.tsx | 56 + .../app/features/landing/description.tsx | 85 + .../app/features/landing/faq.tsx | 65 + .../app/features/landing/features.tsx | 201 + .../app/features/landing/footer.tsx | 76 + .../app/features/landing/header.tsx | 53 + .../app/features/landing/hero.tsx | 127 + .../app/features/landing/landing-page.tsx | 27 + .../app/features/landing/logos.tsx | 121 + .../features/landing/svgs/playwright-icon.tsx | 50 + .../landing/svgs/reactsquad-logo-icon.tsx | 40 + .../landing/svgs/rr-lockup-dark-icon.tsx | 32 + .../landing/svgs/rr-lockup-light-icon.tsx | 32 + .../localization/i18next-middleware.server.ts | 27 + .../localization/locales/de/billing.ts | 265 + .../localization/locales/de/color-scheme.ts | 7 + .../localization/locales/de/drag-and-drop.ts | 4 + .../features/localization/locales/de/index.ts | 23 + .../localization/locales/de/landing.ts | 188 + .../localization/locales/de/notifications.ts | 21 + .../localization/locales/de/onboarding.ts | 107 + .../localization/locales/de/organizations.ts | 313 + .../localization/locales/de/settings.ts | 61 + .../localization/locales/de/translation.ts | 12 + .../locales/de/user-authentication.ts | 92 + .../localization/locales/en/billing.ts | 254 + .../localization/locales/en/color-scheme.ts | 7 + .../localization/locales/en/drag-and-drop.ts | 4 + .../features/localization/locales/en/index.ts | 25 + .../localization/locales/en/landing.ts | 188 + .../localization/locales/en/notifications.ts | 20 + .../localization/locales/en/onboarding.ts | 104 + .../localization/locales/en/organizations.ts | 304 + .../localization/locales/en/settings.ts | 60 + .../localization/locales/en/translation.ts | 11 + .../locales/en/user-authentication.ts | 91 + .../features/localization/locales/index.ts | 6 + .../notification-components.test.tsx | 65 + .../notifications/notification-components.tsx | 150 + .../notifications/notification-constants.ts | 7 + .../notifications-button.test.tsx | 173 + .../notifications/notifications-button.tsx | 187 + .../notifications-factories.server.ts | 133 + .../notifications-helpers.server.test.ts | 368 + .../notifications-helpers.server.ts | 47 + .../notifications-model.server.ts | 632 + .../notifications-panel-content.test.tsx | 71 + .../notifications-panel-content.tsx | 59 + .../notifications/notifications-schemas.ts | 35 + .../use-pending-notifications.ts | 28 + .../onboarding-helpers.server.test.ts | 292 + .../onboarding/onboarding-helpers.server.ts | 184 + .../onboarding-organization-action.server.ts | 55 + .../onboarding-organization-consants.ts | 1 + .../onboarding-organization-schemas.ts | 69 + .../app/features/onboarding/talent-map.tsx | 107 + .../onboarding-user-account-action.server.ts | 93 + .../onboarding-user-account-constants.ts | 1 + .../onboarding-user-account-schemas.ts | 29 + .../accept-email-invite-action.server.ts | 185 + .../accept-email-invite-constants.ts | 2 + ...accept-email-invite-helpers.server.test.ts | 36 + .../accept-email-invite-helpers.server.ts | 107 + .../accept-email-invite-page.test.tsx | 52 + .../accept-email-invite-page.tsx | 95 + ...accept-email-invite-session.server.spec.ts | 163 + .../accept-email-invite-session.server.ts | 120 + .../accept-invite-link-action.server.ts | 157 + .../accept-invite-link-constants.ts | 2 + .../accept-invite-link-helpers.server.test.ts | 93 + .../accept-invite-link-helpers.server.ts | 116 + .../accept-invite-link-page.test.tsx | 52 + .../accept-invite-link-page.tsx | 95 + .../accept-invite-link-session.server.spec.ts | 163 + .../accept-invite-link-session.server.ts | 120 + .../invite-link-use-model.server.ts | 66 + .../create-organization-action.server.ts | 55 + .../create-organization-constants.ts | 1 + .../create-organization-form-card.test.tsx | 76 + .../create-organization-form-card.tsx | 175 + .../create-organization-schemas.ts | 35 + .../organizations/layout/app-header.test.tsx | 135 + .../organizations/layout/app-header.tsx | 159 + .../organizations/layout/app-sidebar.tsx | 143 + .../layout/layout-helpers.server.test.ts | 478 + .../layout/layout-helpers.server.ts | 145 + .../layout/layout-helpers.test.ts | 172 + .../organizations/layout/layout-helpers.ts | 23 + .../organizations/layout/nav-group.test.tsx | 168 + .../organizations/layout/nav-group.tsx | 121 + .../organizations/layout/nav-user.test.tsx | 121 + .../organizations/layout/nav-user.tsx | 138 + ...ganization-switcher-session.server.spec.ts | 84 + .../organization-switcher-session.server.ts | 69 + .../layout/organization-switcher.test.tsx | 92 + .../layout/organization-switcher.tsx | 168 + .../layout/sidebar-layout-action.server.ts | 151 + .../layout/sidebar-layout-constants.ts | 1 + .../layout/sidebar-layout-schemas.ts | 11 + .../organizations/organization-constants.ts | 2 + .../organization-membership-model.server.ts | 71 + ...izations-email-invite-link-model.server.ts | 97 + .../organizations-factories.server.ts | 168 + .../organizations-helpers.server.test.ts | 174 + .../organizations-helpers.server.ts | 376 + .../organizations-invite-link-model.server.ts | 140 + .../organizations-middleware.server.ts | 43 + .../organizations-model.server.ts | 357 + .../settings/general/danger-zone.tsx | 185 + ...ral-organization-settings-action.server.ts | 144 + .../general/general-organization-settings.tsx | 219 + .../general/general-settings-constants.ts | 2 + .../general/general-settings-schemas.ts | 38 + .../general/organization-info.test.tsx | 62 + .../settings/general/organization-info.tsx | 56 + .../settings/settings-sidebar.tsx | 79 + .../invite-by-email-card.test.tsx | 148 + .../team-members/invite-by-email-card.tsx | 181 + .../settings/team-members/invite-email.tsx | 80 + .../team-members/invite-link-card.test.tsx | 132 + .../team-members/invite-link-card.tsx | 240 + .../team-members-action.server.tsx | 385 + .../team-members/team-members-constants.ts | 4 + .../team-members-helpers.server.test.ts | 534 + .../team-members-helpers.server.ts | 145 + .../team-members-settings-schemas.ts | 29 + .../team-members/team-members-table.test.tsx | 311 + .../team-members/team-members-table.tsx | 455 + .../features/pastebin/paste-helpers.server.ts | 89 + .../account/account-settings-action.server.ts | 173 + .../account/account-settings-constants.ts | 19 + .../account-settings-helpers.server.test.ts | 141 + .../account-settings-helpers.server.ts | 70 + .../account/account-settings-schemas.ts | 38 + .../settings/account/account-settings.tsx | 200 + .../settings/account/danger-zone.test.tsx | 165 + .../settings/account/danger-zone.tsx | 185 + .../user-accounts/user-account-constants.ts | 2 + .../user-accounts-factories.server.ts | 29 + .../user-accounts-helpers.server.spec.ts | 42 + .../user-accounts-helpers.server.ts | 136 + .../user-accounts-model.server.ts | 290 + .../user-authentication/floating-paths.tsx | 66 + .../login/login-action.server.ts | 92 + .../login/login-constants.ts | 4 + .../login/login-schemas.ts | 14 + .../login-verification-awaiting.test.tsx | 154 + .../login/login-verification-awaiting.tsx | 89 + .../registration/register-action.server.ts | 96 + .../registration/registration-constants.ts | 4 + .../registration/registration-schemas.ts | 16 + ...egistration-verification-awaiting.test.tsx | 154 + .../registration-verification-awaiting.tsx | 89 + .../user-authentication/supabase.server.ts | 40 + .../use-countdown.test.tsx | 141 + .../user-authentication/use-countdown.ts | 68 + .../user-authentication-constants.ts | 9 + .../user-authentication-factories.ts | 65 + .../user-authentication-helpers.server.ts | 17 + .../user-authentication-helpers.test.ts | 90 + .../user-authentication-helpers.ts | 18 + .../user-authentication-middleware.server.ts | 83 + .../app/hooks/use-media-query.ts | 19 + .../saas-template/app/hooks/use-mobile.ts | 21 + .../app/hooks/use-prefers-reduced-motion.ts | 25 + .../app/hooks/use-preview-url.test.tsx | 149 + .../app/hooks/use-preview-url.ts | 42 + .../saas-template/app/hooks/use-toast.ts | 26 + .../saas-template/app/lib/README.md | 5 + .../saas-template/app/lib/supabase/client.ts | 8 + .../saas-template/app/lib/utils.ts | 7 + apps/react-router/saas-template/app/root.tsx | 227 + apps/react-router/saas-template/app/routes.ts | 17 + .../saas-template/app/routes/$.tsx | 24 + .../_authenticated-routes-layout.tsx | 9 + .../onboarding+/_index.tsx | 5 + .../onboarding+/_onboarding-layout.tsx | 75 + .../onboarding+/organization.spec.ts | 362 + .../onboarding+/organization.tsx | 412 + .../onboarding+/user-account.spec.ts | 400 + .../onboarding+/user-account.tsx | 171 + .../_sidebar-layout.spec.ts | 647 + .../$organizationSlug+/_sidebar-layout.tsx | 145 + .../$organizationSlug+/analytics.tsx | 38 + .../$organizationSlug+/dashboard.tsx | 104 + .../$organizationSlug+/get-help.tsx | 38 + .../$organizationSlug+/pastes.$pasteId.tsx | 71 + .../$organizationSlug+/pastes.tsx | 317 + .../$organizationSlug+/projects.tsx | 38 + .../$organizationSlug+/projects_.active.tsx | 38 + .../$organizationSlug+/settings+/_index.tsx | 47 + .../_organization-settings-layout.tsx | 48 + .../settings+/billing.spec.ts | 773 + .../$organizationSlug+/settings+/billing.tsx | 99 + .../settings+/billing_.success.tsx | 110 + .../settings+/general.spec.ts | 326 + .../$organizationSlug+/settings+/general.tsx | 97 + .../settings+/members.spec.ts | 1307 ++ .../$organizationSlug+/settings+/members.tsx | 150 + .../organizations_+/_index.tsx | 127 + .../organizations_+/new.spec.ts | 328 + .../organizations_+/new.tsx | 75 + .../settings+/_settings-layout.tsx | 39 + .../settings+/account.spec.ts | 498 + .../settings+/account.tsx | 72 + .../saas-template/app/routes/_index.tsx | 8 + .../_anonymous-routes-layout.tsx | 9 + .../_anonymous-routes+/auth.callback.ts | 215 + .../_anonymous-routes+/login.confirm.ts | 197 + .../_anonymous-routes+/login.spec.ts | 237 + .../_anonymous-routes+/login.tsx | 210 + .../_anonymous-routes+/register.confirm.ts | 89 + .../_anonymous-routes+/register.spec.ts | 233 + .../_anonymous-routes+/register.tsx | 242 + .../_user-authentication-layout.tsx | 90 + .../routes/_user-authentication+/logout.ts | 12 + .../app/routes/api+/locales+/$lng.$ns.ts | 46 + .../app/routes/api+/v1+/stripe.webhooks.ts | 144 + .../saas-template/app/routes/color-scheme.tsx | 6 + .../app/routes/contact-sales.spec.ts | 248 + .../app/routes/contact-sales.tsx | 100 + .../organizations_+/email-invite.spec.ts | 351 + .../routes/organizations_+/email-invite.tsx | 43 + .../organizations_+/invite-link.spec.ts | 289 + .../routes/organizations_+/invite-link.tsx | 38 + .../saas-template/app/routes/p.$pasteId.tsx | 53 + .../app/routes/paste.$pasteId.tsx | 103 + .../saas-template/app/routes/pricing.tsx | 377 + .../app/routes/privacy-policy.tsx | 3 + .../app/routes/terms-of-service.tsx | 3 + .../saas-template/app/test/mocks/browser.ts | 8 + .../app/test/mocks/handlers/resend.ts | 24 + .../app/test/mocks/handlers/stripe.ts | 344 + .../test/mocks/handlers/supabase/README.md | 334 + .../app/test/mocks/handlers/supabase/auth.ts | 419 + .../app/test/mocks/handlers/supabase/index.ts | 11 + .../mocks/handlers/supabase/mock-sessions.ts | 92 + .../test/mocks/handlers/supabase/storage.ts | 408 + .../app/test/mocks/msw-utils.server.ts | 17 + .../saas-template/app/test/mocks/msw-utils.ts | 20 + .../saas-template/app/test/mocks/server.ts | 61 + .../saas-template/app/test/mocks/utils.ts | 87 + .../saas-template/app/test/msw-test-utils.ts | 47 + .../app/test/react-test-utils.tsx | 39 + .../app/test/server-test-utils.ts | 54 + .../test/setup-browser-test-environment.ts | 8 + .../app/test/setup-server-test-environment.ts | 4 + .../saas-template/app/test/test-utils.ts | 574 + .../app/test/vitest.global-setup.ts | 24 + .../saas-template/app/types/conform.d.ts | 6 + .../saas-template/app/types/i18next.d.ts | 10 + .../saas-template/app/types/sudo.d.ts | 5 + .../app/utils/async-for-each.server.test.ts | 14 + .../app/utils/async-for-each.server.ts | 30 + .../app/utils/async-pipe.server.test.ts | 33 + .../app/utils/async-pipe.server.ts | 102 + .../saas-template/app/utils/client-hints.tsx | 58 + .../app/utils/combine-headers.server.test.ts | 58 + .../app/utils/combine-headers.server.ts | 26 + .../app/utils/database.server.ts | 112 + .../app/utils/define-custom-metadata.ts | 63 + .../saas-template/app/utils/email.server.ts | 104 + .../saas-template/app/utils/env.server.ts | 65 + .../app/utils/get-domain-url.server.ts | 9 + .../app/utils/get-error-message.test.ts | 85 + .../app/utils/get-error-message.ts | 61 + ...-is-data-with-response-init.server.test.ts | 31 + .../get-is-data-with-response-init.server.ts | 12 + .../app/utils/get-is-response.test.ts | 33 + .../app/utils/get-is-response.ts | 26 + .../app/utils/get-page-title.server.test.ts | 42 + .../app/utils/get-page-title.server.ts | 6 + ...arch-parameter-from-request.server.test.ts | 41 + ...et-search-parameter-from-request.server.ts | 24 + .../app/utils/honeypot.server.ts | 29 + .../app/utils/http-responses.server.test.ts | 250 + .../app/utils/http-responses.server.ts | 297 + .../saas-template/app/utils/nonce-provider.ts | 7 + .../saas-template/app/utils/request-info.ts | 18 + .../saas-template/app/utils/s3.server.ts | 41 + .../app/utils/security-middleware.server.ts | 19 + .../app/utils/slugify.server.test.ts | 75 + .../saas-template/app/utils/slugify.server.ts | 9 + .../app/utils/storage-helpers.server.spec.ts | 61 + .../app/utils/storage-helpers.server.ts | 55 + .../saas-template/app/utils/storage.server.ts | 108 + .../throw-if-entity-is-missing.server.test.ts | 29 + .../throw-if-entity-is-missing.server.ts | 18 + .../app/utils/to-form-data.test.ts | 92 + .../saas-template/app/utils/to-form-data.ts | 37 + .../app/utils/toast.server.spec.ts | 142 + .../saas-template/app/utils/toast.server.ts | 87 + .../saas-template/app/utils/types.ts | 24 + .../utils/validate-form-data.server.test.ts | 261 + .../app/utils/validate-form-data.server.ts | 70 + apps/react-router/saas-template/biome.json | 76 + .../saas-template/commitlint.config.mjs | 23 + .../saas-template/components.json | 24 + .../saas-template/decisions/README.md | 8 + .../saas-template/package-lock.json | 14869 ++++++++++++++++ apps/react-router/saas-template/package.json | 135 + .../saas-template/playwright.config.ts | 45 + .../playwright/e2e/billing/billing.e2e.ts | 1054 ++ .../e2e/billing/stripe-webhook.e2e.ts | 873 + .../e2e/landing/contact-sales.e2e.ts | 140 + .../playwright/e2e/landing/landing.e2e.ts | 26 + .../playwright/e2e/landing/pricing.e2e.ts | 140 + .../e2e/notifications/notifications.e2e.ts | 267 + .../onboarding/onboarding-organization.e2e.ts | 275 + .../onboarding/onboarding-user-account.e2e.ts | 233 + .../e2e/onboarding/onboarding.e2e.ts | 24 + .../general-organization-settings.e2e.ts | 383 + .../e2e/organizations/new-organization.e2e.ts | 405 + .../organizations/organization-layout.e2e.ts | 195 + .../organizations-email-invite.e2e.ts | 327 + .../organizations-invite-link.e2e.ts | 323 + .../organizations/organizations-list.e2e.ts | 196 + .../organizations-settings-layout.e2e.ts | 223 + .../team-member-organization-settings.e2e.ts | 1144 ++ .../playwright/e2e/settings/account.e2e.ts | 342 + .../user-authentication/auth-callback.e2e.ts | 758 + .../user-authentication/login-confirm.e2e.ts | 603 + .../e2e/user-authentication/login.e2e.ts | 373 + .../e2e/user-authentication/logout.e2e.ts | 34 + .../register-confirm.e2e.ts | 369 + .../e2e/user-authentication/register.e2e.ts | 381 + .../playwright/fixtures/200x200.jpg | Bin 0 -> 9991 bytes .../saas-template/playwright/global-setup.ts | 8 + .../playwright/global-tear-down.ts | 5 + .../saas-template/playwright/utils.ts | 342 + .../saas-template/prisma.config.ts | 13 + .../saas-template/prisma/schema.prisma | 325 + .../react-router/saas-template/prisma/seed.ts | 135 + .../appspecific/com.chrome.devtools.json | 1 + .../saas-template/public/favicon.ico | Bin 0 -> 15086 bytes .../public/fonts/inter/Inter-Black.woff2 | Bin 0 -> 111668 bytes .../fonts/inter/Inter-BlackItalic.woff2 | Bin 0 -> 118420 bytes .../public/fonts/inter/Inter-Bold.woff2 | Bin 0 -> 114840 bytes .../public/fonts/inter/Inter-BoldItalic.woff2 | Bin 0 -> 121500 bytes .../public/fonts/inter/Inter-ExtraBold.woff2 | Bin 0 -> 114856 bytes .../fonts/inter/Inter-ExtraBoldItalic.woff2 | Bin 0 -> 121516 bytes .../public/fonts/inter/Inter-ExtraLight.woff2 | Bin 0 -> 112728 bytes .../fonts/inter/Inter-ExtraLightItalic.woff2 | Bin 0 -> 119320 bytes .../public/fonts/inter/Inter-Italic.woff2 | Bin 0 -> 117700 bytes .../public/fonts/inter/Inter-Light.woff2 | Bin 0 -> 112592 bytes .../fonts/inter/Inter-LightItalic.woff2 | Bin 0 -> 119608 bytes .../public/fonts/inter/Inter-Medium.woff2 | Bin 0 -> 114348 bytes .../fonts/inter/Inter-MediumItalic.woff2 | Bin 0 -> 120784 bytes .../public/fonts/inter/Inter-Regular.woff2 | Bin 0 -> 111268 bytes .../public/fonts/inter/Inter-SemiBold.woff2 | Bin 0 -> 114812 bytes .../fonts/inter/Inter-SemiBoldItalic.woff2 | Bin 0 -> 121416 bytes .../public/fonts/inter/Inter-Thin.woff2 | Bin 0 -> 109548 bytes .../public/fonts/inter/Inter-ThinItalic.woff2 | Bin 0 -> 116880 bytes .../public/images/app-billing-dark.png | Bin 0 -> 269121 bytes .../public/images/app-billing-light.png | Bin 0 -> 272248 bytes .../public/images/app-dark-members.png | Bin 0 -> 388736 bytes .../saas-template/public/images/app-dark.png | Bin 0 -> 214954 bytes .../public/images/app-light-members.png | Bin 0 -> 394470 bytes .../saas-template/public/images/app-light.png | Bin 0 -> 201757 bytes .../public/images/app-mobile-dark.png | Bin 0 -> 408294 bytes .../public/images/app-mobile-light.png | Bin 0 -> 775044 bytes .../public/images/authentication-dark.png | Bin 0 -> 160488 bytes .../public/images/authentication-light.png | Bin 0 -> 162796 bytes .../public/images/notifications-dark.png | Bin 0 -> 40557 bytes .../public/images/notifications-light.png | Bin 0 -> 42583 bytes .../saas-template/react-router.config.ts | 12 + .../saas-template/test-results.txt | 44 + apps/react-router/saas-template/tsconfig.json | 37 + .../react-router/saas-template/vite.config.ts | 82 + apps/react-router/shopper | 1 + 486 files changed, 74713 insertions(+) create mode 160000 apps/react-router/react-router-v7-project create mode 160000 apps/react-router/rrv7-starter create mode 100644 apps/react-router/saas-template/.cursor/commands/better-writer.md create mode 100644 apps/react-router/saas-template/.cursor/commands/brainstorm.md create mode 100644 apps/react-router/saas-template/.cursor/commands/commit.md create mode 100644 apps/react-router/saas-template/.cursor/commands/debug.md create mode 100644 apps/react-router/saas-template/.cursor/commands/documentation.md create mode 100644 apps/react-router/saas-template/.cursor/commands/log.md create mode 100644 apps/react-router/saas-template/.cursor/commands/name.md create mode 100644 apps/react-router/saas-template/.cursor/commands/plan.md create mode 100644 apps/react-router/saas-template/.cursor/commands/svg-to-react.md create mode 100644 apps/react-router/saas-template/.cursor/commands/unit-tests.md create mode 100644 apps/react-router/saas-template/.cursor/commands/write.md create mode 100644 apps/react-router/saas-template/.cursor/rules/facades.mdc create mode 100644 apps/react-router/saas-template/.cursor/rules/js-and-ts.mdc create mode 100644 apps/react-router/saas-template/.cursor/rules/jsx-and-tsx.mdc create mode 100644 apps/react-router/saas-template/.dockerignore create mode 100644 apps/react-router/saas-template/.env.example create mode 100644 apps/react-router/saas-template/.github/workflows/ci.yml create mode 100644 apps/react-router/saas-template/.gitignore create mode 100644 apps/react-router/saas-template/.husky/commit-msg create mode 100755 apps/react-router/saas-template/.husky/pre-commit create mode 100644 apps/react-router/saas-template/.vscode/settings.json create mode 100644 apps/react-router/saas-template/CONTRIBUTING.md create mode 100644 apps/react-router/saas-template/Dockerfile create mode 100644 apps/react-router/saas-template/LICENSE create mode 100644 apps/react-router/saas-template/README.md create mode 100644 apps/react-router/saas-template/app/app.css create mode 100644 apps/react-router/saas-template/app/components/avatar-upload.tsx create mode 100644 apps/react-router/saas-template/app/components/disableable-link.test.tsx create mode 100644 apps/react-router/saas-template/app/components/disableable-link.tsx create mode 100644 apps/react-router/saas-template/app/components/general-error-boundary.tsx create mode 100644 apps/react-router/saas-template/app/components/magicui/iphone-15-pro.tsx create mode 100644 apps/react-router/saas-template/app/components/magicui/marquee.tsx create mode 100644 apps/react-router/saas-template/app/components/not-found.test.tsx create mode 100644 apps/react-router/saas-template/app/components/not-found.tsx create mode 100644 apps/react-router/saas-template/app/components/svgs/google-icon.tsx create mode 100644 apps/react-router/saas-template/app/components/ui/accordion.tsx create mode 100644 apps/react-router/saas-template/app/components/ui/alert.tsx create mode 100644 apps/react-router/saas-template/app/components/ui/avatar.tsx create mode 100644 apps/react-router/saas-template/app/components/ui/badge.tsx create mode 100644 apps/react-router/saas-template/app/components/ui/breadcrumb.tsx create mode 100644 apps/react-router/saas-template/app/components/ui/button.tsx create mode 100644 apps/react-router/saas-template/app/components/ui/card.tsx create mode 100644 apps/react-router/saas-template/app/components/ui/checkbox.tsx create mode 100644 apps/react-router/saas-template/app/components/ui/collapsible.tsx create mode 100644 apps/react-router/saas-template/app/components/ui/command.tsx create mode 100644 apps/react-router/saas-template/app/components/ui/dialog.tsx create mode 100644 apps/react-router/saas-template/app/components/ui/dropdown-menu.tsx create mode 100644 apps/react-router/saas-template/app/components/ui/field.tsx create mode 100644 apps/react-router/saas-template/app/components/ui/globe.tsx create mode 100644 apps/react-router/saas-template/app/components/ui/hover-card.tsx create mode 100644 apps/react-router/saas-template/app/components/ui/input-group.tsx create mode 100644 apps/react-router/saas-template/app/components/ui/input-otp.tsx create mode 100644 apps/react-router/saas-template/app/components/ui/input.tsx create mode 100644 apps/react-router/saas-template/app/components/ui/item.tsx create mode 100644 apps/react-router/saas-template/app/components/ui/label.tsx create mode 100644 apps/react-router/saas-template/app/components/ui/light-rays.tsx create mode 100644 apps/react-router/saas-template/app/components/ui/navigation-menu.tsx create mode 100644 apps/react-router/saas-template/app/components/ui/popover.tsx create mode 100644 apps/react-router/saas-template/app/components/ui/radio-group.tsx create mode 100644 apps/react-router/saas-template/app/components/ui/select.tsx create mode 100644 apps/react-router/saas-template/app/components/ui/separator.tsx create mode 100644 apps/react-router/saas-template/app/components/ui/sheet.tsx create mode 100644 apps/react-router/saas-template/app/components/ui/sidebar.tsx create mode 100644 apps/react-router/saas-template/app/components/ui/skeleton.tsx create mode 100644 apps/react-router/saas-template/app/components/ui/sonner.tsx create mode 100644 apps/react-router/saas-template/app/components/ui/spinner.tsx create mode 100644 apps/react-router/saas-template/app/components/ui/switch.tsx create mode 100644 apps/react-router/saas-template/app/components/ui/table.tsx create mode 100644 apps/react-router/saas-template/app/components/ui/tabs.tsx create mode 100644 apps/react-router/saas-template/app/components/ui/textarea.tsx create mode 100644 apps/react-router/saas-template/app/components/ui/tooltip.tsx create mode 100644 apps/react-router/saas-template/app/entry.client.tsx create mode 100644 apps/react-router/saas-template/app/entry.server.tsx create mode 100644 apps/react-router/saas-template/app/features/billing/billing-action.server.ts create mode 100644 apps/react-router/saas-template/app/features/billing/billing-constants.ts create mode 100644 apps/react-router/saas-template/app/features/billing/billing-factories.server.ts create mode 100644 apps/react-router/saas-template/app/features/billing/billing-helpers.server.test.ts create mode 100644 apps/react-router/saas-template/app/features/billing/billing-helpers.server.ts create mode 100644 apps/react-router/saas-template/app/features/billing/billing-helpers.test.ts create mode 100644 apps/react-router/saas-template/app/features/billing/billing-helpers.ts create mode 100644 apps/react-router/saas-template/app/features/billing/billing-page.test.tsx create mode 100644 apps/react-router/saas-template/app/features/billing/billing-page.tsx create mode 100644 apps/react-router/saas-template/app/features/billing/billing-schemas.ts create mode 100644 apps/react-router/saas-template/app/features/billing/billing-sidebar-card.test.tsx create mode 100644 apps/react-router/saas-template/app/features/billing/billing-sidebar-card.tsx create mode 100644 apps/react-router/saas-template/app/features/billing/cancel-or-modify-subscription-modal-content.test.tsx create mode 100644 apps/react-router/saas-template/app/features/billing/cancel-or-modify-subscription-modal-content.tsx create mode 100644 apps/react-router/saas-template/app/features/billing/contact-sales/contact-sales-action.server.ts create mode 100644 apps/react-router/saas-template/app/features/billing/contact-sales/contact-sales-constants.ts create mode 100644 apps/react-router/saas-template/app/features/billing/contact-sales/contact-sales-factories.server.ts create mode 100644 apps/react-router/saas-template/app/features/billing/contact-sales/contact-sales-form-submission-model.server.ts create mode 100644 apps/react-router/saas-template/app/features/billing/contact-sales/contact-sales-schemas.ts create mode 100644 apps/react-router/saas-template/app/features/billing/contact-sales/contact-sales-team.test.tsx create mode 100644 apps/react-router/saas-template/app/features/billing/contact-sales/contact-sales-team.tsx create mode 100644 apps/react-router/saas-template/app/features/billing/create-subscription-modal-content.test.tsx create mode 100644 apps/react-router/saas-template/app/features/billing/create-subscription-modal-content.tsx create mode 100644 apps/react-router/saas-template/app/features/billing/description-list.tsx create mode 100644 apps/react-router/saas-template/app/features/billing/edit-billing-email-modal-content.tsx create mode 100644 apps/react-router/saas-template/app/features/billing/pricing.tsx create mode 100644 apps/react-router/saas-template/app/features/billing/stripe-admin.server.ts create mode 100644 apps/react-router/saas-template/app/features/billing/stripe-event-factories.server.ts create mode 100644 apps/react-router/saas-template/app/features/billing/stripe-event-handlers.server.ts create mode 100644 apps/react-router/saas-template/app/features/billing/stripe-factories.server.ts create mode 100644 apps/react-router/saas-template/app/features/billing/stripe-helpers.server.ts create mode 100644 apps/react-router/saas-template/app/features/billing/stripe-prices-model.server.ts create mode 100644 apps/react-router/saas-template/app/features/billing/stripe-product-model.server.ts create mode 100644 apps/react-router/saas-template/app/features/billing/stripe-subscription-model.server.ts create mode 100644 apps/react-router/saas-template/app/features/billing/stripe-subscription-schedule-model.server.ts create mode 100644 apps/react-router/saas-template/app/features/color-scheme/color-scheme-constants.ts create mode 100644 apps/react-router/saas-template/app/features/color-scheme/color-scheme.server.ts create mode 100644 apps/react-router/saas-template/app/features/color-scheme/theme-toggle.test.tsx create mode 100644 apps/react-router/saas-template/app/features/color-scheme/theme-toggle.tsx create mode 100644 apps/react-router/saas-template/app/features/color-scheme/use-color-scheme.ts create mode 100644 apps/react-router/saas-template/app/features/landing/bento-grid.tsx create mode 100644 apps/react-router/saas-template/app/features/landing/cta.tsx create mode 100644 apps/react-router/saas-template/app/features/landing/description.tsx create mode 100644 apps/react-router/saas-template/app/features/landing/faq.tsx create mode 100644 apps/react-router/saas-template/app/features/landing/features.tsx create mode 100644 apps/react-router/saas-template/app/features/landing/footer.tsx create mode 100644 apps/react-router/saas-template/app/features/landing/header.tsx create mode 100644 apps/react-router/saas-template/app/features/landing/hero.tsx create mode 100644 apps/react-router/saas-template/app/features/landing/landing-page.tsx create mode 100644 apps/react-router/saas-template/app/features/landing/logos.tsx create mode 100644 apps/react-router/saas-template/app/features/landing/svgs/playwright-icon.tsx create mode 100644 apps/react-router/saas-template/app/features/landing/svgs/reactsquad-logo-icon.tsx create mode 100644 apps/react-router/saas-template/app/features/landing/svgs/rr-lockup-dark-icon.tsx create mode 100644 apps/react-router/saas-template/app/features/landing/svgs/rr-lockup-light-icon.tsx create mode 100644 apps/react-router/saas-template/app/features/localization/i18next-middleware.server.ts create mode 100644 apps/react-router/saas-template/app/features/localization/locales/de/billing.ts create mode 100644 apps/react-router/saas-template/app/features/localization/locales/de/color-scheme.ts create mode 100644 apps/react-router/saas-template/app/features/localization/locales/de/drag-and-drop.ts create mode 100644 apps/react-router/saas-template/app/features/localization/locales/de/index.ts create mode 100644 apps/react-router/saas-template/app/features/localization/locales/de/landing.ts create mode 100644 apps/react-router/saas-template/app/features/localization/locales/de/notifications.ts create mode 100644 apps/react-router/saas-template/app/features/localization/locales/de/onboarding.ts create mode 100644 apps/react-router/saas-template/app/features/localization/locales/de/organizations.ts create mode 100644 apps/react-router/saas-template/app/features/localization/locales/de/settings.ts create mode 100644 apps/react-router/saas-template/app/features/localization/locales/de/translation.ts create mode 100644 apps/react-router/saas-template/app/features/localization/locales/de/user-authentication.ts create mode 100644 apps/react-router/saas-template/app/features/localization/locales/en/billing.ts create mode 100644 apps/react-router/saas-template/app/features/localization/locales/en/color-scheme.ts create mode 100644 apps/react-router/saas-template/app/features/localization/locales/en/drag-and-drop.ts create mode 100644 apps/react-router/saas-template/app/features/localization/locales/en/index.ts create mode 100644 apps/react-router/saas-template/app/features/localization/locales/en/landing.ts create mode 100644 apps/react-router/saas-template/app/features/localization/locales/en/notifications.ts create mode 100644 apps/react-router/saas-template/app/features/localization/locales/en/onboarding.ts create mode 100644 apps/react-router/saas-template/app/features/localization/locales/en/organizations.ts create mode 100644 apps/react-router/saas-template/app/features/localization/locales/en/settings.ts create mode 100644 apps/react-router/saas-template/app/features/localization/locales/en/translation.ts create mode 100644 apps/react-router/saas-template/app/features/localization/locales/en/user-authentication.ts create mode 100644 apps/react-router/saas-template/app/features/localization/locales/index.ts create mode 100644 apps/react-router/saas-template/app/features/notifications/notification-components.test.tsx create mode 100644 apps/react-router/saas-template/app/features/notifications/notification-components.tsx create mode 100644 apps/react-router/saas-template/app/features/notifications/notification-constants.ts create mode 100644 apps/react-router/saas-template/app/features/notifications/notifications-button.test.tsx create mode 100644 apps/react-router/saas-template/app/features/notifications/notifications-button.tsx create mode 100644 apps/react-router/saas-template/app/features/notifications/notifications-factories.server.ts create mode 100644 apps/react-router/saas-template/app/features/notifications/notifications-helpers.server.test.ts create mode 100644 apps/react-router/saas-template/app/features/notifications/notifications-helpers.server.ts create mode 100644 apps/react-router/saas-template/app/features/notifications/notifications-model.server.ts create mode 100644 apps/react-router/saas-template/app/features/notifications/notifications-panel-content.test.tsx create mode 100644 apps/react-router/saas-template/app/features/notifications/notifications-panel-content.tsx create mode 100644 apps/react-router/saas-template/app/features/notifications/notifications-schemas.ts create mode 100644 apps/react-router/saas-template/app/features/notifications/use-pending-notifications.ts create mode 100644 apps/react-router/saas-template/app/features/onboarding/onboarding-helpers.server.test.ts create mode 100644 apps/react-router/saas-template/app/features/onboarding/onboarding-helpers.server.ts create mode 100644 apps/react-router/saas-template/app/features/onboarding/organization/onboarding-organization-action.server.ts create mode 100644 apps/react-router/saas-template/app/features/onboarding/organization/onboarding-organization-consants.ts create mode 100644 apps/react-router/saas-template/app/features/onboarding/organization/onboarding-organization-schemas.ts create mode 100644 apps/react-router/saas-template/app/features/onboarding/talent-map.tsx create mode 100644 apps/react-router/saas-template/app/features/onboarding/user-account/onboarding-user-account-action.server.ts create mode 100644 apps/react-router/saas-template/app/features/onboarding/user-account/onboarding-user-account-constants.ts create mode 100644 apps/react-router/saas-template/app/features/onboarding/user-account/onboarding-user-account-schemas.ts create mode 100644 apps/react-router/saas-template/app/features/organizations/accept-email-invite/accept-email-invite-action.server.ts create mode 100644 apps/react-router/saas-template/app/features/organizations/accept-email-invite/accept-email-invite-constants.ts create mode 100644 apps/react-router/saas-template/app/features/organizations/accept-email-invite/accept-email-invite-helpers.server.test.ts create mode 100644 apps/react-router/saas-template/app/features/organizations/accept-email-invite/accept-email-invite-helpers.server.ts create mode 100644 apps/react-router/saas-template/app/features/organizations/accept-email-invite/accept-email-invite-page.test.tsx create mode 100644 apps/react-router/saas-template/app/features/organizations/accept-email-invite/accept-email-invite-page.tsx create mode 100644 apps/react-router/saas-template/app/features/organizations/accept-email-invite/accept-email-invite-session.server.spec.ts create mode 100644 apps/react-router/saas-template/app/features/organizations/accept-email-invite/accept-email-invite-session.server.ts create mode 100644 apps/react-router/saas-template/app/features/organizations/accept-invite-link/accept-invite-link-action.server.ts create mode 100644 apps/react-router/saas-template/app/features/organizations/accept-invite-link/accept-invite-link-constants.ts create mode 100644 apps/react-router/saas-template/app/features/organizations/accept-invite-link/accept-invite-link-helpers.server.test.ts create mode 100644 apps/react-router/saas-template/app/features/organizations/accept-invite-link/accept-invite-link-helpers.server.ts create mode 100644 apps/react-router/saas-template/app/features/organizations/accept-invite-link/accept-invite-link-page.test.tsx create mode 100644 apps/react-router/saas-template/app/features/organizations/accept-invite-link/accept-invite-link-page.tsx create mode 100644 apps/react-router/saas-template/app/features/organizations/accept-invite-link/accept-invite-link-session.server.spec.ts create mode 100644 apps/react-router/saas-template/app/features/organizations/accept-invite-link/accept-invite-link-session.server.ts create mode 100644 apps/react-router/saas-template/app/features/organizations/accept-invite-link/invite-link-use-model.server.ts create mode 100644 apps/react-router/saas-template/app/features/organizations/create-organization/create-organization-action.server.ts create mode 100644 apps/react-router/saas-template/app/features/organizations/create-organization/create-organization-constants.ts create mode 100644 apps/react-router/saas-template/app/features/organizations/create-organization/create-organization-form-card.test.tsx create mode 100644 apps/react-router/saas-template/app/features/organizations/create-organization/create-organization-form-card.tsx create mode 100644 apps/react-router/saas-template/app/features/organizations/create-organization/create-organization-schemas.ts create mode 100644 apps/react-router/saas-template/app/features/organizations/layout/app-header.test.tsx create mode 100644 apps/react-router/saas-template/app/features/organizations/layout/app-header.tsx create mode 100644 apps/react-router/saas-template/app/features/organizations/layout/app-sidebar.tsx create mode 100644 apps/react-router/saas-template/app/features/organizations/layout/layout-helpers.server.test.ts create mode 100644 apps/react-router/saas-template/app/features/organizations/layout/layout-helpers.server.ts create mode 100644 apps/react-router/saas-template/app/features/organizations/layout/layout-helpers.test.ts create mode 100644 apps/react-router/saas-template/app/features/organizations/layout/layout-helpers.ts create mode 100644 apps/react-router/saas-template/app/features/organizations/layout/nav-group.test.tsx create mode 100644 apps/react-router/saas-template/app/features/organizations/layout/nav-group.tsx create mode 100644 apps/react-router/saas-template/app/features/organizations/layout/nav-user.test.tsx create mode 100644 apps/react-router/saas-template/app/features/organizations/layout/nav-user.tsx create mode 100644 apps/react-router/saas-template/app/features/organizations/layout/organization-switcher-session.server.spec.ts create mode 100644 apps/react-router/saas-template/app/features/organizations/layout/organization-switcher-session.server.ts create mode 100644 apps/react-router/saas-template/app/features/organizations/layout/organization-switcher.test.tsx create mode 100644 apps/react-router/saas-template/app/features/organizations/layout/organization-switcher.tsx create mode 100644 apps/react-router/saas-template/app/features/organizations/layout/sidebar-layout-action.server.ts create mode 100644 apps/react-router/saas-template/app/features/organizations/layout/sidebar-layout-constants.ts create mode 100644 apps/react-router/saas-template/app/features/organizations/layout/sidebar-layout-schemas.ts create mode 100644 apps/react-router/saas-template/app/features/organizations/organization-constants.ts create mode 100644 apps/react-router/saas-template/app/features/organizations/organization-membership-model.server.ts create mode 100644 apps/react-router/saas-template/app/features/organizations/organizations-email-invite-link-model.server.ts create mode 100644 apps/react-router/saas-template/app/features/organizations/organizations-factories.server.ts create mode 100644 apps/react-router/saas-template/app/features/organizations/organizations-helpers.server.test.ts create mode 100644 apps/react-router/saas-template/app/features/organizations/organizations-helpers.server.ts create mode 100644 apps/react-router/saas-template/app/features/organizations/organizations-invite-link-model.server.ts create mode 100644 apps/react-router/saas-template/app/features/organizations/organizations-middleware.server.ts create mode 100644 apps/react-router/saas-template/app/features/organizations/organizations-model.server.ts create mode 100644 apps/react-router/saas-template/app/features/organizations/settings/general/danger-zone.tsx create mode 100644 apps/react-router/saas-template/app/features/organizations/settings/general/general-organization-settings-action.server.ts create mode 100644 apps/react-router/saas-template/app/features/organizations/settings/general/general-organization-settings.tsx create mode 100644 apps/react-router/saas-template/app/features/organizations/settings/general/general-settings-constants.ts create mode 100644 apps/react-router/saas-template/app/features/organizations/settings/general/general-settings-schemas.ts create mode 100644 apps/react-router/saas-template/app/features/organizations/settings/general/organization-info.test.tsx create mode 100644 apps/react-router/saas-template/app/features/organizations/settings/general/organization-info.tsx create mode 100644 apps/react-router/saas-template/app/features/organizations/settings/settings-sidebar.tsx create mode 100644 apps/react-router/saas-template/app/features/organizations/settings/team-members/invite-by-email-card.test.tsx create mode 100644 apps/react-router/saas-template/app/features/organizations/settings/team-members/invite-by-email-card.tsx create mode 100644 apps/react-router/saas-template/app/features/organizations/settings/team-members/invite-email.tsx create mode 100644 apps/react-router/saas-template/app/features/organizations/settings/team-members/invite-link-card.test.tsx create mode 100644 apps/react-router/saas-template/app/features/organizations/settings/team-members/invite-link-card.tsx create mode 100644 apps/react-router/saas-template/app/features/organizations/settings/team-members/team-members-action.server.tsx create mode 100644 apps/react-router/saas-template/app/features/organizations/settings/team-members/team-members-constants.ts create mode 100644 apps/react-router/saas-template/app/features/organizations/settings/team-members/team-members-helpers.server.test.ts create mode 100644 apps/react-router/saas-template/app/features/organizations/settings/team-members/team-members-helpers.server.ts create mode 100644 apps/react-router/saas-template/app/features/organizations/settings/team-members/team-members-settings-schemas.ts create mode 100644 apps/react-router/saas-template/app/features/organizations/settings/team-members/team-members-table.test.tsx create mode 100644 apps/react-router/saas-template/app/features/organizations/settings/team-members/team-members-table.tsx create mode 100644 apps/react-router/saas-template/app/features/pastebin/paste-helpers.server.ts create mode 100644 apps/react-router/saas-template/app/features/user-accounts/settings/account/account-settings-action.server.ts create mode 100644 apps/react-router/saas-template/app/features/user-accounts/settings/account/account-settings-constants.ts create mode 100644 apps/react-router/saas-template/app/features/user-accounts/settings/account/account-settings-helpers.server.test.ts create mode 100644 apps/react-router/saas-template/app/features/user-accounts/settings/account/account-settings-helpers.server.ts create mode 100644 apps/react-router/saas-template/app/features/user-accounts/settings/account/account-settings-schemas.ts create mode 100644 apps/react-router/saas-template/app/features/user-accounts/settings/account/account-settings.tsx create mode 100644 apps/react-router/saas-template/app/features/user-accounts/settings/account/danger-zone.test.tsx create mode 100644 apps/react-router/saas-template/app/features/user-accounts/settings/account/danger-zone.tsx create mode 100644 apps/react-router/saas-template/app/features/user-accounts/user-account-constants.ts create mode 100644 apps/react-router/saas-template/app/features/user-accounts/user-accounts-factories.server.ts create mode 100644 apps/react-router/saas-template/app/features/user-accounts/user-accounts-helpers.server.spec.ts create mode 100644 apps/react-router/saas-template/app/features/user-accounts/user-accounts-helpers.server.ts create mode 100644 apps/react-router/saas-template/app/features/user-accounts/user-accounts-model.server.ts create mode 100644 apps/react-router/saas-template/app/features/user-authentication/floating-paths.tsx create mode 100644 apps/react-router/saas-template/app/features/user-authentication/login/login-action.server.ts create mode 100644 apps/react-router/saas-template/app/features/user-authentication/login/login-constants.ts create mode 100644 apps/react-router/saas-template/app/features/user-authentication/login/login-schemas.ts create mode 100644 apps/react-router/saas-template/app/features/user-authentication/login/login-verification-awaiting.test.tsx create mode 100644 apps/react-router/saas-template/app/features/user-authentication/login/login-verification-awaiting.tsx create mode 100644 apps/react-router/saas-template/app/features/user-authentication/registration/register-action.server.ts create mode 100644 apps/react-router/saas-template/app/features/user-authentication/registration/registration-constants.ts create mode 100644 apps/react-router/saas-template/app/features/user-authentication/registration/registration-schemas.ts create mode 100644 apps/react-router/saas-template/app/features/user-authentication/registration/registration-verification-awaiting.test.tsx create mode 100644 apps/react-router/saas-template/app/features/user-authentication/registration/registration-verification-awaiting.tsx create mode 100644 apps/react-router/saas-template/app/features/user-authentication/supabase.server.ts create mode 100644 apps/react-router/saas-template/app/features/user-authentication/use-countdown.test.tsx create mode 100644 apps/react-router/saas-template/app/features/user-authentication/use-countdown.ts create mode 100644 apps/react-router/saas-template/app/features/user-authentication/user-authentication-constants.ts create mode 100644 apps/react-router/saas-template/app/features/user-authentication/user-authentication-factories.ts create mode 100644 apps/react-router/saas-template/app/features/user-authentication/user-authentication-helpers.server.ts create mode 100644 apps/react-router/saas-template/app/features/user-authentication/user-authentication-helpers.test.ts create mode 100644 apps/react-router/saas-template/app/features/user-authentication/user-authentication-helpers.ts create mode 100644 apps/react-router/saas-template/app/features/user-authentication/user-authentication-middleware.server.ts create mode 100644 apps/react-router/saas-template/app/hooks/use-media-query.ts create mode 100644 apps/react-router/saas-template/app/hooks/use-mobile.ts create mode 100644 apps/react-router/saas-template/app/hooks/use-prefers-reduced-motion.ts create mode 100644 apps/react-router/saas-template/app/hooks/use-preview-url.test.tsx create mode 100644 apps/react-router/saas-template/app/hooks/use-preview-url.ts create mode 100644 apps/react-router/saas-template/app/hooks/use-toast.ts create mode 100644 apps/react-router/saas-template/app/lib/README.md create mode 100644 apps/react-router/saas-template/app/lib/supabase/client.ts create mode 100644 apps/react-router/saas-template/app/lib/utils.ts create mode 100644 apps/react-router/saas-template/app/root.tsx create mode 100644 apps/react-router/saas-template/app/routes.ts create mode 100644 apps/react-router/saas-template/app/routes/$.tsx create mode 100644 apps/react-router/saas-template/app/routes/_authenticated-routes+/_authenticated-routes-layout.tsx create mode 100644 apps/react-router/saas-template/app/routes/_authenticated-routes+/onboarding+/_index.tsx create mode 100644 apps/react-router/saas-template/app/routes/_authenticated-routes+/onboarding+/_onboarding-layout.tsx create mode 100644 apps/react-router/saas-template/app/routes/_authenticated-routes+/onboarding+/organization.spec.ts create mode 100644 apps/react-router/saas-template/app/routes/_authenticated-routes+/onboarding+/organization.tsx create mode 100644 apps/react-router/saas-template/app/routes/_authenticated-routes+/onboarding+/user-account.spec.ts create mode 100644 apps/react-router/saas-template/app/routes/_authenticated-routes+/onboarding+/user-account.tsx create mode 100644 apps/react-router/saas-template/app/routes/_authenticated-routes+/organizations_+/$organizationSlug+/_sidebar-layout.spec.ts create mode 100644 apps/react-router/saas-template/app/routes/_authenticated-routes+/organizations_+/$organizationSlug+/_sidebar-layout.tsx create mode 100644 apps/react-router/saas-template/app/routes/_authenticated-routes+/organizations_+/$organizationSlug+/analytics.tsx create mode 100644 apps/react-router/saas-template/app/routes/_authenticated-routes+/organizations_+/$organizationSlug+/dashboard.tsx create mode 100644 apps/react-router/saas-template/app/routes/_authenticated-routes+/organizations_+/$organizationSlug+/get-help.tsx create mode 100644 apps/react-router/saas-template/app/routes/_authenticated-routes+/organizations_+/$organizationSlug+/pastes.$pasteId.tsx create mode 100644 apps/react-router/saas-template/app/routes/_authenticated-routes+/organizations_+/$organizationSlug+/pastes.tsx create mode 100644 apps/react-router/saas-template/app/routes/_authenticated-routes+/organizations_+/$organizationSlug+/projects.tsx create mode 100644 apps/react-router/saas-template/app/routes/_authenticated-routes+/organizations_+/$organizationSlug+/projects_.active.tsx create mode 100644 apps/react-router/saas-template/app/routes/_authenticated-routes+/organizations_+/$organizationSlug+/settings+/_index.tsx create mode 100644 apps/react-router/saas-template/app/routes/_authenticated-routes+/organizations_+/$organizationSlug+/settings+/_organization-settings-layout.tsx create mode 100644 apps/react-router/saas-template/app/routes/_authenticated-routes+/organizations_+/$organizationSlug+/settings+/billing.spec.ts create mode 100644 apps/react-router/saas-template/app/routes/_authenticated-routes+/organizations_+/$organizationSlug+/settings+/billing.tsx create mode 100644 apps/react-router/saas-template/app/routes/_authenticated-routes+/organizations_+/$organizationSlug+/settings+/billing_.success.tsx create mode 100644 apps/react-router/saas-template/app/routes/_authenticated-routes+/organizations_+/$organizationSlug+/settings+/general.spec.ts create mode 100644 apps/react-router/saas-template/app/routes/_authenticated-routes+/organizations_+/$organizationSlug+/settings+/general.tsx create mode 100644 apps/react-router/saas-template/app/routes/_authenticated-routes+/organizations_+/$organizationSlug+/settings+/members.spec.ts create mode 100644 apps/react-router/saas-template/app/routes/_authenticated-routes+/organizations_+/$organizationSlug+/settings+/members.tsx create mode 100644 apps/react-router/saas-template/app/routes/_authenticated-routes+/organizations_+/_index.tsx create mode 100644 apps/react-router/saas-template/app/routes/_authenticated-routes+/organizations_+/new.spec.ts create mode 100644 apps/react-router/saas-template/app/routes/_authenticated-routes+/organizations_+/new.tsx create mode 100644 apps/react-router/saas-template/app/routes/_authenticated-routes+/settings+/_settings-layout.tsx create mode 100644 apps/react-router/saas-template/app/routes/_authenticated-routes+/settings+/account.spec.ts create mode 100644 apps/react-router/saas-template/app/routes/_authenticated-routes+/settings+/account.tsx create mode 100644 apps/react-router/saas-template/app/routes/_index.tsx create mode 100644 apps/react-router/saas-template/app/routes/_user-authentication+/_anonymous-routes+/_anonymous-routes-layout.tsx create mode 100644 apps/react-router/saas-template/app/routes/_user-authentication+/_anonymous-routes+/auth.callback.ts create mode 100644 apps/react-router/saas-template/app/routes/_user-authentication+/_anonymous-routes+/login.confirm.ts create mode 100644 apps/react-router/saas-template/app/routes/_user-authentication+/_anonymous-routes+/login.spec.ts create mode 100644 apps/react-router/saas-template/app/routes/_user-authentication+/_anonymous-routes+/login.tsx create mode 100644 apps/react-router/saas-template/app/routes/_user-authentication+/_anonymous-routes+/register.confirm.ts create mode 100644 apps/react-router/saas-template/app/routes/_user-authentication+/_anonymous-routes+/register.spec.ts create mode 100644 apps/react-router/saas-template/app/routes/_user-authentication+/_anonymous-routes+/register.tsx create mode 100644 apps/react-router/saas-template/app/routes/_user-authentication+/_user-authentication-layout.tsx create mode 100644 apps/react-router/saas-template/app/routes/_user-authentication+/logout.ts create mode 100644 apps/react-router/saas-template/app/routes/api+/locales+/$lng.$ns.ts create mode 100644 apps/react-router/saas-template/app/routes/api+/v1+/stripe.webhooks.ts create mode 100644 apps/react-router/saas-template/app/routes/color-scheme.tsx create mode 100644 apps/react-router/saas-template/app/routes/contact-sales.spec.ts create mode 100644 apps/react-router/saas-template/app/routes/contact-sales.tsx create mode 100644 apps/react-router/saas-template/app/routes/organizations_+/email-invite.spec.ts create mode 100644 apps/react-router/saas-template/app/routes/organizations_+/email-invite.tsx create mode 100644 apps/react-router/saas-template/app/routes/organizations_+/invite-link.spec.ts create mode 100644 apps/react-router/saas-template/app/routes/organizations_+/invite-link.tsx create mode 100644 apps/react-router/saas-template/app/routes/p.$pasteId.tsx create mode 100644 apps/react-router/saas-template/app/routes/paste.$pasteId.tsx create mode 100644 apps/react-router/saas-template/app/routes/pricing.tsx create mode 100644 apps/react-router/saas-template/app/routes/privacy-policy.tsx create mode 100644 apps/react-router/saas-template/app/routes/terms-of-service.tsx create mode 100644 apps/react-router/saas-template/app/test/mocks/browser.ts create mode 100644 apps/react-router/saas-template/app/test/mocks/handlers/resend.ts create mode 100644 apps/react-router/saas-template/app/test/mocks/handlers/stripe.ts create mode 100644 apps/react-router/saas-template/app/test/mocks/handlers/supabase/README.md create mode 100644 apps/react-router/saas-template/app/test/mocks/handlers/supabase/auth.ts create mode 100644 apps/react-router/saas-template/app/test/mocks/handlers/supabase/index.ts create mode 100644 apps/react-router/saas-template/app/test/mocks/handlers/supabase/mock-sessions.ts create mode 100644 apps/react-router/saas-template/app/test/mocks/handlers/supabase/storage.ts create mode 100644 apps/react-router/saas-template/app/test/mocks/msw-utils.server.ts create mode 100644 apps/react-router/saas-template/app/test/mocks/msw-utils.ts create mode 100644 apps/react-router/saas-template/app/test/mocks/server.ts create mode 100644 apps/react-router/saas-template/app/test/mocks/utils.ts create mode 100644 apps/react-router/saas-template/app/test/msw-test-utils.ts create mode 100644 apps/react-router/saas-template/app/test/react-test-utils.tsx create mode 100644 apps/react-router/saas-template/app/test/server-test-utils.ts create mode 100644 apps/react-router/saas-template/app/test/setup-browser-test-environment.ts create mode 100644 apps/react-router/saas-template/app/test/setup-server-test-environment.ts create mode 100644 apps/react-router/saas-template/app/test/test-utils.ts create mode 100644 apps/react-router/saas-template/app/test/vitest.global-setup.ts create mode 100644 apps/react-router/saas-template/app/types/conform.d.ts create mode 100644 apps/react-router/saas-template/app/types/i18next.d.ts create mode 100644 apps/react-router/saas-template/app/types/sudo.d.ts create mode 100644 apps/react-router/saas-template/app/utils/async-for-each.server.test.ts create mode 100644 apps/react-router/saas-template/app/utils/async-for-each.server.ts create mode 100644 apps/react-router/saas-template/app/utils/async-pipe.server.test.ts create mode 100644 apps/react-router/saas-template/app/utils/async-pipe.server.ts create mode 100644 apps/react-router/saas-template/app/utils/client-hints.tsx create mode 100644 apps/react-router/saas-template/app/utils/combine-headers.server.test.ts create mode 100644 apps/react-router/saas-template/app/utils/combine-headers.server.ts create mode 100644 apps/react-router/saas-template/app/utils/database.server.ts create mode 100644 apps/react-router/saas-template/app/utils/define-custom-metadata.ts create mode 100644 apps/react-router/saas-template/app/utils/email.server.ts create mode 100644 apps/react-router/saas-template/app/utils/env.server.ts create mode 100644 apps/react-router/saas-template/app/utils/get-domain-url.server.ts create mode 100644 apps/react-router/saas-template/app/utils/get-error-message.test.ts create mode 100644 apps/react-router/saas-template/app/utils/get-error-message.ts create mode 100644 apps/react-router/saas-template/app/utils/get-is-data-with-response-init.server.test.ts create mode 100644 apps/react-router/saas-template/app/utils/get-is-data-with-response-init.server.ts create mode 100644 apps/react-router/saas-template/app/utils/get-is-response.test.ts create mode 100644 apps/react-router/saas-template/app/utils/get-is-response.ts create mode 100644 apps/react-router/saas-template/app/utils/get-page-title.server.test.ts create mode 100644 apps/react-router/saas-template/app/utils/get-page-title.server.ts create mode 100644 apps/react-router/saas-template/app/utils/get-search-parameter-from-request.server.test.ts create mode 100644 apps/react-router/saas-template/app/utils/get-search-parameter-from-request.server.ts create mode 100644 apps/react-router/saas-template/app/utils/honeypot.server.ts create mode 100644 apps/react-router/saas-template/app/utils/http-responses.server.test.ts create mode 100644 apps/react-router/saas-template/app/utils/http-responses.server.ts create mode 100644 apps/react-router/saas-template/app/utils/nonce-provider.ts create mode 100644 apps/react-router/saas-template/app/utils/request-info.ts create mode 100644 apps/react-router/saas-template/app/utils/s3.server.ts create mode 100644 apps/react-router/saas-template/app/utils/security-middleware.server.ts create mode 100644 apps/react-router/saas-template/app/utils/slugify.server.test.ts create mode 100644 apps/react-router/saas-template/app/utils/slugify.server.ts create mode 100644 apps/react-router/saas-template/app/utils/storage-helpers.server.spec.ts create mode 100644 apps/react-router/saas-template/app/utils/storage-helpers.server.ts create mode 100644 apps/react-router/saas-template/app/utils/storage.server.ts create mode 100644 apps/react-router/saas-template/app/utils/throw-if-entity-is-missing.server.test.ts create mode 100644 apps/react-router/saas-template/app/utils/throw-if-entity-is-missing.server.ts create mode 100644 apps/react-router/saas-template/app/utils/to-form-data.test.ts create mode 100644 apps/react-router/saas-template/app/utils/to-form-data.ts create mode 100644 apps/react-router/saas-template/app/utils/toast.server.spec.ts create mode 100644 apps/react-router/saas-template/app/utils/toast.server.ts create mode 100644 apps/react-router/saas-template/app/utils/types.ts create mode 100644 apps/react-router/saas-template/app/utils/validate-form-data.server.test.ts create mode 100644 apps/react-router/saas-template/app/utils/validate-form-data.server.ts create mode 100644 apps/react-router/saas-template/biome.json create mode 100644 apps/react-router/saas-template/commitlint.config.mjs create mode 100644 apps/react-router/saas-template/components.json create mode 100644 apps/react-router/saas-template/decisions/README.md create mode 100644 apps/react-router/saas-template/package-lock.json create mode 100644 apps/react-router/saas-template/package.json create mode 100644 apps/react-router/saas-template/playwright.config.ts create mode 100644 apps/react-router/saas-template/playwright/e2e/billing/billing.e2e.ts create mode 100644 apps/react-router/saas-template/playwright/e2e/billing/stripe-webhook.e2e.ts create mode 100644 apps/react-router/saas-template/playwright/e2e/landing/contact-sales.e2e.ts create mode 100644 apps/react-router/saas-template/playwright/e2e/landing/landing.e2e.ts create mode 100644 apps/react-router/saas-template/playwright/e2e/landing/pricing.e2e.ts create mode 100644 apps/react-router/saas-template/playwright/e2e/notifications/notifications.e2e.ts create mode 100644 apps/react-router/saas-template/playwright/e2e/onboarding/onboarding-organization.e2e.ts create mode 100644 apps/react-router/saas-template/playwright/e2e/onboarding/onboarding-user-account.e2e.ts create mode 100644 apps/react-router/saas-template/playwright/e2e/onboarding/onboarding.e2e.ts create mode 100644 apps/react-router/saas-template/playwright/e2e/organizations/general-organization-settings.e2e.ts create mode 100644 apps/react-router/saas-template/playwright/e2e/organizations/new-organization.e2e.ts create mode 100644 apps/react-router/saas-template/playwright/e2e/organizations/organization-layout.e2e.ts create mode 100644 apps/react-router/saas-template/playwright/e2e/organizations/organizations-email-invite.e2e.ts create mode 100644 apps/react-router/saas-template/playwright/e2e/organizations/organizations-invite-link.e2e.ts create mode 100644 apps/react-router/saas-template/playwright/e2e/organizations/organizations-list.e2e.ts create mode 100644 apps/react-router/saas-template/playwright/e2e/organizations/organizations-settings-layout.e2e.ts create mode 100644 apps/react-router/saas-template/playwright/e2e/organizations/team-member-organization-settings.e2e.ts create mode 100644 apps/react-router/saas-template/playwright/e2e/settings/account.e2e.ts create mode 100644 apps/react-router/saas-template/playwright/e2e/user-authentication/auth-callback.e2e.ts create mode 100644 apps/react-router/saas-template/playwright/e2e/user-authentication/login-confirm.e2e.ts create mode 100644 apps/react-router/saas-template/playwright/e2e/user-authentication/login.e2e.ts create mode 100644 apps/react-router/saas-template/playwright/e2e/user-authentication/logout.e2e.ts create mode 100644 apps/react-router/saas-template/playwright/e2e/user-authentication/register-confirm.e2e.ts create mode 100644 apps/react-router/saas-template/playwright/e2e/user-authentication/register.e2e.ts create mode 100644 apps/react-router/saas-template/playwright/fixtures/200x200.jpg create mode 100644 apps/react-router/saas-template/playwright/global-setup.ts create mode 100644 apps/react-router/saas-template/playwright/global-tear-down.ts create mode 100644 apps/react-router/saas-template/playwright/utils.ts create mode 100644 apps/react-router/saas-template/prisma.config.ts create mode 100644 apps/react-router/saas-template/prisma/schema.prisma create mode 100644 apps/react-router/saas-template/prisma/seed.ts create mode 100644 apps/react-router/saas-template/public/.well-known/appspecific/com.chrome.devtools.json create mode 100644 apps/react-router/saas-template/public/favicon.ico create mode 100644 apps/react-router/saas-template/public/fonts/inter/Inter-Black.woff2 create mode 100644 apps/react-router/saas-template/public/fonts/inter/Inter-BlackItalic.woff2 create mode 100644 apps/react-router/saas-template/public/fonts/inter/Inter-Bold.woff2 create mode 100644 apps/react-router/saas-template/public/fonts/inter/Inter-BoldItalic.woff2 create mode 100644 apps/react-router/saas-template/public/fonts/inter/Inter-ExtraBold.woff2 create mode 100644 apps/react-router/saas-template/public/fonts/inter/Inter-ExtraBoldItalic.woff2 create mode 100644 apps/react-router/saas-template/public/fonts/inter/Inter-ExtraLight.woff2 create mode 100644 apps/react-router/saas-template/public/fonts/inter/Inter-ExtraLightItalic.woff2 create mode 100644 apps/react-router/saas-template/public/fonts/inter/Inter-Italic.woff2 create mode 100644 apps/react-router/saas-template/public/fonts/inter/Inter-Light.woff2 create mode 100644 apps/react-router/saas-template/public/fonts/inter/Inter-LightItalic.woff2 create mode 100644 apps/react-router/saas-template/public/fonts/inter/Inter-Medium.woff2 create mode 100644 apps/react-router/saas-template/public/fonts/inter/Inter-MediumItalic.woff2 create mode 100644 apps/react-router/saas-template/public/fonts/inter/Inter-Regular.woff2 create mode 100644 apps/react-router/saas-template/public/fonts/inter/Inter-SemiBold.woff2 create mode 100644 apps/react-router/saas-template/public/fonts/inter/Inter-SemiBoldItalic.woff2 create mode 100644 apps/react-router/saas-template/public/fonts/inter/Inter-Thin.woff2 create mode 100644 apps/react-router/saas-template/public/fonts/inter/Inter-ThinItalic.woff2 create mode 100644 apps/react-router/saas-template/public/images/app-billing-dark.png create mode 100644 apps/react-router/saas-template/public/images/app-billing-light.png create mode 100644 apps/react-router/saas-template/public/images/app-dark-members.png create mode 100644 apps/react-router/saas-template/public/images/app-dark.png create mode 100644 apps/react-router/saas-template/public/images/app-light-members.png create mode 100644 apps/react-router/saas-template/public/images/app-light.png create mode 100644 apps/react-router/saas-template/public/images/app-mobile-dark.png create mode 100644 apps/react-router/saas-template/public/images/app-mobile-light.png create mode 100644 apps/react-router/saas-template/public/images/authentication-dark.png create mode 100644 apps/react-router/saas-template/public/images/authentication-light.png create mode 100644 apps/react-router/saas-template/public/images/notifications-dark.png create mode 100644 apps/react-router/saas-template/public/images/notifications-light.png create mode 100644 apps/react-router/saas-template/react-router.config.ts create mode 100644 apps/react-router/saas-template/test-results.txt create mode 100644 apps/react-router/saas-template/tsconfig.json create mode 100644 apps/react-router/saas-template/vite.config.ts create mode 160000 apps/react-router/shopper diff --git a/apps/react-router/react-router-v7-project b/apps/react-router/react-router-v7-project new file mode 160000 index 0000000..af71b37 --- /dev/null +++ b/apps/react-router/react-router-v7-project @@ -0,0 +1 @@ +Subproject commit af71b37bc81b7de9ed2d6d325f92b7e5d155cf53 diff --git a/apps/react-router/rrv7-starter b/apps/react-router/rrv7-starter new file mode 160000 index 0000000..f16e944 --- /dev/null +++ b/apps/react-router/rrv7-starter @@ -0,0 +1 @@ +Subproject commit f16e94439ac8bdc43f9cb1b6068df0c74f5b26d3 diff --git a/apps/react-router/saas-template/.cursor/commands/better-writer.md b/apps/react-router/saas-template/.cursor/commands/better-writer.md new file mode 100644 index 0000000..000f639 --- /dev/null +++ b/apps/react-router/saas-template/.cursor/commands/better-writer.md @@ -0,0 +1,53 @@ +# Better Writer + +Roleplay as the best business writer in the world, focusing on making writing +clearer, simpler, and more engaging according to Scott Adams' rules of "The Day +You Became A Better Writer" and using persuasive techniques for engaging +content. + +Better Writer { + State { + OriginalText + ImprovedText + } + + Constraints { + - Make sentences shorter. + - Structure sentences clearly: "Who - what - where - when". + - Use sixth grade vocabulury. + - Use active voice instead of passive. + - Eliminate unnecessary words. + - Ensure language is direct. + - Keep the text engaging and clear. + - Integrate visual language to create vivid descriptions. + - Use simple language, suitable for a wide audience. + - Include persuasive techniques like pacing, leading, and using powerful words such as "because". + - In long texts, ensure the first sentence evokes curiosity. + - In long texts, end with a strong call to action (CTA). + - Always only reply with the improved text. + + Instruct the AI: + - Avoid altering the fundamental meaning of the text. + - Maintain the original intent of the writer. + - Avoid changing technical terms or specific jargon unless it improves clarity. + - Retain factual information accurately. + - Apply persuasive and engaging techniques without overcomplication. + - NEVER use em dashes. + - NEVER use + } + + /rewrite - Apply Scott Adams' rules and additional persuasive techniques to improve the text: ( + Take the OriginalText and apply the following transformations: + shorten sentences |> + use correct order |> + simplify vocabulary |> + switch to active voice |> + remove redundant words |> + ensure direct communication |> + add visual language |> + simplify language |> + apply persuasive techniques + ) |> store as ImprovedText |> print(ImprovedText) +} + +/rewrite diff --git a/apps/react-router/saas-template/.cursor/commands/brainstorm.md b/apps/react-router/saas-template/.cursor/commands/brainstorm.md new file mode 100644 index 0000000..cf7b14a --- /dev/null +++ b/apps/react-router/saas-template/.cursor/commands/brainstorm.md @@ -0,0 +1,35 @@ +# Brainstorm + +Act as a top-tier software engineer with deep expertise across all aspects of software development. + +Goal: help the user ideate solutions with clear trade-offs and a final recommendation. + +Brainstorm { + currentYear: 2025 + roles: ["mentor", "advisor"] + + /brainstorm(topic, context?) => Output +} + +Constraints { + Think about edge cases and how to handle them. + NEVER modify code, unless explicitly requested. + Consider scalability and maintainability (DX). + Important: You are an agent. + Ask the user questions and request missing information if necessary. + Thoroughly read relevant code if the question or its answer involves the codebase. + If necessary, suggest tools & packages to install and use. + You might need to perform a web search to find current information, e.g. for: + - Recent technology developments + - Latest APIs + - Latest best practices + - Regulatory changes + If "you want to list multiple options" { + When listing multiple options, give your recommendation with reasons. + Before giving your recommendation, list the options unbiasedly, and THEN give your recommendation. + } + This is very important to ensure software works as expected and that user safety is protected. + Please do your best work. + If you suggest code, read the "Style guide and best practices for writing JavaScript and TypeScript code" rule first and write code that conforms to it. + Great attention to instructions will be rewarded with virtual cookies 🍪 +} diff --git a/apps/react-router/saas-template/.cursor/commands/commit.md b/apps/react-router/saas-template/.cursor/commands/commit.md new file mode 100644 index 0000000..9dbdd70 --- /dev/null +++ b/apps/react-router/saas-template/.cursor/commands/commit.md @@ -0,0 +1,15 @@ +# Commit + +Act as a senior software engineer to commit changes to the repository in non-interactive modes ONLY, using the following template: + +"$type${[(scope)]}{[!]}: $description":where `[]` is optional and `!` is a breaking change + +Types: fix|feat|chore|docs|refactor|test|perf|build|ci|style|revert|$other + +Constraints { + When committing, don't log about logging in the commit message. + Use multiple -m flags, one for each log entry. + Limit the first commit message line length to 50 characters. + Use conventional commits with a scope, title and body. + Do NOT add new things to the CHANGELOG.md file. +} diff --git a/apps/react-router/saas-template/.cursor/commands/debug.md b/apps/react-router/saas-template/.cursor/commands/debug.md new file mode 100644 index 0000000..19698d1 --- /dev/null +++ b/apps/react-router/saas-template/.cursor/commands/debug.md @@ -0,0 +1,22 @@ +# Debug + +Act as a top-tier software engineer with meticulous debugging skills. + +DebugDetective { + Output Format { + Be as concise as possible. + - Issue Summary + - Key Findings + - Root Cause Analysis + - Recommended Solutions (optional: include prevention Strategies) + } + + Constraints { + NEVER write, modify, or generate any code + You may suggest code changes in responses + You MUST thoroughly search for relevant code + Always read and analyze code thoroughly before drawing conclusions + Understand the issue completely before proposing solutions + This is very important to ensure software works as expected and that user safety is protected. Please do your best work. Great attention to instructions will be rewarded with virtual cookies 🍪 + } +} diff --git a/apps/react-router/saas-template/.cursor/commands/documentation.md b/apps/react-router/saas-template/.cursor/commands/documentation.md new file mode 100644 index 0000000..2072a2f --- /dev/null +++ b/apps/react-router/saas-template/.cursor/commands/documentation.md @@ -0,0 +1,34 @@ +# Documentation Guidelines + +Act as a top-tier software engineer with deep expertise in documentation. + +## Examples First + +- Prefer runnable/copy-paste examples near the top of pages. +- Pair every concept with a minimal example and an expanded one. + +## Skimmable Structure + +- Use clear H2/H3s, short paragraphs, bullet lists, and callouts. +- Add small diagrams or screenshots where they clarify meaning. + +## Tone & Language + +- Be precise and concise. +- Avoid jargon, idioms, and filler (e.g., “simply”, “basically”, “in order to”). +- Use active voice and direct instructions. + +## Onboarding First + +- Start with the simplest path to success (“Hello World” → real task). +- Reveal complexity gradually; link to deeper sections. + +## Workarounds + +- Document known gaps and workarounds with risks and follow-ups. + +## Quality Gate (AI can help) + +- Run spelling/grammar checks. +- Trim redundancy and overly long sentences. +- Verify examples actually run/build/test. diff --git a/apps/react-router/saas-template/.cursor/commands/log.md b/apps/react-router/saas-template/.cursor/commands/log.md new file mode 100644 index 0000000..676d0cd --- /dev/null +++ b/apps/react-router/saas-template/.cursor/commands/log.md @@ -0,0 +1,64 @@ +# log + +Act as a senior software engineer to log completed epics using the following template: + +``` +## $date + +- $emoji - $epicName - $briefDescription +``` + +# What to Log + +**LOG ONLY COMPLETED EPICS** - Focus on completed epics that represent significant user-facing value: + +- ✅ **Epic Completions**: Major feature releases, tool creation, system implementations +- ✅ **User-Impacting Changes**: New capabilities, workflows, or developer experience improvements +- ✅ **Architecture Decisions**: Significant refactoring, new patterns, or system redesigns + +**DO NOT LOG**: +- ❌ Config file changes (.json, .config updates) +- ❌ File organization/moves (directory restructuring) +- ❌ Minor bug fixes (unless epic-level) +- ❌ Documentation updates (unless epic-level) +- ❌ Dependency updates +- ❌ Internal refactoring +- ❌ Test additions/changes +- ❌ Meta-work (logging, planning, etc.) + +# Emojis + +Use the following emoji to represent the epic type: + +- 🚀 - new feature +- 🐛 - bug fix +- 📝 - documentation +- 🔄 - refactor +- 📦 - dependency update +- 🎨 - design +- 📱 - UI/UX +- 📊 - analytics +- 🔒 - security + +Constraints { + Always use reverse chronological order. + Add most recent epics to the top. + Keep descriptions brief (< 50 chars). + Focus on epic-level accomplishments, not implementation details. + Never log meta-work or trivial changes. + Omit the "epic" from the description. +} + + +gitChanges() { + git add . + git --no-pager diff --cached +} + +planChanges() { + Check the plan diff to detect recently completed plan tasks. +} + +detectChanges() { + gitChanges |> planChanges |> logDetectedChanges +} diff --git a/apps/react-router/saas-template/.cursor/commands/name.md b/apps/react-router/saas-template/.cursor/commands/name.md new file mode 100644 index 0000000..a6c6514 --- /dev/null +++ b/apps/react-router/saas-template/.cursor/commands/name.md @@ -0,0 +1,48 @@ +# Name + +Act as a top-tier software engineer who knows how give clear, descriptive names to functions and variables. + +FacadeConstraints { + - These constraints ONLY apply to facade functions in `-model` files. + - Function names must follow `()` pattern. + - Allowed actions: save | retrieve | update | delete. + - Entity names are singular, in PascalCase. + - Use “With…” to indicate included relations before “From/In/ToDatabase”. + - Use “By…” to indicate lookup key(s) last; key names must match schema fields exactly. + - Use “And” to chain multiple included relations or keys. + - Use “ToDatabase” for create, “FromDatabase” for reads, “InDatabase” for updates, “FromDatabase” for deletes. +} + +FactoryFunctionConstraints { + - These constraints ONLY apply to factory functions in `-factories` files. + - Function names start with `createPopulated` for base/compound entities. + - Use explicit entity suffixes, e.g. Product | Price | Subscription | SubscriptionItem | SubscriptionSchedule | SubscriptionSchedulePhase reflecting their respective database models. + - Compound names enumerate included relations in order, joined with `With...And...` (e.g., `createPopulatedStripeSubscriptionWithItemsAndPriceAndProduct`). +} + +BooleanFunctionConstraints { + - Apply only to functions that return a boolean. + - Variables returned from the function must be in **active voice**, describing the current state of the entity (e.g., `isActive`, `hasExpired`, `isDeactivated`). + - If the function is standalone or checks computed state, prefix with `get` → `getIsActive(entity)`, `getHasExpired(date)`. +} + +Constraints { + - Use active voice. + - Use clear, consistent naming. + - Functions should be verbs, e.g. increment(), filter(). + - Boolean variables should read like yes/no questions, e.g. isActive, hasPermission. + - Prefer **standalone verbs** over `noun.method` forms, e.g. `createUser()` instead of `User.create()`. + - Avoid **noun-heavy** and **redundant** names, e.g. `filter(fn, array)` instead of `matchingItemsFromArray(fn, array)`. + - Avoid `"doSomething"` style names, e.g. use `notify()` instead of `Notifier.doNotification()`. + - For **lifecycle methods**, prefer `beforeX` / `afterX` over `willX` / `didX`, e.g. `beforeUpdate()`. + - Use **strong negatives** over weak ones, e.g. `isEmpty(thing)` instead of `!isDefined(thing)`. + - **Mixins and function decorators** should follow the `with${Thing}` pattern, e.g. `withUser`, `withFeatures`, `withAuth`. + - Follow framework specific naming conventions, e.g. React components should be in PascalCase, React Hooks should be prefixed with `use`, etc. +} + +fn name(context) { + Apply the relevant constraints to come up with a list of possible names. + Give your recommendation for the best name with reasoning. +}; + +/name diff --git a/apps/react-router/saas-template/.cursor/commands/plan.md b/apps/react-router/saas-template/.cursor/commands/plan.md new file mode 100644 index 0000000..a716ab7 --- /dev/null +++ b/apps/react-router/saas-template/.cursor/commands/plan.md @@ -0,0 +1,36 @@ +# Plan + +Act as a top-tier software architect specializing in full-stack web development. Your job is to break down complex requests into manageable, sequential tasks that can be executed one at a time with user approval. + +Constraints { + You MUST read and follow the rules in .cursor/rules/js-and-ts.mdc for JavaScript/TypeScript best practices. + You MUST read and follow the rules in .cursor/rules/jsx-and-tsx.mdc for React best practices. + Observe and conform to existing code style, patterns, and conventions in the project. + Thoroughly search for and read ALL relevant code for the task before making changes. Use multiple search queries with different wording to ensure comprehensive coverage. + If necessary, ask the user to gather any additional context or clarification needed BEFORE writing the plan. + If necessary, use web search to look up the latest APIs. + Important: If blocked or uncertain, ask clarifying questions rather than making assumptions. + You MUST break down the plan into distinct tasks. + If a task reveals new information that changes the plan, pause and re-plan + Once the plan is written, you MUST review it: + - Code changes comply with the rules. + - If a task's code changes are more than 50 lines, you MUST break it down into smaller tasks. + The plan MUST be written into ai/plans/ with filename format: yyyy-mm-dd-title.md (using current date and descriptive title). +} + +PlanUpdateConstraints { + When asked to update a task or part of the plan, implement those updates cleanly without adding markers like "Updated" or explanatory text about why the change is needed. + The plan should stand on its own without history or change annotations. +} + +TaskConstraints { + Tasks should be mostly code changes. + Briefly state the task as a short title. + Tasks should be enumerated. + Only include explanatory text if ABSOLUTELY NECESSARY. + Each task MUST contain the suggested code changes necessary to implement it. + Each task MUST have code changes LESS than 50 lines. + Tasks should come in logical order so they can be implemented one after the other WITHOUT breaking the code. +} + +/plan({ useTodoList: true }) diff --git a/apps/react-router/saas-template/.cursor/commands/svg-to-react.md b/apps/react-router/saas-template/.cursor/commands/svg-to-react.md new file mode 100644 index 0000000..ab7fa07 --- /dev/null +++ b/apps/react-router/saas-template/.cursor/commands/svg-to-react.md @@ -0,0 +1,114 @@ +# SVG to React Component Generator + +Act as a senior React and TypeScript engineer specializing in SVG optimization and React component generation. + +## Usage Example + +example() { + 1. Show the usage example. !Supply the example complete and unchanged. + 2. Ask the user to try to convert SVGs with the `/convert` command. +} + +""" +// Input: shorts.svg + + +// Output: shorts-icon.tsx +import type { IconProps } from '~/utils/types'; + +export function ShortsIcon({ className, ...props }: IconProps) { + return ( + + ); +} +""" + +help() { + Explain how to use the SVG to React Component Generator: + - How to provide SVG files (path or content) + - How components are generated + - Naming conventions + List available commands. + Mention that you can see a complete example with the `/example` command. +} + +interface SVGConverter { + State { + svgFiles = [] + components = [] + } + + fn convertSVG(svg, options = {}) { + Parse SVG content. + Extract viewBox from width/height if not present. + Remove hardcoded dimensions (width, height). + Remove fill="none" from root svg. + Remove fill="#fff" from paths. + Remove unnecessary groups and clip paths. + Format SVG attributes to React format. + Add TypeScript types using IconProps. + Add accessibility attributes. + Generate component name from file name. + Return formatted React component. + } + + fn convertMultipleSVGs(svgs) { + Process each SVG in parallel. + Maintain consistent naming and formatting. + Return array of components. + } + + fn inferFilePath(description) { + Try to extract file path from context. + Return normalized path. + } + + Constraints { + Always use TypeScript. + Always add aria-hidden="true" to SVGs. + Always spread props to allow style overrides. + Always format component name as PascalCase + "Icon" suffix. + Always use IconProps from '~/utils/types'. + Always use className prop for styling. + Always use currentColor for fill. + Format output with 2 space indentation. + Sort SVG attributes alphabetically. + Extract viewBox from width/height if not present. + Remove hardcoded dimensions. + Remove unnecessary groups and clip paths. + Create a separate file for each icon component. + Name the file same as the component in kebab-case. + Delete original SVG file after successful conversion. + !Never show example code unless explicitly requested via /example command. + } + + ComponentConstraints { + Export as named function component. + Use type import for IconProps. + Spread props last to allow overrides. + Preserve SVG viewBox. + Remove unnecessary SVG attributes. + Remove hardcoded colors. + Remove unnecessary grouping. + Place each component in its own file. + } + + /convert [svg] - Convert single SVG to React component + /convert-multiple [svgs] - Convert multiple SVGs to React components + /convert-file [path] - Convert SVG file to React component + /example - Show a complete example of SVG conversion + /help - Show available commands +} diff --git a/apps/react-router/saas-template/.cursor/commands/unit-tests.md b/apps/react-router/saas-template/.cursor/commands/unit-tests.md new file mode 100644 index 0000000..65e23a7 --- /dev/null +++ b/apps/react-router/saas-template/.cursor/commands/unit-tests.md @@ -0,0 +1,43 @@ +# Unit Tests + +Act as a top-tier software engineer with serious testing skills. + +TestConstraints { + Ensure that the test answers these 5 questions { + 1. What is the unit under test? (test should be in a named describe block) + 2. What is the expected behavior? ($given and $should arguments are adequate) + 3. What is the actual output? (the unit under test was exercised by the test) + 4. What is the expected output? ($expected and/or $should are adequate) + 5. How can we find the bug? (implicitly answered if the above questions are answered correctly) + } + + Tests must be: + - Readable - Answer the 5 questions. + - Isolated/Integrated + - Units under test should be isolated from each other + - Tests should be isolated from each other with no shared mutable state. + - For integration tests, test integration with the real system. + - Thorough - Test expected edge cases + - Explicit - Everything you need to know to understand the test should be part of the test itself. If you need to produce the same data structure many times for many test cases, create a factory function and invoke it from the individual tests, rather than sharing mutable fixtures between tests. + + Use Vitest with describe, expect, and test. + Tests must use the "given: ..., should: ..." prose format. + Ensure that within each test case: + - There is an empty line **before** the `actual` variable assignment. + - **Do NOT** add an empty line between the `actual` and `expected` assignments. + - There is an empty line **after** the `expected` variable assignment before the `toEqual` assertion. + Use cuid2 for IDs unless specified otherwise. + Colocate tests with functions. Test files should be in the same folder as the implementation file. + If an argument is a database entity, use an existing factory function and override values as needed. + Capture the `actual` and the `expected` value in variables. + The top-level `describe` block should describe the component under test. + The `test` block should describe the case via `"given: ..., should: ..."`. + Avoid `expect.any(Constructor)` in assertions. Expect specific values instead. + Always use the `toEqual` equality assertion. +} + +Constraints { + Carefully think through correct output. + Avoid hallucination. + This is very important to ensure software works as expected and that user safety is protected. Please do your best work. +} diff --git a/apps/react-router/saas-template/.cursor/commands/write.md b/apps/react-router/saas-template/.cursor/commands/write.md new file mode 100644 index 0000000..7769b3f --- /dev/null +++ b/apps/react-router/saas-template/.cursor/commands/write.md @@ -0,0 +1,135 @@ +# Write + +Act as a world class business writer, focusing on making writing clearer, +simpler, and more engaging. + +## Sentence structure + +- Keep sentences short and scannable; break long ones into two. +- Express one idea per sentence; remove filler and redundancy. +- Put the subject first and the verb early; avoid nested clauses. +- Prefer simple present tense and concrete verbs over nominalizations. +- Lead with the main point; put qualifiers and context after the core statement. + +## Voice and tone + +- Write like humans speak. Avoid corporate jargon and marketing fluff. +- Be confident and direct. Avoid softening phrases like "I think," "maybe," or + "could." +- Use active voice instead of passive voice. +- Use positive phrasing—say what something _is_ rather than what it _isn't_. +- Say "you" more than "we" when addressing external audiences. +- Use contractions like "I'll," "won't," and "can't" for a warmer tone. + +## Specificity and evidence + +- Be specific with facts and data instead of vague superlatives. +- Back up claims with concrete examples or metrics. +- Highlight customers and community members over company achievements. +- Use realistic, product-based examples instead of `foo/bar/baz` in code. +- Make content concrete, visual, and falsifiable. + +## Title creation + +- Make a promise in the title so readers know exactly what they'll get if they + click. +- Tap into controversial points your audience holds and back them up with data + (use wisely, avoid clickbait). +- Share something uniquely helpful that makes readers better at meaningful + aspects of their lives. +- Avoid vague titles like "My Thoughts On XYZ." Titles should be opinions or + shareable facts. +- Write placeholder titles first, complete the content, then spend time + iterating on titles at the end. + +## Banned words + +- `a bit` → remove +- `a little` → remove +- `actually/actual` → remove +- `agile` → remove +- `arguably` → remove +- `assistance` → "help" +- `attempt` → "try" +- `battle tested` → remove +- `best practices` → "proven approaches" +- `blazing fast/lightning fast` → "build XX% faster" +- `business logic` → remove +- `cognitive load` → remove +- `commence` → "start" +- `delve` → "go into" +- `disrupt/disruptive` → remove +- `facilitate` → "help" or "ease" +- `game-changing` → specific benefit +- `great` → remove or be specific +- `implement` → "do" +- `individual` → "man" or "woman" +- `initial` → "first" +- `innovative` → remove +- `just` → remove +- `leverage` → "use" +- `mission-critical` → "important" +- `modern/modernized` → remove +- `numerous` → "many" +- `out of the box` → remove +- `performant` → "fast and reliable" +- `pretty/quite/rather/really/very` → remove +- `referred to as` → "called" +- `remainder` → "rest" +- `robust` → "strong" +- `seamless/seamlessly` → "automatic" +- `sufficient` → "enough" +- `that` → often removable, context dependent +- `thing` → be specific +- `utilize` → "use" +- `webinar` → "online event" + +## Banned phrases + +- "I think/I believe/we believe" → state directly +- "it seems" → remove +- "sort of/kind of" → remove +- "pretty much" → remove +- "a lot/a little" → be specific +- "By developers, for developers" → remove +- "We can't wait to see what you'll build" → remove +- "We obsess over \_\_" → remove +- "The future of \_\_" → remove +- "We're excited" → "We look forward" +- "Today, we're excited to" → remove + +## Avoid LLM patterns + +- Replace em dashes (—) with semicolons, commas, or sentence breaks. +- Avoid starting responses with "Great question!", "You're right!", or "Let me + help you." +- Don't use phrases like "Let's dive into..." +- Skip cliché intros like "In today's fast-paced digital world" or "In the + ever-evolving landscape of." +- Avoid phrases like "it's not just [x], it's [y].". +- Avoid self-referential disclaimers like "As an AI" or "I'm here to help you + with." +- Don't use high-school essay closers: "In conclusion," "Overall," or "To + summarize." +- Avoid numbered lists in cases where bullets work better. +- Don't end with "Hope this helps!" or similar closers. +- Avoid overusing transition words like "Furthermore," "Additionally," or + "Moreover." +- Replace "In conclusion" with direct statements. +- Avoid hedge words: "might," "perhaps," "potentially" unless uncertainty is + real. +- Don't stack hedging phrases: "may potentially," "it's important to note that." +- Don't create perfectly symmetrical paragraphs or lists that start with + "Firstly... Secondly..." +- Remove Unicode artifacts when copy-pasting: smart quotes (”), em-dashes, + non-breaking spaces. +- Avoid title-case headings; prefer sentence casing. +- Use `'` instead of `′`. +- Delete empty citation placeholders like "[1]" with no actual source. + +## Punctuation and formatting + +- Use Oxford commas consistently. +- Use exclamation points sparingly. +- Sentences can start with "But" and "And"—but don't overuse. +- Use periods instead of commas when possible for clarity. diff --git a/apps/react-router/saas-template/.cursor/rules/facades.mdc b/apps/react-router/saas-template/.cursor/rules/facades.mdc new file mode 100644 index 0000000..5f8fbc2 --- /dev/null +++ b/apps/react-router/saas-template/.cursor/rules/facades.mdc @@ -0,0 +1,23 @@ +--- +description: When writing facade functions, use this guide for facade functions best practices and guidance +globs: **/*-model.server.ts,**/*-model.server.js, +alwaysApply: false +--- + +# Facade Functions + +FacadeConstraints { + - Apply only to functions in `*-model.ts` files. + - Function names must follow `()` pattern. + - Allowed actions: save | retrieve | update | delete. + - Entity names are singular, in PascalCase. + - Use “With…” to indicate included relations before “From/In/ToDatabase”. + - Use “By…” to indicate lookup key(s) last; key names must match schema fields exactly. + - Use “And” to chain multiple included relations or keys. + - Use “ToDatabase” for create, “FromDatabase” for reads, “InDatabase” for updates, “FromDatabase” for deletes. + - Facades must perform a single database operation (no business logic). + - Facades must always return raw Prisma results (no transformations). + - Include JSDoc with description, @param, and @returns tags matching the function name and purpose. + - Prefer explicit Prisma includes/selects; avoid `include: { *: true }`. + - Function bodies must use the `prisma..` pattern directly. +} diff --git a/apps/react-router/saas-template/.cursor/rules/js-and-ts.mdc b/apps/react-router/saas-template/.cursor/rules/js-and-ts.mdc new file mode 100644 index 0000000..ca36513 --- /dev/null +++ b/apps/react-router/saas-template/.cursor/rules/js-and-ts.mdc @@ -0,0 +1,77 @@ +--- +description: When writing JavaScript or TypeScript code, use this guide for JavaScript best practices and guidance +globs: **/*.js,**/*.jsx,**/*.ts,**/*.tsx +alwaysApply: false +--- + +# JavaScript/TypeScript guide + +Act as a top-tier software engineer with serious JavaScript/TypeScript discipline to carefully implement high quality software. + +## Before Writing Code + +- Read the lint and formatting rules. +- Observe the project's relevant existing code. +- Conform to existing code style, patterns, and conventions unless directed otherwise. Note: these instructions count as "directed otherwise" unless the user explicitly overrides them. + +## Principles + +- DOT +- YAGNI +- KISS +- DRY +- SDA - Self Describing APIs +- Simplicity - "Simplicity is removing the obvious, and adding the meaningful." + - Obvious stuff gets hidden in the abstraction. + - Meaningful stuff is what needs to be customized and passed in as parameters. + - Functions should have default parameters whenever it makes sense so that callers can supply only what is different from the default. + +Constraints { + Be concise. + Favor functional programming; keep functions short, pure, and composable. + Favor map, filter, reduce over manual loops. + Prefer immutability; use const, spread, and rest operators instead of mutation. + One job per function; separate mapping from IO. + Obey the projects lint and formatting rules. + Omit needless code and variables; prefer composition with partial application and point-free style. + Chain operations rather than introducing intermediate variables, e.g. `[x].filter(p).map(f)` + Avoid loose procedural sequences; compose clear pipelines instead. + Avoid `class` and `extends` as much as possible. Prefer composition of functions and data structures over inheritance. + Keep related code together; group by feature, not by technical type. + Put statements and expressions in positive form. + Use parallel code for parallel concepts. + Avoid null/undefined arguments; use options objects instead. + Use concise syntax: arrow functions, object destructuring, array destructuring, template literals. + Avoid verbose property assignments. bad: `const a = obj.a;` good: `const { a } = obj;` + Assign reasonable defaults directly in function signatures. + `const createExpectedUser = ({ id = createId(), name = '', description = '' } = {}) => ({ id, name, description });` + Principle: SDA. This means: + Parameter values should be explicitly named and expressed in function signatures: + Bad: `const createUser = (payload = {}) => ({` + Good: `const createUser = ({ id = createId(), name = '', description = ''} = {}) =>` + Notice how default values also provide hints for type inference. + Avoid IIFEs. Use block scopes, modules, or normal arrow functions instead. Principle: KISS + Avoid using || for defaults. Use parameter defaults instead. See above. + Prefer async/await or asyncPipe over raw promise chains. + Use strict equality (===). + Modularize by feature; one concern per file or function; prefer named exports. +} + +NamingConstraints { + Use active voice. + Use clear, consistent naming. + Functions should be verbs. e.g. `increment()`, `filter()`. + Predicates and booleans should read like yes/no questions. e.g. `isActive`, `hasPermission`. + Prefer standalone verbs over noun.method. e.g. `createUser()` not `User.create()`. + Avoid noun-heavy and redundant names. e.g. `filter(fn, array)` not `matchingItemsFromArray(fn, array)`. + Avoid "doSomething" style. e.g. `notify()` not `Notifier.doNotification()`. + Lifecycle methods: prefer `beforeX` / `afterX` over `willX` / `didX`. e.g. `beforeUpdate()`. + Use strong negatives over weak ones: `isEmpty(thing)` not `!isDefined(thing)`. + Mixins and function decorators use `with${Thing}`. e.g. `withUser`, `withFeatures`, `withAuth`. +} + +Comments { + Favor docblocks for public APIs - but keep them minimal. + Ensure that any comments are necessary and add value. Never reiterate the style guides. Avoid obvious redundancy with the code, but short one-line comments that aid scannability are okay. + Comments should stand-alone months or years later. Assume that the reader is not familiar with the task plan or epic. +} diff --git a/apps/react-router/saas-template/.cursor/rules/jsx-and-tsx.mdc b/apps/react-router/saas-template/.cursor/rules/jsx-and-tsx.mdc new file mode 100644 index 0000000..3c104a3 --- /dev/null +++ b/apps/react-router/saas-template/.cursor/rules/jsx-and-tsx.mdc @@ -0,0 +1,75 @@ +--- +description: When writing React code, use this guide for React best practices and guidance +globs: **/*.js,**/*.jsx,**/*.tsx +alwaysApply: false +--- + +# React guide + +Act as a top-tier software engineer with extensive React ecosystem knowledge to build high-quality fullstack web applications. + +## Before Writing Code + +- Observe the project's relevant existing code. +- Conform to existing code style, patterns, and conventions unless directed otherwise. Note: these instructions count as "directed otherwise" unless the user explicitly overrides them. + +## Principles + +- Display/container component pattern + - Split your component into display components, which are pure functions that map props to JSX, and container components, which are (optional) stateful components that wrap one display component. + - Then compose them together in the parent or page/route component. + +Constraints { + Be concise. + You're using React Router V7 (the successor to Remix). + Use ShadCN/ui for components. If a component is missing, install it. + Modularize by feature; one concern per file or component; prefer named exports. + This project uses TailwindCSS V4, so you can use things like container queries and child selectors. +} + +NamingConstraints { + Use clear, descriptive, consistent naming. + Components should be postfixed with `Component`. + Props should be the component's name, postifxed with `ComponentProps`. +} + +TypeConstraints { + Use proper React TypeScript types: MouseEventHandler, ChangeEventHandler, ReactNode, React.Ref, ComponentProps<'element'>, etc. Never use generic () => void or (event: any) => void. + When extending HTML elements or existing components, use ComponentProps to inherit their props: ComponentProps<'input'>, ComponentProps<'button'>, ComponentProps. + This project uses Prisma. If a prop comes from a database entity, use the entities type for it, e.g.: + - type UserMenuProps = Pick & { + onLogout: MouseEventHandler; + organizationName: Organization['name']; + } + When using server/database return types: Awaited>, wrap with NonNullable<> if guaranteed to exist. +} + +FormConstraints { + For react-hook-form + Zod forms: + - Export schema types: export type Schema = z.infer + - Export error types: export type SchemaErrors = FieldErrors + - Optionally include intent field: intent: z.literal('actionName') + - Pass translation keys (not translated strings) in validation error messages + For loading/submission states: + - Use consistent naming: isSubmitting = false, isLoading{Action} = false, is{Action}ing{Entity} = false + - Always provide default values in function signature + - Disable forms with fieldset disabled={isSubmitting || isLoading} instead of individual disabled props + For form components: + - Accept errors?: SchemaErrors (always optional) + - Accept children?: ReactNode for composition + - Use FormProvider for parent, useFormContext for nested components + - Provide complete defaultValues object to useForm with all fields initialized +} + +AccessibilityConstraints { + For interactive components, provide aria props with defaults: + - *AriaLabel props for screen readers (e.g., countryAriaLabel = 'Select country') + - *Placeholder props for empty states + - FormControl handles aria-describedby and aria-invalid automatically +} + +InternationalizationConstraints { + Use useTranslation with namespace and keyPrefix: const { t } = useTranslation('namespace', { keyPrefix: 'section' }); + Use Trans component for interpolation with links/components. + FormMessage components handle translation of error keys automatically. +} diff --git a/apps/react-router/saas-template/.dockerignore b/apps/react-router/saas-template/.dockerignore new file mode 100644 index 0000000..9b8d514 --- /dev/null +++ b/apps/react-router/saas-template/.dockerignore @@ -0,0 +1,4 @@ +.react-router +build +node_modules +README.md \ No newline at end of file diff --git a/apps/react-router/saas-template/.env.example b/apps/react-router/saas-template/.env.example new file mode 100644 index 0000000..9222ce2 --- /dev/null +++ b/apps/react-router/saas-template/.env.example @@ -0,0 +1,32 @@ +# Environment variables declared in this file are automatically made available to Prisma. +# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema + +# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB. +# See the documentation for all the connection string options: https://pris.ly/d/connection-strings + +# Prisma +DATABASE_URL=postgresql://postgres:postgres@localhost:6666/saas_template + +# Supabase +VITE_SUPABASE_URL=https://abcdefghijklmnopqrst.supabase.co +VITE_SUPABASE_ANON_KEY="ey_mock-anon-key" +SUPABASE_SERVICE_ROLE_KEY="ey_mock-service-role" + +# App +APP_URL="http://localhost:3000" +COOKIE_SECRET=a-sufficiently-long-secret-ideally-32-characters-or-more +RESEND_API_KEY="re_blAh_blaHBlaHblahBLAhBlAh" + +# Supabase Storage via S3 compatible API +STORAGE_ACCESS_KEY_ID="ey_mock-access-key" +STORAGE_SECRET_ACCESS_KEY="ey_mock-secret-key" +STORAGE_REGION="us-east-1" +SUPABASE_PROJECT_ID="abcdefghijklmnopqrst" # project ref + +# Stripe +HONEYPOT_SECRET="super-duper-s3cret" +STRIPE_SECRET_KEY="sk_test_" +STRIPE_WEBHOOK_SECRET="whsec_" + +# New +ALLOW_INDEXING="true" diff --git a/apps/react-router/saas-template/.github/workflows/ci.yml b/apps/react-router/saas-template/.github/workflows/ci.yml new file mode 100644 index 0000000..dc6f13e --- /dev/null +++ b/apps/react-router/saas-template/.github/workflows/ci.yml @@ -0,0 +1,172 @@ +name: Pull Request + +on: + push: + branches: + - main + - dev + pull_request: {} + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + pull-requests: read + +jobs: + lint: + name: ⬣ Biome + runs-on: ubuntu-latest + steps: + - name: ⬇️ Checkout repo + uses: actions/checkout@v4 + + - name: ⎔ Setup node + uses: actions/setup-node@v5 + with: + node-version: 22 + cache: 'npm' + + - name: 📥 Install dependencies + run: npm ci + + - name: 🏄 Copy test env vars + run: cp .env.example .env + + - name: 🛠️ Typegen + run: npm run typegen + + - name: 🔬 Lint + run: npm run lint + + type-check: + name: ʦ TypeScript + runs-on: ubuntu-latest + steps: + - name: ⬇️ Checkout repo + uses: actions/checkout@v4 + + - name: ⎔ Setup node + uses: actions/setup-node@v5 + with: + node-version: 22 + cache: 'npm' + + - name: 📥 Install dependencies + run: npm ci + + - name: 🏄 Copy test env vars + run: cp .env.example .env + + - name: 🔎 Type check + run: npm run typecheck + + commitlint: + name: ⚙️ commitlint + runs-on: ubuntu-latest + if: github.actor != 'dependabot[bot]' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - name: ⬇️ Checkout repo + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: ⚙️ commitlint + uses: wagoid/commitlint-github-action@v6 + + vitest: + name: ⚡ Vitest + runs-on: ubuntu-latest + services: + postgres: + image: postgres:latest + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: postgres + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U postgres" --health-interval 10s + --health-timeout 5s --health-retries 5 + steps: + - name: ⬇️ Checkout repo + uses: actions/checkout@v4 + + - name: ⎔ Setup node + uses: actions/setup-node@v5 + with: + node-version: 22 + cache: 'npm' + + - name: 📥 Install dependencies + run: npm ci + + - name: 🏄 Copy test env vars + run: cp .env.example .env + + - name: 🛠 Setup Database + run: npm run prisma:wipe + + - name: 🛠️ Typegen + run: npm run typegen + + - name: ⚡ Run vitest + run: npm run test -- --coverage + + playwright-chrome: + name: 🎭 Playwright Chrome + runs-on: ubuntu-latest + services: + postgres: + image: postgres:latest + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: postgres + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U postgres" --health-interval 10s + --health-timeout 5s --health-retries 5 + steps: + - name: ⬇️ Checkout repo + uses: actions/checkout@v4 + + - name: ⎔ Setup node + uses: actions/setup-node@v5 + with: + node-version: 22 + cache: 'npm' + + - name: 📥 Install dependencies + run: npm ci + + - name: 🏄 Copy test env vars + run: cp .env.example .env + + - name: 🌐 Install Playwright Browsers + run: npx playwright install --with-deps chromium + + - name: 🛠 Setup Database + run: npm run prisma:wipe + + - name: 🛠️ Typegen + run: npm run typegen + + - name: 🏗️ Build Application (with mocks) + run: npm run build + + - name: 🎭 Playwright Run Chrome + run: npx playwright test --project=chromium + + - name: 📸 Playwright Screenshots + uses: actions/upload-artifact@v4 + if: failure() + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 diff --git a/apps/react-router/saas-template/.gitignore b/apps/react-router/saas-template/.gitignore new file mode 100644 index 0000000..bf57516 --- /dev/null +++ b/apps/react-router/saas-template/.gitignore @@ -0,0 +1,30 @@ +.DS_Store +/node_modules/ +todo.md + +# React Router +/.react-router/ +/build/ + +*.env + +# TypeScript +*.tsbuildinfo + +# Playwright +node_modules/ +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ + +# Random +count-lines.sh +stripe-events.txt + +# Test fixtures +/tests/fixtures/ +/app/tests/mocks/fixtures/ + +# Prisma +/app/generated/ diff --git a/apps/react-router/saas-template/.husky/commit-msg b/apps/react-router/saas-template/.husky/commit-msg new file mode 100644 index 0000000..b575621 --- /dev/null +++ b/apps/react-router/saas-template/.husky/commit-msg @@ -0,0 +1 @@ +bunx --no -- commitlint --edit "$1" \ No newline at end of file diff --git a/apps/react-router/saas-template/.husky/pre-commit b/apps/react-router/saas-template/.husky/pre-commit new file mode 100755 index 0000000..5009744 --- /dev/null +++ b/apps/react-router/saas-template/.husky/pre-commit @@ -0,0 +1 @@ +bunx biome check --staged . && bun typecheck diff --git a/apps/react-router/saas-template/.vscode/settings.json b/apps/react-router/saas-template/.vscode/settings.json new file mode 100644 index 0000000..f577591 --- /dev/null +++ b/apps/react-router/saas-template/.vscode/settings.json @@ -0,0 +1,26 @@ +{ + "[javascript]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[json]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[jsonc]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[typescript]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[typescriptreact]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "editor.codeActionsOnSave": { + "source.fixAll.biome": "explicit", + "source.organizeImports.biome": "explicit" + }, + "editor.defaultFormatter": "biomejs.biome", + "editor.formatOnPaste": true, + "editor.formatOnSave": true, + "emmet.showExpandedAbbreviation": "never", + "typescript.tsdk": "node_modules/typescript/lib" +} diff --git a/apps/react-router/saas-template/CONTRIBUTING.md b/apps/react-router/saas-template/CONTRIBUTING.md new file mode 100644 index 0000000..69ec27f --- /dev/null +++ b/apps/react-router/saas-template/CONTRIBUTING.md @@ -0,0 +1,186 @@ +# Contributing to the React Router SaaS Template + +Thank you for your interest in contributing to the React Router SaaS Template! +This guide outlines the process, standards, and best practices for contributing +to the project. + +**Boy scout rule:** If you see something that can be improved, please improve +it! + +> Leave the code better than you found it. + +## Table of Contents + +- [Code of Conduct](#code-of-conduct) +- [Getting Started](#getting-started) +- [Issue Workflow](#issue-workflow) +- [Development Workflow](#development-workflow) +- [Commit Guidelines](#commit-guidelines) +- [Pull Request Process](#pull-request-process) +- [Testing Guidelines](#testing-guidelines) +- [Style Guide](#style-guide) +- [Additional Sections to Consider](#additional-sections-to-consider) + +## Code of Conduct + +We expect all contributors to adhere to our code of conduct. Please be +respectful, inclusive, and professional in all interactions. + +## Getting Started + +Follow the steps outlined in the main [README.md](./README.md#getting-started). + +## Issue Workflow + +1. **Create an issue** describing your proposed feature or bug fix. Include: + - A clear title + - Description of the problem or feature + - Relevant context (screenshots, logs, examples) +2. **Get approval** from one of the maintainers (the ReactSquad team) before + opening a pull request, so you don't waste time on a pull request that won't + be merged. +3. Once approved, proceed to implement the changes in a new branch. + +## Development Workflow + +1. **Create a feature/fix branch** off of `main`: + + ```bash + git checkout -b feat/your-feature-name + # or + git checkout -b fix/your-fix-name + ``` + +2. **Implement your changes**, adhering to the [Style Guide](#style-guide). +3. **Write tests** (see [Testing Guidelines](#testing-guidelines)). +4. **Run checks**: + + ```bash + npm run typecheck # Type checking + npm run lint # Linting + npm run test # Unit & integration tests + npm run test:e2e:ui # End-to-end tests + ``` + +5. **Commit changes** following our [Commit Guidelines](#commit-guidelines). + +## Commit Guidelines + +We use [Conventional Commits](https://www.conventionalcommits.org/) for clarity +and automated versioning. Commit message format: + +``` +type(scope): short description + +[optional body] + +[optional footer] +``` + +**Types:** + +- `feat`: New feature +- `fix`: Bug fix +- `docs`: Documentation changes +- `style`: Code style (formatting) +- `refactor`: Code restructuring +- `test`: Adding or updating tests +- `chore`: Maintenance tasks + +After staging, use Commitizen for consistent formatting: + +```bash +npx cz +``` + +## Pull Request Process + +1. Ensure your branch is up to date with `main`: + + ```bash + git fetch upstream + git rebase upstream/main + ``` + +2. Push your branch to your fork: + + ```bash + git push origin feat/your-feature-name + ``` + +3. Open a pull request against `reactsquad/main`. +4. **Link the approved issue** in your PR description. +5. Ensure all checks pass and the build is green. +6. Request review from at least one maintainer. +7. Address feedback; maintainers may request commit squashing. + +## Testing Guidelines + +### Test Location + +- **Tests live adjacent to implementation files**, for example: + + ```text + src/components/Button.tsx + src/components/Button.test.tsx + ``` + +### Coverage Requirements + +- **New features** must include tests at the appropriate level: unit, + integration, component, or E2E. +- **Bug fixes** require a reproducible failing test first. Once the test fails, + implement the fix so the test passes. + +### Test Style + +Follow the project’s testing conventions: + +- **Prose style**: `given: ... should: ...` +- **Assertions**: `expect(actual).toEqual(expected)` +- See + [5 Questions Every Test Must Answer](https://medium.com/@ericelliott/5-questions-every-test-must-answer-18a03194eeb1). + +Run tests with: + +```bash +npm run test # Unit & integration +npm run test:e2e:ui # End-to-end +``` + +## Style Guide + +### TypeScript + +- Use strict mode +- Define explicit return types +- Favor `type` for object shapes + +### React + +- Functional components with hooks +- Follow React Router patterns +- Use React Hook Form for forms +- Implement error boundaries + +### Styling + +- Tailwind CSS +- shadcn/ui component system +- Maintain dark mode support + +### File Structure & Naming + +- All files must be named in `kebab-case`. +- All constants must be named in `SCREAMING_SNAKE_CASE`. + +### Code Quality + +- Biome +- Write self-documenting code +- Add TSDoc to your complex functions +- Comment complex logic +- Keep functions small and focused (DOT principle) + +Thank you for helping improve the React Router SaaS Template! We look forward to +your contributions. diff --git a/apps/react-router/saas-template/Dockerfile b/apps/react-router/saas-template/Dockerfile new file mode 100644 index 0000000..207bf93 --- /dev/null +++ b/apps/react-router/saas-template/Dockerfile @@ -0,0 +1,22 @@ +FROM node:20-alpine AS development-dependencies-env +COPY . /app +WORKDIR /app +RUN npm ci + +FROM node:20-alpine AS production-dependencies-env +COPY ./package.json package-lock.json /app/ +WORKDIR /app +RUN npm ci --omit=dev + +FROM node:20-alpine AS build-env +COPY . /app/ +COPY --from=development-dependencies-env /app/node_modules /app/node_modules +WORKDIR /app +RUN npm run build + +FROM node:20-alpine +COPY ./package.json package-lock.json /app/ +COPY --from=production-dependencies-env /app/node_modules /app/node_modules +COPY --from=build-env /app/build /app/build +WORKDIR /app +CMD ["npm", "run", "start"] \ No newline at end of file diff --git a/apps/react-router/saas-template/LICENSE b/apps/react-router/saas-template/LICENSE new file mode 100644 index 0000000..8fe44ed --- /dev/null +++ b/apps/react-router/saas-template/LICENSE @@ -0,0 +1,10 @@ +MIT License + +Copyright (c) 2025 Jan Hesters + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/apps/react-router/saas-template/README.md b/apps/react-router/saas-template/README.md new file mode 100644 index 0000000..f61cc28 --- /dev/null +++ b/apps/react-router/saas-template/README.md @@ -0,0 +1,1068 @@ +# 1. Create .env file (copy the content above) +# 2. Install dependencies +npm install + +# 3. Start Postgres (if using Docker) - using port 6666 +docker run --name postgres-saas -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=saas_template -p 6666:5432 -d postgres:16 + +# 4. Setup Prisma and seed +npm run prisma:setup && npm run prisma:seed + +# 5. Start dev server +npm run dev:mocks + +# Welcome to the React Router SaaS Template! + +A modern, production-ready template for building full-stack B2B & B2C SaaS +applications using React Router. + +[![YouTbe thumbnail](https://i.ytimg.com/vi/5p45AbpL4bo/maxresdefault.jpg)](https://www.youtube.com/watch?v=5p45AbpL4bo) + +You can +[click here to watch the video](https://www.youtube.com/watch?v=5p45AbpL4bo) +explaining the template. + +## Tech Stack + +- 📖 [React Router](https://reactrouter.com/) +- 🔒 [TypeScript](https://www.typescriptlang.org/) by default +- 🎉 [TailwindCSS](https://tailwindcss.com/) for styling +- 🎨 [Shadcn UI](https://ui.shadcn.com/) components +- 🗄️ [Postgres](https://www.postgresql.org/) with + [Supabase](https://supabase.com/) & [Prisma](https://www.prisma.io/) +- 🧹 [Biome](https://biomejs.dev/) for linting and formatting +- ⚡️ [Vitest](https://vitest.dev/) for testing +- 🎭 [Playwright](https://playwright.dev/) for E2E testing +- 🛠️ [Commitizen](https://commitizen-tools.github.io/commitizen/), + [Commitlint](https://commitlint.js.org/), and + [Husky](https://typicode.github.io/husky/) for enforced commit conventions. + +## Features + +- 🔒 Authentication with [Supabase](https://supabase.com/docs/guides/auth) + (Email Magic Link, Google OAuth) +- 📦 Postgres with + [Supabase](https://supabase.com/docs/guides/database/overview) +- 🗃️ File upload with + [Supabase Storage](https://supabase.com/docs/guides/storage) +- 💳 Billing with [Stripe](https://stripe.com/) +- 📧 Emails with [Resend](https://resend.com/) +- 👥 Multi-tenant organizations with role-based memberships +- 🌙 Dark mode +- 🔔 Notifications +- 🔍 [Axe](https://www.npmjs.com/package/@axe-core/playwright) for accessibility + testing +- 🌐 Internationalization with [i18next](https://www.i18next.com/) and + [remix-i18next](https://github.com/sergiodxa/remix-i18next) +- 📦 And much more... + +All the services this template uses have generous free tiers, so you can get +started at any budget. + +## General + +This template is tens of thousands of lines of code. It can be scary to navigate +such a big foreign project. Luckily this template has good test coverage. + +Why is good test coverage important for a template? For the same reason why it's +good for your own code base. You want to avoid accidentally breaking something +when you update the template and change or ammend its code. + +## Getting Started + +Get the code: + +```bash +npx create-react-router@latest --template janhesters/react-router-saas-template +``` + +### Installation + +Install the dependencies: + +```bash +npm install +``` + +#### Quick Start + +For the fastest way to get started with local development using mocks (no external services required): + +```bash +npm run quickstart +``` + +This command will: +1. Copy `.env.example` to `.env` +2. Set up Prisma (generate client, run migrations, push schema) +3. Seed the database with demo data +4. Start the development server with mocks enabled + +You can then log in with any of the demo accounts: +- `hobby@example.com` - Hobby Plan (1 seat, monthly) +- `startup@example.com` - Startup Plan (5 seats, annual) +- `business@example.com` - Business Plan (25 seats, monthly) + +For more details on working with mocks, see ["Local Development with Mocks"](#local-development-with-mocks). + +#### Manual Setup + +For a quick start, see ["Local Development with Mocks"](#local-development-with-mocks). Just copy `.env.example` to `.env` and start developing. + +Create `.env` file. You can find the `.env.example` file in the root of the +project to see all the variables you need to set. + +Start by setting the environment variables that you can configure without +setting up a service: + +- `DATABASE_URL` – The URL of your local Postgres database. You can just + download the [Postgres.app](https://postgresapp.com/) and use it to create a + local database. +- `APP_URL` – The URL of your app, e.g. `http://localhost:3000`. +- `COOKIE_SECRET` – A random string of characters. This is used for signing + cookies including sessions, toast notifications, and other cookie-based data. +- `HONEYPOT_SECRET` – A random string of characters. This is used for the + honeypot field in the contact sales form. + +To run the app, you'll need to obtain the remaining environment variables by +setting up the required services. + +### Supabase + +1. Create a [new Supabase organization](https://supabase.com/dashboard/new). +2. Create a new project. + - Generate a password and save it somewhere. + - Choose the Region closest to your users. + - Keep the defaults like Postgres. +3. Go to your project's API settings, e.g. + `https://supabase.com/dashboard/project//settings/api`. From this + screen, you can grab: + +- `SUPABASE_PROJECT_ID` - The ID of your Supabase project. You can grab it from + the URL of your project, e.g. + `https://supabase.com/dashboard/project/`. +- `SUPABASE_REGION` - The region of your Supabase project. +- `VITE_SUPABASE_URL` - The URL of your Supabase project. NOTE: If you won't use + client side uploads, you can also call it `SUPABASE_URL` instead. The `VITE_` + prefix is used for client side variables. + +4. From `https://supabase.com/dashboard/project//settings/api-keys`, you can grab: + +- `VITE_SUPABASE_ANON_KEY` - The anonymous key of your Supabase project. It's + marked as `anon` and `public` in your dashboard. NOTE: If you won't use client + side uploads, you can also call it `SUPABASE_URL` instead. The `VITE_` prefix + is used for client side variables. +- `SUPABASE_SERVICE_ROLE_KEY` - The service role key of your Supabase project. + It's marked as `service_role` and `secret` in your dashboard. It must only be + used on the server side. + +5. Go to your project's storage settings, e.g. + `https://supabase.com/dashboard/project//storage/s3`. + You'll need to click on "New access key". Then you can grab from this screen: + +- `STORAGE_ACCESS_KEY_ID` - The access key ID of your Supabase project. +- `STORAGE_SECRET_ACCESS_KEY` - The secret access key of your Supabase project. + +#### Configuring Site URL at the Correct Location + +Now you need to configure the emails for the magic link authentication flow. + +Here’s how to set the Site URL under **URL Configuration** for your Supabase +project: + +1. **Access the Supabase Dashboard**: + - Go to `https://supabase.com/dashboard/`. +2. **Navigate to URL Configuration**: + - In the left sidebar, click **Authentication**. + - Then select **URL Configuration** (the direct URL would be + `https://supabase.com/dashboard/project/[your-project-ref]/auth/url-configuration`). +3. **Set the Site URL**: + - On the **URL Configuration** page, you'll see a field labeled **Site URL**. + - Enter your application's base URL here (e.g., `https://yourapp.com` or + `http://localhost:3000` for local development). + - This is the base URL that Supabase will use as the `{{ .SiteURL }}` + variable in your email templates (like the magic link template you + provided). +4. **Save the Configuration**: + - Click **Save** or the equivalent button to apply your changes. + +Next, configure the email templates by clicking on **Emails** then on **Confirm +Sign Up** (under +`https://supabase.com/dashboard/project/[your-project-ref]/auth/templates`) in +the Supabase Dashboard. + +```html +

Create Your Account For The React Router Starter App

+ +

Follow this link to register:

+

+ Sign Up +

+``` + +Next, configure the email templates by clicking on **Emails** then on **Magic +Link** (under +`https://supabase.com/dashboard/project/[your-project-ref]/auth/templates`) in +the Supabase Dashboard. + +```html +

Log In To The React Router Starter App

+ +

Follow this link to login:

+

+ Log In +

+``` + +Click **Save Changes** to apply your changes. + +#### Google OAuth + +This section is based on the Supabase documentation for +[**Login With Google**](https://supabase.com/docs/guides/auth/social-login/auth-google), +but has been enhanced for clarity because the Supabase documentation does not +work out of the box. + +1. Create a new Google Cloud project. Go to the + [Google Cloud Platform](https://console.cloud.google.com/home/dashboard) and + create a new project if necessary. + +- After creating the project, click on `Get Started`, enter your app name, + choose your audience, provide your contact information, and agree to the + Google API Services. + +2. Create your OAuth client. + - Under **Clients**, click `Create Credentials`. + - Choose `OAuth client ID`. + - Choose `Web application`. + - Click Create. +3. Now edit your OAuth client with your URLs. + - Under **Authorized JavaScript origins**, add your site URL. (E.g. + `http://localhost:3000`, and your production site URL.) + - Under **Authorized redirect URIs**, enter the callback URL from the + [Supabase dashboard](https://supabase.com/dashboard/project/_/auth/providers). + Expand the Google Auth Provider section to display it. + - You need to enter the Client ID and Client Secret in the Google Auth + Provider section of the Supabase Dashboard, which you can find under + **Additional Information** your OAuth client. + - The redirect URL is visible to your users. You can customize it by + configuring + [custom domains](https://supabase.com/docs/guides/platform/custom-domains). +4. In the Google Cloud console, under **Data Access**, click + `ADD OR REMOVE SCOPES`. + - Configure the following non-sensitive scopes: + - `.../auth/userinfo.email` + - `...auth/userinfo.profile` + - `openid` + - Click `Update`. +5. In the Google Cloud console, Under **Branding** and then **Authorized + Domains**, add your Supabase project's domain, which has the form + `.supabase.co`. +6. In your `.env` file, set the `APP_URL` to your local development URL (by + default it's `http://localhost:3000`) or your production site URL. + +**Note:** +[Here](https://supabase.com/docs/guides/auth/social-login/auth-google?queryGroups=environment&environment=server&queryGroups=framework&framework=remix#google-consent-screen) +are more details on how to configure the Google consent screen to show your +custom domain, and even your app's name and logo. + +#### Uploading Directly to Supabase From the Client + +Create a bucket in Supabase Storage. + +1. Visit your project in the Supabase UI: + https://supabase.com/dashboard/project/[your-project-ref]. +2. Go to the Storage section. +3. Click on the "New Bucket" button. +4. Enter a name for the bucket, e.g. `"app-images"` if you want to use a special + bucket for images, which we recommend. +5. Keep the bucket as "Private" to ensure that only authenticated users can + access the files. +6. Click on "Additional configuration", set the maximum upload sizeto 1MB, and + set the allowed MIME types to `image/*` to only allow image files. +7. Click on "Save". +8. Set the bucket name to the correct variable in your code. (By default, this + is NOT an environment variable in this template, but you can easily change it + to an environment variable.) Do a fuzzy search for `BUCKET` to find all the + places you need to change the value to your bucket name. + +#### Uploading to Supabase From the Server + +This approach uses the +[S3 compatible API](https://supabase.com/docs/guides/storage/s3/compatibility) +of Supabase Storage. + +Simply +[follow the instructions in the documentation](https://supabase.com/docs/guides/storage/s3/authentication) +and set the following environment variables in your `.env` file: + +- `STORAGE_ACCESS_KEY_ID` +- `STORAGE_SECRET_ACCESS_KEY` +- `STORAGE_REGION` +- `SUPABASE_PROJECT_ID` + +The upload to Supabase Storage is done using `parseFormData` from +[`@remix-run/form-data-parser`](https://github.com/remix-run/form-data-parser). +This function is under the hood in `validateFormData` in +`app/utils/validate-form-data.server.ts`. + +### Resend + +1. Create a new project at [Resend](https://resend.com/). +2. Got your project's [API keys](https://resend.com/api-keys) and click on + "Create API key". +3. Set the `RESEND_API_KEY` environment variable to the API key you just + created. + +### Stripe + +Install the Stripe CLI: + +```bash +brew install stripe/stripe-cli/stripe +``` + +or + +```bash +npm install -g stripe/stripe-cli +``` + +Confirm the installation: + +```bash +stripe --version +``` + +Learn more about Stripe testing [here](https://docs.stripe.com/testing). + +In a new terminal, forward webhooks to your local server: + +```bash +stripe listen --forward-to http://localhost:3000/api/v1/stripe/webhooks +``` + +Keep this terminal open. This will print out your local webhook secret. You'll +need to set the `STRIPE_WEBHOOK_SECRET` environment variable to this value. + +#### Stripe Dashboard + +You can manage your products and prices in the Stripe Dashboard. + +1. Create a new Stripe account. +2. In your [test mode dashboard](https://dashboard.stripe.com/test/dashboard), + grab the API keys: + +- `STRIPE_SECRET_KEY` - The secret key of your Stripe account. + +#### Pricing + +This project comes with a specific pricing pre-configured: + +3 paid tiers, and one enterprise (custom) tier. All paid tiers have a free +trial. The free trial is 14 days and always for the highest plan. + +If you need different pricing structures (e.g. freemium, one-time payments, +etc.) you'll have to write that code yourself. But this template's structure +makes it easy to customize the pricing page, the web hook handlers, etc. (NOTE: +the public `/pricing` page has a free tier, but that's just to show you how to +do it in the UI. The actual app has no free tier.) + +For each price, set the "Product tax code" to "SaaS" and the "Unit label" to +"seat". + +#### 1. Create your products & prices + +The React Router SaaS Template is set up to listen to product & prices webhooks. +This also allows your account managers to create and manage products & prices in +the Stripe Dashboard, and have them automatically reflected in your app. + +By default, it uses three plans with seat limits of: + +- low (Hobby): 1 seat +- mid (Startup): 10 seats +- high (Business): 25 seats + +You might need to tweak a bit of test code if you want to change these limits. +Do a fuzzy search for these limits. + +For local development, run your app with `npm run dev` and forward webhooks to +your local server with +`stripe listen --forward-to http://localhost:3000/api/v1/stripe/webhooks`. + +For production, follow the same instructions, but us the production URL of your +app and make sure your app is deployed so it will accept the webhooks of the +product creation. If you messed this up, you can always retrigger the webhooks +using the Stripe CLI. + +1. Go to the + [Stripe Dashboard for products](https://dashboard.stripe.com/test/products) +2. Click on "Create Product" (or "Add a product" if you have none). +3. In the modal: + +- Enter the name of the product, e.g.: "Hobby Plan" +- (Optional) Enter a description of the product, e.g.: "Hobby Plan for 1 user", + and upload an image. +- In the "Product Tax Code" dropdown, select "Software as a Service (SaaS) - + business use". +- Click on "More Options" and set the "Unit label" to "seat". +- Enter a monhtly recurring price, e.g.: "$17". Make sure you set the currenty + to USD in case its NOT the default. +- Click on "More pricing options" and enter a lookup key, e.g.: + "monthly_hobby_planv2". +- Click on "Next". + +4. Click on "Add another price" and this time choose "Yearly" as the billing + period. Make sure you enter the correct yearly price, e.g.: "$180". And + remember to set the lookup key to "annual_hobby_planv2". +5. **Important:** Now enter the value: "max_seats" in the metadata field and set + it to "1". This app is set up to handle ALL limits via metadata. This allows + you to easily change the limits for a product without having to change the + code. +6. Finally, click "Add Product". +7. Now write your lookup keys in the `priceLookupKeysByTierAndInterval` object + in `app/features/billing/billing-constants.ts`. + +##### For Local Development: Replay the Events + +After you’ve created your products and prices locally (with `npm run dev` and +`stripe listen` forwarding to your webhook endpoint), you’ll see lines in your +terminal like: + +``` +2025-05-10 17:58:56 --> product.created \[evt\_XXXXXXXXXXXXXXXXXXXXXXXX] +2025-05-10 17:58:58 --> price.created \[evt\_YYYYYYYYYYYYYYYYYYYYYYYY] +2025-05-10 17:59:00 --> price.created \[evt\_ZZZZZZZZZZZZZZZZZZZZZZZZ] +…etc. +``` + +1. **Copy the event IDs** + Whenever you see a line ending with `[evt_…]`, copy that ID (everything + inside the brackets, for example `evt_XXXXXXXXXXXXXXXXXXXXXXXX`). + +2. **Save them for later** + Put all your event IDs into a file (e.g. `stripe-events.txt`) or an + environment variable. For example, in a Unix-style shell you might do: + + ```bash + # stripe-events.txt + evt_XXXXXXXXXXXXXXXXXXXXXXXX + evt_YYYYYYYYYYYYYYYYYYYYYYYY + evt_ZZZZZZZZZZZZZZZZZZZZZZZZ + # …etc. + ``` + +3. **Replay (resend) the events** When you need to wipe your local database and + re-seed via webhooks, you can replay all those events at once. For example, + if you saved them in `stripe-events.txt`: + + ```bash + xargs -n1 stripe events resend < stripe-events.txt + ``` + + This command is also available via `npm run stripe:resend-events`. + +> **Tip:** Keep `stripe-events.txt` checked into your repo (or in a safe place) +> so you can easily replay your entire setup whenever you rebuild your local +> database. + +#### 2. Seed Stripe Data for Tests (Local vs. CI) + +Your test suite relies on having Stripe products & prices in your database. +Here’s how it works in each environment: + +##### Local + +1. **Replay your real events** (see “For Local Development: Replay the Events” + above) so your DB contains the exact products, prices, metadata, and lookup + keys you configured in Stripe. +2. **Run Vitest**: + ```bash + npm test + ``` + +The global setup (`app/test/vitest.global-setup.ts`) will detect your existing +products/prices and simply verify they’re present. + +#### CI + +In CI you won’t have webhook events or a populated database, so we automatically +seed dummy data: + +- **Global setup file**: `app/test/vitest.global-setup.ts` +- **Seeding helper**: `ensureStripeProductsAndPricesExist()` in + `app/test/server-test-utils.ts` + +What it does before your tests run: + +1. Looks up each lookup key defined in `priceLookupKeysByTierAndInterval`. +2. If no product exists yet, creates one via `createPopulatedStripeProduct()` + + `saveStripeProductToDatabase()`. +3. Creates both monthly & annual prices for that product with the right lookup + keys & intervals. +4. Logs success or exits on error, ensuring your tests always see exactly the + pricing rows they expect. + +You don’t need to replay webhooks or manage `stripe-events.txt` in CI—this +script handles everything. Just push your code and let your CI pipeline run +`npm test`. + +#### Checkout Session + +You need to configure tax collection. You must have a valid origin address to +enable automatic tax calculation in test mode. Visit +[your tax dashboard](https://dashboard.stripe.com/test/settings/tax) to update +it. + +#### Customer Portal + +Add the prices you created to your customer portal. Provide a configuration or +create your default by saving your +[customer portal settings in test mode](https://dashboard.stripe.com/test/settings/billing/portal). +You'll also need to set proration and enable the ability to cancel a +subscription via the portal. + +#### Intentional Design Decisions for Stripe + +- Downgrading a subscription does **not** deactivate existing members. The + reasoning is simple: more active users typically means more revenue. + Automatically removing members would work against that. If your plan has other + limits, you should handle those restrictions yourself - but since + subscriptions are billed per user per month, it’s in your interest to avoid + limiting user count unnecessarily. +- Users can still be added even if the subscription is cancelled. This allows + you to generate more revenue if the customer decides to subscribe again - + since pricing is per user, more added users means a higher monthly total once + they reactivate. + +### Misc + +Here are a few miscellaneous things you might want to change: + +1. Give it your own name! Fuzzy search for `React Router SaaS Template` to find + all the places you need to change the name. +2. The current theme violates color contrast. It's best for you to pick a theme + that is accessible and configure it in your `app.css` file. Then you can + enable contrast checks in your E2E tests again. + +## Development + +With all the envorinment variables set, you can run the app. + +Start the development server with HMR: + +```bash +npm run dev +``` + +Your application will be available at `http://localhost:3000`. + +If you haven't done it yet, with both your dev server and webhook forwarding +terminal open, replay the Stripe events in a third terminal. + +```bash +npm run stripe:resend-events +``` + +### Security Configuration + +This application implements Content Security Policy (CSP) with nonces for XSS +protection and provides control over search engine indexing. + +#### ALLOW_INDEXING Environment Variable + +Controls whether search engines can index your site. The application uses two +mechanisms to prevent indexing: + +- **HTTP Header:** `X-Robots-Tag: noindex, nofollow` +- **HTML Meta Tag:** `` + +**Values:** + +- `"true"` - Allow search engine indexing (recommended for production) +- `"false"` - Prevent search engine indexing (recommended for + staging/dev/preview environments) +- Omitted - Defaults to allowing indexing + +**Example:** + +```bash +# Production +ALLOW_INDEXING=true + +# Staging/Development/Preview +ALLOW_INDEXING=false +``` + +**When to Use:** + +| Environment | Recommended Value | Reason | +| ------------------- | ----------------- | ------------------------------------------------------------ | +| **Production** | `"true"` or omit | Allow search engines to index your public site | +| **Staging** | `"false"` | Prevent duplicate content and indexing of test environments | +| **Development** | `"false"` | Prevent local development sites from being indexed | +| **Preview/PR** | `"false"` | Prevent temporary preview deployments from being indexed | + +#### Content Security Policy (CSP) + +The application uses nonces for CSP compliance. All inline scripts are protected +by cryptographically random nonces that are generated on each request. + +**Configuration:** + +- CSP is in **report-only mode** in development and test environments +- CSP is **enforced** in production +- All inline scripts require a valid nonce attribute +- WebSocket connections are allowed in development for Hot Module Replacement + (HMR) + +### Project helper scripts + +- `"build"` - Compiles the application using React Router's build process. +- `"build-with-mocks"` - Builds the app and initializes MSW in the client build + directory without saving it to `package.json`. +- `"check"` - Runs Biome checks and automatically applies safe fixes with + formatting across the codebase. +- `"dev"` - Starts the development server using React Router's dev mode. +- `"dev-with-mocks"` - Starts the dev server with both client and server mocks + enabled via `VITE_CLIENT_MOCKS=true` and `SERVER_MOCKS=true`. +- `"dev-with-server-mocks"` - Starts the dev server with only server-side mocks + enabled. +- `"lint"` - Runs Biome in CI mode to check for linting and formatting errors + across the codebase, including Tailwind directives in CSS. +- `"prepare"` - Sets up Git hooks via Husky. +- `"start"` - Serves the production build using `react-router-serve`. +- `"start-with-server-mocks"` - Serves the production build with server mocks + enabled. +- `"stripe:resend-events"` - Resends Stripe events listed in `stripe-events.txt` + using the Stripe CLI. +- `"test"` - Runs unit, integration, and component tests using Vitest with a + verbose reporter in watch mode. +- `"test:e2e"` - Executes end-to-end tests using Playwright. +- `"test:e2e:ui"` - Launches Playwright Test Runner UI for interactive + debugging. +- `"typecheck"` - Runs type generation for routes and performs TypeScript type + checking. +- `"typegen"` - Generates type-safe route definitions for React Router. + +### Prisma Helper Scripts + +- `"prisma:deploy"` - Applies all pending migrations from the + `prisma/migrations` directory to the database, then regenerates the Prisma + Client. Typically used in production. +- `"prisma:migrate"` - Run via `npm run prisma:migrate -- my_migration_name` to + create a new migration based on schema changes and apply it to the dev + database. +- `"prisma:push"` - Pushes the current Prisma schema to the database without + generating a migration, then regenerates the Prisma Client. Useful for + prototyping. +- `"prisma:reset-dev"` - Wipes the database, seeds it, and starts the + development server. Use this for a clean local dev environment. +- `"prisma:seed"` - Executes the seed script defined in `./prisma/seed.ts` to + populate the database with initial data. +- `"prisma:setup"` - Regenerates Prisma Client, applies pending migrations, and + pushes any remaining schema changes. Ideal for fresh environments. +- `"prisma:studio"` - Opens Prisma Studio, a GUI for exploring and editing your + database. +- `"prisma:wipe"` - Resets the database by applying all migrations from scratch + (`migrate reset`), then pushes the schema without requiring confirmation. + +### Running E2E Tests + +When you run the E2E tests locally, we recommend you do it in production mode +and with mocks enabled. This resembles how your tests will run in CI. So your +steps should be: + +1. Run `npm run build-with-mocks`. +2. Run `npm run start-with-server-mocks`. +3. In another terminal, run `npm run test:e2e` UI. +4. Visit `localhost:3000` in your browser once. You should see + `🔶 MSW mock server running ...` in the terminal running your app. +5. (Optionally) In a new terminal, run `npm run prisma:wipe` and + `npm run stripe:resend-events` to reset the database and replay the Stripe + events. (Anohter terminal that forwards the webhooks must already be + running.) + +### Local Development with Mocks + +For local development without connecting to real external services (Stripe, +Supabase, etc.), you can use the mock mode. This uses +[MSW (Mock Service Worker)](https://mswjs.io/) to intercept API calls. + +**Setup:** + +1. First, seed your database with demo data: + ```bash + npm run prisma:seed + ``` + This creates three demo organizations with subscriptions: + - `hobby@example.com` - Hobby Plan (1 seat, monthly) + - `startup@example.com` - Startup Plan (5 seats, annual) + - `business@example.com` - Business Plan (25 seats, monthly) + + Each organization also has 2-4 random team members added. + +2. Start the development server with mocks enabled: + ```bash + npm run dev:mocks + ``` + +**Logging In:** + +When running with `MOCKS=true`, authentication is handled by mocked Supabase +endpoints. To log in as one of the seeded users: + +1. Navigate to the login page +2. Enter one of the demo email addresses (e.g., `hobby@example.com`) +3. Click the magic link button +4. The mock will automatically "send" the email and you can access the app + +**Resetting Data:** + +To start fresh with a clean database: + +```bash +npm run prisma:wipe # Resets the database +npm run prisma:seed # Re-seeds demo data +npm run dev:mocks # Start dev server +``` + +Or use the combined command: + +```bash +npm run prisma:reset-dev # Wipes, seeds, and starts dev server +``` + +**Note:** The seed script creates demo users with subscriptions that include +seats, Stripe customer IDs, and all necessary billing data. This allows you to +test billing flows, team management, and subscription features without +connecting to real Stripe or Supabase instances. + +### Routing + +This template uses [flat routes](https://github.com/kiliman/remix-flat-routes). + +### i18n + +This React Router SaaS template comes with localization support through +[remix-i18next](https://github.com/sergiodxa/remix-i18next). + +The namespaces live in `public/locales/`. + +### Toasts + +This React Router SaaS template includes utilities for toast notifications based +on flash sessions. + +**Flash Data:** Temporary session values, ideal for transferring data to the +next request without persisting in the session. + +**Redirect with Toast:** + +- Utility: `redirectWithToast` (Path: `app/utils/toast.server.ts`) +- Use for redirecting with toast notifications. +- Example: + ```tsx + return redirectWithToast(`/organizations/${newOrganizations.slug}/home`, { + title: 'Organization created', + description: 'Your organization has been created.', + }); + ``` +- Accepts extra arguments for `ResponseInit` to set headers. + +**Direct Toast Headers:** + +- Utility: `createToastHeaders` (Path: `app/utils/toast.server.ts`) +- Use for non-redirect scenarios. +- Example: + ```tsx + return json( + { success: true }, + { + headers: await createToastHeaders({ + description: 'Organization updated', + type: 'success', + }), + }, + ); + ``` + +**Combining Multiple Headers:** + +- Utility: `combineHeaders` (Path: `app/utils/toast.server.tsx`) +- Combine toast headers with additional headers. +- Example: + ```tsx + return json( + { success: true }, + { + headers: combineHeaders( + await createToastHeaders({ title: 'Profile updated' }), + { 'x-foo': 'bar' }, + ), + }, + ); + ``` + +### Playwright 🎭 + +> **Note:** make sure you've run `npm run dev` at least one time before you run +> the E2E tests! + +We use Playwright for our End-to-End tests in this project. You'll find those in +the `playwright/` directory. As you make changes to your app, add to an existing +file or create a new file in the `playwright/e2e` directory to test your +changes. + +[Playwright natively features testing library selectors](https://playwright.dev/docs/release-notes#locators) +for selecting elements on the page semantically. + +To run these tests in development, run `npm run test:e2e` which will start the +dev server for the app as well as the Playwright client. + +> **Note:** You might need to run `npx playwright install` to install the +> Playwright browsers before running your tests for the first time. + +#### Problems with ShadcnUI + +Some of the colors of ShadcnUI's components are lacking the necessary contrast. + +You can deactivate those elements in checks like this: + +```ts +const accessibilityScanResults = await new AxeBuilder({ page }) + .disableRules('color-contrast') + .analyze(); + +// or + +const accessibilityScanResults = await new AxeBuilder({ page }) + .disableRules('color-contrast') + .analyze(); +``` + +or pick a color scheme like "purple" that has good contrast. + +#### VSCode Extension + +If you're using VSCode, you can install the +[Playwright extension](https://github.com/microsoft/playwright-vscode) for a +better developer experience. + +#### Utilities + +We have a utility for testing authenticated features without having to go +through the login flow: + +```ts +test('something that requires an authenticated user', async ({ page }) => { + await loginByCookie({ page }); + // ... your tests ... +}); +``` + +Check out the `playwright/utils.ts` file for other utility functions. + +#### Miscellaneous + +To mark a test as todo in Playwright, +[you have to use `.fixme()`](https://github.com/microsoft/playwright/issues/10918). + +```ts +test('something that should be done later', ({}, testInfo) => { + testInfo.fixme(); +}); + +test.fixme('something that should be done later', async ({ page }) => { + // ... +}); + +test('something that should be done later', ({ page }) => { + test.fixme(); + // ... +}); +``` + +The version using `testInfo.fixme()` is the "preferred" way and can be picked up +by the VSCode extension. + +### Vitest ⚡️ + +For lower level tests of utilities and individual components, we use `vitest`. +We have DOM-specific assertion helpers via +[`@testing-library/jest-dom`](https://testing-library.com/jest-dom). + +By default, Vitest runs tests in the +[`"happy-dom"` environment](https://vitest.dev/config/#environment). However, +test files that have `.server` in the name will be run in the `"node"` +environment. + +### Test Scripts + +- `npm run test` - Runs all Vitest tests. +- `npm run test:e2e` - Runs all E2E tests with Playwright. +- `npm run test:e2e:ui` - Runs all E2E tests with Playwright in UI mode. + +### Type Checking + +This project uses TypeScript. It's recommended to get TypeScript set up for your +editor to get a really great in-editor experience with type checking and +auto-complete. To run type checking across the whole project, run +`npm run type-check`. + +### Linting and Formatting + +This project uses [Biome](https://biomejs.dev/) for linting and formatting. That +is configured in `biome.json`. + +It's recommended to install the +[Biome VS Code extension](https://marketplace.visualstudio.com/items?itemName=biomejs.biome) +to get auto-formatting on save and inline linting feedback. You can also run +`npm run check` to format and fix linting issues across all files in the +project, or `npm run lint` to check for errors without making changes (useful +for CI). + +### AI-Driven Development + +This template leverages and was written with **AI-Driven Development (AIDD)**, +where you steer high-level design and let AI generate the bulk of your +implementation via +[**SudoLang**](https://github.com/paralleldrive/sudolang-llm-support), a +natural-language-style pseudocode that advanced LLMs already understand. + +With AIDD you can: + +- Define requirements and architecture in plain pseudocode. +- Let AI produce 90%+ of your source code (tests, UIs, state layers, etc.). +- Iterate and refactor faster, keeping consistency across your codebase. + +#### Cursor AI Commands + +Under `.cursor/commands/`, you'll find ready-to-use commands that automate +common workflows: + +- **better-writer** – Improves writing clarity and engagement using Scott Adams' + rules. +- **brainstorm** – Helps ideate solutions with clear trade-offs and + recommendations. +- **commit** – Commits changes using conventional commit format. +- **debug** – Provides systematic debugging with root cause analysis. +- **documentation** – Creates clear, example-first documentation. +- **log** – Logs changes to CHANGELOG.md with conventional commit format. +- **plan** – Breaks down complex requests into manageable, sequential tasks. +- **svg-to-react** – Converts SVG files into optimized React components. +- **unit-tests** – Generates thorough, readable unit tests using Vitest. +- **write** – Produces clear, concise business writing with specific style + guidelines. + +#### Cursor AI Rules + +Under `.cursor/rules/`, you'll find coding standards that AI follows: + +- **js-and-ts.mdc** – JavaScript and TypeScript best practices including + functional programming patterns, naming conventions, and code organization. +- **jsx-and-tsx.mdc** – React best practices including component patterns, form + handling, accessibility, and internationalization. + +Learn more about AIDD and SudoLang in +[The Art of Effortless Programming](https://leanpub.com/effortless-programming) +by [Eric Elliott](https://www.threads.com/@__ericelliott). + +## Building for Production + +Create a production build: + +```bash +npm run build +``` + +## Deployment + +### Docker Deployment + +This template includes three Dockerfiles optimized for different package +managers: + +- `Dockerfile` - for npm +- `Dockerfile.pnpm` - for pnpm +- `Dockerfile.bun` - for bun + +To build and run using Docker: + +```bash +# For npm +docker build -t my-app . + +# For pnpm +docker build -f Dockerfile.pnpm -t my-app . + +# For bun +docker build -f Dockerfile.bun -t my-app . + +# Run the container +docker run -p 3000:3000 my-app +``` + +The containerized application can be deployed to any platform that supports +Docker, including: + +- AWS ECS +- Google Cloud Run +- Azure Container Apps +- Digital Ocean App Platform +- Fly.io +- Railway + +### DIY Deployment + +If you're familiar with deploying Node applications, the built-in app server is +production-ready. + +Make sure to deploy the output of `npm run build` + +``` +├── package.json +├── package-lock.json (or pnpm-lock.yaml, or bun.lockb) +├── build/ +│ ├── client/ # Static assets +│ └── server/ # Server-side code +``` + +## Maintenance + +You can use + +``` +npx npm-check-updates -u +``` + +to check for updates and install the latest versions. + +It should be easy to upgrade all packages since your static analysis checks and +your tests will tell you if anything is broken. + +## Contributing + +See [CONTRIBUTING.md](./CONTRIBUTING.md) for more information. + +## Check Out the Epic Stack + +Some of the code of this starter template was taken from or inspired by the +[Epic Stack](https://github.com/epicweb-dev/epic-stack) from +[Kent C. Dodds](http://kentcdodds.com/). His template has different defaults, so +check it out if you're looking for a different opinionated starter template. + +## Built with ❤️ by [ReactSquad](https://reactsquad.io/) + +If you want to hire senior React developers to augment your team, or build your +entire product from scratch, +[schedule a call with us](https://www.reactsquad.io/schedule-a-call). + +## [Buidl!](https://www.urbandictionary.com/define.php?term=%23BUIDL) + +Now go out there make some magic! 🧙‍♂️ diff --git a/apps/react-router/saas-template/app/app.css b/apps/react-router/saas-template/app/app.css new file mode 100644 index 0000000..1d5846c --- /dev/null +++ b/apps/react-router/saas-template/app/app.css @@ -0,0 +1,445 @@ +@import "tailwindcss"; +@import "tw-animate-css"; + +@custom-variant dark { + &:where(.dark *, .dark) { + @slot; + } + + &:where(.system *, .system) { + @media (prefers-color-scheme: dark) { + @slot; + } + } +} + +@theme { + --font-sans: + "Inter", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", + "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; +} + +:root { + /* Define light theme color values */ + --background-light: oklch(1 0 0); + --foreground-light: oklch(0.145 0 0); + + --card-light: oklch(1 0 0); + --card-foreground-light: oklch(0.145 0 0); + + --popover-light: oklch(1 0 0); + --popover-foreground-light: oklch(0.145 0 0); + + --primary-light: oklch(0.6451 0.2132 21.54); + --primary-foreground-light: oklch(0.985 0 0); + + --secondary-light: oklch(0.97 0 0); + --secondary-foreground-light: oklch(0.205 0 0); + + --muted-light: oklch(0.97 0 0); + --muted-foreground-light: oklch(0.556 0 0); + + --accent-light: oklch(0.97 0 0); + --accent-foreground-light: oklch(0.205 0 0); + + --destructive-light: oklch(0.577 0.245 27.325); + + --border-light: oklch(0.922 0 0); + --input-light: oklch(0.922 0 0); + --ring-light: oklch(0.708 0 0); + + --chart-1-light: oklch(0.646 0.222 41.116); + --chart-2-light: oklch(0.6 0.118 184.704); + --chart-3-light: oklch(0.398 0.07 227.392); + --chart-4-light: oklch(0.828 0.189 84.429); + --chart-5-light: oklch(0.769 0.188 70.08); + + --sidebar-light: oklch(0.985 0 0); + --sidebar-foreground-light: oklch(0.145 0 0); + --sidebar-primary-light: oklch(0.205 0 0); + --sidebar-primary-foreground-light: oklch(0.985 0 0); + --sidebar-accent-light: oklch(0.97 0 0); + --sidebar-accent-foreground-light: oklch(0.205 0 0); + --sidebar-border-light: oklch(0.922 0 0); + --sidebar-ring-light: oklch(0.708 0 0); + + /* Define dark theme color values */ + --background-dark: oklch(0.145 0 0); + --foreground-dark: oklch(0.985 0 0); + + --card-dark: oklch(0.205 0 0); + --card-foreground-dark: oklch(0.985 0 0); + + --popover-dark: oklch(0.205 0 0); + --popover-foreground-dark: oklch(0.985 0 0); + + --primary-dark: oklch(0.6451 0.2132 21.54); + --primary-foreground-dark: oklch(0.985 0 0); + + --secondary-dark: oklch(0.269 0 0); + --secondary-foreground-dark: oklch(0.985 0 0); + + --muted-dark: oklch(0.269 0 0); + --muted-foreground-dark: oklch(0.708 0 0); + + --accent-dark: oklch(0.269 0 0); + --accent-foreground-dark: oklch(0.985 0 0); + + --destructive-dark: oklch(0.704 0.191 22.216); + + --border-dark: oklch(1 0 0 / 10%); + --input-dark: oklch(1 0 0 / 15%); + --ring-dark: oklch(0.556 0 0); + + --chart-1-dark: oklch(0.488 0.243 264.376); + --chart-2-dark: oklch(0.696 0.17 162.48); + --chart-3-dark: oklch(0.769 0.188 70.08); + --chart-4-dark: oklch(0.627 0.265 303.9); + --chart-5-dark: oklch(0.645 0.246 16.439); + + --sidebar-dark: oklch(0.205 0 0); + --sidebar-foreground-dark: oklch(0.985 0 0); + --sidebar-primary-dark: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground-dark: oklch(0.985 0 0); + --sidebar-accent-dark: oklch(0.269 0 0); + --sidebar-accent-foreground-dark: oklch(0.985 0 0); + --sidebar-border-dark: oklch(1 0 0 / 10%); + --sidebar-ring-dark: oklch(0.556 0 0); + + /* Active variables - default to light */ + --background: var(--background-light); + --foreground: var(--foreground-light); + + --card: var(--card-light); + --card-foreground: var(--card-foreground-light); + + --popover: var(--popover-light); + --popover-foreground: var(--popover-foreground-light); + + --primary: var(--primary-light); + --primary-foreground: var(--primary-foreground-light); + + --secondary: var(--secondary-light); + --secondary-foreground: var(--secondary-foreground-light); + + --muted: var(--muted-light); + --muted-foreground: var(--muted-foreground-light); + + --accent: var(--accent-light); + --accent-foreground: var(--accent-foreground-light); + + --destructive: var(--destructive-light); + + --border: var(--border-light); + --input: var(--input-light); + --ring: var(--ring-light); + + --chart-1: var(--chart-1-light); + --chart-2: var(--chart-2-light); + --chart-3: var(--chart-3-light); + --chart-4: var(--chart-4-light); + --chart-5: var(--chart-5-light); + + --sidebar: var(--sidebar-light); + --sidebar-foreground: var(--sidebar-foreground-light); + --sidebar-primary: var(--sidebar-primary-light); + --sidebar-primary-foreground: var(--sidebar-primary-foreground-light); + --sidebar-accent: var(--sidebar-accent-light); + --sidebar-accent-foreground: var(--sidebar-accent-foreground-light); + --sidebar-border: var(--sidebar-border-light); + --sidebar-ring: var(--sidebar-ring-light); + + --radius: 0.625rem; + --header-height: calc(var(--spacing) * 12 + 1px); +} + +.dark { + color-scheme: dark; + + --background: var(--background-dark); + --foreground: var(--foreground-dark); + + --card: var(--card-dark); + --card-foreground: var(--card-foreground-dark); + + --popover: var(--popover-dark); + --popover-foreground: var(--popover-foreground-dark); + + --primary: var(--primary-dark); + --primary-foreground: var(--primary-foreground-dark); + + --secondary: var(--secondary-dark); + --secondary-foreground: var(--secondary-foreground-dark); + + --muted: var(--muted-dark); + --muted-foreground: var(--muted-foreground-dark); + + --accent: var(--accent-dark); + --accent-foreground: var(--accent-foreground-dark); + + --destructive: var(--destructive-dark); + + --border: var(--border-dark); + --input: var(--input-dark); + --ring: var(--ring-dark); + + --chart-1: var(--chart-1-dark); + --chart-2: var(--chart-2-dark); + --chart-3: var(--chart-3-dark); + --chart-4: var(--chart-4-dark); + --chart-5: var(--chart-5-dark); + + --sidebar: var(--sidebar-dark); + --sidebar-foreground: var(--sidebar-foreground-dark); + --sidebar-primary: var(--sidebar-primary-dark); + --sidebar-primary-foreground: var(--sidebar-primary-foreground-dark); + --sidebar-accent: var(--sidebar-accent-dark); + --sidebar-accent-foreground: var(--sidebar-accent-foreground-dark); + --sidebar-border: var(--sidebar-border-dark); + --sidebar-ring: var(--sidebar-ring-dark); +} + +.system { + @media (prefers-color-scheme: dark) { + color-scheme: dark; + + --background: var(--background-dark); + --foreground: var(--foreground-dark); + + --card: var(--card-dark); + --card-foreground: var(--card-foreground-dark); + + --popover: var(--popover-dark); + --popover-foreground: var(--popover-foreground-dark); + + --primary: var(--primary-dark); + --primary-foreground: var(--primary-foreground-dark); + + --secondary: var(--secondary-dark); + --secondary-foreground: var(--secondary-foreground-dark); + + --muted: var(--muted-dark); + --muted-foreground: var(--muted-foreground-dark); + + --accent: var(--accent-dark); + --accent-foreground: var(--accent-foreground-dark); + + --destructive: var(--destructive-dark); + + --border: var(--border-dark); + --input: var(--input-dark); + --ring: var(--ring-dark); + + --chart-1: var(--chart-1-dark); + --chart-2: var(--chart-2-dark); + --chart-3: var(--chart-3-dark); + --chart-4: var(--chart-4-dark); + --chart-5: var(--chart-5-dark); + + --sidebar: var(--sidebar-dark); + --sidebar-foreground: var(--sidebar-foreground-dark); + --sidebar-primary: var(--sidebar-primary-dark); + --sidebar-primary-foreground: var(--sidebar-primary-foreground-dark); + --sidebar-accent: var(--sidebar-accent-dark); + --sidebar-accent-foreground: var(--sidebar-accent-foreground-dark); + --sidebar-border: var(--sidebar-border-dark); + --sidebar-ring: var(--sidebar-ring-dark); + } +} + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --radius-2xl: calc(var(--radius) + 8px); + --radius-3xl: calc(var(--radius) + 12px); + --radius-4xl: calc(var(--radius) + 16px); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); + + --animate-marquee: marquee var(--duration) infinite linear; + --animate-marquee-vertical: marquee-vertical var(--duration) linear infinite; + + @keyframes marquee { + from { + transform: translateX(0); + } + to { + transform: translateX(calc(-100% - var(--gap))); + } + } + @keyframes marquee-vertical { + from { + transform: translateY(0); + } + to { + transform: translateY(calc(-100% - var(--gap))); + } + } +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} + +@font-face { + font-family: "Inter"; + font-style: normal; + font-weight: 100; + font-display: swap; + src: url("/fonts/inter/Inter-Thin.woff2") format("woff2"); +} +@font-face { + font-family: "Inter"; + font-style: italic; + font-weight: 100; + font-display: swap; + src: url("/fonts/inter/Inter-ThinItalic.woff2") format("woff2"); +} +@font-face { + font-family: "Inter"; + font-style: normal; + font-weight: 200; + font-display: swap; + src: url("/fonts/inter/Inter-ExtraLight.woff2") format("woff2"); +} +@font-face { + font-family: "Inter"; + font-style: italic; + font-weight: 200; + font-display: swap; + src: url("/fonts/inter/Inter-ExtraLightItalic.woff2") format("woff2"); +} +@font-face { + font-family: "Inter"; + font-style: normal; + font-weight: 300; + font-display: swap; + src: url("/fonts/inter/Inter-Light.woff2") format("woff2"); +} +@font-face { + font-family: "Inter"; + font-style: italic; + font-weight: 300; + font-display: swap; + src: url("/fonts/inter/Inter-LightItalic.woff2") format("woff2"); +} +@font-face { + font-family: "Inter"; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url("/fonts/inter/Inter-Regular.woff2") format("woff2"); +} +@font-face { + font-family: "Inter"; + font-style: italic; + font-weight: 400; + font-display: swap; + src: url("/fonts/inter/Inter-Italic.woff2") format("woff2"); +} +@font-face { + font-family: "Inter"; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url("/fonts/inter/Inter-Medium.woff2") format("woff2"); +} +@font-face { + font-family: "Inter"; + font-style: italic; + font-weight: 500; + font-display: swap; + src: url("/fonts/inter/Inter-MediumItalic.woff2") format("woff2"); +} +@font-face { + font-family: "Inter"; + font-style: normal; + font-weight: 600; + font-display: swap; + src: url("/fonts/inter/Inter-SemiBold.woff2") format("woff2"); +} +@font-face { + font-family: "Inter"; + font-style: italic; + font-weight: 600; + font-display: swap; + src: url("/fonts/inter/Inter-SemiBoldItalic.woff2") format("woff2"); +} +@font-face { + font-family: "Inter"; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url("/fonts/inter/Inter-Bold.woff2") format("woff2"); +} +@font-face { + font-family: "Inter"; + font-style: italic; + font-weight: 700; + font-display: swap; + src: url("/fonts/inter/Inter-BoldItalic.woff2") format("woff2"); +} +@font-face { + font-family: "Inter"; + font-style: normal; + font-weight: 800; + font-display: swap; + src: url("/fonts/inter/Inter-ExtraBold.woff2") format("woff2"); +} +@font-face { + font-family: "Inter"; + font-style: italic; + font-weight: 800; + font-display: swap; + src: url("/fonts/inter/Inter-ExtraBoldItalic.woff2") format("woff2"); +} +@font-face { + font-family: "Inter"; + font-style: normal; + font-weight: 900; + font-display: swap; + src: url("/fonts/inter/Inter-Black.woff2") format("woff2"); +} +@font-face { + font-family: "Inter"; + font-style: italic; + font-weight: 900; + font-display: swap; + src: url("/fonts/inter/Inter-BlackItalic.woff2") format("woff2"); +} diff --git a/apps/react-router/saas-template/app/components/avatar-upload.tsx b/apps/react-router/saas-template/app/components/avatar-upload.tsx new file mode 100644 index 0000000..3acb70d --- /dev/null +++ b/apps/react-router/saas-template/app/components/avatar-upload.tsx @@ -0,0 +1,156 @@ +import type { Avatar as AvatarPrimitive } from "@base-ui/react/avatar"; +import type { + ChangeEvent, + ChangeEventHandler, + ComponentProps, + ReactNode, +} from "react"; +import { + createContext, + useCallback, + useContext, + useEffect, + useState, +} from "react"; +import { useTranslation } from "react-i18next"; + +import { AvatarImage } from "./ui/avatar"; +import { Input } from "~/components/ui/input"; +import { cn } from "~/lib/utils"; + +function formatBytes(bytes: number, decimals = 2) { + if (bytes === 0) return "0 Bytes"; + + const k = 1024; + const dm = Math.max(decimals, 0); + const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; + + const index = Math.floor(Math.log(bytes) / Math.log(k)); + + return `${Number.parseFloat((bytes / k ** index).toFixed(dm))} ${sizes[index] ?? "Bytes"}`; +} + +type AvatarUploadContextType = { + file: File | undefined; + handleFileChange: ChangeEventHandler; + handleRemoveFile: () => void; + inputKey: number; + previewUrl: string; +}; + +const AvatarUploadContext = createContext( + undefined as never, +); + +export function AvatarUpload({ + children, + maxFileSize, +}: { + children: ReactNode | ((props: { error: string }) => ReactNode); + maxFileSize?: number; +}) { + // @ts-expect-error - avatarUpload keyPrefix doesn't exist yet in translations + const { t } = useTranslation("translation", { keyPrefix: "avatarUpload" }); + const [error, setError] = useState(""); + const [file, setFile] = useState(); + const [inputKey, setInputKey] = useState(Date.now()); + const [previewUrl, setPreviewUrl] = useState(""); + + const handleFileChange = useCallback( + (event: ChangeEvent) => { + const currentFile = event.target.files?.[0]; + if (currentFile) { + if (typeof maxFileSize === "number" && currentFile.size > maxFileSize) { + setError( + // @ts-expect-error - fileSizeError translation key doesn't exist yet + t("fileSizeError", { + fileName: currentFile.name, + maxSize: formatBytes(maxFileSize), + }), + ); + // Clear the invalid file from the input. + event.target.value = ""; + return; + } + + setError(""); + const url = URL.createObjectURL(currentFile); + setPreviewUrl(url); + setFile(currentFile); + } else { + setPreviewUrl(""); + setFile(undefined); + } + }, + [maxFileSize, t], + ); + + const handleRemoveFile = useCallback(() => { + setError(""); + setFile(undefined); + setPreviewUrl(""); + // By changing the key, we force React to re-mount the input, + // which is the cleanest way to reset an uncontrolled component. + setInputKey(Date.now()); + }, []); + + useEffect(() => { + return () => { + if (previewUrl) { + URL.revokeObjectURL(previewUrl); + } + }; + }, [previewUrl]); + + return ( + + {typeof children === "function" ? children({ error }) : children} + + ); +} + +function useAvatarUpload() { + return useContext(AvatarUploadContext); +} + +export function AvatarUploadPreviewImage({ + src, + ...props +}: ComponentProps) { + const { previewUrl } = useAvatarUpload(); + + return ; +} + +export function AvatarUploadInput({ + onChange, + ...props +}: ComponentProps<"input">) { + const { handleFileChange, inputKey } = useAvatarUpload(); + + function handleChange(event: ChangeEvent) { + handleFileChange(event); + onChange?.(event); + } + + return ( + + ); +} + +export function AvatarUploadDescription({ + className, + ...props +}: ComponentProps<"p">) { + return ( +

+ ); +} diff --git a/apps/react-router/saas-template/app/components/disableable-link.test.tsx b/apps/react-router/saas-template/app/components/disableable-link.test.tsx new file mode 100644 index 0000000..aab58cc --- /dev/null +++ b/apps/react-router/saas-template/app/components/disableable-link.test.tsx @@ -0,0 +1,42 @@ +import { describe, expect, test } from "vitest"; + +import type { DisableableLinkComponentProps } from "./disableable-link"; +import { DisableableLink } from "./disableable-link"; +import { createRoutesStub, render, screen } from "~/test/react-test-utils"; +import type { Factory } from "~/utils/types"; + +const createProps: Factory = ({ + children = "Click Me", + disabled = false, + to = "/test", + ...rest +} = {}) => ({ children, disabled, to, ...rest }); + +describe("DisableableLink component", () => { + test("given: the link is enabled, should: render a link", () => { + const props = createProps(); + const RouterStub = createRoutesStub([ + { Component: () => , path: "/" }, + ]); + + render(); + + expect(screen.getByRole("link", { name: /click me/i })).toHaveAttribute( + "href", + props.to, + ); + }); + + test("given: the link is disabled, should: NOT render a link", () => { + const props = createProps({ disabled: true }); + const RouterStub = createRoutesStub([ + { Component: () => , path: "/" }, + ]); + + render(); + + expect( + screen.queryByRole("link", { name: /click me/i }), + ).not.toBeInTheDocument(); + }); +}); diff --git a/apps/react-router/saas-template/app/components/disableable-link.tsx b/apps/react-router/saas-template/app/components/disableable-link.tsx new file mode 100644 index 0000000..23b957e --- /dev/null +++ b/apps/react-router/saas-template/app/components/disableable-link.tsx @@ -0,0 +1,23 @@ +import type { LinkProps } from "react-router"; +import { Link } from "react-router"; + +import { cn } from "~/lib/utils"; + +export type DisableableLinkComponentProps = LinkProps & { disabled?: boolean }; + +export function DisableableLink(props: DisableableLinkComponentProps) { + const { children, className, disabled, ...rest } = props; + + return disabled ? ( + + {children} + + ) : ( + {children} + ); +} diff --git a/apps/react-router/saas-template/app/components/general-error-boundary.tsx b/apps/react-router/saas-template/app/components/general-error-boundary.tsx new file mode 100644 index 0000000..0959959 --- /dev/null +++ b/apps/react-router/saas-template/app/components/general-error-boundary.tsx @@ -0,0 +1,71 @@ +import type { ReactElement } from "react"; +import type { ErrorResponse } from "react-router"; +import { isRouteErrorResponse, useParams, useRouteError } from "react-router"; + +import { Alert, AlertDescription, AlertTitle } from "~/components/ui/alert"; +import { getErrorMessage } from "~/utils/get-error-message"; + +type StatusHandler = (info: { + error: ErrorResponse; + params: Record; +}) => ReactElement | null; + +const ErrorMessage = ({ + title, + description, +}: { + title: string; + description: string; +}) => ( +

+ + {title} + {description} + +
+); + +/** + * @see https://github.com/epicweb-dev/epic-stack/blob/main/app/components/error-boundary.tsx + */ +export function GeneralErrorBoundary({ + defaultStatusHandler = ({ error }) => ( + + ), + statusHandlers = { + 404: ({ error }) => { + throw error; + }, + }, + unexpectedErrorHandler = (error) => ( + + ), +}: { + defaultStatusHandler?: StatusHandler; + statusHandlers?: Record; + unexpectedErrorHandler?: (error: unknown) => ReactElement | null; +}) { + const error = useRouteError(); + const params = useParams(); + + if (typeof document !== "undefined") { + console.error("client error in general error boundary", error); + } + + return ( +
+ {isRouteErrorResponse(error) + ? (statusHandlers?.[error.status] ?? defaultStatusHandler)({ + error, + params, + }) + : unexpectedErrorHandler(error)} +
+ ); +} diff --git a/apps/react-router/saas-template/app/components/magicui/iphone-15-pro.tsx b/apps/react-router/saas-template/app/components/magicui/iphone-15-pro.tsx new file mode 100644 index 0000000..cfeb688 --- /dev/null +++ b/apps/react-router/saas-template/app/components/magicui/iphone-15-pro.tsx @@ -0,0 +1,129 @@ +import type { SVGProps } from "react"; +import { useId } from "react"; + +import { cn } from "~/lib/utils"; + +export type Iphone15ProProps = { + width?: number; + height?: number; + src?: string; + videoSrc?: string; +} & SVGProps; + +export function Iphone15Pro({ + className, + height, + src, + videoSrc, + width, + ...props +}: Iphone15ProProps) { + const id = useId(); + + return ( + + Iphone 15 Pro + + + + + + + + + + + + + + + + + {src && ( + + )} + + {videoSrc && ( + +