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,307 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/nextjs-vite";
|
|
2
|
+
import { AlertCircle, FileText, Mail, Share2, Trash2, Upload } from "lucide-react";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { expect, fn, userEvent, within } from "storybook/test";
|
|
5
|
+
import { Button } from "../button/button";
|
|
6
|
+
import { ResponsiveDialog } from "./responsive-dialog";
|
|
7
|
+
|
|
8
|
+
const meta = {
|
|
9
|
+
argTypes: {
|
|
10
|
+
open: {
|
|
11
|
+
control: "boolean",
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
component: ResponsiveDialog,
|
|
15
|
+
parameters: {
|
|
16
|
+
layout: "centered",
|
|
17
|
+
},
|
|
18
|
+
tags: ["autodocs"],
|
|
19
|
+
} satisfies Meta<typeof ResponsiveDialog>;
|
|
20
|
+
|
|
21
|
+
export default meta;
|
|
22
|
+
type Story = StoryObj<typeof meta>;
|
|
23
|
+
|
|
24
|
+
export const Default: Story = {
|
|
25
|
+
args: {
|
|
26
|
+
open: false,
|
|
27
|
+
setOpen: () => {},
|
|
28
|
+
title: "",
|
|
29
|
+
},
|
|
30
|
+
play: async ({ canvasElement }) => {
|
|
31
|
+
const canvas = within(canvasElement);
|
|
32
|
+
const trigger = canvas.getByRole("button", { name: /open dialog/i });
|
|
33
|
+
|
|
34
|
+
await expect(trigger).toBeInTheDocument();
|
|
35
|
+
|
|
36
|
+
await userEvent.click(trigger);
|
|
37
|
+
},
|
|
38
|
+
render: () => {
|
|
39
|
+
const [open, setOpen] = useState(false);
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<>
|
|
43
|
+
<Button onClick={() => setOpen(true)}>Open Dialog</Button>
|
|
44
|
+
<ResponsiveDialog
|
|
45
|
+
description="This dialog adapts to screen size - it's a dialog on desktop and a drawer on mobile."
|
|
46
|
+
open={open}
|
|
47
|
+
setOpen={setOpen}
|
|
48
|
+
title="Responsive Dialog"
|
|
49
|
+
>
|
|
50
|
+
<div className="space-y-4 py-4">
|
|
51
|
+
<p className="text-sm">
|
|
52
|
+
Try resizing your browser window or viewing this on different devices to see how it
|
|
53
|
+
adapts.
|
|
54
|
+
</p>
|
|
55
|
+
<p className="text-muted-foreground text-sm">
|
|
56
|
+
The component automatically switches between a centered dialog and a bottom drawer
|
|
57
|
+
based on the viewport width.
|
|
58
|
+
</p>
|
|
59
|
+
</div>
|
|
60
|
+
</ResponsiveDialog>
|
|
61
|
+
</>
|
|
62
|
+
);
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const WithForm: Story = {
|
|
67
|
+
args: {
|
|
68
|
+
open: false,
|
|
69
|
+
setOpen: () => {},
|
|
70
|
+
title: "",
|
|
71
|
+
},
|
|
72
|
+
render: () => {
|
|
73
|
+
const [open, setOpen] = useState(false);
|
|
74
|
+
const [email, setEmail] = useState("");
|
|
75
|
+
|
|
76
|
+
const handleSubmit = () => {
|
|
77
|
+
fn()(email);
|
|
78
|
+
setOpen(false);
|
|
79
|
+
setEmail("");
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<>
|
|
84
|
+
<Button onClick={() => setOpen(true)}>
|
|
85
|
+
<Mail size={16} />
|
|
86
|
+
Share Document
|
|
87
|
+
</Button>
|
|
88
|
+
<ResponsiveDialog
|
|
89
|
+
description="Enter the email address of the person you want to share this document with."
|
|
90
|
+
open={open}
|
|
91
|
+
setOpen={setOpen}
|
|
92
|
+
title="Share Document"
|
|
93
|
+
>
|
|
94
|
+
<div className="space-y-4 py-4">
|
|
95
|
+
<div className="space-y-2">
|
|
96
|
+
<label className="text-sm font-medium" htmlFor="email">
|
|
97
|
+
Email Address
|
|
98
|
+
</label>
|
|
99
|
+
<input
|
|
100
|
+
aria-label="Email Address"
|
|
101
|
+
className="border-input bg-background ring-offset-background focus-visible:ring-ring w-full rounded-md border px-3 py-2 text-sm focus-visible:ring-2 focus-visible:outline-none"
|
|
102
|
+
id="email"
|
|
103
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
104
|
+
placeholder="colleague@example.com"
|
|
105
|
+
type="email"
|
|
106
|
+
value={email}
|
|
107
|
+
/>
|
|
108
|
+
</div>
|
|
109
|
+
<div className="space-y-2">
|
|
110
|
+
<label className="text-sm font-medium" htmlFor="message">
|
|
111
|
+
Message (Optional)
|
|
112
|
+
</label>
|
|
113
|
+
<textarea
|
|
114
|
+
aria-label="Message (Optional)"
|
|
115
|
+
className="border-input bg-background ring-offset-background focus-visible:ring-ring w-full rounded-md border px-3 py-2 text-sm focus-visible:ring-2 focus-visible:outline-none"
|
|
116
|
+
id="message"
|
|
117
|
+
placeholder="Add a personal message..."
|
|
118
|
+
rows={3}
|
|
119
|
+
/>
|
|
120
|
+
</div>
|
|
121
|
+
<div className="flex gap-2">
|
|
122
|
+
<Button className="flex-1" disabled={!email} onClick={handleSubmit}>
|
|
123
|
+
<Share2 size={16} />
|
|
124
|
+
Send Invitation
|
|
125
|
+
</Button>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
</ResponsiveDialog>
|
|
129
|
+
</>
|
|
130
|
+
);
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export const UploadFile: Story = {
|
|
135
|
+
args: {
|
|
136
|
+
open: false,
|
|
137
|
+
setOpen: () => {},
|
|
138
|
+
title: "",
|
|
139
|
+
},
|
|
140
|
+
render: () => {
|
|
141
|
+
const [open, setOpen] = useState(false);
|
|
142
|
+
|
|
143
|
+
return (
|
|
144
|
+
<>
|
|
145
|
+
<Button onClick={() => setOpen(true)}>
|
|
146
|
+
<Upload size={16} />
|
|
147
|
+
Upload File
|
|
148
|
+
</Button>
|
|
149
|
+
<ResponsiveDialog
|
|
150
|
+
description="Select a file to upload to your document library."
|
|
151
|
+
open={open}
|
|
152
|
+
setOpen={setOpen}
|
|
153
|
+
title="Upload Document"
|
|
154
|
+
>
|
|
155
|
+
<div className="space-y-4 py-4">
|
|
156
|
+
<div className="border-border bg-accent/50 rounded-lg border-2 border-dashed p-8 text-center">
|
|
157
|
+
<Upload className="text-muted-foreground mx-auto h-12 w-12" />
|
|
158
|
+
<p className="mt-2 text-sm font-medium">Drop files here or click to browse</p>
|
|
159
|
+
<p className="text-muted-foreground mt-1 text-xs">
|
|
160
|
+
Supports: PDF, DOCX, XLSX (Max 10MB)
|
|
161
|
+
</p>
|
|
162
|
+
<Button className="mt-4" size="sm" variant="outline">
|
|
163
|
+
Browse Files
|
|
164
|
+
</Button>
|
|
165
|
+
</div>
|
|
166
|
+
<div className="flex gap-2">
|
|
167
|
+
<Button className="flex-1" onClick={() => setOpen(false)}>
|
|
168
|
+
Start Upload
|
|
169
|
+
</Button>
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
</ResponsiveDialog>
|
|
173
|
+
</>
|
|
174
|
+
);
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
export const DeleteConfirmation: Story = {
|
|
179
|
+
args: {
|
|
180
|
+
open: false,
|
|
181
|
+
setOpen: () => {},
|
|
182
|
+
title: "",
|
|
183
|
+
},
|
|
184
|
+
render: () => {
|
|
185
|
+
const [open, setOpen] = useState(false);
|
|
186
|
+
|
|
187
|
+
const handleDelete = () => {
|
|
188
|
+
fn()();
|
|
189
|
+
setOpen(false);
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
return (
|
|
193
|
+
<>
|
|
194
|
+
<Button onClick={() => setOpen(true)} variant="destructive">
|
|
195
|
+
<Trash2 size={16} />
|
|
196
|
+
Delete Document
|
|
197
|
+
</Button>
|
|
198
|
+
<ResponsiveDialog
|
|
199
|
+
description="This action cannot be undone. This will permanently delete the document and remove all associated data."
|
|
200
|
+
open={open}
|
|
201
|
+
setOpen={setOpen}
|
|
202
|
+
title="Delete Document"
|
|
203
|
+
>
|
|
204
|
+
<div className="space-y-4 py-4">
|
|
205
|
+
<div className="border-destructive/50 bg-destructive/5 flex items-start gap-3 rounded-lg border p-4">
|
|
206
|
+
<AlertCircle className="text-destructive h-5 w-5 shrink-0" />
|
|
207
|
+
<div className="space-y-1">
|
|
208
|
+
<p className="text-sm font-medium">Warning</p>
|
|
209
|
+
<p className="text-muted-foreground text-sm">
|
|
210
|
+
Are you absolutely sure you want to delete this document? This action is
|
|
211
|
+
irreversible.
|
|
212
|
+
</p>
|
|
213
|
+
</div>
|
|
214
|
+
</div>
|
|
215
|
+
<div className="flex gap-2">
|
|
216
|
+
<Button className="flex-1" onClick={handleDelete} variant="destructive">
|
|
217
|
+
<Trash2 size={16} />
|
|
218
|
+
Delete Permanently
|
|
219
|
+
</Button>
|
|
220
|
+
</div>
|
|
221
|
+
</div>
|
|
222
|
+
</ResponsiveDialog>
|
|
223
|
+
</>
|
|
224
|
+
);
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
export const DocumentPreview: Story = {
|
|
229
|
+
args: {
|
|
230
|
+
open: false,
|
|
231
|
+
setOpen: () => {},
|
|
232
|
+
title: "",
|
|
233
|
+
},
|
|
234
|
+
render: () => {
|
|
235
|
+
const [open, setOpen] = useState(false);
|
|
236
|
+
|
|
237
|
+
return (
|
|
238
|
+
<>
|
|
239
|
+
<Button onClick={() => setOpen(true)} variant="outline">
|
|
240
|
+
<FileText size={16} />
|
|
241
|
+
View Details
|
|
242
|
+
</Button>
|
|
243
|
+
<ResponsiveDialog
|
|
244
|
+
description="View information about this document."
|
|
245
|
+
open={open}
|
|
246
|
+
setOpen={setOpen}
|
|
247
|
+
title="Document Details"
|
|
248
|
+
>
|
|
249
|
+
<div className="space-y-4 py-4">
|
|
250
|
+
<div className="space-y-3">
|
|
251
|
+
<div className="border-border bg-accent/50 flex justify-between rounded-lg border p-3">
|
|
252
|
+
<span className="text-sm font-medium">File Name</span>
|
|
253
|
+
<span className="text-muted-foreground text-sm">Annual_Report_2024.pdf</span>
|
|
254
|
+
</div>
|
|
255
|
+
<div className="border-border bg-accent/50 flex justify-between rounded-lg border p-3">
|
|
256
|
+
<span className="text-sm font-medium">Size</span>
|
|
257
|
+
<span className="text-muted-foreground text-sm">2.4 MB</span>
|
|
258
|
+
</div>
|
|
259
|
+
<div className="border-border bg-accent/50 flex justify-between rounded-lg border p-3">
|
|
260
|
+
<span className="text-sm font-medium">Last Modified</span>
|
|
261
|
+
<span className="text-muted-foreground text-sm">Dec 15, 2024</span>
|
|
262
|
+
</div>
|
|
263
|
+
<div className="border-border bg-accent/50 flex justify-between rounded-lg border p-3">
|
|
264
|
+
<span className="text-sm font-medium">Created By</span>
|
|
265
|
+
<span className="text-muted-foreground text-sm">John Doe</span>
|
|
266
|
+
</div>
|
|
267
|
+
</div>
|
|
268
|
+
<div className="flex gap-2">
|
|
269
|
+
<Button className="flex-1" onClick={() => setOpen(false)} variant="outline">
|
|
270
|
+
Download
|
|
271
|
+
</Button>
|
|
272
|
+
<Button className="flex-1" onClick={() => setOpen(false)}>
|
|
273
|
+
Open
|
|
274
|
+
</Button>
|
|
275
|
+
</div>
|
|
276
|
+
</div>
|
|
277
|
+
</ResponsiveDialog>
|
|
278
|
+
</>
|
|
279
|
+
);
|
|
280
|
+
},
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
export const NoDescription: Story = {
|
|
284
|
+
args: {
|
|
285
|
+
open: false,
|
|
286
|
+
setOpen: () => {},
|
|
287
|
+
title: "",
|
|
288
|
+
},
|
|
289
|
+
render: () => {
|
|
290
|
+
const [open, setOpen] = useState(false);
|
|
291
|
+
|
|
292
|
+
return (
|
|
293
|
+
<>
|
|
294
|
+
<Button onClick={() => setOpen(true)} variant="outline">
|
|
295
|
+
Open Simple Dialog
|
|
296
|
+
</Button>
|
|
297
|
+
<ResponsiveDialog open={open} setOpen={setOpen} title="Simple Dialog">
|
|
298
|
+
<div className="py-4">
|
|
299
|
+
<p className="text-sm">
|
|
300
|
+
This responsive dialog doesn't have a description, just a title and content.
|
|
301
|
+
</p>
|
|
302
|
+
</div>
|
|
303
|
+
</ResponsiveDialog>
|
|
304
|
+
</>
|
|
305
|
+
);
|
|
306
|
+
},
|
|
307
|
+
};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useTranslations } from "next-intl";
|
|
4
|
+
import type { Dispatch, PropsWithChildren, SetStateAction } from "react";
|
|
5
|
+
import { useEffect } from "react";
|
|
6
|
+
import {
|
|
7
|
+
Dialog,
|
|
8
|
+
DialogContent,
|
|
9
|
+
DialogDescription,
|
|
10
|
+
DialogHeader,
|
|
11
|
+
DialogTitle,
|
|
12
|
+
} from "@/components/dialog/dialog";
|
|
13
|
+
import {
|
|
14
|
+
Drawer,
|
|
15
|
+
DrawerClose,
|
|
16
|
+
DrawerContent,
|
|
17
|
+
DrawerDescription,
|
|
18
|
+
DrawerFooter,
|
|
19
|
+
DrawerHeader,
|
|
20
|
+
DrawerTitle,
|
|
21
|
+
} from "@/components/drawer/drawer";
|
|
22
|
+
import { useMediaQuery } from "@/lib/hooks/use-media-query";
|
|
23
|
+
import { cn } from "@/lib/utils";
|
|
24
|
+
import { Button } from "../button/button";
|
|
25
|
+
|
|
26
|
+
type Props = PropsWithChildren & {
|
|
27
|
+
/** Extra classes for the desktop dialog panel (e.g. `sm:max-w-md`). */
|
|
28
|
+
className?: string;
|
|
29
|
+
description?: string;
|
|
30
|
+
/** Reset on open, e.g. a react-hook-form `useForm()` result. */
|
|
31
|
+
form?: { reset: () => void };
|
|
32
|
+
isPending?: boolean;
|
|
33
|
+
open: boolean;
|
|
34
|
+
setOpen: Dispatch<SetStateAction<boolean>>;
|
|
35
|
+
title?: string;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export function ResponsiveDialog({
|
|
39
|
+
children,
|
|
40
|
+
className,
|
|
41
|
+
description,
|
|
42
|
+
form,
|
|
43
|
+
isPending = false,
|
|
44
|
+
open,
|
|
45
|
+
setOpen,
|
|
46
|
+
title,
|
|
47
|
+
}: Props) {
|
|
48
|
+
const isDesktop = useMediaQuery("(min-width: 768px)");
|
|
49
|
+
const t = useTranslations("Common");
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
if (open && form) {
|
|
53
|
+
form.reset();
|
|
54
|
+
}
|
|
55
|
+
}, [open, form]);
|
|
56
|
+
|
|
57
|
+
const handleOpenChange = (newOpen: boolean) => {
|
|
58
|
+
if (!newOpen && isPending) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
setOpen(newOpen);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
if (isDesktop) {
|
|
65
|
+
return (
|
|
66
|
+
<Dialog onOpenChange={handleOpenChange} open={open}>
|
|
67
|
+
<DialogContent aria-describedby={description ?? undefined} className={className}>
|
|
68
|
+
<DialogHeader>
|
|
69
|
+
<DialogTitle className={cn(!title && "sr-only")}>{title}</DialogTitle>
|
|
70
|
+
{description && <DialogDescription>{description}</DialogDescription>}
|
|
71
|
+
</DialogHeader>
|
|
72
|
+
{children}
|
|
73
|
+
</DialogContent>
|
|
74
|
+
</Dialog>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<Drawer onOpenChange={handleOpenChange} open={open}>
|
|
80
|
+
<DrawerContent aria-describedby={description ?? undefined}>
|
|
81
|
+
<DrawerHeader className="text-left">
|
|
82
|
+
<DrawerTitle className={cn(!title && "sr-only")}>{title}</DrawerTitle>
|
|
83
|
+
{description && <DrawerDescription>{description}</DrawerDescription>}
|
|
84
|
+
</DrawerHeader>
|
|
85
|
+
<div className="px-5">{children}</div>
|
|
86
|
+
<DrawerFooter className="mx-1 pt-0.5">
|
|
87
|
+
<DrawerClose asChild>
|
|
88
|
+
<Button disabled={isPending} variant="secondary">
|
|
89
|
+
{t("cancel")}
|
|
90
|
+
</Button>
|
|
91
|
+
</DrawerClose>
|
|
92
|
+
</DrawerFooter>
|
|
93
|
+
</DrawerContent>
|
|
94
|
+
</Drawer>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/nextjs-vite";
|
|
2
|
+
import { expect, userEvent, within } from "storybook/test";
|
|
3
|
+
import { Searchbar } from "./searchbar";
|
|
4
|
+
|
|
5
|
+
const meta = {
|
|
6
|
+
argTypes: {
|
|
7
|
+
disabled: {
|
|
8
|
+
control: "boolean",
|
|
9
|
+
description: "Disables the searchbar",
|
|
10
|
+
},
|
|
11
|
+
placeholder: {
|
|
12
|
+
control: "text",
|
|
13
|
+
description: "Placeholder text",
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
component: Searchbar,
|
|
17
|
+
parameters: {
|
|
18
|
+
layout: "centered",
|
|
19
|
+
},
|
|
20
|
+
tags: ["autodocs"],
|
|
21
|
+
} satisfies Meta<typeof Searchbar>;
|
|
22
|
+
|
|
23
|
+
export default meta;
|
|
24
|
+
type Story = StoryObj<typeof meta>;
|
|
25
|
+
|
|
26
|
+
export const Default: Story = {
|
|
27
|
+
args: {
|
|
28
|
+
className: "w-96",
|
|
29
|
+
placeholder: "Suche nach Dokumenten, Karteikarten...",
|
|
30
|
+
},
|
|
31
|
+
play: async ({ canvasElement }) => {
|
|
32
|
+
const canvas = within(canvasElement);
|
|
33
|
+
const field = canvas.getByRole("textbox");
|
|
34
|
+
|
|
35
|
+
await expect(canvas.queryByRole("button", { name: /clear/i })).not.toBeInTheDocument();
|
|
36
|
+
|
|
37
|
+
await userEvent.type(field, "thermodynamics");
|
|
38
|
+
await expect(field).toHaveValue("thermodynamics");
|
|
39
|
+
|
|
40
|
+
await userEvent.click(await canvas.findByRole("button", { name: /clear/i }));
|
|
41
|
+
await expect(field).toHaveValue("");
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const WithLabel: Story = {
|
|
46
|
+
args: {
|
|
47
|
+
className: "w-96",
|
|
48
|
+
label: "Suche",
|
|
49
|
+
placeholder: "Suche nach Dokumenten, Karteikarten...",
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const Disabled: Story = {
|
|
54
|
+
args: {
|
|
55
|
+
className: "w-96",
|
|
56
|
+
disabled: true,
|
|
57
|
+
placeholder: "Suche nach Dokumenten, Karteikarten...",
|
|
58
|
+
},
|
|
59
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Search, X } from "lucide-react";
|
|
4
|
+
import { useTranslations } from "next-intl";
|
|
5
|
+
import { type ComponentProps, useState } from "react";
|
|
6
|
+
import { Input } from "@/components/input/input";
|
|
7
|
+
|
|
8
|
+
export type SearchbarProps = Omit<
|
|
9
|
+
ComponentProps<typeof Input>,
|
|
10
|
+
"leadingIcon" | "trailingIcon" | "type"
|
|
11
|
+
> & {
|
|
12
|
+
onClear?: () => void;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function Searchbar({ onChange, onClear, value, ...props }: SearchbarProps) {
|
|
16
|
+
const t = useTranslations("Common.a11y");
|
|
17
|
+
const [internalValue, setInternalValue] = useState("");
|
|
18
|
+
const currentValue = value ?? internalValue;
|
|
19
|
+
|
|
20
|
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
21
|
+
if (onChange) {
|
|
22
|
+
onChange(e);
|
|
23
|
+
} else {
|
|
24
|
+
setInternalValue(e.target.value);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const handleClear = () => {
|
|
29
|
+
if (onChange) {
|
|
30
|
+
const event = {
|
|
31
|
+
currentTarget: { value: "" },
|
|
32
|
+
target: { value: "" },
|
|
33
|
+
} as React.ChangeEvent<HTMLInputElement>;
|
|
34
|
+
onChange(event);
|
|
35
|
+
} else {
|
|
36
|
+
setInternalValue("");
|
|
37
|
+
}
|
|
38
|
+
onClear?.();
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<Input
|
|
43
|
+
{...props}
|
|
44
|
+
leadingIcon={<Search className="h-5 w-5" />}
|
|
45
|
+
onChange={handleChange}
|
|
46
|
+
trailingIcon={
|
|
47
|
+
currentValue ? (
|
|
48
|
+
<button
|
|
49
|
+
aria-label={t("clearSearch")}
|
|
50
|
+
className="hover:text-foreground flex cursor-pointer items-center justify-center transition-colors"
|
|
51
|
+
onClick={handleClear}
|
|
52
|
+
title={t("clearSearch")}
|
|
53
|
+
type="button"
|
|
54
|
+
>
|
|
55
|
+
<X className="h-5 w-5" />
|
|
56
|
+
</button>
|
|
57
|
+
) : undefined
|
|
58
|
+
}
|
|
59
|
+
value={currentValue}
|
|
60
|
+
/>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
Searchbar.displayName = "Searchbar";
|