@c-rex/components 0.3.0-build.35 → 0.3.0-build.36
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/package.json +28 -36
- package/src/article/article-action-bar.tsx +4 -1
- package/src/{check-article-lang.tsx → article/check-article-lang.tsx} +1 -1
- package/src/article/render-article-highlight.tsx +108 -0
- package/src/article/render-article.tsx +28 -0
- package/src/autocomplete.tsx +2 -2
- package/src/carousel/carousel.tsx +5 -2
- package/src/carousel/information-unit-carousel-item.tsx +1 -1
- package/src/content-unavailable.tsx +20 -0
- package/src/directoryNodes/directory-tree-context.tsx +9 -4
- package/src/documents/description-preview.tsx +14 -4
- package/src/documents/result-list-item.tsx +35 -46
- package/src/favorites/__tests__/favorites-hydration.test.tsx +245 -0
- package/src/favorites/bookmark-button.tsx +38 -20
- package/src/favorites/favorite-button.tsx +23 -24
- package/src/favorites/favorites-context.tsx +287 -0
- package/src/icons/file-icon.tsx +9 -26
- package/src/info/information-unit-metadata-grid-client.tsx +21 -21
- package/src/page-wrapper.tsx +1 -1
- package/src/renditions/html-client.tsx +8 -6
- package/src/renditions/html.tsx +3 -1
- package/src/restriction-menu/restriction-menu-item.tsx +48 -58
- package/src/restriction-menu/restriction-selection-command-menu.tsx +444 -0
- package/src/restriction-menu/restriction-selection-menu.tsx +3 -5
- package/src/restriction-menu/taxonomy-restriction-command-menu.tsx +111 -0
- package/src/restriction-menu/taxonomy-restriction-menu.tsx +1 -7
- package/src/results/filter-navbar.tsx +81 -76
- package/src/results/filter-sidebar/context.tsx +32 -0
- package/src/results/filter-sidebar/index.tsx +44 -35
- package/src/results/generic/search-results-client.tsx +5 -4
- package/src/results/generic/table-result-list.tsx +16 -16
- package/src/results/information-unit-search-results-card-list.tsx +4 -1
- package/src/results/pagination.tsx +43 -40
- package/src/search-input.tsx +4 -2
- package/src/toc/toc-browse-controls.tsx +2 -2
- package/src/toc/toc-tree-panel.tsx +19 -16
- package/src/article/article-content.tsx +0 -19
- package/src/breadcrumb.tsx +0 -124
- package/src/directoryNodes/tree-of-content.tsx +0 -68
- package/src/render-article.tsx +0 -75
- package/src/restriction-menu/restriction-menu-container.tsx +0 -4
- package/src/restriction-menu/restriction-menu.tsx +0 -4
- package/src/stores/__tests__/favorites-store.test.ts +0 -54
- package/src/stores/favorites-store.ts +0 -163
- /package/src/{render-article.module.css → article/render-article.module.css} +0 -0
package/src/search-input.tsx
CHANGED
|
@@ -5,6 +5,7 @@ import { FileCheck, FileX, Search } from "lucide-react";
|
|
|
5
5
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@c-rex/ui/tooltip";
|
|
6
6
|
import * as AutocompleteOptions from "./generated/suggestions";
|
|
7
7
|
import { useQueryState } from "nuqs";
|
|
8
|
+
import { useTranslations } from "next-intl";
|
|
8
9
|
|
|
9
10
|
type PlacedOn = "NAVBAR" | "BODY"
|
|
10
11
|
type Props = {
|
|
@@ -24,6 +25,7 @@ export const SearchInput: FC<Props> = ({
|
|
|
24
25
|
alternativeAutocompleteType,
|
|
25
26
|
alternativeOnSelectPath
|
|
26
27
|
}) => {
|
|
28
|
+
const t = useTranslations();
|
|
27
29
|
const [pkg] = useQueryState("package");
|
|
28
30
|
const [checked, setChecked] = useState<boolean>(true);
|
|
29
31
|
const [autocompleteComponentName, setAutocompleteComponentName] = useState<keyof typeof AutocompleteOptions>(autocompleteType);
|
|
@@ -61,11 +63,11 @@ export const SearchInput: FC<Props> = ({
|
|
|
61
63
|
</TooltipTrigger>
|
|
62
64
|
|
|
63
65
|
<TooltipContent>
|
|
64
|
-
|
|
66
|
+
{t("searchInDocument")}
|
|
65
67
|
</TooltipContent>
|
|
66
68
|
</Tooltip>
|
|
67
69
|
</TooltipProvider>
|
|
68
70
|
</>}
|
|
69
71
|
</>
|
|
70
72
|
);
|
|
71
|
-
};
|
|
73
|
+
};
|
|
@@ -46,7 +46,7 @@ export const TocBrowseControls = ({
|
|
|
46
46
|
>
|
|
47
47
|
<div className={cn("min-w-0", justify === "between" ? "flex-1" : "flex-none")}>
|
|
48
48
|
{previous ? (
|
|
49
|
-
<Button asChild variant="ghost"
|
|
49
|
+
<Button asChild variant="ghost" size="icon" className={buttonClassName}>
|
|
50
50
|
<Link href={previous.href} title={previous.label} className={cn("inline-flex min-w-0 items-center", buttonGapClassName)}>
|
|
51
51
|
<ChevronLeft className="h-4 w-4 shrink-0" />
|
|
52
52
|
{showLabels ? <span className="truncate">{previous.label}</span> : null}
|
|
@@ -59,7 +59,7 @@ export const TocBrowseControls = ({
|
|
|
59
59
|
|
|
60
60
|
<div className={cn("min-w-0 text-right", justify === "between" ? "flex-1" : "flex-none")}>
|
|
61
61
|
{next ? (
|
|
62
|
-
<Button asChild variant="ghost"
|
|
62
|
+
<Button asChild variant="ghost" size="icon" className={buttonClassName}>
|
|
63
63
|
<Link href={next.href} title={next.label} className={cn("inline-flex min-w-0 items-center", buttonGapClassName)}>
|
|
64
64
|
{showLabels ? <span className="truncate">{next.label}</span> : null}
|
|
65
65
|
<ChevronRight className="h-4 w-4 shrink-0" />
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useEffect, useMemo, useRef, useState, type CSSProperties, type FC, type ReactNode } from "react";
|
|
4
4
|
import Link from "next/link";
|
|
5
|
+
import { useTranslations } from "next-intl";
|
|
5
6
|
import { ChevronDown, ChevronRight } from "lucide-react";
|
|
6
7
|
import {
|
|
7
8
|
Sidebar,
|
|
@@ -35,16 +36,6 @@ const DEFAULT_TOC_WIDTH_PX = 336;
|
|
|
35
36
|
const MIN_TOC_WIDTH_PX = 260;
|
|
36
37
|
const MAX_TOC_WIDTH_PX = 640;
|
|
37
38
|
|
|
38
|
-
const loadingSkeleton = (
|
|
39
|
-
<div className="pt-4 space-y-2">
|
|
40
|
-
<Skeleton className="w-auto h-10 mb-2" />
|
|
41
|
-
<Skeleton className="w-auto h-10 mb-2" />
|
|
42
|
-
<Skeleton className="w-auto h-10 mb-2 ml-8" />
|
|
43
|
-
<Skeleton className="w-auto h-10 mb-2 ml-8" />
|
|
44
|
-
<div className="px-2 pt-1 text-xs text-muted-foreground">Inhaltsverzeichnis wird geladen...</div>
|
|
45
|
-
</div>
|
|
46
|
-
);
|
|
47
|
-
|
|
48
39
|
export const TocTreePanel: FC<TocTreePanelProps> = ({
|
|
49
40
|
lang,
|
|
50
41
|
rootNodeId,
|
|
@@ -52,13 +43,25 @@ export const TocTreePanel: FC<TocTreePanelProps> = ({
|
|
|
52
43
|
initialChildrenByParentId = {},
|
|
53
44
|
placeholder,
|
|
54
45
|
headerContent,
|
|
55
|
-
headerTitle
|
|
46
|
+
headerTitle,
|
|
56
47
|
loadChildren,
|
|
57
48
|
buildNodeHref,
|
|
58
49
|
defaultWidthPx = DEFAULT_TOC_WIDTH_PX,
|
|
59
50
|
minWidthPx = MIN_TOC_WIDTH_PX,
|
|
60
51
|
maxWidthPx = MAX_TOC_WIDTH_PX,
|
|
61
52
|
}) => {
|
|
53
|
+
const t = useTranslations();
|
|
54
|
+
const resolvedHeaderTitle = headerTitle ?? t("tableOfContent");
|
|
55
|
+
const loadingSkeleton = (
|
|
56
|
+
<div className="pt-4 space-y-2">
|
|
57
|
+
<Skeleton className="w-auto h-10 mb-2" />
|
|
58
|
+
<Skeleton className="w-auto h-10 mb-2" />
|
|
59
|
+
<Skeleton className="w-auto h-10 mb-2 ml-8" />
|
|
60
|
+
<Skeleton className="w-auto h-10 mb-2 ml-8" />
|
|
61
|
+
<div className="px-2 pt-1 text-xs text-muted-foreground">{t("loadingTableOfContents")}</div>
|
|
62
|
+
</div>
|
|
63
|
+
);
|
|
64
|
+
|
|
62
65
|
const [childrenByParentId, setChildrenByParentId] = useState<Record<string, TocNodeSummary[]>>(initialChildrenByParentId);
|
|
63
66
|
const [loadingParentIds, setLoadingParentIds] = useState<Set<string>>(new Set());
|
|
64
67
|
const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set(activePathIds));
|
|
@@ -184,7 +187,7 @@ export const TocTreePanel: FC<TocTreePanelProps> = ({
|
|
|
184
187
|
{node.hasChildren ? (
|
|
185
188
|
<button
|
|
186
189
|
type="button"
|
|
187
|
-
aria-label={isExpanded ? "
|
|
190
|
+
aria-label={isExpanded ? t("collapseSection") : t("expandSection")}
|
|
188
191
|
className="h-8 w-8 inline-flex items-center justify-center text-muted-foreground hover:text-foreground shrink-0"
|
|
189
192
|
onClick={() => toggleExpanded(node.id, node.hasChildren)}
|
|
190
193
|
>
|
|
@@ -237,8 +240,8 @@ export const TocTreePanel: FC<TocTreePanelProps> = ({
|
|
|
237
240
|
<Sidebar side="left">
|
|
238
241
|
<SidebarHeader className="border-b px-2 py-2">
|
|
239
242
|
<div className="flex min-w-0 w-full items-center justify-between gap-2">
|
|
240
|
-
<div className="min-w-0 overflow-hidden pl-9 text-sm font-medium text-foreground" title={typeof
|
|
241
|
-
<span className="truncate">{
|
|
243
|
+
<div className="min-w-0 overflow-hidden pl-9 text-sm font-medium text-foreground" title={typeof resolvedHeaderTitle === "string" ? resolvedHeaderTitle : undefined}>
|
|
244
|
+
<span className="truncate">{resolvedHeaderTitle}</span>
|
|
242
245
|
</div>
|
|
243
246
|
<div className="ml-auto flex shrink-0 items-center">
|
|
244
247
|
{headerContent}
|
|
@@ -263,8 +266,8 @@ export const TocTreePanel: FC<TocTreePanelProps> = ({
|
|
|
263
266
|
</Sidebar>
|
|
264
267
|
<button
|
|
265
268
|
type="button"
|
|
266
|
-
aria-label="
|
|
267
|
-
title="
|
|
269
|
+
aria-label={t("resizeTableOfContents")}
|
|
270
|
+
title={t("resizeTableOfContents")}
|
|
268
271
|
className="absolute inset-y-0 -right-2 z-20 hidden w-4 cursor-ew-resize lg:block"
|
|
269
272
|
onMouseDown={(event) => {
|
|
270
273
|
resizeStartRef.current = { startX: event.clientX, startWidth: tocWidthPx };
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { FC } from "react";
|
|
2
|
-
import { RenderArticle } from "../render-article";
|
|
3
|
-
|
|
4
|
-
interface Props {
|
|
5
|
-
articleHtml: string;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export const ArticleContent: FC<Props> = async ({ articleHtml }) => {
|
|
9
|
-
return (
|
|
10
|
-
<div className="relative flex flex-col gap-4 md:flex-row md:items-start">
|
|
11
|
-
<div className="relative min-w-0 flex-1 md:pr-8">
|
|
12
|
-
<RenderArticle htmlContent={articleHtml} contentLang="" />
|
|
13
|
-
</div>
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
</div>
|
|
18
|
-
)
|
|
19
|
-
}
|
package/src/breadcrumb.tsx
DELETED
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import React, { FC, Fragment, ReactNode } from "react";
|
|
4
|
-
import {
|
|
5
|
-
Breadcrumb as BreadcrumbComponent,
|
|
6
|
-
BreadcrumbEllipsis,
|
|
7
|
-
BreadcrumbItem,
|
|
8
|
-
BreadcrumbLink,
|
|
9
|
-
BreadcrumbList,
|
|
10
|
-
BreadcrumbPage,
|
|
11
|
-
BreadcrumbSeparator
|
|
12
|
-
} from "@c-rex/ui/breadcrumb";
|
|
13
|
-
import { Button } from "@c-rex/ui/button";
|
|
14
|
-
import { Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerTitle, DrawerTrigger } from "@c-rex/ui/drawer";
|
|
15
|
-
import { TreeOfContent } from "@c-rex/interfaces";
|
|
16
|
-
import { useTranslations } from 'next-intl';
|
|
17
|
-
import Link from "next/link"
|
|
18
|
-
import { useBreakpoint } from "@c-rex/ui/hooks"
|
|
19
|
-
import { DEVICE_OPTIONS } from "@c-rex/constants";
|
|
20
|
-
import { generateBreadcrumbItems } from "@c-rex/utils";
|
|
21
|
-
|
|
22
|
-
interface BreadcrumbProps {
|
|
23
|
-
tree: TreeOfContent[];
|
|
24
|
-
lang: string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export const Breadcrumb: FC<BreadcrumbProps> = ({ tree, lang }) => {
|
|
28
|
-
const t = useTranslations("breadcrumbs");
|
|
29
|
-
const items = generateBreadcrumbItems(tree);
|
|
30
|
-
const device = useBreakpoint();
|
|
31
|
-
const isMobile = device !== null && (device === DEVICE_OPTIONS.MOBILE || device === DEVICE_OPTIONS.TABLET);
|
|
32
|
-
|
|
33
|
-
if (!items) return null
|
|
34
|
-
|
|
35
|
-
const lastItem = items[items.length - 1] as TreeOfContent;
|
|
36
|
-
|
|
37
|
-
const renderDrawer = () => {
|
|
38
|
-
if (items.length < 2) return null
|
|
39
|
-
|
|
40
|
-
return (
|
|
41
|
-
<>
|
|
42
|
-
<BreadcrumbItem>
|
|
43
|
-
<Drawer>
|
|
44
|
-
<DrawerTrigger asChild>
|
|
45
|
-
<BreadcrumbEllipsis className="h-4 w-4" />
|
|
46
|
-
</DrawerTrigger>
|
|
47
|
-
<DrawerContent>
|
|
48
|
-
<DrawerHeader className="text-left">
|
|
49
|
-
<DrawerTitle>Navigate to</DrawerTitle>
|
|
50
|
-
<DrawerDescription>
|
|
51
|
-
Select a page to navigate to.
|
|
52
|
-
</DrawerDescription>
|
|
53
|
-
</DrawerHeader>
|
|
54
|
-
|
|
55
|
-
<div className="grid gap-1 px-4">
|
|
56
|
-
{items.slice(0, -1).map((item) => (
|
|
57
|
-
<Link
|
|
58
|
-
key={item.linkId}
|
|
59
|
-
href={`/topics/${item.linkId}/content`}
|
|
60
|
-
className="py-1 text-sm"
|
|
61
|
-
>
|
|
62
|
-
{item.label}
|
|
63
|
-
</Link>
|
|
64
|
-
))}
|
|
65
|
-
</div>
|
|
66
|
-
|
|
67
|
-
<DrawerFooter className="pt-4">
|
|
68
|
-
<DrawerClose asChild>
|
|
69
|
-
<Button variant="outline">Close</Button>
|
|
70
|
-
</DrawerClose>
|
|
71
|
-
</DrawerFooter>
|
|
72
|
-
</DrawerContent>
|
|
73
|
-
</Drawer>
|
|
74
|
-
</BreadcrumbItem>
|
|
75
|
-
<BreadcrumbSeparator />
|
|
76
|
-
</>
|
|
77
|
-
|
|
78
|
-
);
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
const renderLink = (showLink: boolean, item: TreeOfContent): ReactNode => {
|
|
82
|
-
if (showLink) {
|
|
83
|
-
return <BreadcrumbLink href={`/topics/${item.linkId}/content`}>{item.label}</BreadcrumbLink>;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
return <BreadcrumbPage>{item.label}</BreadcrumbPage>;
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
return (
|
|
90
|
-
<BreadcrumbComponent lang={lang} className="hidden sm:block">
|
|
91
|
-
<BreadcrumbList>
|
|
92
|
-
<BreadcrumbItem>
|
|
93
|
-
<BreadcrumbLink asChild>
|
|
94
|
-
<BreadcrumbLink href="/">{t("home")}</BreadcrumbLink>
|
|
95
|
-
</BreadcrumbLink>
|
|
96
|
-
</BreadcrumbItem>
|
|
97
|
-
<BreadcrumbSeparator />
|
|
98
|
-
|
|
99
|
-
{isMobile ? renderDrawer() : (
|
|
100
|
-
<>
|
|
101
|
-
{items.slice(0, -1).map((item, index) => {
|
|
102
|
-
const isLast = index === items.length - 1;
|
|
103
|
-
|
|
104
|
-
return (
|
|
105
|
-
<Fragment key={`${item.label}-fragment`}>
|
|
106
|
-
<BreadcrumbItem key={`${item.label}-item`}>
|
|
107
|
-
{renderLink(!isLast, item)}
|
|
108
|
-
</BreadcrumbItem>
|
|
109
|
-
{!isLast && (
|
|
110
|
-
<BreadcrumbSeparator key={`${item.label}-separator`} />
|
|
111
|
-
)}
|
|
112
|
-
</Fragment>
|
|
113
|
-
);
|
|
114
|
-
})}
|
|
115
|
-
</>
|
|
116
|
-
)}
|
|
117
|
-
|
|
118
|
-
<BreadcrumbPage>
|
|
119
|
-
{renderLink(false, lastItem)}
|
|
120
|
-
</BreadcrumbPage>
|
|
121
|
-
</BreadcrumbList>
|
|
122
|
-
</BreadcrumbComponent>
|
|
123
|
-
);
|
|
124
|
-
};
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
SidebarGroup,
|
|
3
|
-
SidebarMenu,
|
|
4
|
-
SidebarMenuButton,
|
|
5
|
-
SidebarMenuItem,
|
|
6
|
-
SidebarMenuSub,
|
|
7
|
-
SidebarMenuSubButton,
|
|
8
|
-
SidebarMenuSubItem,
|
|
9
|
-
} from "@c-rex/ui/sidebar";
|
|
10
|
-
import { DirectoryNodeModel, TreeOfContent as TreeOfContentModel } from "@c-rex/interfaces";
|
|
11
|
-
import { generateTreeOfContent } from "@c-rex/utils/directoryNodes";
|
|
12
|
-
import Link from "next/link";
|
|
13
|
-
import { Suspense } from "react";
|
|
14
|
-
import { Skeleton } from "@c-rex/ui/skeleton";
|
|
15
|
-
|
|
16
|
-
interface SidebarProps {
|
|
17
|
-
directoryNodes?: DirectoryNodeModel[];
|
|
18
|
-
tree?: TreeOfContentModel[];
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export async function TreeOfContent({ directoryNodes, tree: providedTree }: SidebarProps) {
|
|
22
|
-
const tree = providedTree ?? (directoryNodes ? await generateTreeOfContent(directoryNodes) : []);
|
|
23
|
-
|
|
24
|
-
return (
|
|
25
|
-
<Suspense
|
|
26
|
-
fallback={
|
|
27
|
-
<SidebarGroup className="pt-4">
|
|
28
|
-
<Skeleton className="w-auto h-10 mb-2" />
|
|
29
|
-
<Skeleton className="w-auto h-10 mb-2" />
|
|
30
|
-
<Skeleton className="w-auto h-10 mb-2 ml-8" />
|
|
31
|
-
<Skeleton className="w-auto h-10 mb-2 ml-8" />
|
|
32
|
-
<Skeleton className="w-auto h-10 mb-2 ml-8" />
|
|
33
|
-
<Skeleton className="w-auto h-10 mb-2 ml-8" />
|
|
34
|
-
<Skeleton className="w-auto h-10 mb-2" />
|
|
35
|
-
</SidebarGroup>
|
|
36
|
-
}
|
|
37
|
-
>
|
|
38
|
-
<SidebarMenu>
|
|
39
|
-
{tree.map((item) => (
|
|
40
|
-
<SidebarMenuItem key={item.id}>
|
|
41
|
-
<SidebarMenuButton asChild isActive={item.active}>
|
|
42
|
-
<Link prefetch={false} href={`/topics/${item.linkId}/pages`} title={item.label}>
|
|
43
|
-
{item.label}
|
|
44
|
-
</Link>
|
|
45
|
-
</SidebarMenuButton>
|
|
46
|
-
|
|
47
|
-
{item.children?.length ? (
|
|
48
|
-
<SidebarMenuSub>
|
|
49
|
-
{item.children.map((item) => (
|
|
50
|
-
<SidebarMenuSubItem key={item.label}>
|
|
51
|
-
<SidebarMenuSubButton
|
|
52
|
-
asChild
|
|
53
|
-
isActive={item.active}
|
|
54
|
-
>
|
|
55
|
-
<Link prefetch={false} href={`/topics/${item.linkId}/pages`} title={item.label}>
|
|
56
|
-
{item.label}
|
|
57
|
-
</Link>
|
|
58
|
-
</SidebarMenuSubButton>
|
|
59
|
-
</SidebarMenuSubItem>
|
|
60
|
-
))}
|
|
61
|
-
</SidebarMenuSub>
|
|
62
|
-
) : null}
|
|
63
|
-
</SidebarMenuItem>
|
|
64
|
-
))}
|
|
65
|
-
</SidebarMenu>
|
|
66
|
-
</Suspense>
|
|
67
|
-
);
|
|
68
|
-
}
|
package/src/render-article.tsx
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { useMemo, useRef, useEffect } from "react";
|
|
4
|
-
import parse from "html-react-parser";
|
|
5
|
-
import { useQueryState } from "nuqs";
|
|
6
|
-
import { useHighlight } from "@c-rex/contexts/highlight-provider";
|
|
7
|
-
import { useHighlightStore } from "./stores/highlight-store";
|
|
8
|
-
import styles from "./render-article.module.css";
|
|
9
|
-
|
|
10
|
-
type Props = {
|
|
11
|
-
htmlContent: string;
|
|
12
|
-
contentLang?: string;
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
export const RenderArticle = ({ htmlContent, contentLang }: Props) => {
|
|
16
|
-
const [query] = useQueryState("q");
|
|
17
|
-
const containerRef = useRef<HTMLElement | null>(null);
|
|
18
|
-
const enableHighlight = useHighlightStore((state) => state.enable);
|
|
19
|
-
const { registerContainer } = useHighlight();
|
|
20
|
-
|
|
21
|
-
const highlightedContent = useMemo(() => {
|
|
22
|
-
if (!enableHighlight) return parse(htmlContent);
|
|
23
|
-
if (!query) return parse(htmlContent);
|
|
24
|
-
|
|
25
|
-
const terms = query
|
|
26
|
-
.split(/[+ ]/)
|
|
27
|
-
.map((t) => t.trim())
|
|
28
|
-
.filter(Boolean);
|
|
29
|
-
|
|
30
|
-
if (terms.length === 0) return parse(htmlContent);
|
|
31
|
-
|
|
32
|
-
const escaped = terms.map((t) =>
|
|
33
|
-
t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
34
|
-
);
|
|
35
|
-
const regex = new RegExp(`(${escaped.join("|")})`, "gi");
|
|
36
|
-
|
|
37
|
-
return parse(htmlContent, {
|
|
38
|
-
replace: (domNode: any) => {
|
|
39
|
-
if (domNode.type === "text") {
|
|
40
|
-
const parts = domNode.data.split(regex);
|
|
41
|
-
if (parts.length === 1) return;
|
|
42
|
-
return (
|
|
43
|
-
<>
|
|
44
|
-
{parts.map((part: any, i: any) =>
|
|
45
|
-
regex.test(part) ? (
|
|
46
|
-
<mark key={i} className="bg-yellow-200">
|
|
47
|
-
{part}
|
|
48
|
-
</mark>
|
|
49
|
-
) : (
|
|
50
|
-
part
|
|
51
|
-
)
|
|
52
|
-
)}
|
|
53
|
-
</>
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
},
|
|
57
|
-
});
|
|
58
|
-
}, [htmlContent, query, enableHighlight]);
|
|
59
|
-
|
|
60
|
-
useEffect(() => {
|
|
61
|
-
registerContainer(containerRef.current);
|
|
62
|
-
}, [highlightedContent, registerContainer]);
|
|
63
|
-
|
|
64
|
-
return (
|
|
65
|
-
<main
|
|
66
|
-
ref={containerRef}
|
|
67
|
-
id="ids-content"
|
|
68
|
-
data-content-scope="dita"
|
|
69
|
-
lang={contentLang}
|
|
70
|
-
className={`ids-content ids-content--dita-ot ${styles.idsContent}`}
|
|
71
|
-
>
|
|
72
|
-
{highlightedContent}
|
|
73
|
-
</main>
|
|
74
|
-
);
|
|
75
|
-
};
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { useFavoritesStore } from "../favorites-store";
|
|
2
|
-
|
|
3
|
-
describe("favorites-store", () => {
|
|
4
|
-
beforeEach(() => {
|
|
5
|
-
useFavoritesStore.setState({
|
|
6
|
-
favorites: [],
|
|
7
|
-
documents: {},
|
|
8
|
-
});
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
it("adds a document and its first topic without duplicating favorites", () => {
|
|
12
|
-
const { favoriteDocument, favoriteTopic } = useFavoritesStore.getState();
|
|
13
|
-
|
|
14
|
-
favoriteDocument("DOC-1", "Document 1");
|
|
15
|
-
favoriteTopic("DOC-1", "TOPIC-1", "Topic 1", "red");
|
|
16
|
-
|
|
17
|
-
const state = useFavoritesStore.getState();
|
|
18
|
-
|
|
19
|
-
expect(state.favorites).toHaveLength(2);
|
|
20
|
-
expect(state.favorites.map((item) => item.id).sort()).toEqual(["DOC-1", "TOPIC-1"]);
|
|
21
|
-
expect(state.documents["DOC-1"]).toEqual({
|
|
22
|
-
label: "Document 1",
|
|
23
|
-
topics: [{ id: "TOPIC-1", label: "Topic 1", color: "red" }],
|
|
24
|
-
});
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it("does not duplicate a document or topic when favorited multiple times", () => {
|
|
28
|
-
const { favoriteDocument, favoriteTopic } = useFavoritesStore.getState();
|
|
29
|
-
|
|
30
|
-
favoriteDocument("DOC-1", "Document 1");
|
|
31
|
-
favoriteDocument("DOC-1", "Document 1");
|
|
32
|
-
favoriteTopic("DOC-1", "TOPIC-1", "Topic 1", "red");
|
|
33
|
-
favoriteTopic("DOC-1", "TOPIC-1", "Topic 1", "red");
|
|
34
|
-
|
|
35
|
-
const state = useFavoritesStore.getState();
|
|
36
|
-
|
|
37
|
-
expect(state.favorites).toHaveLength(2);
|
|
38
|
-
expect(state.documents["DOC-1"]?.topics).toHaveLength(1);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it("does not duplicate the document marker when a topic is favorited after the document", () => {
|
|
42
|
-
const { favoriteDocument, favoriteTopic } = useFavoritesStore.getState();
|
|
43
|
-
|
|
44
|
-
favoriteDocument("DOC-1", "Document 1");
|
|
45
|
-
favoriteTopic("DOC-1", "TOPIC-1", "Topic 1", "red");
|
|
46
|
-
favoriteTopic("DOC-1", "TOPIC-2", "Topic 2", "blue");
|
|
47
|
-
|
|
48
|
-
const state = useFavoritesStore.getState();
|
|
49
|
-
const documentEntries = state.favorites.filter((item) => item.id === "DOC-1");
|
|
50
|
-
|
|
51
|
-
expect(documentEntries).toHaveLength(1);
|
|
52
|
-
expect(state.documents["DOC-1"]?.topics).toHaveLength(2);
|
|
53
|
-
});
|
|
54
|
-
});
|
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
import { Favorite } from "@c-rex/types";
|
|
2
|
-
import { create } from "zustand";
|
|
3
|
-
import { persist } from "zustand/middleware";
|
|
4
|
-
|
|
5
|
-
type FavoriteDocumentState = { topics: Favorite[], label?: string };
|
|
6
|
-
type FavoriteDocumentsMap = Record<string, FavoriteDocumentState>;
|
|
7
|
-
|
|
8
|
-
type FavoritesStore = {
|
|
9
|
-
favorites: Favorite[];
|
|
10
|
-
documents: FavoriteDocumentsMap;
|
|
11
|
-
favoriteTopic: (documentId: string, id: string, label: string, color: string) => void;
|
|
12
|
-
unfavoriteTopic: (documentId: string, id: string) => void;
|
|
13
|
-
favoriteDocument: (id: string, label: string) => void;
|
|
14
|
-
unfavoriteDocument: (id: string) => void;
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
export const useFavoritesStore = create<FavoritesStore>()(
|
|
18
|
-
persist((set) => ({
|
|
19
|
-
documents: {},
|
|
20
|
-
favorites: [],
|
|
21
|
-
favoriteTopic: (documentId: string, id: string, label: string, color: string) =>
|
|
22
|
-
set((state) => {
|
|
23
|
-
const documents = favoriteTopic(state.documents, documentId, id, label, color);
|
|
24
|
-
const favorites = upsertFavorites(state.favorites, [
|
|
25
|
-
{ id, label, color },
|
|
26
|
-
{ id: documentId, label: state.documents[documentId]?.label || "", color: "" },
|
|
27
|
-
]);
|
|
28
|
-
|
|
29
|
-
if (documents === state.documents && favorites === state.favorites) {
|
|
30
|
-
return state;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
return { documents, favorites };
|
|
34
|
-
}),
|
|
35
|
-
unfavoriteTopic: (documentId: string, id: string) =>
|
|
36
|
-
set((state) => ({
|
|
37
|
-
documents: unfavoriteTopic(state.documents, documentId, id),
|
|
38
|
-
favorites: state.favorites.filter((topic) => topic.id !== id),
|
|
39
|
-
})),
|
|
40
|
-
favoriteDocument: (id: string, label: string) =>
|
|
41
|
-
set((state) => {
|
|
42
|
-
const documents = upsertDocument(state.documents, id, label);
|
|
43
|
-
const favorites = upsertFavorites(state.favorites, [{ id, label, color: "" }]);
|
|
44
|
-
|
|
45
|
-
if (documents === state.documents && favorites === state.favorites) {
|
|
46
|
-
return state;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
return {
|
|
50
|
-
documents,
|
|
51
|
-
favorites,
|
|
52
|
-
};
|
|
53
|
-
}),
|
|
54
|
-
unfavoriteDocument: (id: string) =>
|
|
55
|
-
set((state) => {
|
|
56
|
-
const documentsCopy = { ...state.documents };
|
|
57
|
-
if (!documentsCopy[id]) {
|
|
58
|
-
return state;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const favoritesToRemove = documentsCopy[id]?.topics.map(topic => topic.id) || [];
|
|
62
|
-
const newFavorites = state.favorites.filter(fav => fav.id !== id && !favoritesToRemove.includes(fav.id));
|
|
63
|
-
delete documentsCopy[id];
|
|
64
|
-
return { documents: documentsCopy, favorites: newFavorites };
|
|
65
|
-
}),
|
|
66
|
-
}), {
|
|
67
|
-
name: "c-rex-favorites",
|
|
68
|
-
})
|
|
69
|
-
);
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const favoriteTopic = (documents: FavoriteDocumentsMap, documentId: string, id: string, label: string, color: string): FavoriteDocumentsMap => {
|
|
73
|
-
const currentDocument = documents[documentId];
|
|
74
|
-
const currentTopics = currentDocument?.topics ?? [];
|
|
75
|
-
if (currentTopics.some((topic) => topic.id === id)) {
|
|
76
|
-
return documents;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
return {
|
|
80
|
-
...documents,
|
|
81
|
-
[documentId]: {
|
|
82
|
-
...currentDocument,
|
|
83
|
-
topics: [...currentTopics, { id, label, color }],
|
|
84
|
-
},
|
|
85
|
-
};
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
const unfavoriteTopic = (documents: FavoriteDocumentsMap, documentId: string, id: string): FavoriteDocumentsMap => {
|
|
89
|
-
const currentDocument = documents[documentId];
|
|
90
|
-
if (!currentDocument) {
|
|
91
|
-
return documents;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
return {
|
|
95
|
-
...documents,
|
|
96
|
-
[documentId]: {
|
|
97
|
-
...currentDocument,
|
|
98
|
-
topics: currentDocument.topics.filter(topic => topic.id !== id),
|
|
99
|
-
},
|
|
100
|
-
};
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
const upsertDocument = (documents: FavoriteDocumentsMap, documentId: string, label: string): FavoriteDocumentsMap => {
|
|
104
|
-
const currentDocument = documents[documentId];
|
|
105
|
-
|
|
106
|
-
if (!currentDocument) {
|
|
107
|
-
return {
|
|
108
|
-
...documents,
|
|
109
|
-
[documentId]: { topics: [], label },
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
if (currentDocument.label === label) {
|
|
114
|
-
return documents;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
return {
|
|
118
|
-
...documents,
|
|
119
|
-
[documentId]: {
|
|
120
|
-
...currentDocument,
|
|
121
|
-
label,
|
|
122
|
-
},
|
|
123
|
-
};
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
const upsertFavorites = (favorites: Favorite[], entries: Favorite[]): Favorite[] => {
|
|
127
|
-
let nextFavorites = favorites;
|
|
128
|
-
|
|
129
|
-
for (const entry of entries) {
|
|
130
|
-
nextFavorites = upsertFavorite(nextFavorites, entry);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
return nextFavorites;
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
const upsertFavorite = (favorites: Favorite[], entry: Favorite): Favorite[] => {
|
|
137
|
-
const index = favorites.findIndex((favorite) => favorite.id === entry.id);
|
|
138
|
-
if (index === -1) {
|
|
139
|
-
return [...favorites, entry];
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const current = favorites[index];
|
|
143
|
-
if (!current) {
|
|
144
|
-
return [...favorites, entry];
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
const next = {
|
|
148
|
-
...current,
|
|
149
|
-
label: current.label || entry.label,
|
|
150
|
-
color: current.color || entry.color,
|
|
151
|
-
};
|
|
152
|
-
|
|
153
|
-
if (
|
|
154
|
-
current.label === next.label &&
|
|
155
|
-
current.color === next.color
|
|
156
|
-
) {
|
|
157
|
-
return favorites;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const copy = [...favorites];
|
|
161
|
-
copy[index] = next;
|
|
162
|
-
return copy;
|
|
163
|
-
};
|
|
File without changes
|