@aurostack/stacks 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +209 -0
- package/cli/src/extract.mjs +165 -0
- package/cli/src/fsx.mjs +197 -0
- package/cli/src/generate.mjs +235 -0
- package/cli/src/hooks.mjs +55 -0
- package/cli/src/manifest.mjs +163 -0
- package/cli/src/strip.mjs +208 -0
- package/cli/src/tokens.mjs +102 -0
- package/cli/stack.mjs +672 -0
- package/package.json +37 -0
- package/templates/nest-api/files/.bin/db.ts +123 -0
- package/templates/nest-api/files/.bin/dc.ts +74 -0
- package/templates/nest-api/files/.bin/helpers.ts +49 -0
- package/templates/nest-api/files/.bin/secrets.ts +68 -0
- package/templates/nest-api/files/.bin/start.ts +47 -0
- package/templates/nest-api/files/.dockerignore +7 -0
- package/templates/nest-api/files/.env.example +91 -0
- package/templates/nest-api/files/.env.test.example +75 -0
- package/templates/nest-api/files/.forgejo/workflows/build-and-push.yml +41 -0
- package/templates/nest-api/files/.forgejo/workflows/test-e2e.yml +83 -0
- package/templates/nest-api/files/.forgejo/workflows/test.yml +53 -0
- package/templates/nest-api/files/.npmrc.example +1 -0
- package/templates/nest-api/files/.nvmrc +1 -0
- package/templates/nest-api/files/.prettierrc +8 -0
- package/templates/nest-api/files/.swcrc +15 -0
- package/templates/nest-api/files/.watchmanconfig +3 -0
- package/templates/nest-api/files/.yarnrc.yml +1 -0
- package/templates/nest-api/files/Dockerfile +40 -0
- package/templates/nest-api/files/__mocks__/activation-twofa.js +7 -0
- package/templates/nest-api/files/__mocks__/activation.js +7 -0
- package/templates/nest-api/files/__mocks__/forgot-password.js +7 -0
- package/templates/nest-api/files/__mocks__/main-layout.js +7 -0
- package/templates/nest-api/files/__mocks__/password-changed.js +7 -0
- package/templates/nest-api/files/__mocks__/reset-password.js +7 -0
- package/templates/nest-api/files/__mocks__/set-password.js +7 -0
- package/templates/nest-api/files/__mocks__/welcome.js +7 -0
- package/templates/nest-api/files/_gitignore +62 -0
- package/templates/nest-api/files/compose.test.yml +33 -0
- package/templates/nest-api/files/compose.yml +39 -0
- package/templates/nest-api/files/emails/activation-twofa.tsx +58 -0
- package/templates/nest-api/files/emails/activation.tsx +36 -0
- package/templates/nest-api/files/emails/forgot-password.tsx +29 -0
- package/templates/nest-api/files/emails/main-layout.tsx +134 -0
- package/templates/nest-api/files/emails/password-changed.tsx +24 -0
- package/templates/nest-api/files/emails/reset-password.tsx +24 -0
- package/templates/nest-api/files/emails/set-password.tsx +25 -0
- package/templates/nest-api/files/emails/welcome.tsx +39 -0
- package/templates/nest-api/files/eslint.config.mjs +28 -0
- package/templates/nest-api/files/global.d.ts +5 -0
- package/templates/nest-api/files/nest-cli.json +8 -0
- package/templates/nest-api/files/package.json +150 -0
- package/templates/nest-api/files/prisma/schema/auth.prisma +75 -0
- package/templates/nest-api/files/prisma/schema/base.prisma +17 -0
- package/templates/nest-api/files/prisma/schema/enum.prisma +17 -0
- package/templates/nest-api/files/prisma/schema/notification.prisma +14 -0
- package/templates/nest-api/files/prisma.config.ts +13 -0
- package/templates/nest-api/files/public/.gitkeep +0 -0
- package/templates/nest-api/files/seeders/index.ts +27 -0
- package/templates/nest-api/files/seeders/util.ts +88 -0
- package/templates/nest-api/files/src/__tests__/app.controller.spec.ts +29 -0
- package/templates/nest-api/files/src/app.controller.ts +40 -0
- package/templates/nest-api/files/src/app.module.ts +268 -0
- package/templates/nest-api/files/src/app.resolver.ts +32 -0
- package/templates/nest-api/files/src/app.setup.ts +178 -0
- package/templates/nest-api/files/src/common/__tests__/auth.service.spec.ts +31 -0
- package/templates/nest-api/files/src/common/__tests__/cache.service.spec.ts +214 -0
- package/templates/nest-api/files/src/common/__tests__/config.service.spec.ts +56 -0
- package/templates/nest-api/files/src/common/__tests__/generator.service.spec.ts +114 -0
- package/templates/nest-api/files/src/common/__tests__/health.controller.spec.ts +72 -0
- package/templates/nest-api/files/src/common/__tests__/helpers.spec.ts +124 -0
- package/templates/nest-api/files/src/common/__tests__/mail.service.spec.ts +75 -0
- package/templates/nest-api/files/src/common/__tests__/prisma.indicator.spec.ts +39 -0
- package/templates/nest-api/files/src/common/__tests__/prisma.service.spec.ts +15 -0
- package/templates/nest-api/files/src/common/__tests__/records.spec.ts +87 -0
- package/templates/nest-api/files/src/common/__tests__/schemas.spec.ts +142 -0
- package/templates/nest-api/files/src/common/__tests__/throttler-storage.service.spec.ts +80 -0
- package/templates/nest-api/files/src/common/common.module.ts +72 -0
- package/templates/nest-api/files/src/common/constants/index.ts +2 -0
- package/templates/nest-api/files/src/common/constants/options.ts +52 -0
- package/templates/nest-api/files/src/common/constants/pino.ts +24 -0
- package/templates/nest-api/files/src/common/controllers/health.controller.ts +54 -0
- package/templates/nest-api/files/src/common/controllers/index.ts +2 -0
- package/templates/nest-api/files/src/common/controllers/metrics.controller.ts +21 -0
- package/templates/nest-api/files/src/common/decorators/index.ts +2 -0
- package/templates/nest-api/files/src/common/decorators/misc.decorator.ts +49 -0
- package/templates/nest-api/files/src/common/decorators/multi-part.decorator.ts +6 -0
- package/templates/nest-api/files/src/common/dto/filters.dto.ts +25 -0
- package/templates/nest-api/files/src/common/dto/index.ts +1 -0
- package/templates/nest-api/files/src/common/entity/index.ts +2 -0
- package/templates/nest-api/files/src/common/entity/ok.entity.ts +11 -0
- package/templates/nest-api/files/src/common/entity/pagination.entity.ts +24 -0
- package/templates/nest-api/files/src/common/exceptions/index.ts +1 -0
- package/templates/nest-api/files/src/common/exceptions/prisma.exception.ts +57 -0
- package/templates/nest-api/files/src/common/guards/feature-flag.guard.ts +51 -0
- package/templates/nest-api/files/src/common/guards/index.ts +1 -0
- package/templates/nest-api/files/src/common/interceptors/index.ts +1 -0
- package/templates/nest-api/files/src/common/interceptors/metrics.interceptor.ts +37 -0
- package/templates/nest-api/files/src/common/misc/helpers.ts +26 -0
- package/templates/nest-api/files/src/common/misc/index.ts +3 -0
- package/templates/nest-api/files/src/common/misc/origin-builder.ts +56 -0
- package/templates/nest-api/files/src/common/misc/prisma.indicator.ts +24 -0
- package/templates/nest-api/files/src/common/modules/index.ts +3 -0
- package/templates/nest-api/files/src/common/modules/logger.module.ts +104 -0
- package/templates/nest-api/files/src/common/modules/metrics.module.ts +28 -0
- package/templates/nest-api/files/src/common/modules/queue.module.ts +23 -0
- package/templates/nest-api/files/src/common/processors/index.ts +1 -0
- package/templates/nest-api/files/src/common/processors/mail/index.ts +47 -0
- package/templates/nest-api/files/src/common/processors/mail/process.ts +225 -0
- package/templates/nest-api/files/src/common/schemas/fields.ts +76 -0
- package/templates/nest-api/files/src/common/schemas/index.ts +1 -0
- package/templates/nest-api/files/src/common/services/auth-rate-limit.storage.ts +79 -0
- package/templates/nest-api/files/src/common/services/auth.service.ts +16 -0
- package/templates/nest-api/files/src/common/services/cache.service.ts +118 -0
- package/templates/nest-api/files/src/common/services/config.service.ts +388 -0
- package/templates/nest-api/files/src/common/services/feature-flag.service.ts +117 -0
- package/templates/nest-api/files/src/common/services/generator.service.ts +54 -0
- package/templates/nest-api/files/src/common/services/index.ts +10 -0
- package/templates/nest-api/files/src/common/services/logger.service.ts +14 -0
- package/templates/nest-api/files/src/common/services/mail.service.ts +44 -0
- package/templates/nest-api/files/src/common/services/prisma.service.ts +106 -0
- package/templates/nest-api/files/src/common/services/throttler-storage.service.ts +112 -0
- package/templates/nest-api/files/src/common/types/email.ts +55 -0
- package/templates/nest-api/files/src/common/types/index.ts +3 -0
- package/templates/nest-api/files/src/common/types/misc.ts +20 -0
- package/templates/nest-api/files/src/common/types/prisma.ts +18 -0
- package/templates/nest-api/files/src/common/utils/enum.ts +39 -0
- package/templates/nest-api/files/src/common/utils/helpers.ts +139 -0
- package/templates/nest-api/files/src/common/utils/index.ts +5 -0
- package/templates/nest-api/files/src/common/utils/ip.ts +22 -0
- package/templates/nest-api/files/src/common/utils/records.ts +66 -0
- package/templates/nest-api/files/src/common/utils/redis.keys.ts +31 -0
- package/templates/nest-api/files/src/lib/__tests__/access.spec.ts +33 -0
- package/templates/nest-api/files/src/lib/access.ts +114 -0
- package/templates/nest-api/files/src/lib/auth.ts +76 -0
- package/templates/nest-api/files/src/lib/rate-limit.ts +20 -0
- package/templates/nest-api/files/src/main.ts +28 -0
- package/templates/nest-api/files/src/media/__tests__/media.service.spec.ts +60 -0
- package/templates/nest-api/files/src/media/__tests__/s3.service.spec.ts +159 -0
- package/templates/nest-api/files/src/media/entity/file.entity.ts +7 -0
- package/templates/nest-api/files/src/media/entity/index.ts +1 -0
- package/templates/nest-api/files/src/media/media.module.ts +20 -0
- package/templates/nest-api/files/src/media/processors/index.ts +44 -0
- package/templates/nest-api/files/src/media/services/index.ts +2 -0
- package/templates/nest-api/files/src/media/services/media.service.ts +29 -0
- package/templates/nest-api/files/src/media/services/s3.service.ts +168 -0
- package/templates/nest-api/files/src/notifications/controllers/index.ts +1 -0
- package/templates/nest-api/files/src/notifications/controllers/notifications.controller.ts +72 -0
- package/templates/nest-api/files/src/notifications/dto/index.ts +1 -0
- package/templates/nest-api/files/src/notifications/dto/notification.dto.ts +11 -0
- package/templates/nest-api/files/src/notifications/entity/index.ts +1 -0
- package/templates/nest-api/files/src/notifications/entity/notification.entity.ts +37 -0
- package/templates/nest-api/files/src/notifications/notifications.module.ts +11 -0
- package/templates/nest-api/files/src/notifications/services/index.ts +1 -0
- package/templates/nest-api/files/src/notifications/services/notifications.service.ts +98 -0
- package/templates/nest-api/files/src/realtime/__tests__/events.gateway.spec.ts +88 -0
- package/templates/nest-api/files/src/realtime/__tests__/rooms.spec.ts +29 -0
- package/templates/nest-api/files/src/realtime/__tests__/ws-permissions.guard.spec.ts +54 -0
- package/templates/nest-api/files/src/realtime/adapters/redis-io.adapter.ts +57 -0
- package/templates/nest-api/files/src/realtime/constants/index.ts +68 -0
- package/templates/nest-api/files/src/realtime/decorators/index.ts +1 -0
- package/templates/nest-api/files/src/realtime/decorators/ws-permissions.decorator.ts +14 -0
- package/templates/nest-api/files/src/realtime/gateways/events.gateway.ts +187 -0
- package/templates/nest-api/files/src/realtime/guards/index.ts +2 -0
- package/templates/nest-api/files/src/realtime/guards/ws-auth.guard.ts +20 -0
- package/templates/nest-api/files/src/realtime/guards/ws-permissions.guard.ts +35 -0
- package/templates/nest-api/files/src/realtime/misc/session-from-socket.ts +54 -0
- package/templates/nest-api/files/src/realtime/realtime.module.ts +26 -0
- package/templates/nest-api/files/src/realtime/services/presence.service.ts +56 -0
- package/templates/nest-api/files/src/realtime/services/realtime.service.ts +72 -0
- package/templates/nest-api/files/src/users/__tests__/current-user.controller.spec.ts +77 -0
- package/templates/nest-api/files/src/users/__tests__/current-user.service.spec.ts +159 -0
- package/templates/nest-api/files/src/users/__tests__/user.entity.spec.ts +175 -0
- package/templates/nest-api/files/src/users/__tests__/users.controller.spec.ts +89 -0
- package/templates/nest-api/files/src/users/__tests__/users.service.spec.ts +398 -0
- package/templates/nest-api/files/src/users/controllers/current-user.controller.ts +82 -0
- package/templates/nest-api/files/src/users/controllers/index.ts +2 -0
- package/templates/nest-api/files/src/users/controllers/users.controller.ts +40 -0
- package/templates/nest-api/files/src/users/decorators/index.ts +1 -0
- package/templates/nest-api/files/src/users/decorators/permissions.decorator.ts +24 -0
- package/templates/nest-api/files/src/users/dto/avatar.dto.ts +13 -0
- package/templates/nest-api/files/src/users/dto/filters.dto.ts +8 -0
- package/templates/nest-api/files/src/users/dto/index.ts +2 -0
- package/templates/nest-api/files/src/users/entity/index.ts +1 -0
- package/templates/nest-api/files/src/users/entity/user.entity.ts +53 -0
- package/templates/nest-api/files/src/users/services/current-user.service.ts +46 -0
- package/templates/nest-api/files/src/users/services/index.ts +2 -0
- package/templates/nest-api/files/src/users/services/users.service.ts +88 -0
- package/templates/nest-api/files/src/users/users.module.ts +10 -0
- package/templates/nest-api/files/test/app.spec.ts +102 -0
- package/templates/nest-api/files/test/auth.spec.ts +108 -0
- package/templates/nest-api/files/test/current-user.spec.ts +154 -0
- package/templates/nest-api/files/test/factory/app.ts +172 -0
- package/templates/nest-api/files/test/factory/constants.ts +22 -0
- package/templates/nest-api/files/test/factory/dto.ts +27 -0
- package/templates/nest-api/files/test/factory/entity.ts +70 -0
- package/templates/nest-api/files/test/factory/gen.ts +75 -0
- package/templates/nest-api/files/test/factory/util.ts +21 -0
- package/templates/nest-api/files/test/notifications.spec.ts +176 -0
- package/templates/nest-api/files/test/rate-limit.spec.ts +180 -0
- package/templates/nest-api/files/test/setup.ts +1 -0
- package/templates/nest-api/files/test/users.spec.ts +437 -0
- package/templates/nest-api/files/tsconfig.build.json +13 -0
- package/templates/nest-api/files/tsconfig.json +35 -0
- package/templates/nest-api/files/vitest.config.ts +53 -0
- package/templates/nest-api/template.json +452 -0
- package/templates/node-worker/files/.bin/db.ts +123 -0
- package/templates/node-worker/files/.bin/helpers.ts +49 -0
- package/templates/node-worker/files/.bin/secrets.ts +68 -0
- package/templates/node-worker/files/.dockerignore +5 -0
- package/templates/node-worker/files/.env.example +21 -0
- package/templates/node-worker/files/.forgejo/workflows/build-and-push.yml +41 -0
- package/templates/node-worker/files/.nvmrc +1 -0
- package/templates/node-worker/files/.prettierignore +4 -0
- package/templates/node-worker/files/.prettierrc +8 -0
- package/templates/node-worker/files/.yarnrc.yml +5 -0
- package/templates/node-worker/files/Dockerfile +63 -0
- package/templates/node-worker/files/Dockerfile.dev +59 -0
- package/templates/node-worker/files/_gitignore +48 -0
- package/templates/node-worker/files/ecosystem.config.cjs +10 -0
- package/templates/node-worker/files/eslint.config.js +37 -0
- package/templates/node-worker/files/package.json +67 -0
- package/templates/node-worker/files/prisma/_gitignore +1 -0
- package/templates/node-worker/files/prisma/schema/base.prisma +36 -0
- package/templates/node-worker/files/prisma.config.ts +13 -0
- package/templates/node-worker/files/src/config.ts +55 -0
- package/templates/node-worker/files/src/db.ts +118 -0
- package/templates/node-worker/files/src/index.ts +7 -0
- package/templates/node-worker/files/src/logger.ts +45 -0
- package/templates/node-worker/files/src/types.ts +23 -0
- package/templates/node-worker/files/src/utils/browser.ts +16 -0
- package/templates/node-worker/files/src/utils/date.ts +68 -0
- package/templates/node-worker/files/src/utils/helpers.ts +17 -0
- package/templates/node-worker/files/src/utils/number.ts +47 -0
- package/templates/node-worker/files/src/utils/worker.ts +29 -0
- package/templates/node-worker/files/src/workers/constants.ts +6 -0
- package/templates/node-worker/files/src/workers/example/index.ts +27 -0
- package/templates/node-worker/files/src/workers/index.ts +1 -0
- package/templates/node-worker/files/tsconfig.json +30 -0
- package/templates/node-worker/template.json +135 -0
- package/templates/py-worker/files/.dockerignore +176 -0
- package/templates/py-worker/files/.env.example +20 -0
- package/templates/py-worker/files/.forgejo/workflows/build-amd-push.yml +39 -0
- package/templates/py-worker/files/Dockerfile +10 -0
- package/templates/py-worker/files/README.md +3 -0
- package/templates/py-worker/files/_gitignore +176 -0
- package/templates/py-worker/files/config.py +89 -0
- package/templates/py-worker/files/models/decorators.py +42 -0
- package/templates/py-worker/files/models/enums.py +32 -0
- package/templates/py-worker/files/models/mixins.py +61 -0
- package/templates/py-worker/files/requirements.txt +18 -0
- package/templates/py-worker/files/utils/db.py +67 -0
- package/templates/py-worker/files/utils/logger.py +97 -0
- package/templates/py-worker/files/utils/misc.py +45 -0
- package/templates/py-worker/files/utils/worker.py +80 -0
- package/templates/py-worker/files/worker.py +41 -0
- package/templates/py-worker/files/workers/hello/process.py +22 -0
- package/templates/py-worker/template.json +87 -0
- package/templates/react-app/README.md +78 -0
- package/templates/react-app/derive.sh +128 -0
- package/templates/react-app/files/.bin/openapi.js +32 -0
- package/templates/react-app/files/.bin/secrets.js +47 -0
- package/templates/react-app/files/.dockerignore +16 -0
- package/templates/react-app/files/.editorconfig +12 -0
- package/templates/react-app/files/.env.example +34 -0
- package/templates/react-app/files/.forgejo/workflows/build-and-push.yml +55 -0
- package/templates/react-app/files/.forgejo/workflows/ci.yml +39 -0
- package/templates/react-app/files/.husky/pre-commit +1 -0
- package/templates/react-app/files/.nvmrc +1 -0
- package/templates/react-app/files/.prettierignore +6 -0
- package/templates/react-app/files/.prettierrc +9 -0
- package/templates/react-app/files/.yarnrc.yml +2 -0
- package/templates/react-app/files/Dockerfile +52 -0
- package/templates/react-app/files/_gitignore +33 -0
- package/templates/react-app/files/build.sh +53 -0
- package/templates/react-app/files/docker/nginx.conf +134 -0
- package/templates/react-app/files/eslint.config.mjs +75 -0
- package/templates/react-app/files/index.html +62 -0
- package/templates/react-app/files/openapi-ts.config.ts +22 -0
- package/templates/react-app/files/package.json +80 -0
- package/templates/react-app/files/src/app/nav.tsx +32 -0
- package/templates/react-app/files/src/app/providers.tsx +39 -0
- package/templates/react-app/files/src/app/router.tsx +142 -0
- package/templates/react-app/files/src/app/store.ts +20 -0
- package/templates/react-app/files/src/features/auth/components/auth-card.tsx +23 -0
- package/templates/react-app/files/src/features/auth/components/checkbox-field.tsx +32 -0
- package/templates/react-app/files/src/features/auth/components/google-button.tsx +41 -0
- package/templates/react-app/files/src/features/auth/components/or-divider.tsx +9 -0
- package/templates/react-app/files/src/features/auth/components/password-field.tsx +70 -0
- package/templates/react-app/files/src/features/auth/components/submit-button.tsx +16 -0
- package/templates/react-app/files/src/features/auth/lib/auth-actions.ts +68 -0
- package/templates/react-app/files/src/features/auth/lib/password-strength.ts +20 -0
- package/templates/react-app/files/src/features/auth/lib/redirect.ts +6 -0
- package/templates/react-app/files/src/features/auth/lib/schemas.ts +37 -0
- package/templates/react-app/files/src/features/seo/routes.ts +65 -0
- package/templates/react-app/files/src/lib/env.ts +28 -0
- package/templates/react-app/files/src/lib/paths.ts +58 -0
- package/templates/react-app/files/src/main.tsx +19 -0
- package/templates/react-app/files/src/routes/admin/home.tsx +24 -0
- package/templates/react-app/files/src/routes/auth/forgot-password.tsx +93 -0
- package/templates/react-app/files/src/routes/auth/login.tsx +87 -0
- package/templates/react-app/files/src/routes/auth/reset-password.tsx +83 -0
- package/templates/react-app/files/src/routes/auth/signup.tsx +117 -0
- package/templates/react-app/files/src/routes/auth/verified.tsx +31 -0
- package/templates/react-app/files/src/routes/auth/verify-email.tsx +97 -0
- package/templates/react-app/files/src/routes/home.tsx +17 -0
- package/templates/react-app/files/src/routes/legal/privacy.tsx +26 -0
- package/templates/react-app/files/src/routes/legal/terms.tsx +28 -0
- package/templates/react-app/files/src/routes/marketing/home.tsx +18 -0
- package/templates/react-app/files/src/routes/settings.tsx +10 -0
- package/templates/react-app/files/src/shared/api/base-api.ts +24 -0
- package/templates/react-app/files/src/shared/api/base-query.ts +29 -0
- package/templates/react-app/files/src/shared/api/config.ts +21 -0
- package/templates/react-app/files/src/shared/api/index.ts +15 -0
- package/templates/react-app/files/src/shared/api/realtime-events.ts +34 -0
- package/templates/react-app/files/src/shared/api/socket.ts +41 -0
- package/templates/react-app/files/src/shared/api/store.ts +39 -0
- package/templates/react-app/files/src/shared/api/upload-query.ts +115 -0
- package/templates/react-app/files/src/shared/auth/client.ts +27 -0
- package/templates/react-app/files/src/shared/auth/config.ts +28 -0
- package/templates/react-app/files/src/shared/auth/guards.tsx +75 -0
- package/templates/react-app/files/src/shared/auth/hooks.ts +13 -0
- package/templates/react-app/files/src/shared/auth/index.ts +20 -0
- package/templates/react-app/files/src/shared/auth/logout.ts +23 -0
- package/templates/react-app/files/src/shared/auth/provider.tsx +52 -0
- package/templates/react-app/files/src/shared/auth/redirect.ts +65 -0
- package/templates/react-app/files/src/shared/auth/slice.ts +54 -0
- package/templates/react-app/files/src/shared/hooks/index.ts +6 -0
- package/templates/react-app/files/src/shared/layouts/app-shell/app-shell.tsx +225 -0
- package/templates/react-app/files/src/shared/layouts/app-shell/user-menu.tsx +74 -0
- package/templates/react-app/files/src/shared/layouts/auth-layout.tsx +137 -0
- package/templates/react-app/files/src/shared/layouts/error-boundary.tsx +38 -0
- package/templates/react-app/files/src/shared/layouts/index.ts +23 -0
- package/templates/react-app/files/src/shared/layouts/marketing/marketing-layout.tsx +29 -0
- package/templates/react-app/files/src/shared/layouts/marketing/section-heading.tsx +38 -0
- package/templates/react-app/files/src/shared/layouts/marketing/section.tsx +30 -0
- package/templates/react-app/files/src/shared/layouts/states/app-shell-skeleton.tsx +47 -0
- package/templates/react-app/files/src/shared/layouts/states/empty.tsx +22 -0
- package/templates/react-app/files/src/shared/layouts/states/forbidden.tsx +14 -0
- package/templates/react-app/files/src/shared/layouts/states/loading.tsx +11 -0
- package/templates/react-app/files/src/shared/layouts/states/not-found.tsx +21 -0
- package/templates/react-app/files/src/shared/layouts/states/page-skeleton.tsx +39 -0
- package/templates/react-app/files/src/shared/layouts/states/route-fallback.tsx +26 -0
- package/templates/react-app/files/src/shared/layouts/states/state-screen.tsx +22 -0
- package/templates/react-app/files/src/shared/types/README.md +28 -0
- package/templates/react-app/files/src/shared/types/api/index.ts +9 -0
- package/templates/react-app/files/src/shared/types/api/zod.gen.ts +5 -0
- package/templates/react-app/files/src/shared/types/index.ts +4 -0
- package/templates/react-app/files/src/shared/ui/components/brand/logo.tsx +51 -0
- package/templates/react-app/files/src/shared/ui/components/brand/social-icons.tsx +37 -0
- package/templates/react-app/files/src/shared/ui/components/form/form.tsx +120 -0
- package/templates/react-app/files/src/shared/ui/components/ui/alert.tsx +25 -0
- package/templates/react-app/files/src/shared/ui/components/ui/button.tsx +43 -0
- package/templates/react-app/files/src/shared/ui/components/ui/calendar.tsx +78 -0
- package/templates/react-app/files/src/shared/ui/components/ui/card.tsx +44 -0
- package/templates/react-app/files/src/shared/ui/components/ui/checkbox.tsx +16 -0
- package/templates/react-app/files/src/shared/ui/components/ui/date-picker.tsx +76 -0
- package/templates/react-app/files/src/shared/ui/components/ui/input.tsx +22 -0
- package/templates/react-app/files/src/shared/ui/components/ui/label.tsx +19 -0
- package/templates/react-app/files/src/shared/ui/components/ui/modal.tsx +94 -0
- package/templates/react-app/files/src/shared/ui/components/ui/popover.tsx +31 -0
- package/templates/react-app/files/src/shared/ui/components/ui/progress.tsx +33 -0
- package/templates/react-app/files/src/shared/ui/components/ui/search-input.tsx +69 -0
- package/templates/react-app/files/src/shared/ui/components/ui/select.tsx +80 -0
- package/templates/react-app/files/src/shared/ui/components/ui/skeleton.tsx +12 -0
- package/templates/react-app/files/src/shared/ui/components/ui/sonner.tsx +23 -0
- package/templates/react-app/files/src/shared/ui/components/ui/spinner.tsx +8 -0
- package/templates/react-app/files/src/shared/ui/components/ui/textarea.tsx +22 -0
- package/templates/react-app/files/src/shared/ui/index.ts +45 -0
- package/templates/react-app/files/src/shared/ui/lib/utils.ts +7 -0
- package/templates/react-app/files/src/shared/ui/lib/zod-setup.ts +28 -0
- package/templates/react-app/files/src/shared/ui/meta/brand.ts +18 -0
- package/templates/react-app/files/src/shared/ui/meta/meta-context.ts +30 -0
- package/templates/react-app/files/src/shared/ui/meta/meta-provider.tsx +24 -0
- package/templates/react-app/files/src/shared/ui/meta/page-meta.tsx +69 -0
- package/templates/react-app/files/src/shared/ui/meta/use-meta-config.ts +11 -0
- package/templates/react-app/files/src/shared/ui/styles/globals.css +285 -0
- package/templates/react-app/files/src/shared/ui/styles/theme.css +48 -0
- package/templates/react-app/files/src/shared/ui/theme/init-script.ts +23 -0
- package/templates/react-app/files/src/shared/ui/theme/theme-colors.ts +14 -0
- package/templates/react-app/files/src/shared/ui/theme/theme-context.ts +60 -0
- package/templates/react-app/files/src/shared/ui/theme/theme-provider.tsx +101 -0
- package/templates/react-app/files/src/shared/ui/theme/theme-toggle.tsx +40 -0
- package/templates/react-app/files/src/shared/ui/theme/use-theme.ts +11 -0
- package/templates/react-app/files/src/vite-env.d.ts +13 -0
- package/templates/react-app/files/tsconfig.json +26 -0
- package/templates/react-app/files/tsconfig.node.json +25 -0
- package/templates/react-app/files/vite.config.ts +344 -0
- package/templates/react-app/overrides/src/routes/auth/forgot-password.tsx +93 -0
- package/templates/react-app/overrides/src/routes/auth/login.tsx +87 -0
- package/templates/react-app/overrides/src/routes/auth/signup.tsx +117 -0
- package/templates/react-app/overrides/src/routes/auth/verified.tsx +31 -0
- package/templates/react-app/overrides/src/routes/auth/verify-email.tsx +97 -0
- package/templates/react-app/overrides/src/shared/auth/config.ts +28 -0
- package/templates/react-app/overrides/src/shared/auth/guards.tsx +75 -0
- package/templates/react-app/overrides/src/shared/auth/index.ts +20 -0
- package/templates/react-app/overrides/src/shared/auth/redirect.ts +65 -0
- package/templates/react-app/overrides/src/shared/hooks/index.ts +6 -0
- package/templates/react-app/overrides/src/shared/types/README.md +28 -0
- package/templates/react-app/template.json +222 -0
- package/templates/react-monorepo/files/.dockerignore +19 -0
- package/templates/react-monorepo/files/.editorconfig +12 -0
- package/templates/react-monorepo/files/.forgejo/workflows/build-and-push.yml +98 -0
- package/templates/react-monorepo/files/.forgejo/workflows/ci.yml +35 -0
- package/templates/react-monorepo/files/.husky/pre-commit +1 -0
- package/templates/react-monorepo/files/.nvmrc +1 -0
- package/templates/react-monorepo/files/.prettierignore +6 -0
- package/templates/react-monorepo/files/.prettierrc +9 -0
- package/templates/react-monorepo/files/.yarnrc.yml +2 -0
- package/templates/react-monorepo/files/Dockerfile +90 -0
- package/templates/react-monorepo/files/_gitignore +35 -0
- package/templates/react-monorepo/files/apps/admin/.bin/secrets.js +57 -0
- package/templates/react-monorepo/files/apps/admin/.env.example +16 -0
- package/templates/react-monorepo/files/apps/admin/eslint.config.mjs +3 -0
- package/templates/react-monorepo/files/apps/admin/index.html +57 -0
- package/templates/react-monorepo/files/apps/admin/package.json +33 -0
- package/templates/react-monorepo/files/apps/admin/src/App.tsx +3 -0
- package/templates/react-monorepo/files/apps/admin/src/app/nav.tsx +14 -0
- package/templates/react-monorepo/files/apps/admin/src/app/providers.tsx +42 -0
- package/templates/react-monorepo/files/apps/admin/src/app/router.tsx +50 -0
- package/templates/react-monorepo/files/apps/admin/src/app/store.ts +15 -0
- package/templates/react-monorepo/files/apps/admin/src/lib/env.ts +12 -0
- package/templates/react-monorepo/files/apps/admin/src/main.tsx +14 -0
- package/templates/react-monorepo/files/apps/admin/src/routes/home.tsx +17 -0
- package/templates/react-monorepo/files/apps/admin/src/routes/settings.tsx +10 -0
- package/templates/react-monorepo/files/apps/admin/src/vite-env.d.ts +14 -0
- package/templates/react-monorepo/files/apps/admin/tsconfig.json +10 -0
- package/templates/react-monorepo/files/apps/admin/vite.config.ts +13 -0
- package/templates/react-monorepo/files/apps/auth/.bin/secrets.js +57 -0
- package/templates/react-monorepo/files/apps/auth/.env.example +16 -0
- package/templates/react-monorepo/files/apps/auth/eslint.config.mjs +3 -0
- package/templates/react-monorepo/files/apps/auth/index.html +57 -0
- package/templates/react-monorepo/files/apps/auth/package.json +32 -0
- package/templates/react-monorepo/files/apps/auth/src/App.tsx +3 -0
- package/templates/react-monorepo/files/apps/auth/src/app/providers.tsx +42 -0
- package/templates/react-monorepo/files/apps/auth/src/app/router.tsx +19 -0
- package/templates/react-monorepo/files/apps/auth/src/app/store.ts +15 -0
- package/templates/react-monorepo/files/apps/auth/src/components/auth-card.tsx +23 -0
- package/templates/react-monorepo/files/apps/auth/src/components/checkbox-field.tsx +32 -0
- package/templates/react-monorepo/files/apps/auth/src/components/google-button.tsx +41 -0
- package/templates/react-monorepo/files/apps/auth/src/components/or-divider.tsx +9 -0
- package/templates/react-monorepo/files/apps/auth/src/components/password-field.tsx +70 -0
- package/templates/react-monorepo/files/apps/auth/src/components/submit-button.tsx +16 -0
- package/templates/react-monorepo/files/apps/auth/src/lib/auth-actions.ts +68 -0
- package/templates/react-monorepo/files/apps/auth/src/lib/env.ts +8 -0
- package/templates/react-monorepo/files/apps/auth/src/lib/password-strength.ts +20 -0
- package/templates/react-monorepo/files/apps/auth/src/lib/redirect.ts +6 -0
- package/templates/react-monorepo/files/apps/auth/src/lib/schemas.ts +37 -0
- package/templates/react-monorepo/files/apps/auth/src/main.tsx +14 -0
- package/templates/react-monorepo/files/apps/auth/src/routes/forgot-password.tsx +91 -0
- package/templates/react-monorepo/files/apps/auth/src/routes/login.tsx +82 -0
- package/templates/react-monorepo/files/apps/auth/src/routes/reset-password.tsx +83 -0
- package/templates/react-monorepo/files/apps/auth/src/routes/signup.tsx +117 -0
- package/templates/react-monorepo/files/apps/auth/src/routes/verified.tsx +30 -0
- package/templates/react-monorepo/files/apps/auth/src/routes/verify-email.tsx +96 -0
- package/templates/react-monorepo/files/apps/auth/src/vite-env.d.ts +14 -0
- package/templates/react-monorepo/files/apps/auth/tsconfig.json +10 -0
- package/templates/react-monorepo/files/apps/auth/vite.config.ts +13 -0
- package/templates/react-monorepo/files/apps/client/.bin/secrets.js +57 -0
- package/templates/react-monorepo/files/apps/client/.env.example +16 -0
- package/templates/react-monorepo/files/apps/client/eslint.config.mjs +3 -0
- package/templates/react-monorepo/files/apps/client/index.html +57 -0
- package/templates/react-monorepo/files/apps/client/package.json +35 -0
- package/templates/react-monorepo/files/apps/client/src/App.tsx +3 -0
- package/templates/react-monorepo/files/apps/client/src/app/nav.tsx +14 -0
- package/templates/react-monorepo/files/apps/client/src/app/providers.tsx +42 -0
- package/templates/react-monorepo/files/apps/client/src/app/router.tsx +50 -0
- package/templates/react-monorepo/files/apps/client/src/app/store.ts +15 -0
- package/templates/react-monorepo/files/apps/client/src/lib/env.ts +12 -0
- package/templates/react-monorepo/files/apps/client/src/main.tsx +19 -0
- package/templates/react-monorepo/files/apps/client/src/routes/home.tsx +17 -0
- package/templates/react-monorepo/files/apps/client/src/routes/settings.tsx +10 -0
- package/templates/react-monorepo/files/apps/client/src/vite-env.d.ts +15 -0
- package/templates/react-monorepo/files/apps/client/tsconfig.json +10 -0
- package/templates/react-monorepo/files/apps/client/vite.config.ts +120 -0
- package/templates/react-monorepo/files/apps/landing/.bin/secrets.js +57 -0
- package/templates/react-monorepo/files/apps/landing/.env.example +20 -0
- package/templates/react-monorepo/files/apps/landing/eslint.config.mjs +3 -0
- package/templates/react-monorepo/files/apps/landing/index.html +51 -0
- package/templates/react-monorepo/files/apps/landing/package.json +33 -0
- package/templates/react-monorepo/files/apps/landing/src/App.tsx +3 -0
- package/templates/react-monorepo/files/apps/landing/src/app/providers.tsx +42 -0
- package/templates/react-monorepo/files/apps/landing/src/app/router.tsx +21 -0
- package/templates/react-monorepo/files/apps/landing/src/app/store.ts +18 -0
- package/templates/react-monorepo/files/apps/landing/src/features/seo/routes.ts +65 -0
- package/templates/react-monorepo/files/apps/landing/src/lib/env.ts +10 -0
- package/templates/react-monorepo/files/apps/landing/src/main.tsx +14 -0
- package/templates/react-monorepo/files/apps/landing/src/routes/home.tsx +18 -0
- package/templates/react-monorepo/files/apps/landing/src/vite-env.d.ts +15 -0
- package/templates/react-monorepo/files/apps/landing/tsconfig.json +10 -0
- package/templates/react-monorepo/files/apps/landing/vite.config.ts +169 -0
- package/templates/react-monorepo/files/build.sh +61 -0
- package/templates/react-monorepo/files/docker/nginx.conf +136 -0
- package/templates/react-monorepo/files/package.json +37 -0
- package/templates/react-monorepo/files/packages/.gitkeep +0 -0
- package/templates/react-monorepo/files/packages/api/eslint.config.mjs +3 -0
- package/templates/react-monorepo/files/packages/api/package.json +30 -0
- package/templates/react-monorepo/files/packages/api/src/base-api.ts +24 -0
- package/templates/react-monorepo/files/packages/api/src/base-query.ts +29 -0
- package/templates/react-monorepo/files/packages/api/src/config.ts +21 -0
- package/templates/react-monorepo/files/packages/api/src/index.ts +15 -0
- package/templates/react-monorepo/files/packages/api/src/realtime-events.ts +34 -0
- package/templates/react-monorepo/files/packages/api/src/socket.ts +41 -0
- package/templates/react-monorepo/files/packages/api/src/store.ts +39 -0
- package/templates/react-monorepo/files/packages/api/src/upload-query.ts +115 -0
- package/templates/react-monorepo/files/packages/api/tsconfig.json +4 -0
- package/templates/react-monorepo/files/packages/auth/eslint.config.mjs +3 -0
- package/templates/react-monorepo/files/packages/auth/package.json +33 -0
- package/templates/react-monorepo/files/packages/auth/src/client.ts +27 -0
- package/templates/react-monorepo/files/packages/auth/src/config.ts +22 -0
- package/templates/react-monorepo/files/packages/auth/src/guards.tsx +61 -0
- package/templates/react-monorepo/files/packages/auth/src/hooks.ts +13 -0
- package/templates/react-monorepo/files/packages/auth/src/index.ts +20 -0
- package/templates/react-monorepo/files/packages/auth/src/logout.ts +23 -0
- package/templates/react-monorepo/files/packages/auth/src/provider.tsx +52 -0
- package/templates/react-monorepo/files/packages/auth/src/redirect.ts +37 -0
- package/templates/react-monorepo/files/packages/auth/src/slice.ts +54 -0
- package/templates/react-monorepo/files/packages/auth/tsconfig.json +4 -0
- package/templates/react-monorepo/files/packages/config/README.md +47 -0
- package/templates/react-monorepo/files/packages/config/eslint/base.mjs +83 -0
- package/templates/react-monorepo/files/packages/config/package.json +37 -0
- package/templates/react-monorepo/files/packages/config/tailwind/theme.css +48 -0
- package/templates/react-monorepo/files/packages/config/tsconfig/base.json +22 -0
- package/templates/react-monorepo/files/packages/config/tsconfig/node.json +9 -0
- package/templates/react-monorepo/files/packages/config/tsconfig/react.json +9 -0
- package/templates/react-monorepo/files/packages/config/vite/index.d.ts +18 -0
- package/templates/react-monorepo/files/packages/config/vite/index.mjs +129 -0
- package/templates/react-monorepo/files/packages/hooks/eslint.config.mjs +3 -0
- package/templates/react-monorepo/files/packages/hooks/package.json +16 -0
- package/templates/react-monorepo/files/packages/hooks/src/index.ts +3 -0
- package/templates/react-monorepo/files/packages/hooks/tsconfig.json +4 -0
- package/templates/react-monorepo/files/packages/layouts/eslint.config.mjs +3 -0
- package/templates/react-monorepo/files/packages/layouts/package.json +30 -0
- package/templates/react-monorepo/files/packages/layouts/src/app-shell/app-shell.tsx +225 -0
- package/templates/react-monorepo/files/packages/layouts/src/app-shell/user-menu.tsx +74 -0
- package/templates/react-monorepo/files/packages/layouts/src/auth-layout.tsx +137 -0
- package/templates/react-monorepo/files/packages/layouts/src/error-boundary.tsx +38 -0
- package/templates/react-monorepo/files/packages/layouts/src/index.ts +23 -0
- package/templates/react-monorepo/files/packages/layouts/src/marketing/marketing-layout.tsx +29 -0
- package/templates/react-monorepo/files/packages/layouts/src/marketing/section-heading.tsx +38 -0
- package/templates/react-monorepo/files/packages/layouts/src/marketing/section.tsx +30 -0
- package/templates/react-monorepo/files/packages/layouts/src/states/app-shell-skeleton.tsx +47 -0
- package/templates/react-monorepo/files/packages/layouts/src/states/empty.tsx +22 -0
- package/templates/react-monorepo/files/packages/layouts/src/states/forbidden.tsx +14 -0
- package/templates/react-monorepo/files/packages/layouts/src/states/loading.tsx +11 -0
- package/templates/react-monorepo/files/packages/layouts/src/states/not-found.tsx +21 -0
- package/templates/react-monorepo/files/packages/layouts/src/states/page-skeleton.tsx +39 -0
- package/templates/react-monorepo/files/packages/layouts/src/states/route-fallback.tsx +26 -0
- package/templates/react-monorepo/files/packages/layouts/src/states/state-screen.tsx +22 -0
- package/templates/react-monorepo/files/packages/layouts/tsconfig.json +4 -0
- package/templates/react-monorepo/files/packages/types/.bin/openapi.js +32 -0
- package/templates/react-monorepo/files/packages/types/.bin/secrets.js +57 -0
- package/templates/react-monorepo/files/packages/types/README.md +28 -0
- package/templates/react-monorepo/files/packages/types/eslint.config.mjs +3 -0
- package/templates/react-monorepo/files/packages/types/openapi-ts.config.ts +22 -0
- package/templates/react-monorepo/files/packages/types/package.json +29 -0
- package/templates/react-monorepo/files/packages/types/src/index.ts +4 -0
- package/templates/react-monorepo/files/packages/types/src/types/api/index.ts +9 -0
- package/templates/react-monorepo/files/packages/types/src/types/api/zod.gen.ts +5 -0
- package/templates/react-monorepo/files/packages/types/tsconfig.json +7 -0
- package/templates/react-monorepo/files/packages/ui/components.json +21 -0
- package/templates/react-monorepo/files/packages/ui/eslint.config.mjs +13 -0
- package/templates/react-monorepo/files/packages/ui/package.json +44 -0
- package/templates/react-monorepo/files/packages/ui/src/components/brand/logo.tsx +51 -0
- package/templates/react-monorepo/files/packages/ui/src/components/brand/social-icons.tsx +37 -0
- package/templates/react-monorepo/files/packages/ui/src/components/form/form.tsx +120 -0
- package/templates/react-monorepo/files/packages/ui/src/components/ui/alert.tsx +25 -0
- package/templates/react-monorepo/files/packages/ui/src/components/ui/button.tsx +43 -0
- package/templates/react-monorepo/files/packages/ui/src/components/ui/calendar.tsx +78 -0
- package/templates/react-monorepo/files/packages/ui/src/components/ui/card.tsx +44 -0
- package/templates/react-monorepo/files/packages/ui/src/components/ui/checkbox.tsx +16 -0
- package/templates/react-monorepo/files/packages/ui/src/components/ui/date-picker.tsx +76 -0
- package/templates/react-monorepo/files/packages/ui/src/components/ui/input.tsx +22 -0
- package/templates/react-monorepo/files/packages/ui/src/components/ui/label.tsx +19 -0
- package/templates/react-monorepo/files/packages/ui/src/components/ui/modal.tsx +94 -0
- package/templates/react-monorepo/files/packages/ui/src/components/ui/popover.tsx +31 -0
- package/templates/react-monorepo/files/packages/ui/src/components/ui/progress.tsx +33 -0
- package/templates/react-monorepo/files/packages/ui/src/components/ui/search-input.tsx +69 -0
- package/templates/react-monorepo/files/packages/ui/src/components/ui/select.tsx +80 -0
- package/templates/react-monorepo/files/packages/ui/src/components/ui/skeleton.tsx +12 -0
- package/templates/react-monorepo/files/packages/ui/src/components/ui/sonner.tsx +23 -0
- package/templates/react-monorepo/files/packages/ui/src/components/ui/spinner.tsx +8 -0
- package/templates/react-monorepo/files/packages/ui/src/components/ui/textarea.tsx +22 -0
- package/templates/react-monorepo/files/packages/ui/src/index.ts +45 -0
- package/templates/react-monorepo/files/packages/ui/src/lib/utils.ts +7 -0
- package/templates/react-monorepo/files/packages/ui/src/lib/zod-setup.ts +28 -0
- package/templates/react-monorepo/files/packages/ui/src/meta/brand.ts +18 -0
- package/templates/react-monorepo/files/packages/ui/src/meta/meta-context.ts +30 -0
- package/templates/react-monorepo/files/packages/ui/src/meta/meta-provider.tsx +24 -0
- package/templates/react-monorepo/files/packages/ui/src/meta/page-meta.tsx +69 -0
- package/templates/react-monorepo/files/packages/ui/src/meta/use-meta-config.ts +11 -0
- package/templates/react-monorepo/files/packages/ui/src/styles/globals.css +285 -0
- package/templates/react-monorepo/files/packages/ui/src/theme/init-script.ts +23 -0
- package/templates/react-monorepo/files/packages/ui/src/theme/theme-colors.ts +14 -0
- package/templates/react-monorepo/files/packages/ui/src/theme/theme-context.ts +60 -0
- package/templates/react-monorepo/files/packages/ui/src/theme/theme-provider.tsx +101 -0
- package/templates/react-monorepo/files/packages/ui/src/theme/theme-toggle.tsx +40 -0
- package/templates/react-monorepo/files/packages/ui/src/theme/use-theme.ts +11 -0
- package/templates/react-monorepo/files/packages/ui/tsconfig.json +9 -0
- package/templates/react-monorepo/files/turbo.json +24 -0
- package/templates/react-monorepo/template.json +230 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { Suspense, lazy, type ReactNode } from 'react';
|
|
2
|
+
import { createBrowserRouter } from 'react-router';
|
|
3
|
+
import { ProtectedRoute, signOut, useAuth } from '@/shared/auth';
|
|
4
|
+
import { RoleRoute } from '@/shared/auth'; // @feature admin
|
|
5
|
+
import { AppShell, AppShellSkeleton, ErrorBoundary, MarketingLayout, NotFound, RouteFallback } from '@/shared/layouts';
|
|
6
|
+
import { Forbidden } from '@/shared/layouts'; // @feature admin
|
|
7
|
+
import { appPath } from '@/lib/paths';
|
|
8
|
+
import { ADMIN_BASE } from '@/lib/paths'; // @feature admin
|
|
9
|
+
import { NAV_ITEMS } from './nav';
|
|
10
|
+
import { ADMIN_NAV_ITEMS } from './nav'; // @feature admin
|
|
11
|
+
import { HomeRoute } from '@/routes/home';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Everything but the first page is lazy, so the initial download is the shell
|
|
15
|
+
* plus one route. The auth screens are lazy too: a logged-in user should never
|
|
16
|
+
* pay for the login form, and a logged-out one never pays for the app.
|
|
17
|
+
*/
|
|
18
|
+
const SettingsRoute = lazy(() => import('@/routes/settings').then((m) => ({ default: m.SettingsRoute })));
|
|
19
|
+
const TermsRoute = lazy(() => import('@/routes/legal/terms').then((m) => ({ default: m.TermsRoute })));
|
|
20
|
+
const PrivacyRoute = lazy(() => import('@/routes/legal/privacy').then((m) => ({ default: m.PrivacyRoute })));
|
|
21
|
+
// @feature:start marketing
|
|
22
|
+
const LandingRoute = lazy(() => import('@/routes/marketing/home').then((m) => ({ default: m.HomeRoute })));
|
|
23
|
+
// @feature:end
|
|
24
|
+
// @feature:start auth-screens
|
|
25
|
+
const LoginRoute = lazy(() => import('@/routes/auth/login').then((m) => ({ default: m.LoginRoute })));
|
|
26
|
+
const SignupRoute = lazy(() => import('@/routes/auth/signup').then((m) => ({ default: m.SignupRoute })));
|
|
27
|
+
const ForgotPasswordRoute = lazy(() =>
|
|
28
|
+
import('@/routes/auth/forgot-password').then((m) => ({ default: m.ForgotPasswordRoute }))
|
|
29
|
+
);
|
|
30
|
+
const ResetPasswordRoute = lazy(() =>
|
|
31
|
+
import('@/routes/auth/reset-password').then((m) => ({ default: m.ResetPasswordRoute }))
|
|
32
|
+
);
|
|
33
|
+
const VerifyEmailRoute = lazy(() =>
|
|
34
|
+
import('@/routes/auth/verify-email').then((m) => ({ default: m.VerifyEmailRoute }))
|
|
35
|
+
);
|
|
36
|
+
const VerifiedRoute = lazy(() => import('@/routes/auth/verified').then((m) => ({ default: m.VerifiedRoute })));
|
|
37
|
+
// @feature:end
|
|
38
|
+
// @feature:start admin
|
|
39
|
+
const AdminHomeRoute = lazy(() => import('@/routes/admin/home').then((m) => ({ default: m.AdminHomeRoute })));
|
|
40
|
+
// @feature:end
|
|
41
|
+
|
|
42
|
+
const lazyRoute = (element: ReactNode) => <Suspense fallback={<RouteFallback />}>{element}</Suspense>;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The shell reads the session itself rather than taking it as a prop —
|
|
46
|
+
* `ProtectedRoute` has already established the user is authenticated by the time
|
|
47
|
+
* this renders.
|
|
48
|
+
*
|
|
49
|
+
* `ErrorBoundary` wraps the shell rather than being passed as the route's
|
|
50
|
+
* `errorElement`: it is a plain React error boundary taking `children`, so it
|
|
51
|
+
* catches render errors anywhere below, including inside a lazy chunk.
|
|
52
|
+
*/
|
|
53
|
+
function Shell() {
|
|
54
|
+
const { user } = useAuth();
|
|
55
|
+
return (
|
|
56
|
+
<ErrorBoundary>
|
|
57
|
+
<AppShell nav={NAV_ITEMS} user={user ?? {}} onSignOut={() => void signOut()} />
|
|
58
|
+
</ErrorBoundary>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// @feature:start admin
|
|
63
|
+
function AdminShell() {
|
|
64
|
+
const { user } = useAuth();
|
|
65
|
+
return (
|
|
66
|
+
<ErrorBoundary>
|
|
67
|
+
<AppShell nav={ADMIN_NAV_ITEMS} user={user ?? {}} onSignOut={() => void signOut()} />
|
|
68
|
+
</ErrorBoundary>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
// @feature:end
|
|
72
|
+
|
|
73
|
+
export const router = createBrowserRouter([
|
|
74
|
+
// @feature:start marketing
|
|
75
|
+
// The public marketing site. It owns `/`, which is the whole reason the
|
|
76
|
+
// authenticated app is mounted under a base path — see src/lib/paths.ts.
|
|
77
|
+
{
|
|
78
|
+
element: (
|
|
79
|
+
<ErrorBoundary>
|
|
80
|
+
<MarketingLayout />
|
|
81
|
+
</ErrorBoundary>
|
|
82
|
+
),
|
|
83
|
+
children: [{ index: true, element: lazyRoute(<LandingRoute />) }]
|
|
84
|
+
},
|
|
85
|
+
// @feature:end
|
|
86
|
+
// Legal pages ship whether or not the marketing site does: the signup consent
|
|
87
|
+
// gate links to them, so they have to resolve either way.
|
|
88
|
+
{
|
|
89
|
+
element: (
|
|
90
|
+
<ErrorBoundary>
|
|
91
|
+
<MarketingLayout />
|
|
92
|
+
</ErrorBoundary>
|
|
93
|
+
),
|
|
94
|
+
children: [
|
|
95
|
+
{ path: 'terms', element: lazyRoute(<TermsRoute />) },
|
|
96
|
+
{ path: 'privacy', element: lazyRoute(<PrivacyRoute />) }
|
|
97
|
+
]
|
|
98
|
+
},
|
|
99
|
+
// @feature:start auth-screens
|
|
100
|
+
// Public, and deliberately flat: these paths are baked into verification and
|
|
101
|
+
// password-reset emails that are already in people's inboxes, so moving one
|
|
102
|
+
// breaks links that have already been sent.
|
|
103
|
+
{ path: '/login', element: lazyRoute(<LoginRoute />) },
|
|
104
|
+
{ path: '/signup', element: lazyRoute(<SignupRoute />) },
|
|
105
|
+
{ path: '/forgot-password', element: lazyRoute(<ForgotPasswordRoute />) },
|
|
106
|
+
{ path: '/reset-password', element: lazyRoute(<ResetPasswordRoute />) },
|
|
107
|
+
{ path: '/verify-email', element: lazyRoute(<VerifyEmailRoute />) },
|
|
108
|
+
{ path: '/verified', element: lazyRoute(<VerifiedRoute />) },
|
|
109
|
+
// @feature:end
|
|
110
|
+
{
|
|
111
|
+
// `/app` with the marketing site, `/` without it. Children stay relative so
|
|
112
|
+
// they follow it either way.
|
|
113
|
+
path: appPath(),
|
|
114
|
+
// `fallback` is required and must not be blank: the session round trip takes
|
|
115
|
+
// most of a second on a cold load, and rendering nothing for that long reads
|
|
116
|
+
// as a broken app.
|
|
117
|
+
element: (
|
|
118
|
+
<ProtectedRoute fallback={<AppShellSkeleton />}>
|
|
119
|
+
<Shell />
|
|
120
|
+
</ProtectedRoute>
|
|
121
|
+
),
|
|
122
|
+
children: [
|
|
123
|
+
{ index: true, element: <HomeRoute /> },
|
|
124
|
+
{ path: 'settings', element: lazyRoute(<SettingsRoute />) }
|
|
125
|
+
]
|
|
126
|
+
},
|
|
127
|
+
// @feature:start admin
|
|
128
|
+
{
|
|
129
|
+
path: ADMIN_BASE,
|
|
130
|
+
// The role gate is UI, not security: it decides what to render from the
|
|
131
|
+
// session the browser holds. Every endpoint behind it enforces the same
|
|
132
|
+
// role server-side, or the console is an honour system.
|
|
133
|
+
element: (
|
|
134
|
+
<RoleRoute allow={['admin', 'superuser']} fallback={<AppShellSkeleton />} forbidden={<Forbidden />}>
|
|
135
|
+
<AdminShell />
|
|
136
|
+
</RoleRoute>
|
|
137
|
+
),
|
|
138
|
+
children: [{ index: true, element: lazyRoute(<AdminHomeRoute />) }]
|
|
139
|
+
},
|
|
140
|
+
// @feature:end
|
|
141
|
+
{ path: '*', element: <NotFound /> }
|
|
142
|
+
]);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { makeStore } from '@/shared/api';
|
|
2
|
+
import { authReducer } from '@/shared/auth';
|
|
3
|
+
import { useDispatch, useSelector } from 'react-redux';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The one store. `makeStore` wires the shared RTK Query `baseApi`; slices are
|
|
7
|
+
* added here. Feature endpoint slices do *not* register themselves — they extend
|
|
8
|
+
* `baseApi` via `injectEndpoints`, which needs no change in this file.
|
|
9
|
+
*/
|
|
10
|
+
export const store = makeStore({
|
|
11
|
+
reducer: {
|
|
12
|
+
auth: authReducer
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export type RootState = ReturnType<typeof store.getState>;
|
|
17
|
+
export type AppDispatch = typeof store.dispatch;
|
|
18
|
+
|
|
19
|
+
export const useAppDispatch = useDispatch.withTypes<AppDispatch>();
|
|
20
|
+
export const useAppSelector = useSelector.withTypes<RootState>();
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/shared/ui';
|
|
3
|
+
|
|
4
|
+
type AuthCardProps = {
|
|
5
|
+
title: string;
|
|
6
|
+
description?: ReactNode;
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
footer?: ReactNode;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/** Standard auth surface: white 24px card, 900-weight heading, optional footer. */
|
|
12
|
+
export function AuthCard({ title, description, children, footer }: AuthCardProps) {
|
|
13
|
+
return (
|
|
14
|
+
<Card>
|
|
15
|
+
<CardHeader>
|
|
16
|
+
<CardTitle className="font-black">{title}</CardTitle>
|
|
17
|
+
{description ? <CardDescription className="text-body">{description}</CardDescription> : null}
|
|
18
|
+
</CardHeader>
|
|
19
|
+
<CardContent>{children}</CardContent>
|
|
20
|
+
{footer ? <p className="text-center text-sm text-body">{footer}</p> : null}
|
|
21
|
+
</Card>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { useField } from 'formik';
|
|
3
|
+
import { Checkbox, FormError } from '@/shared/ui';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Formik-bound checkbox with the design's inline label treatment.
|
|
7
|
+
*
|
|
8
|
+
* `label` is a node, not a string, because the signup consent needs links to
|
|
9
|
+
* Terms and Privacy inside it. Clicking one does not toggle the box: HTML
|
|
10
|
+
* defines a label's activation behaviour as doing nothing for events targeted
|
|
11
|
+
* at interactive descendants.
|
|
12
|
+
*
|
|
13
|
+
* `items-start` rather than the design's `center`: the design only has this on
|
|
14
|
+
* the single-line "Remember me", and consent copy wraps to two or three lines on
|
|
15
|
+
* a phone — centring would then float the box against the middle of the block.
|
|
16
|
+
* `mt-0.5` puts it on the first line's optical centre, so the single-line case
|
|
17
|
+
* looks unchanged.
|
|
18
|
+
*/
|
|
19
|
+
export function CheckboxField({ name, label }: { name: string; label: ReactNode }) {
|
|
20
|
+
const [field, meta] = useField({ name, type: 'checkbox' });
|
|
21
|
+
const showError = meta.touched && Boolean(meta.error);
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div className="flex flex-col gap-1.5">
|
|
25
|
+
<label className="flex cursor-pointer items-start gap-2.5">
|
|
26
|
+
<Checkbox {...field} checked={field.value} aria-invalid={showError || undefined} className="mt-0.5" />
|
|
27
|
+
<span className="text-sm text-body">{label}</span>
|
|
28
|
+
</label>
|
|
29
|
+
{showError ? <FormError>{meta.error}</FormError> : null}
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Button } from '@/shared/ui';
|
|
2
|
+
|
|
3
|
+
function GoogleIcon() {
|
|
4
|
+
return (
|
|
5
|
+
<svg width="16" height="16" viewBox="0 0 24 24" aria-hidden="true">
|
|
6
|
+
<path
|
|
7
|
+
fill="#4285F4"
|
|
8
|
+
d="M23.5 12.3c0-.9-.1-1.5-.3-2.2H12v4.1h6.5c-.1 1.1-.8 2.7-2.4 3.8l3.7 2.9c2.3-2.1 3.7-5.1 3.7-8.6z"
|
|
9
|
+
/>
|
|
10
|
+
<path
|
|
11
|
+
fill="#34A853"
|
|
12
|
+
d="M12 24c3.2 0 6-1.1 7.9-2.9l-3.7-2.9c-1 .7-2.4 1.2-4.2 1.2-3.2 0-6-2.1-6.9-5.1L1.2 17.2C3.2 21.2 7.3 24 12 24z"
|
|
13
|
+
/>
|
|
14
|
+
<path
|
|
15
|
+
fill="#FBBC05"
|
|
16
|
+
d="M5.1 14.3c-.2-.7-.4-1.5-.4-2.3s.2-1.6.4-2.3L1.2 6.8C.4 8.4 0 10.1 0 12s.4 3.6 1.2 5.2l3.9-2.9z"
|
|
17
|
+
/>
|
|
18
|
+
<path
|
|
19
|
+
fill="#EA4335"
|
|
20
|
+
d="M12 4.6c1.8 0 3 .8 3.7 1.4l3.3-3.2C17 1 14.2 0 12 0 7.3 0 3.2 2.8 1.2 6.8l3.9 2.9c1-3 3.7-5.1 6.9-5.1z"
|
|
21
|
+
/>
|
|
22
|
+
</svg>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function GoogleButton({
|
|
27
|
+
onClick,
|
|
28
|
+
disabled,
|
|
29
|
+
label = 'Continue with Google'
|
|
30
|
+
}: {
|
|
31
|
+
onClick: () => void;
|
|
32
|
+
disabled?: boolean;
|
|
33
|
+
label?: string;
|
|
34
|
+
}) {
|
|
35
|
+
return (
|
|
36
|
+
<Button type="button" variant="outline" className="w-full" onClick={onClick} disabled={disabled}>
|
|
37
|
+
<GoogleIcon />
|
|
38
|
+
{label}
|
|
39
|
+
</Button>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export function OrDivider({ label = 'or continue with' }: { label?: string }) {
|
|
2
|
+
return (
|
|
3
|
+
<div className="flex items-center gap-3">
|
|
4
|
+
<span className="h-px flex-1 bg-border" />
|
|
5
|
+
<span className="text-xs font-medium text-mute">{label}</span>
|
|
6
|
+
<span className="h-px flex-1 bg-border" />
|
|
7
|
+
</div>
|
|
8
|
+
);
|
|
9
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { type ReactNode, useState } from 'react';
|
|
2
|
+
import { useField } from 'formik';
|
|
3
|
+
import { FormError, Input, Label, cn } from '@/shared/ui';
|
|
4
|
+
import { scorePassword } from '../lib/password-strength';
|
|
5
|
+
|
|
6
|
+
function StrengthMeter({ score }: { score: number }) {
|
|
7
|
+
const activeColor = ['bg-border', 'bg-negative', 'bg-warning', 'bg-positive'][score];
|
|
8
|
+
return (
|
|
9
|
+
<div className="mt-1 flex gap-1" aria-hidden="true">
|
|
10
|
+
{[1, 2, 3].map((bar) => (
|
|
11
|
+
<span key={bar} className={cn('h-1 flex-1 rounded-full', bar <= score ? activeColor : 'bg-border')} />
|
|
12
|
+
))}
|
|
13
|
+
</div>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type PasswordFieldProps = {
|
|
18
|
+
name: string;
|
|
19
|
+
label?: string;
|
|
20
|
+
placeholder?: string;
|
|
21
|
+
autoComplete?: string;
|
|
22
|
+
showStrength?: boolean;
|
|
23
|
+
/** Replaces the show/hide toggle in the label row (e.g. a "Forgot password?" link). */
|
|
24
|
+
action?: ReactNode;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export function PasswordField({
|
|
28
|
+
name,
|
|
29
|
+
label = 'Password',
|
|
30
|
+
placeholder,
|
|
31
|
+
autoComplete = 'current-password',
|
|
32
|
+
showStrength = false,
|
|
33
|
+
action
|
|
34
|
+
}: PasswordFieldProps) {
|
|
35
|
+
const [field, meta] = useField(name);
|
|
36
|
+
const [visible, setVisible] = useState(false);
|
|
37
|
+
const showError = meta.touched && Boolean(meta.error);
|
|
38
|
+
const strength = showStrength ? scorePassword((field.value as string) ?? '') : null;
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div className="flex flex-col gap-1.5">
|
|
42
|
+
<div className="flex items-center justify-between">
|
|
43
|
+
<Label htmlFor={name}>{label}</Label>
|
|
44
|
+
{action ?? (
|
|
45
|
+
<button
|
|
46
|
+
type="button"
|
|
47
|
+
onClick={() => setVisible((value) => !value)}
|
|
48
|
+
className="text-xs font-semibold text-ink-deep"
|
|
49
|
+
>
|
|
50
|
+
{visible ? 'Hide' : 'Show'}
|
|
51
|
+
</button>
|
|
52
|
+
)}
|
|
53
|
+
</div>
|
|
54
|
+
<Input
|
|
55
|
+
id={name}
|
|
56
|
+
type={visible ? 'text' : 'password'}
|
|
57
|
+
placeholder={placeholder}
|
|
58
|
+
autoComplete={autoComplete}
|
|
59
|
+
aria-invalid={showError || undefined}
|
|
60
|
+
{...field}
|
|
61
|
+
/>
|
|
62
|
+
{strength ? <StrengthMeter score={strength.score} /> : null}
|
|
63
|
+
{showError ? (
|
|
64
|
+
<FormError>{meta.error}</FormError>
|
|
65
|
+
) : strength ? (
|
|
66
|
+
<p className="text-xs text-mute">{strength.label}</p>
|
|
67
|
+
) : null}
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ComponentProps, ReactNode } from 'react';
|
|
2
|
+
import { Button, Spinner, cn } from '@/shared/ui';
|
|
3
|
+
|
|
4
|
+
type SubmitButtonProps = ComponentProps<typeof Button> & {
|
|
5
|
+
loading?: boolean;
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export function SubmitButton({ loading, children, className, disabled, ...props }: SubmitButtonProps) {
|
|
10
|
+
return (
|
|
11
|
+
<Button type="submit" className={cn('w-full', className)} disabled={disabled || loading} {...props}>
|
|
12
|
+
{loading ? <Spinner /> : null}
|
|
13
|
+
{children}
|
|
14
|
+
</Button>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { getAuthClient } from '@/shared/auth';
|
|
2
|
+
|
|
3
|
+
type ActionResult = { error?: string };
|
|
4
|
+
|
|
5
|
+
function messageOf(error: { message?: string } | null | undefined, fallback: string): string {
|
|
6
|
+
return error?.message ?? fallback;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function loginWithEmail(input: {
|
|
10
|
+
email: string;
|
|
11
|
+
password: string;
|
|
12
|
+
remember?: boolean;
|
|
13
|
+
}): Promise<ActionResult> {
|
|
14
|
+
const { error } = await getAuthClient().signIn.email({
|
|
15
|
+
email: input.email,
|
|
16
|
+
password: input.password,
|
|
17
|
+
rememberMe: input.remember ?? true
|
|
18
|
+
});
|
|
19
|
+
return error ? { error: messageOf(error, 'Could not log in. Check your details and try again.') } : {};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function signupWithEmail(input: {
|
|
23
|
+
name: string;
|
|
24
|
+
email: string;
|
|
25
|
+
password: string;
|
|
26
|
+
callbackURL?: string;
|
|
27
|
+
}): Promise<ActionResult> {
|
|
28
|
+
const { error } = await getAuthClient().signUp.email({
|
|
29
|
+
name: input.name,
|
|
30
|
+
email: input.email,
|
|
31
|
+
password: input.password,
|
|
32
|
+
callbackURL: input.callbackURL
|
|
33
|
+
});
|
|
34
|
+
return error ? { error: messageOf(error, 'Could not create your account.') } : {};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function signInWithGoogle(options: {
|
|
38
|
+
/** Where existing users land after sign-in (usually the client app). */
|
|
39
|
+
callbackURL?: string;
|
|
40
|
+
/** Where first-time users land (the onboarding wizard). */
|
|
41
|
+
newUserCallbackURL?: string;
|
|
42
|
+
}): Promise<void> {
|
|
43
|
+
await getAuthClient().signIn.social({
|
|
44
|
+
provider: 'google',
|
|
45
|
+
callbackURL: options.callbackURL,
|
|
46
|
+
newUserCallbackURL: options.newUserCallbackURL
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function requestPasswordReset(input: { email: string; redirectTo?: string }): Promise<ActionResult> {
|
|
51
|
+
const { error } = await getAuthClient().requestPasswordReset({ email: input.email, redirectTo: input.redirectTo });
|
|
52
|
+
return error ? { error: messageOf(error, 'Could not send the reset link.') } : {};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function resetPasswordWithToken(input: { newPassword: string; token: string }): Promise<ActionResult> {
|
|
56
|
+
const { error } = await getAuthClient().resetPassword({ newPassword: input.newPassword, token: input.token });
|
|
57
|
+
return error ? { error: messageOf(error, 'Could not reset your password. The link may have expired.') } : {};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function resendVerification(input: { email: string; callbackURL?: string }): Promise<ActionResult> {
|
|
61
|
+
const { error } = await getAuthClient().sendVerificationEmail({ email: input.email, callbackURL: input.callbackURL });
|
|
62
|
+
return error ? { error: messageOf(error, 'Could not resend the email.') } : {};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function verifyEmailToken(token: string): Promise<ActionResult> {
|
|
66
|
+
const { error } = await getAuthClient().verifyEmail({ query: { token } });
|
|
67
|
+
return error ? { error: messageOf(error, 'Could not verify your email. The link may have expired.') } : {};
|
|
68
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type PasswordStrength = {
|
|
2
|
+
/** 0 (empty/too short) … 3 (strong) */
|
|
3
|
+
score: 0 | 1 | 2 | 3;
|
|
4
|
+
label: string;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
/** Lightweight strength heuristic driving the 3-bar meter on the signup screen. */
|
|
8
|
+
export function scorePassword(password: string): PasswordStrength {
|
|
9
|
+
if (!password) {
|
|
10
|
+
return { score: 0, label: 'Use 8+ characters with a mix of letters and numbers.' };
|
|
11
|
+
}
|
|
12
|
+
let points = 0;
|
|
13
|
+
if (password.length >= 8) points += 1;
|
|
14
|
+
if (/[a-z]/.test(password) && /[A-Z]/.test(password)) points += 1;
|
|
15
|
+
if (/\d/.test(password) || /[^A-Za-z0-9]/.test(password)) points += 1;
|
|
16
|
+
|
|
17
|
+
const score = Math.min(points, 3) as 0 | 1 | 2 | 3;
|
|
18
|
+
const labels = ['Too short', 'Weak — add more variety', 'Getting there', 'Strong password'];
|
|
19
|
+
return { score, label: labels[score] };
|
|
20
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Append the current `redirect` param to an in-app path (threads it across screens). */
|
|
2
|
+
export function withRedirect(path: string, redirect: string | null): string {
|
|
3
|
+
if (!redirect) return path;
|
|
4
|
+
const separator = path.includes('?') ? '&' : '?';
|
|
5
|
+
return `${path}${separator}redirect=${encodeURIComponent(redirect)}`;
|
|
6
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
export const loginSchema = z.object({
|
|
4
|
+
email: z.string().min(1, 'Email is required').email('Enter a valid email'),
|
|
5
|
+
password: z.string().min(1, 'Password is required'),
|
|
6
|
+
remember: z.boolean().optional()
|
|
7
|
+
});
|
|
8
|
+
export type LoginValues = z.infer<typeof loginSchema>;
|
|
9
|
+
|
|
10
|
+
export const signupSchema = z.object({
|
|
11
|
+
name: z.string().min(1, 'Enter your name'),
|
|
12
|
+
email: z.string().min(1, 'Email is required').email('Enter a valid email'),
|
|
13
|
+
password: z.string().min(8, 'At least 8 characters'),
|
|
14
|
+
// Consent is a gate, not a preference, so it is validated rather than merely
|
|
15
|
+
// displayed. It is stripped before the payload reaches the API — see
|
|
16
|
+
// `SignupRoute`; nothing on the server takes it.
|
|
17
|
+
acceptedTerms: z.boolean().refine((accepted) => accepted, {
|
|
18
|
+
message: 'Please accept the terms to continue'
|
|
19
|
+
})
|
|
20
|
+
});
|
|
21
|
+
export type SignupValues = z.infer<typeof signupSchema>;
|
|
22
|
+
|
|
23
|
+
export const forgotSchema = z.object({
|
|
24
|
+
email: z.string().min(1, 'Email is required').email('Enter a valid email')
|
|
25
|
+
});
|
|
26
|
+
export type ForgotValues = z.infer<typeof forgotSchema>;
|
|
27
|
+
|
|
28
|
+
export const resetSchema = z
|
|
29
|
+
.object({
|
|
30
|
+
password: z.string().min(8, 'At least 8 characters'),
|
|
31
|
+
confirm: z.string().min(1, 'Confirm your password')
|
|
32
|
+
})
|
|
33
|
+
.refine((values) => values.password === values.confirm, {
|
|
34
|
+
message: "Passwords don't match yet.",
|
|
35
|
+
path: ['confirm']
|
|
36
|
+
});
|
|
37
|
+
export type ResetValues = z.infer<typeof resetSchema>;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every indexable route, with the metadata that describes it.
|
|
3
|
+
*
|
|
4
|
+
* **This is the single source.** `PageSeo` renders per-route tags from it and
|
|
5
|
+
* the build emits `sitemap.xml` from it (see `vite.config.ts`), so the sitemap
|
|
6
|
+
* cannot list a route that does not exist or miss one that does — the classic
|
|
7
|
+
* way a sitemap rots. Add a route here on the same commit that adds it to
|
|
8
|
+
* `app/router.tsx`, and remove it the same way; a sitemap advertising a 404
|
|
9
|
+
* stays invisible until a crawler finds it.
|
|
10
|
+
*
|
|
11
|
+
* Kept free of React imports: `vite.config.ts` imports this directly, and the
|
|
12
|
+
* config is bundled with esbuild.
|
|
13
|
+
*/
|
|
14
|
+
export type SeoRoute = {
|
|
15
|
+
/** Path as it appears in the router, leading slash, no trailing slash. */
|
|
16
|
+
path: string;
|
|
17
|
+
/** Page segment only — `MetaProvider` appends the brand suffix. */
|
|
18
|
+
title: string;
|
|
19
|
+
description: string;
|
|
20
|
+
/** Relative priority for the sitemap; the homepage leads. */
|
|
21
|
+
priority: string;
|
|
22
|
+
changefreq: 'daily' | 'weekly' | 'monthly' | 'yearly';
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const SEO_ROUTES: SeoRoute[] = [
|
|
26
|
+
{
|
|
27
|
+
path: '/',
|
|
28
|
+
title: 'Home',
|
|
29
|
+
description: 'Replace with the description search results should show.',
|
|
30
|
+
priority: '1.0',
|
|
31
|
+
changefreq: 'weekly'
|
|
32
|
+
}
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The one origin that may be indexed. Staging and preview builds compare
|
|
37
|
+
* unequal and are served `noindex`, so a preview deploy cannot outrank
|
|
38
|
+
* production for its own content.
|
|
39
|
+
*/
|
|
40
|
+
export const PRODUCTION_ORIGIN = 'https://example.com';
|
|
41
|
+
|
|
42
|
+
export const seoRouteFor = (path: string) => SEO_ROUTES.find((route) => route.path === path);
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Absolute URL for a path — canonical, `og:url` and sitemap entries all need one.
|
|
46
|
+
*
|
|
47
|
+
* `origin` is passed in rather than read here so this file stays free of
|
|
48
|
+
* `import.meta.env`: it is imported by `vite.config.ts`, where `import.meta.env`
|
|
49
|
+
* does not exist. App code gets the origin from its env, the build from its own
|
|
50
|
+
* `loadEnv`.
|
|
51
|
+
*/
|
|
52
|
+
export const absoluteUrl = (path: string, origin: string) => (path === '/' ? `${origin}/` : `${origin}${path}`);
|
|
53
|
+
|
|
54
|
+
/** Strip a trailing slash so joins don't produce `//about`. */
|
|
55
|
+
export const normalizeOrigin = (origin: string) => origin.replace(/\/$/, '');
|
|
56
|
+
|
|
57
|
+
/*
|
|
58
|
+
* Not included here: **prerendering**. `vite build` emits a single SPA shell,
|
|
59
|
+
* so a crawler that does not execute JavaScript sees an empty `#root` for every
|
|
60
|
+
* route. `robots.txt` and `sitemap.xml` are emitted from this table at build
|
|
61
|
+
* time (see `vite.config.ts`), which is the half that matters for discovery —
|
|
62
|
+
* but if these pages need to rank on their content, add a post-build pass that
|
|
63
|
+
* renders each `SEO_ROUTES` entry to `dist/<path>/index.html`. Drive it from
|
|
64
|
+
* this table, not a second list, or the sitemap and the prerendered set drift.
|
|
65
|
+
*/
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const env = import.meta.env;
|
|
2
|
+
|
|
3
|
+
/** Backend base URL. Better Auth lives at `${API_URL}/auth`. */
|
|
4
|
+
export const API_URL = env.VITE_APP_API_URL as string;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* This app's own origin, read from the browser rather than from a build-time
|
|
8
|
+
* variable.
|
|
9
|
+
*
|
|
10
|
+
* One deployment means one origin, so there is nothing a `VITE_APP_URL` could
|
|
11
|
+
* add here and plenty it could get wrong: Vite inlines env at build time, so a
|
|
12
|
+
* stale value baked into an image sends every verification link to the previous
|
|
13
|
+
* environment — and that failure is invisible until a user clicks one.
|
|
14
|
+
*
|
|
15
|
+
* The only place an absolute URL is genuinely needed is a link that lands in an
|
|
16
|
+
* email, and by then the browser has already told us where we are.
|
|
17
|
+
*/
|
|
18
|
+
export const APP_ORIGIN = window.location.origin;
|
|
19
|
+
|
|
20
|
+
// @feature:start marketing
|
|
21
|
+
/**
|
|
22
|
+
* Canonical public origin, for SEO tags only — the one origin that may be
|
|
23
|
+
* indexed. Unset (previews, staging) means "not production", and the build
|
|
24
|
+
* emits `noindex` plus a disallowing robots.txt rather than letting a preview
|
|
25
|
+
* deploy outrank production for its own content. See `vite.config.ts`.
|
|
26
|
+
*/
|
|
27
|
+
export const SITE_URL = (env.VITE_APP_SITE_URL as string | undefined) ?? '';
|
|
28
|
+
// @feature:end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where things live in the URL space.
|
|
3
|
+
*
|
|
4
|
+
* One deployment serves the marketing pages, the auth screens and the
|
|
5
|
+
* authenticated app, so unlike a multi-app build these cannot each own `/` on
|
|
6
|
+
* their own host — the split has to happen in the path, and it has to happen in
|
|
7
|
+
* one place or the router, the nav and the post-login redirect will disagree.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The mount point of the authenticated app, as a path split on `/`.
|
|
12
|
+
*
|
|
13
|
+
* A list rather than two competing `const` declarations because a generated
|
|
14
|
+
* project has to compile in *every* feature combination, and only one of two
|
|
15
|
+
* declarations of the same name can survive the strip. Drop `marketing` and only
|
|
16
|
+
* the leading empty segment is left, putting the app back at the root.
|
|
17
|
+
*
|
|
18
|
+
* The `''` is not filler: an absolute path's first segment *is* empty —
|
|
19
|
+
* `'/app'.split('/')` is `['', 'app']`. It also keeps this array non-empty in
|
|
20
|
+
* every build, which matters because a literal that strips down to `[\n]` is no
|
|
21
|
+
* longer formatted the way Prettier wants it, and `format:check` runs in CI.
|
|
22
|
+
*/
|
|
23
|
+
const BASE_SEGMENTS: string[] = [
|
|
24
|
+
'', // the leading slash
|
|
25
|
+
'app' // @feature marketing
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
/** `/app` when the marketing site owns `/`; otherwise `''`. */
|
|
29
|
+
export const APP_BASE = BASE_SEGMENTS.join('/');
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Absolute path to a route inside the authenticated app.
|
|
33
|
+
*
|
|
34
|
+
* appPath() -> '/app' (or '/')
|
|
35
|
+
* appPath('settings') -> '/app/settings' (or '/settings')
|
|
36
|
+
*
|
|
37
|
+
* Use it for anything that has to name a route from outside the router — nav
|
|
38
|
+
* items, redirect targets, links in emails. Routes *inside* the app shell can
|
|
39
|
+
* keep using relative paths and need none of this.
|
|
40
|
+
*/
|
|
41
|
+
export const appPath = (path = '') => `${APP_BASE}/${path}`.replace(/\/+/g, '/').replace(/(.)\/$/, '$1');
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Legal pages ship whether or not the marketing site does: the signup consent
|
|
45
|
+
* gate links to them, and a consent checkbox pointing at a 404 is worse than no
|
|
46
|
+
* checkbox at all.
|
|
47
|
+
*/
|
|
48
|
+
export const TERMS_PATH = '/terms';
|
|
49
|
+
export const PRIVACY_PATH = '/privacy';
|
|
50
|
+
|
|
51
|
+
// @feature:start admin
|
|
52
|
+
/**
|
|
53
|
+
* The admin console. A separate branch of the router rather than a route inside
|
|
54
|
+
* the app shell, so the role check wraps the whole section instead of being
|
|
55
|
+
* repeated per page — and so an admin-only nav never renders for anyone else.
|
|
56
|
+
*/
|
|
57
|
+
export const ADMIN_BASE = '/admin';
|
|
58
|
+
// @feature:end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { StrictMode } from 'react';
|
|
2
|
+
import { createRoot } from 'react-dom/client';
|
|
3
|
+
import { RouterProvider } from 'react-router';
|
|
4
|
+
import '@/shared/ui/styles/globals.css';
|
|
5
|
+
import { Providers } from './app/providers';
|
|
6
|
+
import { router } from './app/router';
|
|
7
|
+
|
|
8
|
+
// If the first screen needs data immediately, kick its query off here rather
|
|
9
|
+
// than in a component effect: by this point every module above has finished
|
|
10
|
+
// evaluating and `providers` has already called `configureApi` at module scope,
|
|
11
|
+
// so the fetch races the session check instead of queueing behind it.
|
|
12
|
+
|
|
13
|
+
createRoot(document.getElementById('root')!).render(
|
|
14
|
+
<StrictMode>
|
|
15
|
+
<Providers>
|
|
16
|
+
<RouterProvider router={router} />
|
|
17
|
+
</Providers>
|
|
18
|
+
</StrictMode>
|
|
19
|
+
);
|