@carlonicora/nextjs-jsonapi 1.68.0 → 1.70.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.
Files changed (103) hide show
  1. package/dist/{AuthComponent-NwQ_ZXsv.d.mts → AuthComponent-DXe3kPzb.d.mts} +1 -1
  2. package/dist/{AuthComponent-DL1D3y7f.d.ts → AuthComponent-Di8DsZ2I.d.ts} +1 -1
  3. package/dist/{BlockNoteEditor-6FDECIS2.mjs → BlockNoteEditor-6XV2IXLY.mjs} +15 -9
  4. package/dist/BlockNoteEditor-6XV2IXLY.mjs.map +1 -0
  5. package/dist/{BlockNoteEditor-DXHROT4C.js → BlockNoteEditor-NVPUPZXB.js} +25 -19
  6. package/dist/BlockNoteEditor-NVPUPZXB.js.map +1 -0
  7. package/dist/HowToInterface-DtVWAE1s.d.mts +17 -0
  8. package/dist/HowToInterface-NaqSG9sE.d.ts +17 -0
  9. package/dist/{auth.interface-BX_1qZZJ.d.ts → auth.interface-BTco8PWs.d.ts} +1 -1
  10. package/dist/{auth.interface-yeLelxdI.d.mts → auth.interface-C4uJzBec.d.mts} +1 -1
  11. package/dist/billing/index.js +346 -346
  12. package/dist/billing/index.mjs +3 -3
  13. package/dist/{chunk-37KYO2UD.js → chunk-56VU7A4I.js} +172 -18
  14. package/dist/chunk-56VU7A4I.js.map +1 -0
  15. package/dist/{chunk-WOJIRXIP.js → chunk-6ROMPIIP.js} +11 -11
  16. package/dist/{chunk-WOJIRXIP.js.map → chunk-6ROMPIIP.js.map} +1 -1
  17. package/dist/{chunk-IOMDNRX5.mjs → chunk-GZNHBAZF.mjs} +155 -1
  18. package/dist/chunk-GZNHBAZF.mjs.map +1 -0
  19. package/dist/{chunk-H4ZS3R76.mjs → chunk-LQEKQYUJ.mjs} +2569 -1603
  20. package/dist/chunk-LQEKQYUJ.mjs.map +1 -0
  21. package/dist/{chunk-WVTBEVAL.mjs → chunk-WJYWWOTG.mjs} +2 -2
  22. package/dist/{chunk-ELTHSXBI.js → chunk-ZKOLKFAS.js} +1664 -698
  23. package/dist/chunk-ZKOLKFAS.js.map +1 -0
  24. package/dist/client/index.d.mts +5 -6
  25. package/dist/client/index.d.ts +5 -6
  26. package/dist/client/index.js +4 -4
  27. package/dist/client/index.mjs +3 -3
  28. package/dist/components/index.d.mts +83 -10
  29. package/dist/components/index.d.ts +83 -10
  30. package/dist/components/index.js +26 -4
  31. package/dist/components/index.js.map +1 -1
  32. package/dist/components/index.mjs +25 -3
  33. package/dist/{config-D-mqttuF.d.mts → config-Bmr_0qTn.d.mts} +1 -1
  34. package/dist/{config-CyCAWW-d.d.ts → config-n0lfSf27.d.ts} +1 -1
  35. package/dist/contexts/index.d.mts +16 -4
  36. package/dist/contexts/index.d.ts +16 -4
  37. package/dist/contexts/index.js +8 -4
  38. package/dist/contexts/index.js.map +1 -1
  39. package/dist/contexts/index.mjs +7 -3
  40. package/dist/core/index.d.mts +61 -11
  41. package/dist/core/index.d.ts +61 -11
  42. package/dist/core/index.js +10 -2
  43. package/dist/core/index.js.map +1 -1
  44. package/dist/core/index.mjs +9 -1
  45. package/dist/index.d.mts +9 -10
  46. package/dist/index.d.ts +9 -10
  47. package/dist/index.js +11 -3
  48. package/dist/index.js.map +1 -1
  49. package/dist/index.mjs +10 -2
  50. package/dist/{notification.interface-ItBxq2au.d.ts → notification.interface-DYDZENx2.d.ts} +18 -1
  51. package/dist/{notification.interface-C6UcmJqu.d.mts → notification.interface-DrHu_1MM.d.mts} +18 -1
  52. package/dist/{s3.service-N1g0piXD.d.ts → s3.service-DK2KKXbR.d.ts} +2 -3
  53. package/dist/{s3.service-CHOTwfWA.d.mts → s3.service-TsN2unZr.d.mts} +2 -3
  54. package/dist/server/index.d.mts +3 -4
  55. package/dist/server/index.d.ts +3 -4
  56. package/dist/server/index.js +3 -3
  57. package/dist/server/index.mjs +1 -1
  58. package/dist/{useRbacState-CUj0hp8t.d.ts → useRbacState-BYaSdA78.d.ts} +1 -1
  59. package/dist/{useRbacState-Btk1gkQg.d.mts → useRbacState-CQEJ_ysV.d.mts} +1 -1
  60. package/dist/{useSocket-BSUN9s3p.d.ts → useSocket-Cjt_qvkI.d.ts} +1 -1
  61. package/dist/{useSocket-DKI92Fbg.d.mts → useSocket-VAGetcT3.d.mts} +1 -1
  62. package/package.json +1 -1
  63. package/src/components/editors/BlockNoteEditor.tsx +7 -1
  64. package/src/components/forms/FormBlockNote.tsx +6 -0
  65. package/src/components/forms/FormSelect.tsx +3 -0
  66. package/src/components/index.ts +1 -0
  67. package/src/contexts/index.ts +1 -0
  68. package/src/core/index.ts +2 -0
  69. package/src/core/registry/ModuleRegistry.ts +19 -0
  70. package/src/features/how-to/HowToModule.ts +18 -0
  71. package/src/features/how-to/components/containers/HowToCommand.tsx +230 -0
  72. package/src/features/how-to/components/containers/HowToCommandViewer.tsx +76 -0
  73. package/src/features/how-to/components/containers/HowToContainer.tsx +27 -0
  74. package/src/features/how-to/components/containers/HowToListContainer.tsx +17 -0
  75. package/src/features/how-to/components/details/HowToContent.tsx +16 -0
  76. package/src/features/how-to/components/details/HowToDetails.tsx +52 -0
  77. package/src/features/how-to/components/forms/HowToDeleter.tsx +31 -0
  78. package/src/features/how-to/components/forms/HowToEditor.tsx +270 -0
  79. package/src/features/how-to/components/forms/HowToMultiSelector.tsx +152 -0
  80. package/src/features/how-to/components/forms/HowToSelector.tsx +164 -0
  81. package/src/features/how-to/components/index.ts +11 -0
  82. package/src/features/how-to/components/lists/HowToList.tsx +39 -0
  83. package/src/features/how-to/contexts/HowToContext.tsx +101 -0
  84. package/src/features/how-to/data/HowTo.ts +69 -0
  85. package/src/features/how-to/data/HowToFields.ts +10 -0
  86. package/src/features/how-to/data/HowToInterface.ts +11 -0
  87. package/src/features/how-to/data/HowToService.ts +61 -0
  88. package/src/features/how-to/data/index.ts +4 -0
  89. package/src/features/how-to/hooks/useHowToTableStructure.tsx +86 -0
  90. package/src/features/how-to/index.ts +2 -0
  91. package/src/features/how-to/utils/blocknote.ts +108 -0
  92. package/src/features/how-to/utils/index.ts +1 -0
  93. package/dist/BlockNoteEditor-6FDECIS2.mjs.map +0 -1
  94. package/dist/BlockNoteEditor-DXHROT4C.js.map +0 -1
  95. package/dist/breadcrumb.item.data.interface-CgB4_1EE.d.mts +0 -6
  96. package/dist/breadcrumb.item.data.interface-CgB4_1EE.d.ts +0 -6
  97. package/dist/chunk-37KYO2UD.js.map +0 -1
  98. package/dist/chunk-ELTHSXBI.js.map +0 -1
  99. package/dist/chunk-H4ZS3R76.mjs.map +0 -1
  100. package/dist/chunk-IOMDNRX5.mjs.map +0 -1
  101. package/dist/content.interface-8T5-G84c.d.mts +0 -21
  102. package/dist/content.interface-D-xdYxjt.d.ts +0 -21
  103. /package/dist/{chunk-WVTBEVAL.mjs.map → chunk-WJYWWOTG.mjs.map} +0 -0
@@ -1,13 +1,12 @@
1
1
  import { A as ApiData } from '../ApiData-DPKNfY-9.js';
2
- import { M as ModuleWithPermissions, A as Action } from '../notification.interface-ItBxq2au.js';
2
+ import { M as ModuleWithPermissions, A as Action } from '../notification.interface-DYDZENx2.js';
3
3
  import { A as ApiRequestDataTypeInterface } from '../ApiRequestDataTypeInterface-CYEcRUrh.js';
4
4
  import { A as ApiResponseInterface } from '../ApiResponseInterface-CAIAeP5d.js';
5
- export { A as ServerAuthService, C as ServerCompanyService, a as ServerContentService, F as ServerFeatureService, N as ServerNotificationService, P as ServerPushService, R as ServerRoleService, S as ServerS3Service, U as ServerUserService } from '../s3.service-N1g0piXD.js';
5
+ export { A as ServerAuthService, C as ServerCompanyService, a as ServerContentService, F as ServerFeatureService, N as ServerNotificationService, P as ServerPushService, R as ServerRoleService, S as ServerS3Service, U as ServerUserService } from '../s3.service-DK2KKXbR.js';
6
6
  import 'lucide-react';
7
7
  import '../ApiDataInterface-DPP8s46n.js';
8
8
  import '../feature.interface-CIWxo8NP.js';
9
- import '../auth.interface-BX_1qZZJ.js';
10
- import '../content.interface-D-xdYxjt.js';
9
+ import '../auth.interface-BTco8PWs.js';
11
10
 
12
11
  type CacheProfile = "seconds" | "minutes" | "hours" | "days" | "weeks" | "max" | "default";
13
12
  /**
@@ -15,7 +15,7 @@ var _chunk3ZPK4QOBjs = require('../chunk-3ZPK4QOB.js');
15
15
 
16
16
 
17
17
 
18
- var _chunk37KYO2UDjs = require('../chunk-37KYO2UD.js');
18
+ var _chunk56VU7A4Ijs = require('../chunk-56VU7A4I.js');
19
19
  require('../chunk-LXKSUWAV.js');
20
20
  require('../chunk-IBS6NI7D.js');
21
21
 
@@ -86,7 +86,7 @@ var ServerSession = class {
86
86
  if (!rawModules) return false;
87
87
  const modules = JSON.parse(_pako2.default.ungzip(Buffer.from(rawModules, "base64"), { to: "string" }));
88
88
  const selectedModule = modules.find((module) => module.id === params.module.moduleId);
89
- return _chunk37KYO2UDjs.checkPermissionsFromServer.call(void 0, {
89
+ return _chunk56VU7A4Ijs.checkPermissionsFromServer.call(void 0, {
90
90
  module: params.module,
91
91
  action: params.action,
92
92
  data: params.data,
@@ -296,5 +296,5 @@ _chunk7QVYU63Ejs.__name.call(void 0, ServerJsonApiDelete, "ServerJsonApiDelete")
296
296
 
297
297
 
298
298
 
299
- exports.ServerAuthService = _chunk37KYO2UDjs.AuthService; exports.ServerCompanyService = _chunk37KYO2UDjs.CompanyService; exports.ServerContentService = _chunk37KYO2UDjs.ContentService; exports.ServerFeatureService = _chunk37KYO2UDjs.FeatureService; exports.ServerJsonApiDelete = ServerJsonApiDelete; exports.ServerJsonApiGet = ServerJsonApiGet; exports.ServerJsonApiPatch = ServerJsonApiPatch; exports.ServerJsonApiPost = ServerJsonApiPost; exports.ServerJsonApiPut = ServerJsonApiPut; exports.ServerNotificationService = _chunk37KYO2UDjs.NotificationService; exports.ServerPushService = _chunk37KYO2UDjs.PushService; exports.ServerRoleService = _chunk37KYO2UDjs.RoleService; exports.ServerS3Service = _chunk37KYO2UDjs.S3Service; exports.ServerSession = ServerSession; exports.ServerUserService = _chunk37KYO2UDjs.UserService; exports.configureServerJsonApi = configureServerJsonApi; exports.getServerApiUrl = getServerApiUrl; exports.getServerAppUrl = getServerAppUrl; exports.getServerToken = _chunkYUO55Q5Ajs.getServerToken; exports.getServerTrackablePages = getServerTrackablePages; exports.invalidateCacheTag = invalidateCacheTag; exports.invalidateCacheTags = invalidateCacheTags; exports.serverRequest = _chunk3ZPK4QOBjs.serverRequest;
299
+ exports.ServerAuthService = _chunk56VU7A4Ijs.AuthService; exports.ServerCompanyService = _chunk56VU7A4Ijs.CompanyService; exports.ServerContentService = _chunk56VU7A4Ijs.ContentService; exports.ServerFeatureService = _chunk56VU7A4Ijs.FeatureService; exports.ServerJsonApiDelete = ServerJsonApiDelete; exports.ServerJsonApiGet = ServerJsonApiGet; exports.ServerJsonApiPatch = ServerJsonApiPatch; exports.ServerJsonApiPost = ServerJsonApiPost; exports.ServerJsonApiPut = ServerJsonApiPut; exports.ServerNotificationService = _chunk56VU7A4Ijs.NotificationService; exports.ServerPushService = _chunk56VU7A4Ijs.PushService; exports.ServerRoleService = _chunk56VU7A4Ijs.RoleService; exports.ServerS3Service = _chunk56VU7A4Ijs.S3Service; exports.ServerSession = ServerSession; exports.ServerUserService = _chunk56VU7A4Ijs.UserService; exports.configureServerJsonApi = configureServerJsonApi; exports.getServerApiUrl = getServerApiUrl; exports.getServerAppUrl = getServerAppUrl; exports.getServerToken = _chunkYUO55Q5Ajs.getServerToken; exports.getServerTrackablePages = getServerTrackablePages; exports.invalidateCacheTag = invalidateCacheTag; exports.invalidateCacheTags = invalidateCacheTags; exports.serverRequest = _chunk3ZPK4QOBjs.serverRequest;
300
300
  //# sourceMappingURL=index.js.map
@@ -15,7 +15,7 @@ import {
15
15
  S3Service,
16
16
  UserService,
17
17
  checkPermissionsFromServer
18
- } from "../chunk-IOMDNRX5.mjs";
18
+ } from "../chunk-GZNHBAZF.mjs";
19
19
  import "../chunk-AUXK7QSA.mjs";
20
20
  import "../chunk-C7C7VY4F.mjs";
21
21
  import {
@@ -1,5 +1,5 @@
1
1
  import { F as FeatureInterface } from './feature.interface-CIWxo8NP.js';
2
- import { R as RoleInterface } from './notification.interface-ItBxq2au.js';
2
+ import { R as RoleInterface } from './notification.interface-DYDZENx2.js';
3
3
  import { P as PermissionMappingInterface, M as ModulePathsInterface, A as ActionType, a as PermissionValue, b as PermissionsMap } from './ModulePathsInterface-wVS5Raa4.js';
4
4
 
5
5
  type PageInfo = {
@@ -1,5 +1,5 @@
1
1
  import { F as FeatureInterface } from './feature.interface-BxFFOPNq.mjs';
2
- import { R as RoleInterface } from './notification.interface-C6UcmJqu.mjs';
2
+ import { R as RoleInterface } from './notification.interface-DrHu_1MM.mjs';
3
3
  import { P as PermissionMappingInterface, M as ModulePathsInterface, A as ActionType, a as PermissionValue, b as PermissionsMap } from './ModulePathsInterface-49EWvbWy.mjs';
4
4
 
5
5
  type PageInfo = {
@@ -1,4 +1,4 @@
1
- import { N as NotificationInterface } from './notification.interface-ItBxq2au.js';
1
+ import { N as NotificationInterface } from './notification.interface-DYDZENx2.js';
2
2
 
3
3
  interface UseSocketOptions {
4
4
  token: string;
@@ -1,4 +1,4 @@
1
- import { N as NotificationInterface } from './notification.interface-C6UcmJqu.mjs';
1
+ import { N as NotificationInterface } from './notification.interface-DrHu_1MM.mjs';
2
2
 
3
3
  interface UseSocketOptions {
4
4
  token: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@carlonicora/nextjs-jsonapi",
3
- "version": "1.68.0",
3
+ "version": "1.70.0",
4
4
  "description": "Next.js JSON:API client with server/client support and caching",
5
5
  "author": "Carlo Nicora",
6
6
  "license": "GPL-3.0-or-later",
@@ -27,6 +27,8 @@ export type BlockNoteEditorProps = {
27
27
  diffContent?: PartialBlock[];
28
28
  placeholder?: string;
29
29
  bordered?: boolean;
30
+ inlineContentSpecs?: Record<string, any>;
31
+ renderOverlays?: (editor: any) => React.ReactNode;
30
32
  };
31
33
 
32
34
  const createDiffActionsInlineContentSpec = (
@@ -88,6 +90,8 @@ export default function BlockNoteEditor({
88
90
  diffContent,
89
91
  placeholder,
90
92
  bordered,
93
+ inlineContentSpecs,
94
+ renderOverlays,
91
95
  }: BlockNoteEditorProps): React.JSX.Element {
92
96
  const t = useTranslations();
93
97
  const { company } = useCurrentUserContext<UserInterface>();
@@ -141,9 +145,10 @@ export default function BlockNoteEditor({
141
145
  inlineContentSpecs: {
142
146
  ...defaultInlineContentSpecs,
143
147
  diffActions: DiffActionsInlineContent,
148
+ ...inlineContentSpecs,
144
149
  },
145
150
  } as any),
146
- [DiffActionsInlineContent],
151
+ [DiffActionsInlineContent, inlineContentSpecs],
147
152
  );
148
153
 
149
154
  const uploadImage = useCallback(
@@ -422,6 +427,7 @@ export default function BlockNoteEditor({
422
427
  className={cn(`BlockNoteView flex-1 ${onChange ? "p-4" : ""}`, size === "sm" && "small")}
423
428
  >
424
429
  <BlockNoteEditorFormattingToolbar />
430
+ {renderOverlays?.(editor)}
425
431
  </BlockNoteView>
426
432
  </div>
427
433
  );
@@ -14,6 +14,8 @@ export function FormBlockNote({
14
14
  description,
15
15
  testId,
16
16
  onEmptyChange,
17
+ inlineContentSpecs,
18
+ renderOverlays,
17
19
  }: {
18
20
  form: any;
19
21
  id: string;
@@ -24,6 +26,8 @@ export function FormBlockNote({
24
26
  description?: string;
25
27
  testId?: string;
26
28
  onEmptyChange?: (isEmpty: boolean) => void;
29
+ inlineContentSpecs?: Record<string, any>;
30
+ renderOverlays?: (editor: any) => React.ReactNode;
27
31
  }) {
28
32
  return (
29
33
  <div className="flex w-full flex-col">
@@ -46,6 +50,8 @@ export function FormBlockNote({
46
50
  }}
47
51
  placeholder={placeholder}
48
52
  bordered
53
+ inlineContentSpecs={inlineContentSpecs}
54
+ renderOverlays={renderOverlays}
49
55
  />
50
56
  )}
51
57
  </FormFieldWrapper>
@@ -16,6 +16,7 @@ export function FormSelect({
16
16
  useRows,
17
17
  testId,
18
18
  allowEmpty,
19
+ isRequired,
19
20
  }: {
20
21
  form: any;
21
22
  id: string;
@@ -27,6 +28,7 @@ export function FormSelect({
27
28
  useRows?: boolean;
28
29
  testId?: string;
29
30
  allowEmpty?: boolean;
31
+ isRequired?: boolean;
30
32
  }) {
31
33
  return (
32
34
  <div className="flex w-full flex-col">
@@ -34,6 +36,7 @@ export function FormSelect({
34
36
  form={form}
35
37
  name={id}
36
38
  label={name}
39
+ isRequired={isRequired}
37
40
  orientation={useRows ? "horizontal" : "vertical"}
38
41
  testId={testId}
39
42
  >
@@ -19,6 +19,7 @@ export * from "../features/auth/components";
19
19
  // Billing components moved to separate entry point: @carlonicora/nextjs-jsonapi/billing
20
20
  export * from "../features/company/components";
21
21
  export * from "../features/content/components";
22
+ export * from "../features/how-to/components";
22
23
  export * from "../features/feature/components";
23
24
  export * from "../features/notification/components";
24
25
  export * from "../features/onboarding/components";
@@ -3,6 +3,7 @@ export * from "../features/notification/contexts/NotificationContext";
3
3
  export * from "../features/onboarding/contexts";
4
4
  export * from "../features/role/contexts/RoleContext";
5
5
  export * from "../features/user/contexts";
6
+ export * from "../features/how-to/contexts/HowToContext";
6
7
  export * from "./CommonContext";
7
8
  export * from "./HeaderChildrenContext";
8
9
  export * from "./SharedContext";
package/src/core/index.ts CHANGED
@@ -59,6 +59,8 @@ export * from "../features/company/company.module";
59
59
  export * from "../features/company/data";
60
60
  export * from "../features/content/content.module";
61
61
  export * from "../features/content/data";
62
+ export * from "../features/how-to/HowToModule";
63
+ export * from "../features/how-to/data";
62
64
  export * from "../features/feature/data";
63
65
  export * from "../features/feature/feature.module";
64
66
  export * from "../features/module";
@@ -15,6 +15,7 @@ export interface FoundationModuleDefinitions {
15
15
  Feature: ModuleWithPermissions;
16
16
  Module: ModuleWithPermissions;
17
17
  Content: ModuleWithPermissions;
18
+ HowTo: ModuleWithPermissions;
18
19
  // Billing modules - READ: all users, UPDATE: CompanyAdministrator, ADMIN: Administrator
19
20
  Billing: ModuleWithPermissions;
20
21
  StripeCustomer: ModuleWithPermissions;
@@ -122,6 +123,24 @@ class ModuleRegistryClass {
122
123
  throw new Error(`Module not found: ${moduleName}`);
123
124
  }
124
125
 
126
+ getAllPageUrls(): { id: string; text: string }[] {
127
+ if (this._modules.size === 0) {
128
+ tryBootstrap();
129
+ }
130
+
131
+ const seen = new Set<string>();
132
+ const result: { id: string; text: string }[] = [];
133
+ for (const [key, module] of this._modules.entries()) {
134
+ const m = module as ModuleWithPermissions;
135
+ if (m.pageUrl && !seen.has(m.pageUrl)) {
136
+ seen.add(m.pageUrl);
137
+ result.push({ id: m.pageUrl, text: key });
138
+ result.push({ id: `${m.pageUrl}/:id`, text: `${key} (detail)` });
139
+ }
140
+ }
141
+ return result.sort((a, b) => a.text.localeCompare(b.text));
142
+ }
143
+
125
144
  findByModelName(modelName: string): ModuleWithPermissions {
126
145
  // Direct lookup by registry key (e.g., "Article", "Document")
127
146
  let module = this._modules.get(modelName);
@@ -0,0 +1,18 @@
1
+ import { LifeBuoyIcon } from "lucide-react";
2
+ import { createJsonApiInclusion } from "../../core";
3
+ import { ModuleFactory } from "../../permissions";
4
+ import { HowTo } from "./data/HowTo";
5
+
6
+ export const HowToModule = (factory: ModuleFactory) =>
7
+ factory({
8
+ moduleId: "6f975207-0df3-4c0d-b541-ed5dc04487b2",
9
+ pageUrl: "/administration/howtos",
10
+ name: "howtos",
11
+ model: HowTo,
12
+ icon: LifeBuoyIcon,
13
+ inclusions: {
14
+ lists: {
15
+ fields: [createJsonApiInclusion("howtos", [`name`, `description`, `pages`])],
16
+ },
17
+ },
18
+ });
@@ -0,0 +1,230 @@
1
+ "use client";
2
+
3
+ import { ArrowRight, LifeBuoyIcon } from "lucide-react";
4
+ import { useTranslations } from "next-intl";
5
+ import { ReactNode, useCallback, useEffect, useMemo, useRef, useState } from "react";
6
+
7
+ import {
8
+ Command,
9
+ CommandEmpty,
10
+ CommandGroup,
11
+ CommandInput,
12
+ CommandItem,
13
+ CommandList,
14
+ Dialog,
15
+ DialogContent,
16
+ DialogDescription,
17
+ DialogHeader,
18
+ DialogTitle,
19
+ SidebarMenuButton,
20
+ } from "../../../../shadcnui";
21
+ import { Modules } from "../../../../core";
22
+ import { DataListRetriever, useDataListRetriever, useDebounce } from "../../../../hooks";
23
+ import { HowTo } from "../../data/HowTo";
24
+ import { HowToInterface } from "../../data/HowToInterface";
25
+ import { HowToService } from "../../data/HowToService";
26
+ import HowToCommandViewer from "./HowToCommandViewer";
27
+
28
+ function matchPage(pathname: string, pattern: string): boolean {
29
+ if (pattern.includes(":")) {
30
+ const pathSegments = pathname.split("/").filter(Boolean);
31
+ const patternSegments = pattern.split("/").filter(Boolean);
32
+ if (pathSegments.length !== patternSegments.length) return false;
33
+ return patternSegments.every((seg, i) => (seg.startsWith(":") ? true : seg === pathSegments[i]));
34
+ }
35
+ return pathname.toLowerCase().includes(pattern.toLowerCase());
36
+ }
37
+
38
+ type HowToCommandProps = {
39
+ /** Current pathname for page relevance matching */
40
+ pathname: string;
41
+ /** Optional extra command groups to render when not searching */
42
+ extraGroups?: ReactNode;
43
+ /** Called when user starts a chat from the viewer */
44
+ onStartChat?: () => void;
45
+ };
46
+
47
+ export default function HowToCommand({ pathname, extraGroups, onStartChat }: HowToCommandProps) {
48
+ const t = useTranslations();
49
+
50
+ const [dialogOpen, setDialogOpen] = useState<boolean>(false);
51
+ const [selectedHowTo, setSelectedHowTo] = useState<HowToInterface | null>(null);
52
+
53
+ const searchTermRef = useRef<string>("");
54
+ const [searchTerm, setSearchTerm] = useState<string>("");
55
+
56
+ const data: DataListRetriever<HowToInterface> = useDataListRetriever({
57
+ retriever: (params) => {
58
+ return HowToService.findMany(params);
59
+ },
60
+ retrieverParams: {},
61
+ module: Modules.HowTo,
62
+ });
63
+
64
+ // Split HowTos into relevant (matching current page) and others
65
+ const { relevantHowTos, otherHowTos } = useMemo(() => {
66
+ if (!data.data) return { relevantHowTos: [], otherHowTos: [] };
67
+
68
+ const relevant: HowToInterface[] = [];
69
+ const other: HowToInterface[] = [];
70
+
71
+ (data.data as HowToInterface[]).forEach((howTo) => {
72
+ const pages = HowTo.parsePagesFromString(howTo.pages);
73
+ const isRelevant = pages.some((page) => page && matchPage(pathname, page));
74
+ if (isRelevant) {
75
+ relevant.push(howTo);
76
+ } else {
77
+ other.push(howTo);
78
+ }
79
+ });
80
+
81
+ return { relevantHowTos: relevant, otherHowTos: other };
82
+ }, [data.data, pathname]);
83
+
84
+ const search = useCallback(
85
+ async (searchedTerm: string) => {
86
+ if (searchedTerm === searchTermRef.current) return;
87
+ searchTermRef.current = searchedTerm;
88
+ await data.search(searchedTerm);
89
+ },
90
+ [searchTermRef, data],
91
+ );
92
+
93
+ const updateSearchTerm = useDebounce(search, 500);
94
+
95
+ useEffect(() => {
96
+ updateSearchTerm(searchTerm);
97
+ }, [updateSearchTerm, searchTerm]);
98
+
99
+ // Reset search when dialog closes
100
+ useEffect(() => {
101
+ if (!dialogOpen) {
102
+ setSearchTerm("");
103
+ searchTermRef.current = "";
104
+ }
105
+ }, [dialogOpen]);
106
+
107
+ // Keyboard shortcut: Cmd+K or Ctrl+K to toggle dialog
108
+ useEffect(() => {
109
+ const down = (e: KeyboardEvent) => {
110
+ if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
111
+ e.preventDefault();
112
+ setDialogOpen((open) => !open);
113
+ }
114
+ };
115
+
116
+ document.addEventListener("keydown", down);
117
+ return () => document.removeEventListener("keydown", down);
118
+ }, []);
119
+
120
+ const handleStartChat = () => {
121
+ setDialogOpen(false);
122
+ setSelectedHowTo(null);
123
+ if (onStartChat) onStartChat();
124
+ };
125
+
126
+ return (
127
+ <>
128
+ <SidebarMenuButton
129
+ tooltip={t(`howto.command.trigger`)}
130
+ onClick={() => setDialogOpen(true)}
131
+ className="text-muted-foreground"
132
+ >
133
+ <LifeBuoyIcon />
134
+ <span>{t(`howto.command.trigger`)}</span>
135
+ </SidebarMenuButton>
136
+
137
+ {/* Search HowTos Dialog */}
138
+ <Dialog
139
+ open={dialogOpen}
140
+ onOpenChange={(open) => {
141
+ setDialogOpen(open);
142
+ if (!open) setSelectedHowTo(null);
143
+ }}
144
+ modal={true}
145
+ >
146
+ <DialogContent
147
+ className={`flex flex-col gap-0 overflow-hidden p-0 ${selectedHowTo ? "h-[80vh] max-w-3xl" : "max-w-lg"}`}
148
+ >
149
+ <DialogHeader className="sr-only">
150
+ <DialogTitle>{selectedHowTo ? selectedHowTo.name : t("howto.command.title")}</DialogTitle>
151
+ <DialogDescription> </DialogDescription>
152
+ </DialogHeader>
153
+
154
+ {selectedHowTo ? (
155
+ <HowToCommandViewer
156
+ howTo={selectedHowTo}
157
+ onBack={() => setSelectedHowTo(null)}
158
+ onStartChat={handleStartChat}
159
+ />
160
+ ) : (
161
+ <Command
162
+ shouldFilter={false}
163
+ className="[&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5"
164
+ >
165
+ <CommandInput
166
+ onKeyDown={(e) => {
167
+ if (e.key === "Escape") {
168
+ setSearchTerm("");
169
+ searchTermRef.current = "";
170
+ }
171
+ }}
172
+ onValueChange={setSearchTerm}
173
+ value={searchTerm}
174
+ placeholder={t(`howto.search.placeholder`)}
175
+ />
176
+ <CommandList className="max-h-[60vh] overflow-y-auto">
177
+ <CommandEmpty>{t(`howto.command.empty`)}</CommandEmpty>
178
+
179
+ {/* App-specific extra groups (support, tours, etc.) */}
180
+ {!searchTerm && extraGroups}
181
+
182
+ {/* Relevant to this page */}
183
+ {relevantHowTos.length > 0 && (
184
+ <CommandGroup heading={t(`howto.command.relevant`)}>
185
+ {relevantHowTos.map((howTo: HowToInterface) => (
186
+ <CommandItem key={howTo.id} onSelect={() => setSelectedHowTo(howTo)} className="cursor-pointer">
187
+ <ArrowRight className="h-4 w-4" />
188
+ <span>{howTo.name}</span>
189
+ </CommandItem>
190
+ ))}
191
+ </CommandGroup>
192
+ )}
193
+
194
+ {/* All HowTos / Search Results */}
195
+ {otherHowTos.length > 0 && (
196
+ <CommandGroup heading={searchTerm ? undefined : t(`howto.command.all`)}>
197
+ {otherHowTos.map((howTo: HowToInterface) => (
198
+ <CommandItem key={howTo.id} onSelect={() => setSelectedHowTo(howTo)} className="cursor-pointer">
199
+ <ArrowRight className="h-4 w-4" />
200
+ <span>{howTo.name}</span>
201
+ </CommandItem>
202
+ ))}
203
+ </CommandGroup>
204
+ )}
205
+ </CommandList>
206
+
207
+ {/* Keyboard hints footer */}
208
+ <div className="text-muted-foreground flex items-center justify-center gap-4 border-t px-3 py-2 text-xs">
209
+ <span className="flex items-center gap-1">
210
+ <kbd className="bg-muted rounded border px-1.5 py-0.5 font-mono text-[10px]">&#9166;</kbd>
211
+ {t(`howto.command.keyboard.select`)}
212
+ </span>
213
+ <span className="flex items-center gap-1">
214
+ <kbd className="bg-muted rounded border px-1.5 py-0.5 font-mono text-[10px]">&#8593;&#8595;</kbd>
215
+ {t(`howto.command.keyboard.navigate`)}
216
+ </span>
217
+ <span className="flex items-center gap-1">
218
+ <kbd className="bg-muted rounded border px-1.5 py-0.5 font-mono text-[10px]">
219
+ {t(`howto.keyboard.esc`)}
220
+ </kbd>
221
+ {t(`howto.command.keyboard.close`)}
222
+ </span>
223
+ </div>
224
+ </Command>
225
+ )}
226
+ </DialogContent>
227
+ </Dialog>
228
+ </>
229
+ );
230
+ }
@@ -0,0 +1,76 @@
1
+ "use client";
2
+
3
+ import { ArrowLeft, BookOpen, MessageSquare } from "lucide-react";
4
+ import { useTranslations } from "next-intl";
5
+ import { useMemo } from "react";
6
+
7
+ import { BlockNoteEditorContainer } from "../../../../components";
8
+ import { Button } from "../../../../shadcnui";
9
+ import { HowToInterface } from "../../data/HowToInterface";
10
+ import { calculateReadingTime, extractHeadings } from "../../utils/blocknote";
11
+
12
+ type HowToCommandViewerProps = {
13
+ howTo: HowToInterface;
14
+ onBack: () => void;
15
+ onStartChat?: () => void;
16
+ };
17
+
18
+ export default function HowToCommandViewer({ howTo, onBack, onStartChat }: HowToCommandViewerProps) {
19
+ const t = useTranslations();
20
+
21
+ const readingTime = useMemo(() => calculateReadingTime(howTo.description), [howTo.description]);
22
+ const headings = useMemo(() => extractHeadings(howTo.description), [howTo.description]);
23
+
24
+ return (
25
+ <div className="flex h-full flex-col">
26
+ {/* Header with back button, title, and reading time */}
27
+ <div className="flex items-center gap-3 border-b px-4 py-3">
28
+ <Button variant="ghost" size="sm" onClick={onBack} className="h-8 px-2">
29
+ <ArrowLeft className="h-4 w-4" />
30
+ <span className="ml-1">{t("howto.command.back")}</span>
31
+ </Button>
32
+ <h2 className="flex-1 truncate text-lg font-semibold">{howTo.name}</h2>
33
+ <div className="text-muted-foreground flex items-center gap-1.5 text-sm">
34
+ <BookOpen className="h-4 w-4" />
35
+ <span>{t("howto.reading_time.label", { minutes: readingTime })}</span>
36
+ </div>
37
+ </div>
38
+
39
+ {/* Two-column body */}
40
+ <div className="flex min-h-0 flex-1">
41
+ {/* Left sidebar - table of contents */}
42
+ {headings.length > 0 && (
43
+ <div className="w-50 shrink-0 overflow-y-auto border-r p-4">
44
+ <nav className="space-y-1">
45
+ {headings.map((heading) => (
46
+ <a
47
+ key={heading.id}
48
+ href={`#${heading.id}`}
49
+ className="text-muted-foreground hover:text-foreground block truncate text-sm"
50
+ style={{ paddingLeft: `${(heading.level - 1) * 0.75}rem` }}
51
+ >
52
+ {heading.text}
53
+ </a>
54
+ ))}
55
+ </nav>
56
+ </div>
57
+ )}
58
+
59
+ {/* Right content - scrollable */}
60
+ <div id="howto-viewer-content" className="min-w-0 flex-1 overflow-y-auto p-4">
61
+ <BlockNoteEditorContainer id={howTo.id} type="howto" initialContent={howTo.description} />
62
+ </div>
63
+ </div>
64
+
65
+ {/* Full-width footer */}
66
+ {onStartChat && (
67
+ <div className="flex items-center justify-end gap-2 border-t px-4 py-3">
68
+ <Button onClick={onStartChat} variant="default" size="sm">
69
+ <MessageSquare className="mr-2 h-4 w-4" />
70
+ {t("howto.command.chat_button")}
71
+ </Button>
72
+ </div>
73
+ )}
74
+ </div>
75
+ );
76
+ }
@@ -0,0 +1,27 @@
1
+ "use client";
2
+
3
+ import { RoundPageContainer } from "../../../../components";
4
+ import { Modules } from "../../../../core";
5
+ import { useHowToContext } from "../../contexts/HowToContext";
6
+ import { HowToInterface } from "../../data/HowToInterface";
7
+ import HowToContent from "../details/HowToContent";
8
+ import HowToDetails from "../details/HowToDetails";
9
+
10
+ type HowToContainerProps = {
11
+ howTo: HowToInterface;
12
+ };
13
+
14
+ function HowToContainerInternal({ howTo }: HowToContainerProps) {
15
+ return (
16
+ <RoundPageContainer module={Modules.HowTo} details={<HowToDetails />}>
17
+ <HowToContent />
18
+ </RoundPageContainer>
19
+ );
20
+ }
21
+
22
+ export default function HowToContainer() {
23
+ const { howTo } = useHowToContext();
24
+ if (!howTo) return null;
25
+
26
+ return <HowToContainerInternal howTo={howTo} />;
27
+ }
@@ -0,0 +1,17 @@
1
+ "use client";
2
+
3
+ import { RoundPageContainer } from "../../../../components";
4
+ import { Modules } from "../../../../core";
5
+ import HowToList from "../lists/HowToList";
6
+
7
+ function HowToListContainerInternal() {
8
+ return (
9
+ <RoundPageContainer module={Modules.HowTo} fullWidth>
10
+ <HowToList fullWidth />
11
+ </RoundPageContainer>
12
+ );
13
+ }
14
+
15
+ export default function HowToListContainer() {
16
+ return <HowToListContainerInternal />;
17
+ }
@@ -0,0 +1,16 @@
1
+ "use client";
2
+
3
+ import { BlockNoteEditorContainer } from "../../../../components";
4
+ import { Card } from "../../../../shadcnui";
5
+ import { useHowToContext } from "../../contexts/HowToContext";
6
+
7
+ export default function HowToContent() {
8
+ const { howTo } = useHowToContext();
9
+ if (!howTo) return null;
10
+
11
+ return (
12
+ <Card className="flex w-full flex-col p-4">
13
+ <BlockNoteEditorContainer id={howTo.id} type="howto" initialContent={howTo.description} />
14
+ </Card>
15
+ );
16
+ }