@c-rex/components 0.3.0-build.36 → 0.3.0-build.39
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 +6 -2
- package/src/article/article-action-bar.tsx +9 -2
- package/src/autocomplete.tsx +5 -23
- package/src/blog/blog-author-card.tsx +116 -0
- package/src/documents/result-list-item.tsx +6 -1
- package/src/navbar/navbar.tsx +16 -30
- package/src/navbar/settings.tsx +1 -1
- package/src/page-wrapper.tsx +3 -3
- package/src/restriction-menu/restriction-menu-item.tsx +8 -2
- package/src/restriction-menu/restriction-selection-command-menu.tsx +6 -11
- package/src/restriction-menu/restriction-selection-menu.tsx +13 -4
- package/src/restriction-menu/taxonomy-restriction-command-menu.tsx +1 -4
- package/src/restriction-menu/taxonomy-restriction-menu.tsx +24 -5
- package/src/results/filter-sidebar/index.tsx +0 -4
- package/src/results/information-unit-search-results-cards.tsx +169 -69
- package/src/toc/toc-breadcrumb.tsx +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@c-rex/components",
|
|
3
|
-
"version": "0.3.0-build.
|
|
3
|
+
"version": "0.3.0-build.39",
|
|
4
4
|
"files": [
|
|
5
5
|
"src"
|
|
6
6
|
],
|
|
@@ -232,7 +232,11 @@
|
|
|
232
232
|
"./taxonomy-restriction-command-menu": {
|
|
233
233
|
"types": "./src/restriction-menu/taxonomy-restriction-command-menu.tsx",
|
|
234
234
|
"import": "./src/restriction-menu/taxonomy-restriction-command-menu.tsx"
|
|
235
|
-
}
|
|
235
|
+
},
|
|
236
|
+
"./blog-author-card": {
|
|
237
|
+
"types": "./src/blog/blog-author-card.tsx",
|
|
238
|
+
"import": "./src/blog/blog-author-card.tsx"
|
|
239
|
+
}
|
|
236
240
|
},
|
|
237
241
|
"scripts": {
|
|
238
242
|
"storybook": "storybook dev -p 6006",
|
|
@@ -15,8 +15,10 @@ type Props = {
|
|
|
15
15
|
id: string;
|
|
16
16
|
articleType: ResultTypes;
|
|
17
17
|
favoriteLabel: string;
|
|
18
|
+
className?: string;
|
|
19
|
+
children?: React.ReactNode;
|
|
18
20
|
}
|
|
19
|
-
export const ArticleActionBar: FC<Props> = ({ id, articleType, favoriteLabel }) => {
|
|
21
|
+
export const ArticleActionBar: FC<Props> = ({ id, articleType, favoriteLabel, className, children }) => {
|
|
20
22
|
const t = useTranslations();
|
|
21
23
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
22
24
|
const { next } = useHighlight();
|
|
@@ -32,7 +34,10 @@ export const ArticleActionBar: FC<Props> = ({ id, articleType, favoriteLabel })
|
|
|
32
34
|
}, [open]);
|
|
33
35
|
|
|
34
36
|
return (
|
|
35
|
-
<div className=
|
|
37
|
+
<div className={cn(
|
|
38
|
+
"w-9 flex gap-2 transition-all duration-300 z-20 items-end flex-col sticky top-32 self-start",
|
|
39
|
+
className
|
|
40
|
+
)}>
|
|
36
41
|
|
|
37
42
|
<SidebarTrigger side="right" />
|
|
38
43
|
|
|
@@ -101,6 +106,8 @@ export const ArticleActionBar: FC<Props> = ({ id, articleType, favoriteLabel })
|
|
|
101
106
|
|
|
102
107
|
</>
|
|
103
108
|
)}
|
|
109
|
+
|
|
110
|
+
{children}
|
|
104
111
|
</div>
|
|
105
112
|
)
|
|
106
113
|
}
|
package/src/autocomplete.tsx
CHANGED
|
@@ -101,27 +101,6 @@ export const AutoComplete = ({
|
|
|
101
101
|
});
|
|
102
102
|
};
|
|
103
103
|
|
|
104
|
-
const clearSearch = () => {
|
|
105
|
-
setQuery("");
|
|
106
|
-
setOpen(false);
|
|
107
|
-
const nextParams = new URLSearchParams(keepParams ? searchParams.toString() : "");
|
|
108
|
-
nextParams.set("page", "1");
|
|
109
|
-
nextParams.delete("search");
|
|
110
|
-
const queryString = nextParams.toString();
|
|
111
|
-
|
|
112
|
-
onSelectParams?.forEach(param => {
|
|
113
|
-
nextParams.set(param.key, param.value);
|
|
114
|
-
});
|
|
115
|
-
const targetUrl = queryString ? `${onSelectPath}?${queryString}` : onSelectPath;
|
|
116
|
-
const currentUrl = `${pathname}${searchParams.toString().length > 0 ? `?${searchParams.toString()}` : ""}`;
|
|
117
|
-
if (targetUrl === currentUrl) return;
|
|
118
|
-
|
|
119
|
-
startSearchNavigation();
|
|
120
|
-
startNavigation(() => {
|
|
121
|
-
router.push(targetUrl);
|
|
122
|
-
});
|
|
123
|
-
};
|
|
124
|
-
|
|
125
104
|
useEffect(() => setQuery(initialValue), [initialValue]);
|
|
126
105
|
useEffect(() => {
|
|
127
106
|
stopSearchNavigation();
|
|
@@ -199,6 +178,7 @@ export const AutoComplete = ({
|
|
|
199
178
|
setQuery(e.target.value);
|
|
200
179
|
setOpen(true);
|
|
201
180
|
}}
|
|
181
|
+
onFocus={() => setOpen(true)}
|
|
202
182
|
/>
|
|
203
183
|
{isNavigating || searchNavigationPending ? (
|
|
204
184
|
<InputGroupAddon align="inline-end">
|
|
@@ -210,7 +190,7 @@ export const AutoComplete = ({
|
|
|
210
190
|
size="icon-xs"
|
|
211
191
|
variant="ghost"
|
|
212
192
|
aria-label={t("clearSearch")}
|
|
213
|
-
onClick={
|
|
193
|
+
onClick={() => setQuery("")}
|
|
214
194
|
>
|
|
215
195
|
<X className="size-3" />
|
|
216
196
|
</InputGroupButton>
|
|
@@ -221,7 +201,9 @@ export const AutoComplete = ({
|
|
|
221
201
|
|
|
222
202
|
{open && (
|
|
223
203
|
<ul className="suggestions-list absolute z-10 w-full bg-white border border-gray-300 rounded-lg shadow-lg max-h-60 overflow-y-auto">
|
|
224
|
-
{
|
|
204
|
+
{query.length === 0 ? (
|
|
205
|
+
<li className="px-4 py-2 text-sm text-muted-foreground">{t("typingHint")}</li>
|
|
206
|
+
) : loading ? (
|
|
225
207
|
<li>
|
|
226
208
|
<div className="flex items-center justify-center py-4">
|
|
227
209
|
<div className="animate-spin rounded-full h-6 w-6 border-2 border-gray-300 border-t-gray-950" />
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { User, Mail, Link as LinkIcon } from "lucide-react";
|
|
2
|
+
import Link from "next/link";
|
|
3
|
+
import { cn } from "@c-rex/utils";
|
|
4
|
+
import { Card, CardContent, CardHeader, CardTitle } from "@c-rex/ui/card";
|
|
5
|
+
|
|
6
|
+
export type BlogAuthor = {
|
|
7
|
+
name: string;
|
|
8
|
+
photo: string | null;
|
|
9
|
+
title?: string | null;
|
|
10
|
+
role?: string | null;
|
|
11
|
+
emails?: string[];
|
|
12
|
+
urls?: string[];
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type Props = {
|
|
16
|
+
authors: BlogAuthor[];
|
|
17
|
+
label: string;
|
|
18
|
+
embedded?: boolean;
|
|
19
|
+
fields?: Array<keyof BlogAuthor>;
|
|
20
|
+
className?: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const DEFAULT_FIELDS: Array<keyof BlogAuthor> = ["photo", "name", "title", "role"];
|
|
24
|
+
|
|
25
|
+
const AuthorAvatar = ({ author }: { author: BlogAuthor }) => (
|
|
26
|
+
author.photo ? (
|
|
27
|
+
<img
|
|
28
|
+
src={author.photo}
|
|
29
|
+
alt={author.name}
|
|
30
|
+
className="size-10 rounded-full object-cover shrink-0"
|
|
31
|
+
/>
|
|
32
|
+
) : (
|
|
33
|
+
<div className="size-10 rounded-full bg-muted shrink-0 flex items-center justify-center">
|
|
34
|
+
<User className="size-6" />
|
|
35
|
+
</div>
|
|
36
|
+
)
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const AuthorEntry = ({ author, fields }: { author: BlogAuthor; fields: Array<keyof BlogAuthor> }) => {
|
|
40
|
+
const showPhoto = fields.includes("photo");
|
|
41
|
+
const showName = fields.includes("name");
|
|
42
|
+
const showTitle = fields.includes("title");
|
|
43
|
+
const showRole = fields.includes("role");
|
|
44
|
+
const showEmails = fields.includes("emails");
|
|
45
|
+
const showUrls = fields.includes("urls");
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div className="flex items-start gap-3">
|
|
49
|
+
{showPhoto && <AuthorAvatar author={author} />}
|
|
50
|
+
<div className="flex flex-col gap-0.5 min-w-0">
|
|
51
|
+
{showName && (
|
|
52
|
+
<span className="text-sm font-medium truncate">{author.name}</span>
|
|
53
|
+
)}
|
|
54
|
+
{showTitle && author.title && (
|
|
55
|
+
<span className="text-xs text-muted-foreground truncate">{author.title}</span>
|
|
56
|
+
)}
|
|
57
|
+
{showRole && author.role && (
|
|
58
|
+
<span className="text-xs text-muted-foreground truncate">{author.role}</span>
|
|
59
|
+
)}
|
|
60
|
+
{showEmails && author.emails && author.emails.length > 0 && (
|
|
61
|
+
<div className="flex flex-col gap-0.5 mt-1">
|
|
62
|
+
{author.emails.map((email) => (
|
|
63
|
+
<Link
|
|
64
|
+
key={email}
|
|
65
|
+
href={`mailto:${email}`}
|
|
66
|
+
className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground"
|
|
67
|
+
>
|
|
68
|
+
<Mail className="size-3 shrink-0" />
|
|
69
|
+
<span className="truncate">{email}</span>
|
|
70
|
+
</Link>
|
|
71
|
+
))}
|
|
72
|
+
</div>
|
|
73
|
+
)}
|
|
74
|
+
{showUrls && author.urls && author.urls.length > 0 && (
|
|
75
|
+
<div className="flex flex-col gap-0.5 mt-1">
|
|
76
|
+
{author.urls.map((url) => (
|
|
77
|
+
<Link
|
|
78
|
+
key={url}
|
|
79
|
+
href={url}
|
|
80
|
+
target="_blank"
|
|
81
|
+
rel="noopener noreferrer"
|
|
82
|
+
className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground"
|
|
83
|
+
>
|
|
84
|
+
<LinkIcon className="size-3 shrink-0" />
|
|
85
|
+
<span className="truncate">{url}</span>
|
|
86
|
+
</Link>
|
|
87
|
+
))}
|
|
88
|
+
</div>
|
|
89
|
+
)}
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export const BlogAuthorCard = ({ authors, label, embedded = false, fields = DEFAULT_FIELDS, className }: Props) => {
|
|
96
|
+
if (authors.length === 0) return null;
|
|
97
|
+
|
|
98
|
+
const content = (
|
|
99
|
+
<CardContent className="space-y-3 pb-4">
|
|
100
|
+
{authors.map((author, i) => (
|
|
101
|
+
<AuthorEntry key={i} author={author} fields={fields} />
|
|
102
|
+
))}
|
|
103
|
+
</CardContent>
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
if (embedded) return content;
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<Card className={cn("gap-0", className)}>
|
|
110
|
+
<CardHeader>
|
|
111
|
+
<CardTitle className="text-lg">{label}</CardTitle>
|
|
112
|
+
</CardHeader>
|
|
113
|
+
{content}
|
|
114
|
+
</Card>
|
|
115
|
+
);
|
|
116
|
+
};
|
|
@@ -83,7 +83,12 @@ const DocumentResultListRowContent: FC<RowContentProps> = ({
|
|
|
83
83
|
</span>
|
|
84
84
|
|
|
85
85
|
<span className="text-lg font-medium">
|
|
86
|
-
<
|
|
86
|
+
<Link
|
|
87
|
+
className="hover:underline [overflow-wrap:anywhere] hyphens-auto"
|
|
88
|
+
href={itemLink}
|
|
89
|
+
>
|
|
90
|
+
{title}
|
|
91
|
+
</Link>
|
|
87
92
|
</span>
|
|
88
93
|
|
|
89
94
|
<div>
|
package/src/navbar/navbar.tsx
CHANGED
|
@@ -11,7 +11,7 @@ import { cn } from "@c-rex/utils";
|
|
|
11
11
|
import { getTranslations } from "next-intl/server";
|
|
12
12
|
import { Button } from "@c-rex/ui/button";
|
|
13
13
|
import { DropdownHoverItem } from "@c-rex/ui/dropdown-hover-item";
|
|
14
|
-
import { Menu } from "lucide-react";
|
|
14
|
+
import { Menu, Search } from "lucide-react";
|
|
15
15
|
import { getOrganizationBranding } from "@c-rex/services/vcard";
|
|
16
16
|
|
|
17
17
|
type NavBarProps = {
|
|
@@ -53,14 +53,9 @@ export const NavBar: FC<NavBarProps> = async ({
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
return (
|
|
56
|
-
<header className="sticky flex flex-col top-0 z-40 w-full
|
|
57
|
-
<div className="w-full flex items-center justify-between gap-2">
|
|
58
|
-
<div
|
|
59
|
-
className={cn(
|
|
60
|
-
"flex items-center gap-4",
|
|
61
|
-
title && "lg:w-[calc(16rem-16px)]"
|
|
62
|
-
)}
|
|
63
|
-
>
|
|
56
|
+
<header className="sticky flex flex-col top-0 z-40 w-full backdrop-blur-xl transition-all bg-transparent border-b">
|
|
57
|
+
<div className="w-full flex items-center justify-between gap-2 p-4">
|
|
58
|
+
<div className="flex items-center gap-4">
|
|
64
59
|
{showMenu && (
|
|
65
60
|
<DropdownHoverItem
|
|
66
61
|
label={
|
|
@@ -107,28 +102,22 @@ export const NavBar: FC<NavBarProps> = async ({
|
|
|
107
102
|
<img
|
|
108
103
|
src={organizationBranding.logoSrc}
|
|
109
104
|
alt={`${organizationBranding.organizationName} logo`}
|
|
110
|
-
className="h-
|
|
105
|
+
className="h-10"
|
|
111
106
|
/>
|
|
112
107
|
</Link>
|
|
113
108
|
)}
|
|
114
109
|
</div>
|
|
115
110
|
|
|
116
|
-
{title && (
|
|
117
|
-
<div className="flex-1 hidden md:flex md:justify-center lg:justify-start">
|
|
118
|
-
<h1 className="md:text-2xl lg:text-3xl font-bold tracking-tight text-balance">{title}</h1>
|
|
119
|
-
</div>
|
|
120
|
-
)}
|
|
121
|
-
|
|
122
111
|
<div className="flex gap-2">
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
<Button rounded="full" size="sm" className="w-8" variant="ghost">
|
|
115
|
+
<Search className="!size-5" />
|
|
116
|
+
</Button>
|
|
117
|
+
|
|
118
|
+
{clientConfigs.languageSwitcher.enabled && (
|
|
119
|
+
<SettingsMenu />
|
|
120
|
+
)}
|
|
132
121
|
|
|
133
122
|
{clientConfigs.OIDC.userEnabled && (
|
|
134
123
|
<>
|
|
@@ -140,15 +129,12 @@ export const NavBar: FC<NavBarProps> = async ({
|
|
|
140
129
|
</>
|
|
141
130
|
)}
|
|
142
131
|
|
|
143
|
-
{clientConfigs.languageSwitcher.enabled && (
|
|
144
|
-
<SettingsMenu />
|
|
145
|
-
)}
|
|
146
132
|
</div>
|
|
147
133
|
</div>
|
|
148
134
|
|
|
149
135
|
{title && (
|
|
150
|
-
<div className="flex-1 flex justify-center
|
|
151
|
-
<h1 className="text-2xl font-bold tracking-tight text-balance">{title}</h1>
|
|
136
|
+
<div className="flex-1 flex justify-center border-t py-2">
|
|
137
|
+
<h1 className="text-2xl font-bold tracking-tight text-balance text-center">{title}</h1>
|
|
152
138
|
</div>
|
|
153
139
|
)}
|
|
154
140
|
</header>
|
package/src/navbar/settings.tsx
CHANGED
|
@@ -25,7 +25,7 @@ export const SettingsMenu: FC = () => {
|
|
|
25
25
|
return (
|
|
26
26
|
<DropdownMenu>
|
|
27
27
|
<DropdownMenuTrigger asChild>
|
|
28
|
-
<Button
|
|
28
|
+
<Button rounded="full" size="sm" className="w-8" variant="ghost">
|
|
29
29
|
<Settings className="!size-5" />
|
|
30
30
|
</Button>
|
|
31
31
|
|
package/src/page-wrapper.tsx
CHANGED
|
@@ -22,7 +22,7 @@ export const PageWrapper = ({
|
|
|
22
22
|
renderRestrictionMenu,
|
|
23
23
|
restrictField,
|
|
24
24
|
requestType,
|
|
25
|
-
|
|
25
|
+
itemsByRow,
|
|
26
26
|
onlyUsedEntries = true,
|
|
27
27
|
enableHierarchy = false,
|
|
28
28
|
fetchMode = "deferred",
|
|
@@ -33,7 +33,7 @@ export const PageWrapper = ({
|
|
|
33
33
|
const restrictionMenuProps: ComponentProps<typeof TaxonomyRestrictionMenu> = {
|
|
34
34
|
restrictField: restrictField ?? "informationSubjects",
|
|
35
35
|
requestType: requestType ?? "InformationSubjectsGetAllClient",
|
|
36
|
-
|
|
36
|
+
itemsByRow,
|
|
37
37
|
onlyUsedEntries,
|
|
38
38
|
enableHierarchy,
|
|
39
39
|
fetchMode,
|
|
@@ -46,7 +46,7 @@ export const PageWrapper = ({
|
|
|
46
46
|
<NavBar showInput={showInput} showPkgFilter={showPkgFilter} {...props} />
|
|
47
47
|
|
|
48
48
|
{showRestrictMenu && (
|
|
49
|
-
<div className="container pt-
|
|
49
|
+
<div className="container pt-4">
|
|
50
50
|
{renderRestrictionMenu ? renderRestrictionMenu(restrictionMenuProps) : (
|
|
51
51
|
<TaxonomyRestrictionMenu {...restrictionMenuProps} />
|
|
52
52
|
)}
|
|
@@ -15,6 +15,7 @@ type Props = {
|
|
|
15
15
|
removeRestrictParam?: boolean;
|
|
16
16
|
selected?: boolean;
|
|
17
17
|
onClick?: () => void;
|
|
18
|
+
multipleSelection?: boolean;
|
|
18
19
|
};
|
|
19
20
|
|
|
20
21
|
export const RestrictionNavigationItem: FC<Props> = ({
|
|
@@ -23,6 +24,7 @@ export const RestrictionNavigationItem: FC<Props> = ({
|
|
|
23
24
|
restrictField,
|
|
24
25
|
removeRestrictParam = false,
|
|
25
26
|
selected = false,
|
|
27
|
+
multipleSelection = true,
|
|
26
28
|
}) => {
|
|
27
29
|
const [restrict, setRestrict] = useQueryState("restrict", {
|
|
28
30
|
shallow: false,
|
|
@@ -35,6 +37,7 @@ export const RestrictionNavigationItem: FC<Props> = ({
|
|
|
35
37
|
restrictField,
|
|
36
38
|
removeRestrictParam,
|
|
37
39
|
selected,
|
|
40
|
+
multipleSelection,
|
|
38
41
|
currentRestrict: restrict,
|
|
39
42
|
});
|
|
40
43
|
|
|
@@ -72,6 +75,7 @@ export const RestrictionDropdownItem: FC<Props> = ({
|
|
|
72
75
|
restrictField,
|
|
73
76
|
selected = false,
|
|
74
77
|
onClick,
|
|
78
|
+
multipleSelection = true
|
|
75
79
|
}) => {
|
|
76
80
|
const [restrict, setRestrict] = useQueryState("restrict", {
|
|
77
81
|
shallow: false,
|
|
@@ -83,6 +87,7 @@ export const RestrictionDropdownItem: FC<Props> = ({
|
|
|
83
87
|
shortId,
|
|
84
88
|
restrictField,
|
|
85
89
|
selected,
|
|
90
|
+
multipleSelection,
|
|
86
91
|
currentRestrict: restrict,
|
|
87
92
|
});
|
|
88
93
|
|
|
@@ -121,7 +126,7 @@ function getRestrictionValue({
|
|
|
121
126
|
restrictField,
|
|
122
127
|
removeRestrictParam = false,
|
|
123
128
|
selected = false,
|
|
124
|
-
|
|
129
|
+
multipleSelection = true,
|
|
125
130
|
currentRestrict,
|
|
126
131
|
}: {
|
|
127
132
|
shortId?: string;
|
|
@@ -129,11 +134,12 @@ function getRestrictionValue({
|
|
|
129
134
|
removeRestrictParam?: boolean;
|
|
130
135
|
selected?: boolean;
|
|
131
136
|
currentRestrict: string | null;
|
|
137
|
+
multipleSelection?: boolean;
|
|
132
138
|
}): { restrictionValue: string | null; shouldRemoveRestrictParam: boolean } {
|
|
133
139
|
let restrictParam = "";
|
|
134
140
|
let shouldRemoveRestrictParam = removeRestrictParam;
|
|
135
141
|
|
|
136
|
-
if (currentRestrict) {
|
|
142
|
+
if (currentRestrict && multipleSelection) {
|
|
137
143
|
if (selected) {
|
|
138
144
|
const restrictionsLength = currentRestrict.split(",").length;
|
|
139
145
|
//if there is only one restriction, we can remove the whole restrict param
|
|
@@ -15,7 +15,7 @@ import { useRestrictionStore } from "../stores/restriction-store";
|
|
|
15
15
|
import { useBreakpoint } from "@c-rex/ui/hooks";
|
|
16
16
|
import { DEVICE_OPTIONS } from "@c-rex/constants";
|
|
17
17
|
import { Command, CommandEmpty, CommandInput, CommandItem, CommandList } from "@c-rex/ui/command";
|
|
18
|
-
import { Dialog, DialogContent } from "@c-rex/ui/dialog";
|
|
18
|
+
import { Dialog, DialogContent, DialogTitle } from "@c-rex/ui/dialog";
|
|
19
19
|
import { Button } from "@c-rex/ui/button";
|
|
20
20
|
import { Check, ChevronDown } from "lucide-react";
|
|
21
21
|
import { useSearchNavigationStore } from "../stores/search-navigation-store";
|
|
@@ -29,7 +29,6 @@ type Props = {
|
|
|
29
29
|
hasMoreItems?: boolean,
|
|
30
30
|
showAllWhenEmpty?: boolean,
|
|
31
31
|
onRequestMore?: () => void,
|
|
32
|
-
stripLabelPrefix?: string,
|
|
33
32
|
itemsByRow?: {
|
|
34
33
|
[DEVICE_OPTIONS.MOBILE]: number,
|
|
35
34
|
[DEVICE_OPTIONS.TABLET]: number,
|
|
@@ -209,6 +208,7 @@ const RestrictionCommandDialog: FC<RestrictionCommandDialogProps> = ({
|
|
|
209
208
|
|
|
210
209
|
<Dialog open={open} onOpenChange={setOpen}>
|
|
211
210
|
<DialogContent className="overflow-hidden p-0">
|
|
211
|
+
<DialogTitle className="sr-only">{t("search")}</DialogTitle>
|
|
212
212
|
<Command shouldFilter={false} className="[&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
|
213
213
|
<CommandInput placeholder={t("search")} value={search} onValueChange={setSearch} />
|
|
214
214
|
<CommandList ref={listRef}>
|
|
@@ -263,8 +263,7 @@ export const RestrictionSelectionCommandMenu: FC<Props> = ({
|
|
|
263
263
|
hasMoreItems = false,
|
|
264
264
|
showAllWhenEmpty = true,
|
|
265
265
|
onRequestMore,
|
|
266
|
-
|
|
267
|
-
navigationMenuListClassName = "items-center justify-between flex-row",
|
|
266
|
+
navigationMenuListClassName = "items-center justify-start gap-4 flex-row",
|
|
268
267
|
itemsByRow = {
|
|
269
268
|
[DEVICE_OPTIONS.MOBILE]: 2,
|
|
270
269
|
[DEVICE_OPTIONS.TABLET]: 4,
|
|
@@ -273,9 +272,6 @@ export const RestrictionSelectionCommandMenu: FC<Props> = ({
|
|
|
273
272
|
}) => {
|
|
274
273
|
const t = useTranslations();
|
|
275
274
|
const setRestrictionList = useRestrictionStore((state) => state.setRestrictionList);
|
|
276
|
-
const formatLabel = stripLabelPrefix
|
|
277
|
-
? (label: string) => label.replace(new RegExp(`^${stripLabelPrefix}`, "i"), "")
|
|
278
|
-
: undefined;
|
|
279
275
|
|
|
280
276
|
const [params] = useQueryStates({
|
|
281
277
|
restrict: parseAsString,
|
|
@@ -372,7 +368,7 @@ export const RestrictionSelectionCommandMenu: FC<Props> = ({
|
|
|
372
368
|
|
|
373
369
|
{!enableHierarchy && visibleItems.map((item) => {
|
|
374
370
|
const rawLabel = getLabelByLang(item.labels, lang);
|
|
375
|
-
const label = rawLabel
|
|
371
|
+
const label = rawLabel
|
|
376
372
|
return (
|
|
377
373
|
<RestrictionNavigationItem
|
|
378
374
|
key={item.shortId}
|
|
@@ -388,7 +384,7 @@ export const RestrictionSelectionCommandMenu: FC<Props> = ({
|
|
|
388
384
|
const shortId = rootNode.item.shortId || "";
|
|
389
385
|
const hasChildren = rootNode.children.length > 0;
|
|
390
386
|
const rawLabel = getLabelByLang(rootNode.item.labels, lang);
|
|
391
|
-
const label = rawLabel
|
|
387
|
+
const label = rawLabel
|
|
392
388
|
const rootSelected = restrictionValues.includes(shortId);
|
|
393
389
|
const hasActiveDescendant = hasChildren && hasSelectedDescendant(rootNode, selectedRestrictionIds);
|
|
394
390
|
const shouldHighlightBranch = hasActiveDescendant && !rootSelected;
|
|
@@ -414,7 +410,7 @@ export const RestrictionSelectionCommandMenu: FC<Props> = ({
|
|
|
414
410
|
selectedRestrictionIds={selectedRestrictionIds}
|
|
415
411
|
lang={lang}
|
|
416
412
|
highlighted={shouldHighlightBranch}
|
|
417
|
-
|
|
413
|
+
|
|
418
414
|
/>
|
|
419
415
|
</NavigationMenuItem>
|
|
420
416
|
);
|
|
@@ -433,7 +429,6 @@ export const RestrictionSelectionCommandMenu: FC<Props> = ({
|
|
|
433
429
|
selectedRestrictionIds={selectedRestrictionIds}
|
|
434
430
|
lang={lang}
|
|
435
431
|
highlighted={false}
|
|
436
|
-
formatLabel={formatLabel}
|
|
437
432
|
/>
|
|
438
433
|
</NavigationMenuItem>
|
|
439
434
|
)}
|
|
@@ -32,6 +32,8 @@ type Props = {
|
|
|
32
32
|
[DEVICE_OPTIONS.TABLET]: number,
|
|
33
33
|
[DEVICE_OPTIONS.DESKTOP]: number,
|
|
34
34
|
}
|
|
35
|
+
multipleSelection?: boolean;
|
|
36
|
+
updatePosition?: boolean;
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
type RestrictionTreeNode = {
|
|
@@ -117,12 +119,14 @@ export const RestrictionSelectionMenu: FC<Props> = ({
|
|
|
117
119
|
hasMoreItems = false,
|
|
118
120
|
showAllWhenEmpty = true,
|
|
119
121
|
onRequestMore,
|
|
120
|
-
navigationMenuListClassName = "items-center justify-
|
|
122
|
+
navigationMenuListClassName = "items-center justify-start gap-4 flex-row",
|
|
121
123
|
itemsByRow = {
|
|
122
124
|
[DEVICE_OPTIONS.MOBILE]: 2,
|
|
123
125
|
[DEVICE_OPTIONS.TABLET]: 4,
|
|
124
126
|
[DEVICE_OPTIONS.DESKTOP]: 7,
|
|
125
|
-
}
|
|
127
|
+
},
|
|
128
|
+
multipleSelection = true,
|
|
129
|
+
updatePosition = true
|
|
126
130
|
}) => {
|
|
127
131
|
const t = useTranslations();
|
|
128
132
|
const setRestrictionList = useRestrictionStore((state) => state.setRestrictionList);
|
|
@@ -154,6 +158,8 @@ export const RestrictionSelectionMenu: FC<Props> = ({
|
|
|
154
158
|
const sortedItems = useMemo(() => {
|
|
155
159
|
//if shortId it is on the restrictionValues, it should be on top of the list, otherwise keep the original order
|
|
156
160
|
|
|
161
|
+
if (updatePosition === false) return items;
|
|
162
|
+
|
|
157
163
|
const sorted = [...items].sort((a, b) => {
|
|
158
164
|
const aShortId = a.shortId || "";
|
|
159
165
|
const bShortId = b.shortId || "";
|
|
@@ -173,7 +179,7 @@ export const RestrictionSelectionMenu: FC<Props> = ({
|
|
|
173
179
|
return aIndex - bIndex; // sort by index in restrictionValues
|
|
174
180
|
});
|
|
175
181
|
return sorted;
|
|
176
|
-
}, [items, restrictionValues]);
|
|
182
|
+
}, [items, restrictionValues, updatePosition]);
|
|
177
183
|
|
|
178
184
|
const hierarchyRoots = useMemo(() => {
|
|
179
185
|
if (!enableHierarchy) return [];
|
|
@@ -277,7 +283,7 @@ export const RestrictionSelectionMenu: FC<Props> = ({
|
|
|
277
283
|
};
|
|
278
284
|
|
|
279
285
|
return (
|
|
280
|
-
<NavigationMenu viewport={false} className="max-w-full w-full c-rex-restriction-menu">
|
|
286
|
+
<NavigationMenu viewport={false} className="max-w-full w-full c-rex-restriction-menu overflow-auto pb-4">
|
|
281
287
|
<NavigationMenuList className={cn("w-full", navigationMenuListClassName)}>
|
|
282
288
|
|
|
283
289
|
|
|
@@ -293,6 +299,7 @@ export const RestrictionSelectionMenu: FC<Props> = ({
|
|
|
293
299
|
<RestrictionNavigationItem
|
|
294
300
|
key={item.shortId}
|
|
295
301
|
shortId={item.shortId!}
|
|
302
|
+
multipleSelection={multipleSelection}
|
|
296
303
|
restrictField={restrictField as string}
|
|
297
304
|
label={getLabelByLang(item.labels, lang)}
|
|
298
305
|
selected={restrictionValues.includes(item.shortId!)}
|
|
@@ -315,6 +322,7 @@ export const RestrictionSelectionMenu: FC<Props> = ({
|
|
|
315
322
|
restrictField={restrictField}
|
|
316
323
|
label={label}
|
|
317
324
|
selected={restrictionValues.includes(shortId)}
|
|
325
|
+
multipleSelection={multipleSelection}
|
|
318
326
|
/>
|
|
319
327
|
);
|
|
320
328
|
}
|
|
@@ -364,6 +372,7 @@ export const RestrictionSelectionMenu: FC<Props> = ({
|
|
|
364
372
|
>
|
|
365
373
|
<RestrictionDropdownItem
|
|
366
374
|
shortId={item.shortId!}
|
|
375
|
+
multipleSelection={multipleSelection}
|
|
367
376
|
restrictField={restrictField as string}
|
|
368
377
|
label={getLabelByLang(item.labels, lang)}
|
|
369
378
|
selected={restrictionValues.includes(item.shortId!)}
|
|
@@ -29,7 +29,6 @@ export type TaxonomyRestrictionCommandMenuProps = {
|
|
|
29
29
|
fetchMode?: RestrictionMenuFetchMode;
|
|
30
30
|
showAllWhenEmpty?: boolean;
|
|
31
31
|
queryParams?: GenericQueryParams;
|
|
32
|
-
stripLabelPrefix?: string;
|
|
33
32
|
};
|
|
34
33
|
|
|
35
34
|
export const TaxonomyRestrictionCommandMenu: FC<TaxonomyRestrictionCommandMenuProps> = ({
|
|
@@ -41,8 +40,7 @@ export const TaxonomyRestrictionCommandMenu: FC<TaxonomyRestrictionCommandMenuPr
|
|
|
41
40
|
enableHierarchy = false,
|
|
42
41
|
fetchMode = "deferred",
|
|
43
42
|
showAllWhenEmpty = true,
|
|
44
|
-
navigationMenuListClassName = "items-center justify-
|
|
45
|
-
stripLabelPrefix,
|
|
43
|
+
navigationMenuListClassName = "items-center justify-start gap-4 flex-row",
|
|
46
44
|
}) => {
|
|
47
45
|
const [loadAll, setLoadAll] = useState(false);
|
|
48
46
|
const RequestComponent = ComponentOptions[requestType] as unknown as FC<GenericRequestProps>;
|
|
@@ -102,7 +100,6 @@ export const TaxonomyRestrictionCommandMenu: FC<TaxonomyRestrictionCommandMenuPr
|
|
|
102
100
|
}
|
|
103
101
|
}}
|
|
104
102
|
navigationMenuListClassName={navigationMenuListClassName}
|
|
105
|
-
stripLabelPrefix={stripLabelPrefix}
|
|
106
103
|
/>
|
|
107
104
|
);
|
|
108
105
|
}}
|
|
@@ -5,6 +5,7 @@ import { FC, ReactNode, useMemo, useState } from "react";
|
|
|
5
5
|
import * as ComponentOptions from "../generated/client-components";
|
|
6
6
|
import { Skeleton } from "@c-rex/ui/skeleton";
|
|
7
7
|
import { RestrictionSelectionMenu } from "./restriction-selection-menu";
|
|
8
|
+
import { DEVICE_OPTIONS } from "@c-rex/constants";
|
|
8
9
|
|
|
9
10
|
type GenericRequestData = {
|
|
10
11
|
items?: DomainEntityModel[];
|
|
@@ -22,25 +23,39 @@ type RestrictionMenuFetchMode = "all" | "deferred";
|
|
|
22
23
|
export type TaxonomyRestrictionMenuProps = {
|
|
23
24
|
restrictField: string;
|
|
24
25
|
navigationMenuListClassName?: string;
|
|
25
|
-
|
|
26
|
+
itemsByRow?: {
|
|
27
|
+
[DEVICE_OPTIONS.MOBILE]: number;
|
|
28
|
+
[DEVICE_OPTIONS.TABLET]: number;
|
|
29
|
+
[DEVICE_OPTIONS.DESKTOP]: number;
|
|
30
|
+
};
|
|
26
31
|
requestType: keyof typeof ComponentOptions;
|
|
27
32
|
onlyUsedEntries?: boolean;
|
|
28
33
|
enableHierarchy?: boolean;
|
|
29
34
|
fetchMode?: RestrictionMenuFetchMode;
|
|
30
35
|
showAllWhenEmpty?: boolean;
|
|
31
36
|
queryParams?: GenericQueryParams;
|
|
37
|
+
multipleSelection?: boolean;
|
|
38
|
+
updatePosition?: boolean;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const DEFAULT_ITEMS_BY_ROW = {
|
|
42
|
+
[DEVICE_OPTIONS.MOBILE]: 7,
|
|
43
|
+
[DEVICE_OPTIONS.TABLET]: 7,
|
|
44
|
+
[DEVICE_OPTIONS.DESKTOP]: 7,
|
|
32
45
|
};
|
|
33
46
|
|
|
34
47
|
export const TaxonomyRestrictionMenu: FC<TaxonomyRestrictionMenuProps> = ({
|
|
35
48
|
queryParams,
|
|
36
49
|
restrictField,
|
|
37
|
-
|
|
50
|
+
itemsByRow = DEFAULT_ITEMS_BY_ROW,
|
|
38
51
|
requestType,
|
|
39
52
|
onlyUsedEntries = true,
|
|
40
53
|
enableHierarchy = false,
|
|
41
54
|
fetchMode = "deferred",
|
|
42
55
|
showAllWhenEmpty = true,
|
|
43
|
-
navigationMenuListClassName = "items-center justify-
|
|
56
|
+
navigationMenuListClassName = "items-center justify-start gap-4 flex-row",
|
|
57
|
+
multipleSelection = true,
|
|
58
|
+
updatePosition = true,
|
|
44
59
|
}) => {
|
|
45
60
|
const [loadAll, setLoadAll] = useState(false);
|
|
46
61
|
const RequestComponent = ComponentOptions[requestType] as unknown as FC<GenericRequestProps>;
|
|
@@ -50,11 +65,12 @@ export const TaxonomyRestrictionMenu: FC<TaxonomyRestrictionMenuProps> = ({
|
|
|
50
65
|
Number.isFinite(Number(queryParams?.PageSize)) && Number(queryParams?.PageSize) > 0
|
|
51
66
|
? Number(queryParams?.PageSize)
|
|
52
67
|
: undefined;
|
|
68
|
+
const maxItemsByRow = useMemo(() => Math.max(...Object.values(itemsByRow)), [itemsByRow]);
|
|
53
69
|
const resolvedPageSize = useMemo(() => {
|
|
54
70
|
if (explicitPageSize) return explicitPageSize;
|
|
55
|
-
if (fetchMode === "deferred" && !loadAll) return Math.max(
|
|
71
|
+
if (fetchMode === "deferred" && !loadAll) return Math.max(maxItemsByRow, 1);
|
|
56
72
|
return 100;
|
|
57
|
-
}, [explicitPageSize, fetchMode,
|
|
73
|
+
}, [explicitPageSize, fetchMode, maxItemsByRow, loadAll]);
|
|
58
74
|
const requestedFields = Array.isArray(queryParams?.Fields) ? queryParams.Fields : undefined;
|
|
59
75
|
const resolvedFields = useMemo(() => {
|
|
60
76
|
const baseFields = requestedFields && requestedFields.length > 0 ? requestedFields : ["labels"];
|
|
@@ -94,12 +110,15 @@ export const TaxonomyRestrictionMenu: FC<TaxonomyRestrictionMenuProps> = ({
|
|
|
94
110
|
enableHierarchy={enableHierarchy}
|
|
95
111
|
hasMoreItems={hasMoreItems}
|
|
96
112
|
showAllWhenEmpty={showAllWhenEmpty}
|
|
113
|
+
itemsByRow={itemsByRow}
|
|
97
114
|
onRequestMore={() => {
|
|
98
115
|
if (fetchMode === "deferred" && !explicitPageSize) {
|
|
99
116
|
setLoadAll(true);
|
|
100
117
|
}
|
|
101
118
|
}}
|
|
102
119
|
navigationMenuListClassName={navigationMenuListClassName}
|
|
120
|
+
multipleSelection={multipleSelection}
|
|
121
|
+
updatePosition={updatePosition}
|
|
103
122
|
/>
|
|
104
123
|
);
|
|
105
124
|
}}
|
|
@@ -69,10 +69,6 @@ const FilterSidebar: FC<FilterSidebarProps> = ({
|
|
|
69
69
|
});
|
|
70
70
|
const startSearchNavigation = useSearchNavigationStore((state) => state.start);
|
|
71
71
|
|
|
72
|
-
useEffect(() => {
|
|
73
|
-
console.log(isMobile, isMobileFiltersOpen)
|
|
74
|
-
}, [isMobile, isMobileFiltersOpen]);
|
|
75
|
-
|
|
76
72
|
const filteredTags = useMemo(() => {
|
|
77
73
|
const resolved = memoizeFilteredTags(tags, params.filter, params.packages, {
|
|
78
74
|
uiLanguage: locale,
|
|
@@ -1,96 +1,196 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
|
-
import { FC, useState } from "react";
|
|
3
|
+
import { FC, useEffect, useRef, useState } from "react";
|
|
4
4
|
|
|
5
5
|
import Link from "next/link";
|
|
6
|
-
import { cn } from "@c-rex/utils";
|
|
6
|
+
import { cn, formatDateToLocale } from "@c-rex/utils";
|
|
7
7
|
import { Badge } from "@c-rex/ui/badge";
|
|
8
|
-
import {
|
|
8
|
+
import { useLocale } from "next-intl";
|
|
9
9
|
import { TopicsResponseItem } from "@c-rex/interfaces";
|
|
10
10
|
import { Card } from "@c-rex/ui/card";
|
|
11
11
|
import { useQueryState } from "nuqs";
|
|
12
|
+
import { User } from "lucide-react";
|
|
12
13
|
|
|
13
14
|
interface InformationUnitSearchResultsCardsProps {
|
|
14
15
|
items: TopicsResponseItem[];
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
type CardData = {
|
|
19
|
+
image: string | null;
|
|
20
|
+
description: string | null;
|
|
21
|
+
authors: { name: string; photo: string | null }[];
|
|
22
|
+
loaded: boolean;
|
|
23
|
+
};
|
|
21
24
|
|
|
22
|
-
return (
|
|
23
|
-
<div className="grid gap-6 grid-cols-2">
|
|
24
25
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
26
|
+
const InformationUnitSearchResultCard: FC<{
|
|
27
|
+
item: TopicsResponseItem;
|
|
28
|
+
index: number;
|
|
29
|
+
query: string | null;
|
|
30
|
+
}> = ({ item, index, query }) => {
|
|
31
|
+
const locale = useLocale();
|
|
32
|
+
const date = formatDateToLocale(item.created!, locale);
|
|
33
|
+
const cardRef = useRef<HTMLDivElement>(null);
|
|
34
|
+
const [cardData, setCardData] = useState<CardData>({ image: null, description: null, authors: [], loaded: false });
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
const el = cardRef.current;
|
|
38
|
+
if (!el) return;
|
|
39
|
+
|
|
40
|
+
const observer = new IntersectionObserver(
|
|
41
|
+
([entry]) => {
|
|
42
|
+
if (!entry.isIntersecting) return;
|
|
43
|
+
observer.disconnect();
|
|
44
|
+
|
|
45
|
+
const fetches: Promise<void>[] = [];
|
|
46
|
+
|
|
47
|
+
if (item.renditionUrl) {
|
|
48
|
+
fetches.push(
|
|
49
|
+
fetch(item.renditionUrl)
|
|
50
|
+
.then(r => r.text())
|
|
51
|
+
.then(html => {
|
|
52
|
+
const doc = new DOMParser().parseFromString(html, "text/html");
|
|
53
|
+
setCardData(prev => ({
|
|
54
|
+
...prev,
|
|
55
|
+
image: doc.querySelector(".teaserfig img")?.getAttribute("src") || null,
|
|
56
|
+
description: doc.querySelector(".teaser-p")?.textContent || null,
|
|
57
|
+
}));
|
|
58
|
+
})
|
|
59
|
+
.catch(() => { })
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (item.vcardUrls?.length) {
|
|
64
|
+
fetches.push(
|
|
65
|
+
Promise.all(
|
|
66
|
+
item.vcardUrls.map(url =>
|
|
67
|
+
fetch(url)
|
|
68
|
+
.then(r => r.json())
|
|
69
|
+
.then(vcard => ({ name: vcard.fullName || "", photo: vcard.photos?.[0]?.source || null }))
|
|
70
|
+
.catch(() => null)
|
|
71
|
+
)
|
|
72
|
+
).then(results => {
|
|
73
|
+
setCardData(prev => ({
|
|
74
|
+
...prev,
|
|
75
|
+
authors: results.filter(Boolean) as { name: string; photo: string | null }[],
|
|
76
|
+
}));
|
|
77
|
+
})
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
Promise.all(fetches).then(() => {
|
|
82
|
+
setCardData(prev => ({ ...prev, loaded: true }));
|
|
83
|
+
});
|
|
84
|
+
},
|
|
85
|
+
{ rootMargin: "200px" }
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
observer.observe(el);
|
|
89
|
+
return () => observer.disconnect();
|
|
90
|
+
}, [item.renditionUrl, item.vcardUrls]);
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<div ref={cardRef} className={cn(index == 0 ? "md:col-span-2" : "")}>
|
|
94
|
+
<Card
|
|
95
|
+
className={cn(
|
|
96
|
+
"c-rex-result-item information-unit-search-results-card relative p-0 gap-0",
|
|
97
|
+
`c-rex-result-${item.type.toLowerCase()}`,
|
|
98
|
+
index == 0
|
|
99
|
+
? "grid grid-cols-1 md:grid-cols-2"
|
|
100
|
+
: "h-full flex flex-col space-y-2",
|
|
101
|
+
item.disabled ? "c-rex-result-item-disabled" : ""
|
|
102
|
+
)}
|
|
103
|
+
>
|
|
104
|
+
<div className={cn(
|
|
105
|
+
"w-full overflow-hidden",
|
|
106
|
+
index == 0 ? "rounded-t-xl md:rounded-t-none md:rounded-tl-xl md:rounded-bl-xl" : "rounded-t-xl"
|
|
107
|
+
)}>
|
|
108
|
+
{cardData.image ? (
|
|
109
|
+
<img
|
|
110
|
+
src={cardData.image}
|
|
111
|
+
alt={item.title}
|
|
112
|
+
className="size-full object-cover object-center"
|
|
113
|
+
style={{ width: "100%", height: index == 0 ? "100%" : "190px" }}
|
|
114
|
+
loading={index == 0 ? "eager" : "lazy"}
|
|
115
|
+
/>
|
|
116
|
+
) : (
|
|
117
|
+
<div
|
|
118
|
+
style={{ height: "190px" }}
|
|
119
|
+
className={cn(
|
|
120
|
+
"w-full bg-gray-100 animate-pulse",
|
|
121
|
+
index == 0 ? "" : "rounded-t-xl"
|
|
122
|
+
)}
|
|
123
|
+
/>
|
|
34
124
|
)}
|
|
35
|
-
>
|
|
36
|
-
|
|
37
|
-
<div className={
|
|
38
|
-
cn(
|
|
39
|
-
"w-full overflow-hidden",
|
|
40
|
-
index == 0 ? "rounded-tl-xl rounded-bl-xl" : "rounded-t-xl"
|
|
41
|
-
)}>
|
|
42
|
-
{item.image && (
|
|
43
|
-
<img
|
|
44
|
-
src={item.image}
|
|
45
|
-
alt={item.title}
|
|
46
|
-
className={cn(
|
|
47
|
-
"size-full object-cover object-center",
|
|
48
|
-
isLoading ? "bg-gray-300 animate-pulse" : "",
|
|
49
|
-
)}
|
|
50
|
-
style={{
|
|
51
|
-
width: "100%", height: index == 0 ? "auto" : "190px"
|
|
52
|
-
}}
|
|
53
|
-
loading={index == 0 ? "eager" : "lazy"}
|
|
54
|
-
onLoad={() => setLoading(false)}
|
|
55
|
-
onError={() => setLoading(false)}
|
|
56
|
-
/>
|
|
57
|
-
)}
|
|
125
|
+
</div>
|
|
58
126
|
|
|
127
|
+
<div className="flex flex-1 flex-col p-4 gap-4 justify-between">
|
|
128
|
+
<div className="w-full flex flex-col items-start gap-4">
|
|
129
|
+
<h2 className="line-clamp-2 font-heading text-2xl">
|
|
130
|
+
{item.title}
|
|
131
|
+
</h2>
|
|
132
|
+
<Badge variant="secondary"> {date} </Badge>
|
|
59
133
|
</div>
|
|
60
134
|
|
|
61
|
-
<div
|
|
62
|
-
|
|
63
|
-
"
|
|
64
|
-
|
|
135
|
+
<div className="flex items-end flex-row">
|
|
136
|
+
{cardData.description ? (
|
|
137
|
+
<p className="m-0 flex-1 text-sm text-muted-foreground">
|
|
138
|
+
{cardData.description}
|
|
139
|
+
</p>
|
|
140
|
+
) : (
|
|
141
|
+
<div className="flex-1 space-y-2">
|
|
142
|
+
<div className="h-3 w-full rounded bg-gray-100 animate-pulse" />
|
|
143
|
+
<div className="h-3 w-4/5 rounded bg-gray-100 animate-pulse" />
|
|
144
|
+
</div>
|
|
65
145
|
)}
|
|
66
|
-
>
|
|
67
|
-
<div className="w-full">
|
|
68
|
-
<h2 className="my-1.5 line-clamp-2 font-heading text-2xl">
|
|
69
|
-
{item.title}
|
|
70
|
-
</h2>
|
|
71
|
-
|
|
72
|
-
{item.type && (
|
|
73
|
-
<Badge variant="secondary">
|
|
74
|
-
{item.type}
|
|
75
|
-
</Badge>
|
|
76
|
-
)}
|
|
77
|
-
</div>
|
|
78
|
-
<div className="mt-4 flex items-end flex-row gap-2">
|
|
79
|
-
<p className="m-0 flex-1 text-sm text-muted-foreground">{item.description?.substring(0, 100)}...</p>
|
|
146
|
+
</div>
|
|
80
147
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
)
|
|
148
|
+
{cardData.authors.length > 0 && (
|
|
149
|
+
<div className="flex flex-wrap gap-4">
|
|
150
|
+
{cardData.authors.map((author, i) => (
|
|
151
|
+
<div key={i} className="flex items-center gap-2">
|
|
84
152
|
|
|
85
|
-
</div>
|
|
86
|
-
</div>
|
|
87
153
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
154
|
+
{author.photo ? (
|
|
155
|
+
<img
|
|
156
|
+
src={author.photo}
|
|
157
|
+
alt={author.name}
|
|
158
|
+
className="size-6 rounded-full object-cover"
|
|
159
|
+
/>
|
|
160
|
+
) : (
|
|
161
|
+
<div className="size-6 rounded-full bg-muted flex items-center justify-center" >
|
|
162
|
+
<User className="size-4" />
|
|
163
|
+
</div>
|
|
164
|
+
)}
|
|
165
|
+
<span className="text-xs text-muted-foreground">{author.name}</span>
|
|
166
|
+
</div>
|
|
167
|
+
))}
|
|
168
|
+
</div>
|
|
92
169
|
)}
|
|
93
|
-
</
|
|
170
|
+
</div>
|
|
171
|
+
|
|
172
|
+
{!item.disabled && (
|
|
173
|
+
<Link href={item.link} className="absolute inset-0">
|
|
174
|
+
<span className="sr-only">View article</span>
|
|
175
|
+
</Link>
|
|
176
|
+
)}
|
|
177
|
+
</Card>
|
|
178
|
+
</div>
|
|
179
|
+
);
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const InformationUnitSearchResultsCards: FC<InformationUnitSearchResultsCardsProps> = ({ items }) => {
|
|
183
|
+
const [query] = useQueryState("search");
|
|
184
|
+
|
|
185
|
+
return (
|
|
186
|
+
<div className="grid gap-6 md:grid-cols-2">
|
|
187
|
+
{items.map((item, index) => (
|
|
188
|
+
<InformationUnitSearchResultCard
|
|
189
|
+
key={item.shortId}
|
|
190
|
+
item={item}
|
|
191
|
+
index={index}
|
|
192
|
+
query={query}
|
|
193
|
+
/>
|
|
94
194
|
))}
|
|
95
195
|
</div>
|
|
96
196
|
);
|