create-nexst-monorepo 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/dist/index.js +1727 -0
- package/package.json +37 -0
- package/template/.agents/plugins/marketplace.json +21 -0
- package/template/.claude/settings.json +6 -0
- package/template/.claude/skills/frontend-design/LICENSE.txt +177 -0
- package/template/.claude/skills/frontend-design/SKILL.md +154 -0
- package/template/.claude/skills/theme-factory/SKILL.md +95 -0
- package/template/.claude/skills/theme-factory/references/schema.md +104 -0
- package/template/.claude/skills/theme-factory/scripts/theme.mjs +261 -0
- package/template/.dockerignore +43 -0
- package/template/.editorconfig +10 -0
- package/template/.github/workflows/ci.yaml +289 -0
- package/template/.github/workflows/publish-app.yaml +66 -0
- package/template/.github/workflows/publish.yaml +148 -0
- package/template/.github/workflows/snyk.yaml +34 -0
- package/template/.vscode/settings.json +8 -0
- package/template/AGENTS.md +42 -0
- package/template/Dockerfile.base +9 -0
- package/template/README.md +54 -0
- package/template/_gitignore +10 -0
- package/template/apps/api/.env.example +32 -0
- package/template/apps/api/AGENTS.md +44 -0
- package/template/apps/api/Dockerfile +69 -0
- package/template/apps/api/_gitignore +8 -0
- package/template/apps/api/knip.config.ts +22 -0
- package/template/apps/api/kustomize/base/application.properties +16 -0
- package/template/apps/api/kustomize/base/deployment.yaml +94 -0
- package/template/apps/api/kustomize/base/kustomization.yaml +17 -0
- package/template/apps/api/kustomize/base/service.yaml +10 -0
- package/template/apps/api/kustomize/overlays/prod/kustomization.yaml +17 -0
- package/template/apps/api/kustomize/overlays/staging/application.properties +1 -0
- package/template/apps/api/kustomize/overlays/staging/kustomization.yaml +21 -0
- package/template/apps/api/kustomize/secret.keys +14 -0
- package/template/apps/api/oxfmt.config.ts +8 -0
- package/template/apps/api/oxlint.config.ts +107 -0
- package/template/apps/api/package.json +79 -0
- package/template/apps/api/prisma/migrations/20260927000000_init/migration.sql +113 -0
- package/template/apps/api/prisma/migrations/migration_lock.toml +3 -0
- package/template/apps/api/prisma/schema.prisma +113 -0
- package/template/apps/api/prisma.config.ts +7 -0
- package/template/apps/api/scripts/auth.cli.ts +19 -0
- package/template/apps/api/scripts/seal-secret.sh +171 -0
- package/template/apps/api/sonar-project.properties +9 -0
- package/template/apps/api/src/app-info/app-info.dto.ts +10 -0
- package/template/apps/api/src/app-info/app-info.module.ts +7 -0
- package/template/apps/api/src/app-info/app-info.resolver.spec.ts +14 -0
- package/template/apps/api/src/app-info/app-info.resolver.ts +21 -0
- package/template/apps/api/src/app.module.spec.ts +93 -0
- package/template/apps/api/src/app.module.ts +77 -0
- package/template/apps/api/src/auth/auth.guard.spec.ts +90 -0
- package/template/apps/api/src/auth/auth.guard.ts +97 -0
- package/template/apps/api/src/auth/auth.ts +129 -0
- package/template/apps/api/src/auth/decorator/allow-anonymous.decorator.ts +17 -0
- package/template/apps/api/src/auth/decorator/is-email-verified.decorator.spec.ts +41 -0
- package/template/apps/api/src/auth/decorator/is-email-verified.decorator.ts +20 -0
- package/template/apps/api/src/auth/decorator/roles.decorator.ts +27 -0
- package/template/apps/api/src/auth/testing/make-session.ts +31 -0
- package/template/apps/api/src/auth/types/auth.types.ts +27 -0
- package/template/apps/api/src/bootstrap/register-graphql-enums.ts +12 -0
- package/template/apps/api/src/bootstrap/security.ts +24 -0
- package/template/apps/api/src/bootstrap/swagger.ts +28 -0
- package/template/apps/api/src/config/app.config.spec.ts +22 -0
- package/template/apps/api/src/config/app.config.ts +122 -0
- package/template/apps/api/src/config/graphql-depth-limit.ts +103 -0
- package/template/apps/api/src/config/graphql.config.ts +77 -0
- package/template/apps/api/src/config/resources/app.yaml +29 -0
- package/template/apps/api/src/errors/app-parse-uuid.pipe.ts +27 -0
- package/template/apps/api/src/errors/app.exception.ts +47 -0
- package/template/apps/api/src/errors/error-code.enum.ts +54 -0
- package/template/apps/api/src/errors/http-status-fallback-code.util.ts +27 -0
- package/template/apps/api/src/errors/validation-exception.factory.ts +43 -0
- package/template/apps/api/src/health/health.controller.spec.ts +82 -0
- package/template/apps/api/src/health/health.controller.ts +57 -0
- package/template/apps/api/src/health/health.module.ts +12 -0
- package/template/apps/api/src/logging/context-logger.ts +18 -0
- package/template/apps/api/src/logging/logger.config.ts +23 -0
- package/template/apps/api/src/logging/logging.module.ts +22 -0
- package/template/apps/api/src/mail/mail.module.ts +12 -0
- package/template/apps/api/src/mail/mail.service.spec.ts +47 -0
- package/template/apps/api/src/mail/mail.service.ts +123 -0
- package/template/apps/api/src/main.ts +41 -0
- package/template/apps/api/src/prisma/prisma.module.ts +12 -0
- package/template/apps/api/src/prisma/prisma.service.spec.ts +136 -0
- package/template/apps/api/src/prisma/prisma.service.ts +138 -0
- package/template/apps/api/src/queue/dto/enqueue-example.input.ts +11 -0
- package/template/apps/api/src/queue/example-queue.resolver.ts +14 -0
- package/template/apps/api/src/queue/example-queue.service.spec.ts +21 -0
- package/template/apps/api/src/queue/example-queue.service.ts +32 -0
- package/template/apps/api/src/queue/queue.module.ts +27 -0
- package/template/apps/api/src/r2/r2.module.ts +12 -0
- package/template/apps/api/src/r2/r2.service.spec.ts +117 -0
- package/template/apps/api/src/r2/r2.service.ts +131 -0
- package/template/apps/api/src/user/dto/user.dto.ts +26 -0
- package/template/apps/api/src/user/user.module.ts +10 -0
- package/template/apps/api/src/user/user.resolver.spec.ts +18 -0
- package/template/apps/api/src/user/user.resolver.ts +24 -0
- package/template/apps/api/src/utils/catch-everything-exception.filter.spec.ts +292 -0
- package/template/apps/api/src/utils/catch-everything-exception.filter.ts +70 -0
- package/template/apps/api/src/utils/error-handler.util.spec.ts +20 -0
- package/template/apps/api/src/utils/error-handler.util.ts +12 -0
- package/template/apps/api/src/utils/escape-like-pattern.util.spec.ts +29 -0
- package/template/apps/api/src/utils/escape-like-pattern.util.ts +11 -0
- package/template/apps/api/src/utils/pagination.input.ts +20 -0
- package/template/apps/api/src/utils/prisma-exception.filter.spec.ts +186 -0
- package/template/apps/api/src/utils/prisma-exception.filter.ts +45 -0
- package/template/apps/api/src/utils/prisma-graphql-exception.filter.spec.ts +216 -0
- package/template/apps/api/src/utils/prisma-graphql-exception.filter.ts +43 -0
- package/template/apps/api/tests/app.e2e-spec.ts +35 -0
- package/template/apps/api/tests/auth.e2e-spec.ts +38 -0
- package/template/apps/api/tests/helpers/api.ts +27 -0
- package/template/apps/api/tsconfig.json +11 -0
- package/template/apps/api/vitest.config.e2e.ts +16 -0
- package/template/apps/api/vitest.config.ts +40 -0
- package/template/apps/web/.env.example +7 -0
- package/template/apps/web/.storybook/decorators.tsx +38 -0
- package/template/apps/web/.storybook/main.ts +17 -0
- package/template/apps/web/.storybook/preview.ts +38 -0
- package/template/apps/web/.storybook/vitest.setup.ts +5 -0
- package/template/apps/web/AGENTS.md +29 -0
- package/template/apps/web/Dockerfile +49 -0
- package/template/apps/web/_gitignore +16 -0
- package/template/apps/web/knip.config.ts +15 -0
- package/template/apps/web/kustomize/base/application.properties +5 -0
- package/template/apps/web/kustomize/base/deployment.yaml +60 -0
- package/template/apps/web/kustomize/base/kustomization.yaml +17 -0
- package/template/apps/web/kustomize/base/service.yaml +10 -0
- package/template/apps/web/kustomize/overlays/prod/kustomization.yaml +12 -0
- package/template/apps/web/kustomize/overlays/staging/application.properties +2 -0
- package/template/apps/web/kustomize/overlays/staging/kustomization.yaml +18 -0
- package/template/apps/web/messages/de.json +124 -0
- package/template/apps/web/messages/en.json +124 -0
- package/template/apps/web/next-env.d.ts +7 -0
- package/template/apps/web/next.config.ts +39 -0
- package/template/apps/web/oxfmt.config.ts +9 -0
- package/template/apps/web/oxlint.config.ts +36 -0
- package/template/apps/web/package.json +80 -0
- package/template/apps/web/playwright.config.ts +34 -0
- package/template/apps/web/postcss.config.mjs +7 -0
- package/template/apps/web/sonar-project.properties +9 -0
- package/template/apps/web/src/app/[locale]/(auth)/recovery/page.tsx +22 -0
- package/template/apps/web/src/app/[locale]/(auth)/reset-password/page.tsx +29 -0
- package/template/apps/web/src/app/[locale]/(auth)/sign-in/page.tsx +24 -0
- package/template/apps/web/src/app/[locale]/(auth)/sign-up/page.tsx +24 -0
- package/template/apps/web/src/app/[locale]/(auth)/verify/page.tsx +24 -0
- package/template/apps/web/src/app/[locale]/dashboard/page.tsx +34 -0
- package/template/apps/web/src/app/[locale]/layout.tsx +60 -0
- package/template/apps/web/src/app/[locale]/page.tsx +23 -0
- package/template/apps/web/src/app/api/health/route.ts +3 -0
- package/template/apps/web/src/app/global-error.tsx +19 -0
- package/template/apps/web/src/app/globals.css +98 -0
- package/template/apps/web/src/app/icon.svg +1 -0
- package/template/apps/web/src/app/robots.ts +15 -0
- package/template/apps/web/src/app/sitemap.ts +24 -0
- package/template/apps/web/src/components/alert/alert.stories.tsx +25 -0
- package/template/apps/web/src/components/alert/alert.tsx +50 -0
- package/template/apps/web/src/components/avatar/avatar.stories.tsx +116 -0
- package/template/apps/web/src/components/avatar/avatar.tsx +88 -0
- package/template/apps/web/src/components/button/button-variants.ts +28 -0
- package/template/apps/web/src/components/button/button.stories.tsx +33 -0
- package/template/apps/web/src/components/button/button.tsx +32 -0
- package/template/apps/web/src/components/checkbox/checkbox.stories.tsx +312 -0
- package/template/apps/web/src/components/checkbox/checkbox.tsx +38 -0
- package/template/apps/web/src/components/dialog/danger-panel.stories.tsx +41 -0
- package/template/apps/web/src/components/dialog/danger-panel.tsx +32 -0
- package/template/apps/web/src/components/dialog/dialog.stories.tsx +239 -0
- package/template/apps/web/src/components/dialog/dialog.tsx +136 -0
- package/template/apps/web/src/components/dialog-buttons.stories.tsx +55 -0
- package/template/apps/web/src/components/dialog-buttons.tsx +48 -0
- package/template/apps/web/src/components/drawer/drawer.stories.tsx +251 -0
- package/template/apps/web/src/components/drawer/drawer.tsx +120 -0
- package/template/apps/web/src/components/dropdown-menu/dropdown-menu.stories.tsx +329 -0
- package/template/apps/web/src/components/dropdown-menu/dropdown-menu.tsx +263 -0
- package/template/apps/web/src/components/empty-state.stories.tsx +49 -0
- package/template/apps/web/src/components/empty-state.tsx +33 -0
- package/template/apps/web/src/components/field-group.stories.tsx +47 -0
- package/template/apps/web/src/components/field-group.tsx +20 -0
- package/template/apps/web/src/components/field-label.stories.tsx +34 -0
- package/template/apps/web/src/components/field-label.tsx +28 -0
- package/template/apps/web/src/components/icons.tsx +25 -0
- package/template/apps/web/src/components/input/input-otp.stories.tsx +86 -0
- package/template/apps/web/src/components/input/input-otp.tsx +184 -0
- package/template/apps/web/src/components/input/input.stories.tsx +35 -0
- package/template/apps/web/src/components/input/input.tsx +180 -0
- package/template/apps/web/src/components/popover/popover.stories.tsx +479 -0
- package/template/apps/web/src/components/popover/popover.tsx +45 -0
- package/template/apps/web/src/components/responsive-dialog/responsive-dialog.stories.tsx +307 -0
- package/template/apps/web/src/components/responsive-dialog/responsive-dialog.tsx +96 -0
- package/template/apps/web/src/components/searchbar/searchbar.stories.tsx +59 -0
- package/template/apps/web/src/components/searchbar/searchbar.tsx +64 -0
- package/template/apps/web/src/components/select/select.stories.tsx +293 -0
- package/template/apps/web/src/components/select/select.tsx +445 -0
- package/template/apps/web/src/components/skeleton/skeleton.stories.tsx +76 -0
- package/template/apps/web/src/components/skeleton/skeleton.tsx +13 -0
- package/template/apps/web/src/components/spinner/spinner.tsx +18 -0
- package/template/apps/web/src/components/switch/switch.stories.tsx +71 -0
- package/template/apps/web/src/components/switch/switch.tsx +35 -0
- package/template/apps/web/src/components/tabs/tabs.stories.tsx +209 -0
- package/template/apps/web/src/components/tabs/tabs.tsx +69 -0
- package/template/apps/web/src/components/tabs/url-tabs.stories.tsx +68 -0
- package/template/apps/web/src/components/tabs/url-tabs.tsx +42 -0
- package/template/apps/web/src/components/textarea/textarea.stories.tsx +144 -0
- package/template/apps/web/src/components/textarea/textarea.tsx +115 -0
- package/template/apps/web/src/components/toast/toaster.tsx +38 -0
- package/template/apps/web/src/components/tooltip/tooltip.stories.tsx +237 -0
- package/template/apps/web/src/components/tooltip/tooltip.tsx +49 -0
- package/template/apps/web/src/config/csp.ts +59 -0
- package/template/apps/web/src/config/routes.ts +14 -0
- package/template/apps/web/src/config/site.ts +5 -0
- package/template/apps/web/src/env.ts +15 -0
- package/template/apps/web/src/features/auth/auth-card.tsx +20 -0
- package/template/apps/web/src/features/auth/auth-divider.tsx +14 -0
- package/template/apps/web/src/features/auth/google-auth-button.tsx +49 -0
- package/template/apps/web/src/features/auth/google-one-tap.tsx +23 -0
- package/template/apps/web/src/features/auth/lib/auth-error.test.ts +22 -0
- package/template/apps/web/src/features/auth/lib/auth-error.ts +16 -0
- package/template/apps/web/src/features/auth/lib/return-to.test.ts +41 -0
- package/template/apps/web/src/features/auth/lib/return-to.ts +19 -0
- package/template/apps/web/src/features/auth/lib/validator.test.ts +45 -0
- package/template/apps/web/src/features/auth/lib/validator.ts +49 -0
- package/template/apps/web/src/features/auth/passkey-settings.tsx +106 -0
- package/template/apps/web/src/features/auth/recovery-form.tsx +61 -0
- package/template/apps/web/src/features/auth/resend-verification-button.tsx +36 -0
- package/template/apps/web/src/features/auth/reset-password-form.tsx +62 -0
- package/template/apps/web/src/features/auth/sign-in-form.stories.tsx +22 -0
- package/template/apps/web/src/features/auth/sign-in-form.tsx +116 -0
- package/template/apps/web/src/features/auth/sign-out-button.tsx +26 -0
- package/template/apps/web/src/features/auth/sign-up-form.tsx +107 -0
- package/template/apps/web/src/features/shared/provider.tsx +16 -0
- package/template/apps/web/src/features/shared/site-header.tsx +48 -0
- package/template/apps/web/src/features/shared/theme-toggle.tsx +24 -0
- package/template/apps/web/src/i18n/navigation.ts +5 -0
- package/template/apps/web/src/i18n/request.ts +21 -0
- package/template/apps/web/src/i18n/routing.ts +6 -0
- package/template/apps/web/src/lib/auth-client.ts +19 -0
- package/template/apps/web/src/lib/auth-server.ts +46 -0
- package/template/apps/web/src/lib/auth.ts +8 -0
- package/template/apps/web/src/lib/codegen.ts +30 -0
- package/template/apps/web/src/lib/graphql-client.ts +29 -0
- package/template/apps/web/src/lib/graphql-server.ts +28 -0
- package/template/apps/web/src/lib/graphql-utils.test.ts +32 -0
- package/template/apps/web/src/lib/graphql-utils.ts +76 -0
- package/template/apps/web/src/lib/hooks/use-media-query.ts +21 -0
- package/template/apps/web/src/lib/metadata.ts +17 -0
- package/template/apps/web/src/lib/network-error.test.ts +30 -0
- package/template/apps/web/src/lib/network-error.ts +17 -0
- package/template/apps/web/src/lib/proxy-utils.ts +42 -0
- package/template/apps/web/src/lib/query-client.ts +29 -0
- package/template/apps/web/src/lib/utils.ts +5 -0
- package/template/apps/web/src/proxy.ts +57 -0
- package/template/apps/web/test-results/.last-run.json +4 -0
- package/template/apps/web/tests/auth.spec.ts +17 -0
- package/template/apps/web/tests/csp-theme-script.spec.ts +37 -0
- package/template/apps/web/tests/home.spec.ts +25 -0
- package/template/apps/web/tsconfig.build.json +4 -0
- package/template/apps/web/tsconfig.json +28 -0
- package/template/apps/web/vitest.config.ts +40 -0
- package/template/apps/web/vitest.shims.d.ts +1 -0
- package/template/apps/worker/.env.example +4 -0
- package/template/apps/worker/AGENTS.md +11 -0
- package/template/apps/worker/Dockerfile +35 -0
- package/template/apps/worker/knip.config.ts +3 -0
- package/template/apps/worker/kustomize/base/application.properties +4 -0
- package/template/apps/worker/kustomize/base/deployment.yaml +62 -0
- package/template/apps/worker/kustomize/base/kustomization.yaml +16 -0
- package/template/apps/worker/kustomize/overlays/prod/kustomization.yaml +12 -0
- package/template/apps/worker/kustomize/overlays/staging/application.properties +1 -0
- package/template/apps/worker/kustomize/overlays/staging/kustomization.yaml +18 -0
- package/template/apps/worker/oxfmt.config.ts +8 -0
- package/template/apps/worker/oxlint.config.ts +15 -0
- package/template/apps/worker/package.json +33 -0
- package/template/apps/worker/sonar-project.properties +8 -0
- package/template/apps/worker/src/env.ts +13 -0
- package/template/apps/worker/src/lib/graceful-shutdown.ts +27 -0
- package/template/apps/worker/src/lib/logger.ts +20 -0
- package/template/apps/worker/src/main.ts +17 -0
- package/template/apps/worker/src/processors/example-processor.ts +15 -0
- package/template/apps/worker/src/worker/redis.ts +19 -0
- package/template/apps/worker/src/worker/worker.ts +37 -0
- package/template/apps/worker/tests/example-processor.test.ts +13 -0
- package/template/apps/worker/tests/graceful-shutdown.test.ts +34 -0
- package/template/apps/worker/tsconfig.json +4 -0
- package/template/apps/worker/vitest.config.ts +13 -0
- package/template/bun.lock +3053 -0
- package/template/bunfig.toml +2 -0
- package/template/compose.yaml +53 -0
- package/template/docker-bake.hcl +86 -0
- package/template/package.json +47 -0
- package/template/packages/lint/knip.ts +40 -0
- package/template/packages/lint/oxfmt.ts +8 -0
- package/template/packages/lint/oxlint.ts +136 -0
- package/template/packages/lint/package.json +13 -0
- package/template/packages/queues/oxfmt.config.ts +8 -0
- package/template/packages/queues/oxlint.config.ts +7 -0
- package/template/packages/queues/package.json +15 -0
- package/template/packages/queues/src/example-job.ts +11 -0
- package/template/packages/queues/src/index.ts +3 -0
- package/template/packages/queues/src/queue-names.ts +2 -0
- package/template/packages/queues/src/redis-connection.ts +27 -0
- package/template/packages/queues/tsconfig.json +4 -0
- package/template/packages/tsconfig/base.json +23 -0
- package/template/packages/tsconfig/node.json +11 -0
- package/template/packages/tsconfig/package.json +4 -0
- package/template/turbo.json +50 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { getTranslations } from "next-intl/server";
|
|
2
|
+
import { AuthCard } from "@/features/auth/auth-card";
|
|
3
|
+
import { SignInForm } from "@/features/auth/sign-in-form";
|
|
4
|
+
import { Link } from "@/i18n/navigation";
|
|
5
|
+
|
|
6
|
+
export default async function SignInPage() {
|
|
7
|
+
const t = await getTranslations("Auth.SignIn");
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<AuthCard
|
|
11
|
+
footer={
|
|
12
|
+
<>
|
|
13
|
+
{t("noAccount")}{" "}
|
|
14
|
+
<Link className="text-link font-medium hover:underline" href="/sign-up">
|
|
15
|
+
{t("signUpLink")}
|
|
16
|
+
</Link>
|
|
17
|
+
</>
|
|
18
|
+
}
|
|
19
|
+
title={t("title")}
|
|
20
|
+
>
|
|
21
|
+
<SignInForm />
|
|
22
|
+
</AuthCard>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { getTranslations } from "next-intl/server";
|
|
2
|
+
import { AuthCard } from "@/features/auth/auth-card";
|
|
3
|
+
import { SignUpForm } from "@/features/auth/sign-up-form";
|
|
4
|
+
import { Link } from "@/i18n/navigation";
|
|
5
|
+
|
|
6
|
+
export default async function SignUpPage() {
|
|
7
|
+
const t = await getTranslations("Auth.SignUp");
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<AuthCard
|
|
11
|
+
footer={
|
|
12
|
+
<>
|
|
13
|
+
{t("haveAccount")}{" "}
|
|
14
|
+
<Link className="text-link font-medium hover:underline" href="/sign-in">
|
|
15
|
+
{t("signInLink")}
|
|
16
|
+
</Link>
|
|
17
|
+
</>
|
|
18
|
+
}
|
|
19
|
+
title={t("title")}
|
|
20
|
+
>
|
|
21
|
+
<SignUpForm />
|
|
22
|
+
</AuthCard>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { getTranslations } from "next-intl/server";
|
|
2
|
+
import { Suspense } from "react";
|
|
3
|
+
import { AuthCard } from "@/features/auth/auth-card";
|
|
4
|
+
import { ResendVerificationButton } from "@/features/auth/resend-verification-button";
|
|
5
|
+
import { getSession } from "@/lib/auth";
|
|
6
|
+
|
|
7
|
+
/** Shown after sign-up: the API has mailed a verification link. */
|
|
8
|
+
export default async function VerifyPage() {
|
|
9
|
+
const t = await getTranslations("Auth.Verify");
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<AuthCard description={t("description")} title={t("title")}>
|
|
13
|
+
<Suspense>
|
|
14
|
+
<ResendVerification />
|
|
15
|
+
</Suspense>
|
|
16
|
+
</AuthCard>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function ResendVerification() {
|
|
21
|
+
const session = await getSession();
|
|
22
|
+
if (!session || session.user.emailVerified) return null;
|
|
23
|
+
return <ResendVerificationButton email={session.user.email} />;
|
|
24
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { getTranslations } from "next-intl/server";
|
|
2
|
+
import { Suspense } from "react";
|
|
3
|
+
import { Alert } from "@/components/alert/alert";
|
|
4
|
+
import { PasskeySettings } from "@/features/auth/passkey-settings";
|
|
5
|
+
import { getSession } from "@/lib/auth";
|
|
6
|
+
|
|
7
|
+
/** Example protected page: the proxy redirects here only with a session. */
|
|
8
|
+
export default async function DashboardPage() {
|
|
9
|
+
const t = await getTranslations("Dashboard");
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<section className="space-y-4 pt-16">
|
|
13
|
+
<h1 className="text-3xl font-bold tracking-tight">{t("title")}</h1>
|
|
14
|
+
<Suspense>
|
|
15
|
+
<SessionSummary />
|
|
16
|
+
</Suspense>
|
|
17
|
+
<div className="pt-6">
|
|
18
|
+
<PasskeySettings />
|
|
19
|
+
</div>
|
|
20
|
+
</section>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function SessionSummary() {
|
|
25
|
+
const [session, t] = await Promise.all([getSession(), getTranslations("Dashboard")]);
|
|
26
|
+
if (!session) return null;
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div className="space-y-3">
|
|
30
|
+
<p className="text-muted-foreground">{t("signedInAs", { email: session.user.email })}</p>
|
|
31
|
+
{session.user.emailVerified ? null : <Alert message={t("unverified")} variant="warning" />}
|
|
32
|
+
</div>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { hasLocale, NextIntlClientProvider } from "next-intl";
|
|
2
|
+
import { Inter } from "next/font/google";
|
|
3
|
+
import { notFound } from "next/navigation";
|
|
4
|
+
import type { ReactNode } from "react";
|
|
5
|
+
import { Suspense } from "react";
|
|
6
|
+
import { Toaster } from "@/components/toast/toaster";
|
|
7
|
+
import { Provider } from "@/features/shared/provider";
|
|
8
|
+
import { HeaderFrame, SiteHeader } from "@/features/shared/site-header";
|
|
9
|
+
import { routing } from "@/i18n/routing";
|
|
10
|
+
import { constructMetadata } from "@/lib/metadata";
|
|
11
|
+
import { cn } from "@/lib/utils";
|
|
12
|
+
import "../globals.css";
|
|
13
|
+
|
|
14
|
+
const inter = Inter({
|
|
15
|
+
display: "swap",
|
|
16
|
+
subsets: ["latin", "latin-ext"],
|
|
17
|
+
variable: "--font-inter",
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export const metadata = constructMetadata();
|
|
21
|
+
export const viewport = {
|
|
22
|
+
themeColor: [
|
|
23
|
+
{ color: "white", media: "(prefers-color-scheme: light)" },
|
|
24
|
+
{ color: "black", media: "(prefers-color-scheme: dark)" },
|
|
25
|
+
],
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
type Props = {
|
|
29
|
+
children: ReactNode;
|
|
30
|
+
params: Promise<{ locale: string }>;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export function generateStaticParams() {
|
|
34
|
+
return routing.locales.map((locale) => ({ locale }));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export default async function LocaleLayout({ children, params }: Props) {
|
|
38
|
+
const { locale } = await params;
|
|
39
|
+
if (!hasLocale(routing.locales, locale)) {
|
|
40
|
+
notFound();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<html lang={locale} suppressHydrationWarning>
|
|
45
|
+
<body className={cn("antialiased", inter.className)}>
|
|
46
|
+
<Provider>
|
|
47
|
+
<NextIntlClientProvider>
|
|
48
|
+
<Suspense fallback={<HeaderFrame />}>
|
|
49
|
+
<SiteHeader />
|
|
50
|
+
</Suspense>
|
|
51
|
+
<main className="mx-auto min-h-[calc(100dvh-4rem)] max-w-5xl px-4 pt-16">
|
|
52
|
+
{children}
|
|
53
|
+
</main>
|
|
54
|
+
</NextIntlClientProvider>
|
|
55
|
+
</Provider>
|
|
56
|
+
<Toaster />
|
|
57
|
+
</body>
|
|
58
|
+
</html>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { getTranslations } from "next-intl/server";
|
|
2
|
+
import { buttonVariants } from "@/components/button/button-variants"; // @if auth
|
|
3
|
+
import { GoogleOneTap } from "@/features/auth/google-one-tap"; // @if auth
|
|
4
|
+
import { Link } from "@/i18n/navigation"; // @if auth
|
|
5
|
+
|
|
6
|
+
export default async function HomePage() {
|
|
7
|
+
const t = await getTranslations("Home");
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<section className="flex flex-col items-center gap-6 pt-28 text-center md:pt-40">
|
|
11
|
+
<h1 className="max-w-2xl text-4xl font-bold tracking-tight text-balance md:text-5xl">
|
|
12
|
+
{t("title")}
|
|
13
|
+
</h1>
|
|
14
|
+
<p className="text-muted-foreground max-w-xl text-base md:text-lg">{t("description")}</p>
|
|
15
|
+
{/* @if auth */}
|
|
16
|
+
<GoogleOneTap />
|
|
17
|
+
<Link className={buttonVariants({ size: "lg" })} href="/sign-up">
|
|
18
|
+
{t("cta")}
|
|
19
|
+
</Link>
|
|
20
|
+
{/* @endif */}
|
|
21
|
+
</section>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import NextError from "next/error";
|
|
4
|
+
import { useEffect } from "react";
|
|
5
|
+
import { routing } from "@/i18n/routing";
|
|
6
|
+
|
|
7
|
+
export default function GlobalError({ error }: Readonly<{ error: Error }>) {
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
console.error("Unhandled application error:", error);
|
|
10
|
+
}, [error]);
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<html lang={routing.defaultLocale}>
|
|
14
|
+
<body>
|
|
15
|
+
<NextError statusCode={0} />
|
|
16
|
+
</body>
|
|
17
|
+
</html>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
@plugin 'tailwindcss-animate';
|
|
3
|
+
@custom-variant dark (&:is(.dark *));
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
* Semantic tokens. Components use these (bg-background, text-muted-foreground,
|
|
7
|
+
* border-border, …), never raw hex or ramp steps, so light and dark stay in sync.
|
|
8
|
+
* The .dark block below redefines every token.
|
|
9
|
+
*/
|
|
10
|
+
@theme {
|
|
11
|
+
--color-brand-50: #eef3ff;
|
|
12
|
+
--color-brand-100: #d9e4ff;
|
|
13
|
+
--color-brand-200: #bacdff;
|
|
14
|
+
--color-brand-300: #8dabff;
|
|
15
|
+
--color-brand-400: #5b82ff;
|
|
16
|
+
--color-brand-500: #2f5cf7;
|
|
17
|
+
--color-brand-600: #1f47e0;
|
|
18
|
+
--color-brand-700: #1a39bd;
|
|
19
|
+
|
|
20
|
+
--color-background: #f5f5f7;
|
|
21
|
+
--color-foreground: #1d1d1f;
|
|
22
|
+
--color-card: #ffffff;
|
|
23
|
+
--color-card-foreground: #1d1d1f;
|
|
24
|
+
--color-popover: #ffffff;
|
|
25
|
+
--color-popover-foreground: #1d1d1f;
|
|
26
|
+
--color-primary: var(--color-brand-500);
|
|
27
|
+
--color-primary-hover: var(--color-brand-600);
|
|
28
|
+
--color-primary-foreground: #ffffff;
|
|
29
|
+
--color-secondary: #e8e8ed;
|
|
30
|
+
--color-secondary-foreground: #1d1d1f;
|
|
31
|
+
--color-muted: #e8e8ed;
|
|
32
|
+
--color-muted-foreground: #636366;
|
|
33
|
+
--color-accent: #eff2f7;
|
|
34
|
+
--color-accent-foreground: #1d1d1f;
|
|
35
|
+
--color-link: var(--color-brand-600);
|
|
36
|
+
--color-destructive: #be123c;
|
|
37
|
+
--color-destructive-solid: #e11d48;
|
|
38
|
+
--color-destructive-foreground: #ffffff;
|
|
39
|
+
--color-success: #16a34a;
|
|
40
|
+
--color-border: #d2d2d7;
|
|
41
|
+
--color-input: #c7c7cc;
|
|
42
|
+
--color-ring: var(--color-brand-400);
|
|
43
|
+
|
|
44
|
+
--radius-sm: 0.5rem;
|
|
45
|
+
--radius-md: 0.625rem;
|
|
46
|
+
--radius-lg: 0.75rem;
|
|
47
|
+
--radius-xl: 1rem;
|
|
48
|
+
--radius-2xl: 1.25rem;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
@layer theme {
|
|
52
|
+
.dark {
|
|
53
|
+
--color-brand-400: #3d9dff;
|
|
54
|
+
--color-brand-500: #0a84ff;
|
|
55
|
+
--color-brand-600: #0a6fd8;
|
|
56
|
+
|
|
57
|
+
--color-background: #000000;
|
|
58
|
+
--color-foreground: #f5f5f7;
|
|
59
|
+
--color-card: #1c1c1e;
|
|
60
|
+
--color-card-foreground: #f5f5f7;
|
|
61
|
+
--color-popover: #1c1c1e;
|
|
62
|
+
--color-popover-foreground: #f5f5f7;
|
|
63
|
+
--color-primary: var(--color-brand-500);
|
|
64
|
+
--color-primary-hover: var(--color-brand-400);
|
|
65
|
+
--color-primary-foreground: #ffffff;
|
|
66
|
+
--color-secondary: #2c2c2e;
|
|
67
|
+
--color-secondary-foreground: #f2f2f7;
|
|
68
|
+
--color-muted: #2c2c2e;
|
|
69
|
+
--color-muted-foreground: #aeaeb2;
|
|
70
|
+
--color-accent: #2c2c2e;
|
|
71
|
+
--color-accent-foreground: #f2f2f7;
|
|
72
|
+
--color-link: #6bb5ff;
|
|
73
|
+
--color-destructive: #fb7185;
|
|
74
|
+
--color-destructive-solid: #be123c;
|
|
75
|
+
--color-destructive-foreground: #ffffff;
|
|
76
|
+
--color-success: #4ade80;
|
|
77
|
+
--color-border: #3a3a3c;
|
|
78
|
+
--color-input: #48484a;
|
|
79
|
+
--color-ring: #5e9cff;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
@layer base {
|
|
84
|
+
*,
|
|
85
|
+
::before,
|
|
86
|
+
::after {
|
|
87
|
+
border-color: var(--color-border);
|
|
88
|
+
}
|
|
89
|
+
html {
|
|
90
|
+
@apply bg-background;
|
|
91
|
+
}
|
|
92
|
+
body {
|
|
93
|
+
@apply bg-background text-foreground;
|
|
94
|
+
}
|
|
95
|
+
button {
|
|
96
|
+
cursor: pointer;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" rx="8" fill="#2f5cf7"/><path d="M10 22V10h2.4l7.2 8.2V10H22v12h-2.4l-7.2-8.2V22z" fill="#fff"/></svg>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { MetadataRoute } from "next";
|
|
2
|
+
import { env } from "@/env";
|
|
3
|
+
|
|
4
|
+
export default function robots(): MetadataRoute.Robots {
|
|
5
|
+
const base = env.NEXT_PUBLIC_HOST_URL.replace(/\/$/, "");
|
|
6
|
+
|
|
7
|
+
return {
|
|
8
|
+
rules: {
|
|
9
|
+
allow: "/",
|
|
10
|
+
disallow: "/*/dashboard", // @if auth
|
|
11
|
+
userAgent: "*",
|
|
12
|
+
},
|
|
13
|
+
sitemap: `${base}/sitemap.xml`,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { MetadataRoute } from "next";
|
|
2
|
+
import { env } from "@/env";
|
|
3
|
+
import { routing } from "@/i18n/routing";
|
|
4
|
+
|
|
5
|
+
/** Public, indexable pages (locale prefix is added per locale). */
|
|
6
|
+
const staticPaths = [
|
|
7
|
+
"",
|
|
8
|
+
// @if auth
|
|
9
|
+
"/sign-in",
|
|
10
|
+
"/sign-up",
|
|
11
|
+
// @endif
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
export default function sitemap(): MetadataRoute.Sitemap {
|
|
15
|
+
const base = env.NEXT_PUBLIC_HOST_URL.replace(/\/$/, "");
|
|
16
|
+
const lastModified = new Date();
|
|
17
|
+
|
|
18
|
+
return routing.locales.flatMap((locale) =>
|
|
19
|
+
staticPaths.map((path) => ({
|
|
20
|
+
lastModified,
|
|
21
|
+
url: `${base}/${locale}${path}`,
|
|
22
|
+
})),
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/nextjs-vite";
|
|
2
|
+
import { withContainer } from "~sb/decorators";
|
|
3
|
+
import { Alert } from "./alert";
|
|
4
|
+
|
|
5
|
+
const meta = {
|
|
6
|
+
args: { message: "Your changes were saved." },
|
|
7
|
+
component: Alert,
|
|
8
|
+
decorators: [withContainer()],
|
|
9
|
+
tags: ["autodocs"],
|
|
10
|
+
} satisfies Meta<typeof Alert>;
|
|
11
|
+
|
|
12
|
+
export default meta;
|
|
13
|
+
type Story = StoryObj<typeof meta>;
|
|
14
|
+
|
|
15
|
+
export const Info: Story = {};
|
|
16
|
+
|
|
17
|
+
export const Error: Story = {
|
|
18
|
+
args: { message: "Invalid email or password.", variant: "error" },
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const Success: Story = { args: { variant: "success" } };
|
|
22
|
+
|
|
23
|
+
export const Warning: Story = {
|
|
24
|
+
args: { message: "Your session expires soon.", variant: "warning" },
|
|
25
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { VariantProps } from "class-variance-authority";
|
|
2
|
+
import { cva } from "class-variance-authority";
|
|
3
|
+
import { AlertCircle, AlertTriangle, CheckCircle2, Info } from "lucide-react";
|
|
4
|
+
import { cn } from "@/lib/utils";
|
|
5
|
+
|
|
6
|
+
const alertVariants = cva(
|
|
7
|
+
"mb-4 flex items-center gap-2.5 overflow-hidden rounded-xl border border-border/60 border-l-[3px] bg-card px-3.5 py-2.5 text-foreground shadow-xs dark:border-white/10",
|
|
8
|
+
{
|
|
9
|
+
defaultVariants: {
|
|
10
|
+
variant: "default",
|
|
11
|
+
},
|
|
12
|
+
variants: {
|
|
13
|
+
variant: {
|
|
14
|
+
default:
|
|
15
|
+
"border-l-blue-500 [&>svg]:text-blue-500 dark:border-l-blue-400 dark:[&>svg]:text-blue-400",
|
|
16
|
+
error:
|
|
17
|
+
"border-l-red-500 [&>svg]:text-red-500 dark:border-l-red-400 dark:[&>svg]:text-red-400",
|
|
18
|
+
success:
|
|
19
|
+
"border-l-emerald-500 [&>svg]:text-emerald-500 dark:border-l-emerald-400 dark:[&>svg]:text-emerald-400",
|
|
20
|
+
warning:
|
|
21
|
+
"border-l-amber-500 [&>svg]:text-amber-500 dark:border-l-amber-400 dark:[&>svg]:text-amber-400",
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
type AlertProps = React.HTMLAttributes<HTMLDivElement> &
|
|
28
|
+
VariantProps<typeof alertVariants> & {
|
|
29
|
+
message: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function Alert({ className, message, variant = "default", ...props }: AlertProps) {
|
|
33
|
+
const Icon = {
|
|
34
|
+
default: Info,
|
|
35
|
+
error: AlertTriangle,
|
|
36
|
+
success: CheckCircle2,
|
|
37
|
+
warning: AlertCircle,
|
|
38
|
+
}[variant || "default"];
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div className={cn(alertVariants({ variant }), className)} {...props}>
|
|
42
|
+
<Icon className="size-[18px] shrink-0" />
|
|
43
|
+
<p className="text-[13px] leading-snug font-medium">{message}</p>
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
Alert.displayName = "Alert";
|
|
49
|
+
|
|
50
|
+
export { Alert };
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/nextjs-vite";
|
|
2
|
+
import { expect, within } from "storybook/test";
|
|
3
|
+
import { Avatar } from "./avatar";
|
|
4
|
+
|
|
5
|
+
const meta = {
|
|
6
|
+
argTypes: {
|
|
7
|
+
color: {
|
|
8
|
+
control: "select",
|
|
9
|
+
options: [
|
|
10
|
+
"red",
|
|
11
|
+
"orange",
|
|
12
|
+
"amber",
|
|
13
|
+
"yellow",
|
|
14
|
+
"lime",
|
|
15
|
+
"green",
|
|
16
|
+
"emerald",
|
|
17
|
+
"cyan",
|
|
18
|
+
"blue",
|
|
19
|
+
"indigo",
|
|
20
|
+
"purple",
|
|
21
|
+
"pink",
|
|
22
|
+
],
|
|
23
|
+
},
|
|
24
|
+
size: {
|
|
25
|
+
control: "select",
|
|
26
|
+
options: ["sm", "md", "lg", "xl"],
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
component: Avatar,
|
|
30
|
+
parameters: {
|
|
31
|
+
layout: "centered",
|
|
32
|
+
},
|
|
33
|
+
tags: ["autodocs"],
|
|
34
|
+
} satisfies Meta<typeof Avatar>;
|
|
35
|
+
|
|
36
|
+
export default meta;
|
|
37
|
+
type Story = StoryObj<typeof meta>;
|
|
38
|
+
|
|
39
|
+
export const Default: Story = {
|
|
40
|
+
args: {
|
|
41
|
+
fallback: "H",
|
|
42
|
+
},
|
|
43
|
+
play: async ({ canvasElement }) => {
|
|
44
|
+
await expect(within(canvasElement).getByText("H")).toBeInTheDocument();
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const Small: Story = {
|
|
49
|
+
args: {
|
|
50
|
+
fallback: "S",
|
|
51
|
+
size: "sm",
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const Medium: Story = {
|
|
56
|
+
args: {
|
|
57
|
+
fallback: "M",
|
|
58
|
+
size: "md",
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const Large: Story = {
|
|
63
|
+
args: {
|
|
64
|
+
fallback: "L",
|
|
65
|
+
size: "lg",
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export const ExtraLarge: Story = {
|
|
70
|
+
args: {
|
|
71
|
+
fallback: "X",
|
|
72
|
+
size: "xl",
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export const WithColor: Story = {
|
|
77
|
+
args: {
|
|
78
|
+
color: "blue",
|
|
79
|
+
fallback: "H",
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export const RainbowColors: Story = {
|
|
84
|
+
render: () => (
|
|
85
|
+
<div className="flex flex-wrap gap-3">
|
|
86
|
+
<Avatar color="red" fallback="R" />
|
|
87
|
+
<Avatar color="orange" fallback="O" />
|
|
88
|
+
<Avatar color="amber" fallback="A" />
|
|
89
|
+
<Avatar color="yellow" fallback="Y" />
|
|
90
|
+
<Avatar color="lime" fallback="L" />
|
|
91
|
+
<Avatar color="green" fallback="G" />
|
|
92
|
+
<Avatar color="emerald" fallback="E" />
|
|
93
|
+
<Avatar color="cyan" fallback="C" />
|
|
94
|
+
<Avatar color="blue" fallback="B" />
|
|
95
|
+
<Avatar color="indigo" fallback="I" />
|
|
96
|
+
<Avatar color="purple" fallback="P" />
|
|
97
|
+
<Avatar color="pink" fallback="K" />
|
|
98
|
+
</div>
|
|
99
|
+
),
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export const Group: Story = {
|
|
103
|
+
render: () => (
|
|
104
|
+
<div className="flex -space-x-2">
|
|
105
|
+
{(["red", "green", "blue", "purple"] as const).map((color, i) => (
|
|
106
|
+
<Avatar
|
|
107
|
+
className="outline-background outline-2"
|
|
108
|
+
color={color}
|
|
109
|
+
fallback={["H", "A", "C", "3"][i]}
|
|
110
|
+
key={color}
|
|
111
|
+
size="md"
|
|
112
|
+
/>
|
|
113
|
+
))}
|
|
114
|
+
</div>
|
|
115
|
+
),
|
|
116
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { HTMLAttributes, RefObject } from "react";
|
|
4
|
+
import { cn } from "@/lib/utils";
|
|
5
|
+
|
|
6
|
+
export type AvatarProps = HTMLAttributes<HTMLDivElement> & {
|
|
7
|
+
className?: string;
|
|
8
|
+
color?: AvatarColor;
|
|
9
|
+
fallback?: string;
|
|
10
|
+
size?: "lg" | "md" | "sm" | "xl";
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type AvatarColor =
|
|
14
|
+
| "amber"
|
|
15
|
+
| "blue"
|
|
16
|
+
| "cyan"
|
|
17
|
+
| "emerald"
|
|
18
|
+
| "green"
|
|
19
|
+
| "indigo"
|
|
20
|
+
| "lime"
|
|
21
|
+
| "orange"
|
|
22
|
+
| "pink"
|
|
23
|
+
| "purple"
|
|
24
|
+
| "red"
|
|
25
|
+
| "yellow";
|
|
26
|
+
|
|
27
|
+
const sizeMap: Record<string, string> = {
|
|
28
|
+
lg: "h-12 w-12 text-sm",
|
|
29
|
+
md: "h-10 w-10 text-xs",
|
|
30
|
+
sm: "h-8 w-8 text-[0.625rem]",
|
|
31
|
+
xl: "h-14 w-14 text-base",
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const colorMap: Record<AvatarColor, string> = {
|
|
35
|
+
amber: "bg-amber-500 text-white ring-amber-600/20 dark:bg-amber-600 dark:ring-amber-400/25",
|
|
36
|
+
blue: "bg-brand-500 text-white ring-brand-600/20 dark:bg-brand-500 dark:ring-brand-400/25",
|
|
37
|
+
cyan: "bg-cyan-500 text-white ring-cyan-600/20 dark:bg-cyan-600 dark:ring-cyan-400/25",
|
|
38
|
+
emerald:
|
|
39
|
+
"bg-emerald-500 text-white ring-emerald-600/20 dark:bg-emerald-600 dark:ring-emerald-400/25",
|
|
40
|
+
green: "bg-green-500 text-white ring-green-600/20 dark:bg-green-600 dark:ring-green-400/25",
|
|
41
|
+
indigo: "bg-indigo-500 text-white ring-indigo-600/20 dark:bg-indigo-600 dark:ring-indigo-400/25",
|
|
42
|
+
lime: "bg-lime-600 text-white ring-lime-700/20 dark:bg-lime-700 dark:ring-lime-400/25",
|
|
43
|
+
orange: "bg-orange-500 text-white ring-orange-600/20 dark:bg-orange-600 dark:ring-orange-400/25",
|
|
44
|
+
pink: "bg-pink-500 text-white ring-pink-600/20 dark:bg-pink-600 dark:ring-pink-400/25",
|
|
45
|
+
purple: "bg-purple-500 text-white ring-purple-600/20 dark:bg-purple-600 dark:ring-purple-400/25",
|
|
46
|
+
red: "bg-red-500 text-white ring-red-600/20 dark:bg-red-600 dark:ring-red-400/25",
|
|
47
|
+
yellow: "bg-yellow-600 text-white ring-yellow-700/20 dark:bg-yellow-700 dark:ring-yellow-400/25",
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const neutralColor = "bg-muted text-foreground/80 ring-border/70";
|
|
51
|
+
|
|
52
|
+
function Avatar({ ref, ...props }: AvatarProps & { ref?: RefObject<HTMLDivElement | null> }) {
|
|
53
|
+
const { className, color, fallback = "A", size = "md", ...divProps } = props;
|
|
54
|
+
|
|
55
|
+
const colors = color ? colorMap[color] : neutralColor;
|
|
56
|
+
|
|
57
|
+
const getInitials = (text: string): string => {
|
|
58
|
+
const parts = text.split(/\s+/);
|
|
59
|
+
if (parts.length > 1) {
|
|
60
|
+
const first = parts[0].charAt(0);
|
|
61
|
+
const last = (parts.at(-1) ?? "0").charAt(0);
|
|
62
|
+
return `${first}${last}`.toUpperCase();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return text.slice(0, 2).toUpperCase();
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<div
|
|
70
|
+
className={cn(
|
|
71
|
+
"inline-flex shrink-0 select-none items-center justify-center rounded-full",
|
|
72
|
+
"font-semibold tracking-tight ring-1 ring-inset cursor-pointer",
|
|
73
|
+
|
|
74
|
+
sizeMap[size],
|
|
75
|
+
colors,
|
|
76
|
+
className,
|
|
77
|
+
)}
|
|
78
|
+
ref={ref}
|
|
79
|
+
{...divProps}
|
|
80
|
+
>
|
|
81
|
+
{getInitials(fallback)}
|
|
82
|
+
</div>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
Avatar.displayName = "Avatar";
|
|
87
|
+
|
|
88
|
+
export { Avatar };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { cva } from "class-variance-authority";
|
|
2
|
+
|
|
3
|
+
export const buttonVariants = cva(
|
|
4
|
+
"inline-flex cursor-pointer items-center justify-center gap-2 rounded-full border text-sm font-semibold transition-[background-color,border-color,box-shadow] duration-200 outline-none select-none focus-visible:ring-2 focus-visible:ring-ring/60 focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-45",
|
|
5
|
+
{
|
|
6
|
+
defaultVariants: {
|
|
7
|
+
size: "default",
|
|
8
|
+
variant: "primary",
|
|
9
|
+
},
|
|
10
|
+
variants: {
|
|
11
|
+
size: {
|
|
12
|
+
default: "h-11 px-4 py-2",
|
|
13
|
+
icon: "size-9 p-0",
|
|
14
|
+
lg: "h-12 px-8 text-base",
|
|
15
|
+
sm: "h-9 px-3",
|
|
16
|
+
},
|
|
17
|
+
variant: {
|
|
18
|
+
destructive:
|
|
19
|
+
"border-transparent bg-destructive-solid text-destructive-foreground hover:bg-destructive-solid/90",
|
|
20
|
+
ghost: "border-transparent bg-transparent text-foreground hover:bg-foreground/8",
|
|
21
|
+
outline: "border-border bg-card text-foreground shadow-xs hover:bg-accent",
|
|
22
|
+
primary:
|
|
23
|
+
"border-transparent bg-primary text-primary-foreground shadow-sm hover:bg-primary-hover",
|
|
24
|
+
secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-accent",
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
);
|