@c-rex/templates 0.1.9 → 0.1.10

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@c-rex/templates",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "src"
@@ -1,11 +1,19 @@
1
- import { extractHtmlContent, getInfoWithCache, getPrimaryInfo } from "./utils";
2
- import { BLOG_TYPE_AND_LINK } from "@c-rex/constants";
1
+ import { JSX } from "react";
2
+ import { extractHtmlContent, getInfoWithCache, getPrimaryInfo } from "../utils";
3
+ import { BLOG_TYPE_AND_LINK, CONTENT_LANG_KEY } from "@c-rex/constants";
3
4
  import { PageWrapper } from "@c-rex/components/page-wrapper";
4
5
  import { Metadata } from "next";
5
6
  import { RenderArticle } from "@c-rex/components/render-article";
6
- import { AppSidebar } from "@c-rex/components/sidebar";
7
- import { SidebarInset, SidebarProvider } from "@c-rex/ui/sidebar";
7
+ import { InfoCard } from "@c-rex/components/info-card";
8
8
  import { CheckArticleLangToast } from "@c-rex/components/check-article-lang";
9
+ import { SidebarMenu, SidebarMenuSubButton, SidebarMenuSubItem } from "@c-rex/ui/sidebar";
10
+ import * as Flags from 'country-flag-icons/react/3x2';
11
+ import { getTranslations } from 'next-intl/server';
12
+ import { getConfigs } from "@c-rex/utils/next-cookies";
13
+ import { TopicsService } from "@c-rex/services";
14
+ import { cookies } from "next/headers";
15
+ import { Card, CardContent, CardHeader, CardTitle } from "@c-rex/ui/card";
16
+
9
17
 
10
18
  const infoCache = new Map<string, Awaited<ReturnType<typeof getPrimaryInfo>>>();
11
19
 
@@ -30,26 +38,85 @@ export const generateMetadata = async (
30
38
  }
31
39
 
32
40
  export const BlogPageTemplate = async ({ params }: { params: { id: string } }) => {
33
- const { htmlContent, articleLang, availableVersions } = await getInfoWithCache(params.id, BLOG_TYPE_AND_LINK, infoCache);
41
+ const config = getConfigs();
42
+ const service = new TopicsService();
43
+ const defaultLang = config.languageSwitcher.default;
44
+ const contentLang = cookies().get(CONTENT_LANG_KEY)?.value || defaultLang
45
+
46
+ const [articleList, articleInfo, t] = await Promise.all([
47
+ service.getList({
48
+ languages: [contentLang] as never[],
49
+ pageSize: 4,
50
+ }),
51
+ getInfoWithCache(params.id, BLOG_TYPE_AND_LINK, infoCache),
52
+ getTranslations()
53
+ ]);
54
+
55
+ const { htmlContent, articleLang, availableVersions } = articleInfo
34
56
  const { articleHtml } = extractHtmlContent(htmlContent)
35
57
 
58
+ articleList.items = articleList.items.filter(item => item.shortId !== params.id)
59
+
36
60
  return (
37
61
  <PageWrapper title="" pageType="BLOG">
38
- <SidebarProvider>
39
-
40
- <CheckArticleLangToast availableVersions={availableVersions} />
41
-
42
- <AppSidebar
43
- lang={articleLang}
44
- data={[]}
45
- availableVersions={availableVersions}
46
- />
47
- <SidebarInset>
48
- <div className="container flex flex-col">
49
- <RenderArticle htmlContent={articleHtml} contentLang={articleLang} />
62
+ <CheckArticleLangToast availableVersions={availableVersions} />
63
+
64
+ <div className="container pt-4 flex flex-row gap-4">
65
+ <div className="flex-1">
66
+ <RenderArticle htmlContent={articleHtml} contentLang={articleLang} />
67
+ </div>
68
+
69
+ <div className="w-60 relative">
70
+ <div className="sticky top-24">
71
+ <Card className="mb-4">
72
+
73
+ <CardHeader>
74
+ <CardTitle className="text-lg">
75
+ {t("availableIn")}:
76
+ </CardTitle>
77
+ </CardHeader>
78
+ <CardContent className="space-y-3">
79
+ <SidebarMenu>
80
+ {availableVersions.map((item) => {
81
+ return (
82
+ <SidebarMenuSubItem key={item.shortId}>
83
+ <SidebarMenuSubButton
84
+ className="cursor-pointer"
85
+ isActive={item.active}
86
+ key={item.shortId}
87
+ href={item.link} title={item.lang}
88
+ >
89
+ {getFlagIcon(item.country)} {item.lang}
90
+ </SidebarMenuSubButton>
91
+ </SidebarMenuSubItem>
92
+ )
93
+ })}
94
+ </SidebarMenu>
95
+ </CardContent>
96
+ </Card>
97
+
98
+ <InfoCard
99
+ title={t("recentPosts")}
100
+ items={articleList.items.map(item => ({
101
+ label: item.title,
102
+ value: item.created,
103
+ link: item.link
104
+ }))}
105
+ />
106
+
50
107
  </div>
51
- </SidebarInset>
52
- </SidebarProvider>
108
+ </div>
109
+
110
+ </div>
53
111
  </PageWrapper>
54
112
  );
113
+ };
114
+
115
+
116
+ const getFlagIcon = (countryCode: string): JSX.Element | null => {
117
+ type CountryCode = keyof typeof Flags;
118
+ const FlagComponent = Flags[countryCode as CountryCode];
119
+ if (!FlagComponent) return null;
120
+
121
+ return <FlagComponent />;
55
122
  };
@@ -1,59 +1,72 @@
1
- import { informationUnitsResponse } from "@c-rex/interfaces";
2
- import { InformationUnitsService } from "@c-rex/services";
1
+ import { DocumentTypesService, TopicsService } from "@c-rex/services";
3
2
  import { HomePage } from "./page";
4
3
  import { PageWrapper } from "@c-rex/components/page-wrapper";
5
4
  import { WildCardType } from "@c-rex/types";
5
+ import { cookies } from "next/headers";
6
+ import { getConfigs } from "@c-rex/utils/next-cookies";
7
+ import { CONTENT_LANG_KEY } from "@c-rex/constants";
8
+ import { getTeaserInfo } from "../utils";
9
+ import { SearchProvider } from "@c-rex/contexts/search";
6
10
 
7
- interface HomeProps {
8
- searchParams: {
11
+ interface BlogHomeProps {
12
+ searchParams?: {
9
13
  search?: string;
10
- page: string;
11
- language: string;
12
- wildcard: string;
13
- operator: string;
14
- like: string;
15
- packages?: string;
14
+ page?: string;
15
+ wildcard?: string;
16
+ operator?: string;
17
+ like?: string;
16
18
  filter?: string;
17
19
  };
18
20
  }
19
21
 
20
- export const HomeLayout = async ({ searchParams }: HomeProps) => {
21
- const { search, page, language, wildcard, operator, like, packages, filter } = searchParams;
22
+ export const BlogHomeLayout = async ({ searchParams = {} }: BlogHomeProps) => {
23
+ const {
24
+ search = "",
25
+ page = "1",
26
+ wildcard = "",
27
+ operator = "",
28
+ filter = ""
29
+ } = searchParams;
22
30
 
23
- let data = {
24
- items: [],
25
- pageInfo: {
26
- pageCount: 0,
27
- pageNumber: 0
28
- }
29
- } as unknown as informationUnitsResponse;
30
-
31
- if (search !== undefined) {
32
-
33
- const filters: string[] = filter?.split(",") || []
34
- const restrict: string[] = []
35
-
36
- if (packages && packages.length > 0) {
37
- restrict.push(`packages.shortId=${packages}`)
38
- }
39
- const service = new InformationUnitsService();
31
+ const config = getConfigs();
32
+ const service = new TopicsService();
33
+ const defaultLang = config.languageSwitcher.default;
34
+ const contentLang = cookies().get(CONTENT_LANG_KEY)?.value || defaultLang
35
+ const documentService = new DocumentTypesService();
36
+ const filters: string[] = filter.split(",") || []
40
37
 
41
- data = await service.getList({
38
+ const [data, tags] = await Promise.all([
39
+ service.getList({
42
40
  queries: search,
43
- page: Number(page),
44
- fields: ["renditions", "class", "languages", "labels"],
45
- languages: language.split(","),
41
+ page: Number(page || 1),
42
+ fields: ["renditions", "class", "languages", "labels", "created", "applicableForTypes"],
43
+ languages: [contentLang] as never[],
46
44
  wildcard: wildcard as WildCardType,
47
- restrict: restrict,
48
45
  operator: operator,
49
- like: Boolean(like === "true"),
50
- filters: filters
51
- });
52
- }
46
+ pageSize: 11,
47
+ filters,
48
+ }),
49
+ documentService.getLabels()
50
+ ]);
51
+
52
+
53
+ data.items = await Promise.all(data.items.map(async (item) => {
54
+ const response = await fetch(item.renditionUrl, { method: "GET" });
55
+ const text = await response.text();
56
+ const { imageSrc, desc } = getTeaserInfo(text);
57
+
58
+ return {
59
+ ...item,
60
+ image: imageSrc,
61
+ description: desc
62
+ }
63
+ }))
53
64
 
54
65
  return (
55
- <PageWrapper title="" pageType="HOME">
56
- <HomePage data={data} />
57
- </PageWrapper>
66
+ <SearchProvider>
67
+ <PageWrapper title="" pageType="HOME">
68
+ <HomePage data={data} tagsParam={tags} />
69
+ </PageWrapper>
70
+ </SearchProvider>
58
71
  );
59
72
  };
@@ -1,58 +1,42 @@
1
1
  "use client";
2
2
 
3
- import React, { FC, useEffect, useState } from "react";
4
- import { Trash2, X } from "lucide-react"
3
+ import React, { FC, Fragment, useEffect, useState } from "react";
5
4
  import { useTranslations } from 'next-intl'
6
- import { parseAsBoolean, parseAsInteger, parseAsString, useQueryStates } from 'nuqs'
7
- import { informationUnitsResponse } from "@c-rex/interfaces";
8
- import { Button } from "@c-rex/ui/button";
9
- import { Badge } from "@c-rex/ui/badge";
5
+ import { parseAsString, useQueryStates } from 'nuqs'
6
+ import { Card } from "@c-rex/ui/card";
10
7
  import {
11
8
  SidebarContent,
12
9
  SidebarGroup,
13
- SidebarGroupContent,
14
- SidebarGroupLabel,
15
10
  SidebarHeader,
16
11
  SidebarMenu,
17
- SidebarMenuSub,
18
12
  SidebarMenuSubButton,
19
13
  SidebarMenuSubItem
20
14
  } from "@c-rex/ui/sidebar";
21
- import { ResultList } from "@c-rex/components/result-list";
22
- import { DialogFilter } from "@c-rex/components/dialog-filter";
23
- import { useAppConfig } from "@c-rex/contexts/config-provider";
15
+ import { Pagination } from "@c-rex/components/pagination";
24
16
  import { AutoComplete } from "@c-rex/components/autocomplete";
25
- import { OPERATOR_OPTIONS, WILD_CARD_OPTIONS } from "@c-rex/constants";
26
- import { Loading } from "@c-rex/components/loading";
17
+ import { Empty } from "@c-rex/components/empty";
18
+ import BlogView from "@c-rex/components/result-view/blog";
19
+ import { useSearchContext } from "@c-rex/contexts/search";
20
+ import { DefaultResponse, TopicsResponseItem } from "@c-rex/interfaces";
27
21
 
28
22
  interface HomePageProps {
29
- data: informationUnitsResponse;
23
+ data: DefaultResponse<TopicsResponseItem, null>;
24
+ tagsParam: tagsType[];
30
25
  }
31
26
 
32
- type filterModel = {
33
- key: string
34
- name?: string
35
- value: string
36
- default?: string | boolean | null
37
- removable: boolean
27
+ type tagsType = {
28
+ shortId: string;
29
+ label: string;
30
+ active?: boolean
38
31
  }
39
32
 
40
- export const HomePage: FC<HomePageProps> = ({ data }) => {
33
+ export const HomePage: FC<HomePageProps> = ({ data: dataAux, tagsParam }) => {
41
34
  const t = useTranslations();
42
- const { configs } = useAppConfig()
43
-
44
- const [tags, setTags] = useState<{ [key: string]: any[] }>(data.tags || {});
45
- const [filters, setFilters] = useState<filterModel[] | null>(null)
46
- const [disabled, setDisabled] = useState<boolean>(false)
47
- const [loading, setLoading] = useState<boolean>(true)
35
+ const { setLoading } = useSearchContext();
36
+ const [tags, setTags] = useState<tagsType[]>(tagsParam);
37
+ const [data, setData] = useState<DefaultResponse<TopicsResponseItem, null>>(dataAux);
48
38
  const [params, setParams] = useQueryStates({
49
- language: parseAsString,
50
- page: parseAsInteger,
51
- wildcard: parseAsString,
52
- operator: parseAsString,
53
- packages: parseAsString,
54
39
  filter: parseAsString,
55
- like: parseAsBoolean,
56
40
  search: {
57
41
  defaultValue: "",
58
42
  parse(value) {
@@ -65,258 +49,113 @@ export const HomePage: FC<HomePageProps> = ({ data }) => {
65
49
  });
66
50
 
67
51
  useEffect(() => {
68
- if (params.search.length > 0) {
69
- setDisabled(false)
70
- generateFiltersObj()
71
- } else {
72
- setDisabled(true)
73
- setFilters(null)
74
- }
75
- }, [params])
52
+ setLoading(false);
53
+ setData(dataAux);
76
54
 
77
- useEffect(() => {
78
- const newTags = { ...data.tags }
55
+ }, [dataAux]);
79
56
 
80
- if (params.filter !== null) {
81
- const splittedParam = params.filter.split(",")
82
57
 
83
- splittedParam.forEach((item) => {
84
- const aux = item.split(".shortId=")
85
- const name = aux[0]
86
- const shortId = aux[1]
87
-
88
- if (!newTags[name]) {
89
- newTags[name] = []
90
- }
58
+ useEffect(() => {
59
+ const newTags = [
60
+ {
61
+ label: t("filter.all"),
62
+ shortId: "all",
63
+ active: true
64
+ },
65
+ ...tagsParam.sort((a, b) => {
66
+ if (a.shortId < b.shortId) return -1;
67
+ if (a.shortId > b.shortId) return 1;
68
+ return 0;
69
+ })];
91
70
 
92
- newTags[name].forEach((el) => {
93
- if (el.shortId == shortId) {
94
- el.active = true
95
- } else {
96
- el.active = false
97
- }
98
- })
99
- })
100
- }
71
+ if (params.filter !== null) {
72
+ const shortId = params.filter.split("=")[1]
101
73
 
102
- if (params.packages !== null && newTags["packages"]) {
103
- newTags["packages"].forEach((el) => {
104
- if (el.shortId == params.packages) {
105
- el.active = true
74
+ newTags.forEach((item) => {
75
+ if (item.shortId == shortId) {
76
+ item.active = true
106
77
  } else {
107
- el.active = false
78
+ item.active = false
108
79
  }
109
80
  })
110
81
  }
111
82
 
112
83
  setTags(newTags)
113
84
  setLoading(false)
114
- }, [data])
115
-
116
- const generateFiltersObj = () => {
117
- const filters: filterModel[] = [{
118
- key: "operator",
119
- name: t("filter.operator"),
120
- value: `${params?.operator !== OPERATOR_OPTIONS.OR}`,
121
- default: OPERATOR_OPTIONS.OR,
122
- removable: params?.operator !== OPERATOR_OPTIONS.OR
123
- }, {
124
- key: "like",
125
- name: t("filter.like"),
126
- value: `${params.like}`,
127
- default: false,
128
- removable: params?.like as boolean
129
- }, {
130
- key: "wildcard",
131
- value: params.wildcard as string,
132
- default: WILD_CARD_OPTIONS.NONE,
133
- removable: params?.wildcard !== WILD_CARD_OPTIONS.NONE
134
- }]
135
-
136
- const languages = params.language?.split(",")
137
- languages?.forEach((item) => {
138
- const aux = languages?.filter(langItem => langItem !== item)
139
- filters.push({ key: "language", value: item, removable: languages.length > 1, default: aux.join(",") })
140
- })
141
-
142
- if (params.filter !== null) {
143
- const splittedParam = params.filter.split(",")
144
-
145
- splittedParam.forEach((item, index) => {
146
- const aux = item.split(".shortId=")
147
- const name = aux[0]
148
- const shortId = aux[1]
149
-
150
- const defaultValue = [...splittedParam]
151
- defaultValue.splice(index, 1)
152
-
153
- if (!tags[name]) return;
154
-
155
- const tag = tags[name].find(el => el.shortId === shortId)
156
- if (!tag) return;
157
-
158
- const value = defaultValue.length == 0 ? null : defaultValue.join(",")
159
-
160
- filters.push({ key: "filter", name: t(`filter.tags.${name}`), value: tag.label, removable: true, default: value })
161
- })
162
- }
163
-
164
- if (params.packages !== null && tags["packages"]) {
165
- const packageTag = tags["packages"]?.find(el => el.shortId === params.packages)
166
- filters.push({
167
- key: "packages",
168
- name: t("filter.tags.packages"),
169
- value: packageTag.label,
170
- removable: true,
171
- default: null
172
- })
173
- }
174
-
175
- Object.keys(params)
176
- .filter(item => !["page", "search", "language", "operator", "like", "wildcard", "filter", "packages"].includes(item))
177
- .filter(item => params[item] != null)
178
- .forEach(item => {
179
- filters.push({ key: item, value: params[item], removable: true, default: null })
180
- })
181
-
182
- setFilters(filters)
183
- }
85
+ }, [tagsParam])
184
86
 
185
- const updateFilterParam = (key: string, item: any) => {
87
+ const updateFilterParam = (item: tagsType) => {
186
88
  setLoading(true)
187
- if (key === "packages") {
188
- setParams({ packages: item.shortId })
189
- return;
190
- } else {
191
- const value = `${key}.shortId=${item.shortId}`
192
- let aux = value
193
-
194
- if (params.filter != null) {
195
- const splittedParam = params.filter.split(",")
196
- const finalValue = [...splittedParam]
197
-
198
- const hasParams = params.filter.includes(key)
199
-
200
- if (hasParams) {
201
- let mainIndex = -1
202
-
203
- splittedParam.forEach((el, index) => {
204
- if (el.includes(key)) {
205
- mainIndex = index
206
- }
207
- })
208
- finalValue[mainIndex] = value
209
- } else {
210
- finalValue.push(value)
211
- }
212
89
 
213
- aux = finalValue.join(",")
214
- }
215
-
216
- setParams({ filter: aux })
90
+ if (item.shortId === "all") {
91
+ setParams({ filter: null })
92
+ return;
217
93
  }
94
+
95
+ const value = `applicableForTypes.shortId=${item.shortId}`
96
+ setParams({ filter: value })
218
97
  };
219
98
 
220
99
  return (
221
- <div className="container">
222
- {loading && <Loading opacity={true} />}
223
-
224
- <div className="grid grid-cols-12 gap-4 py-6">
225
- <div className="col-span-12 sm:col-span-10 md:col-span-10">
226
- <AutoComplete
227
- embedded={false}
228
- initialValue={params.search}
229
- searchByPackage={false}
230
- />
231
- </div>
232
- <div className="col-span-12 sm:col-span-2 md:col-span-2">
233
- <div className="flex justify-end">
234
- <DialogFilter
235
- setLoading={setLoading}
236
- trigger={(
237
- <Button variant="default" disabled={disabled}>{t("filter.filters")}</Button>
238
- )}
100
+ <div className="container pt-6">
101
+ <div className="flex flex-row gap-6 pb-6">
102
+ <div className="flex-1">
103
+ <div className="pb-6">
104
+ <AutoComplete
105
+ embedded={false}
106
+ initialValue={params.search}
107
+ searchByPackage={false}
239
108
  />
240
109
  </div>
241
- </div>
242
- </div>
243
110
 
244
- {filters != null && filters.length > 0 && (
245
- <div className="pb-4 flex justify-between">
246
- <div>
247
-
248
- {filters?.map((item) => (
249
- <Badge key={`${item.key}-${item?.value}`} variant="outline" className="mr-2 mb-2 h-6">
250
- {item?.name ? item.name : item.key}: {item.value}
251
- {item.removable && (
252
- <Button size="xs" variant="ghost" onClick={() => {
253
- setLoading(true)
254
- setParams({ [item.key]: item?.default })
255
- }}>
256
- <X className="h-2" />
257
- </Button>
258
- )}
259
- </Badge>
260
- ))}
261
-
262
- </div>
263
- <Button
264
- size="sm"
265
- variant="outline"
266
- disabled={params.filter === null}
267
- onClick={() => {
268
- setLoading(true)
269
- setParams({ filter: null, packages: null })
270
- }}
271
- >
272
- {t("filter.clearFilters")}
273
- <Trash2 className="h-2" />
274
- </Button>
111
+ {data?.items?.length == 0 ? (
112
+ <Empty />
113
+ ) : (
114
+ <Fragment>
115
+ <BlogView items={data.items} />
116
+
117
+ <Pagination
118
+ totalPages={data.pageInfo.pageCount}
119
+ currentPage={data.pageInfo.pageNumber}
120
+ />
121
+ </Fragment>
122
+ )}
275
123
  </div>
276
- )}
277
124
 
278
- <div className="flex flex-row gap-6 pb-6">
279
- {data.items.length > 0 && (
280
- <div className="w-80 bg-sidebar rounded-md border pb-4">
281
- <SidebarHeader className="text-center font-bold">
282
- Search Filters
283
- </SidebarHeader>
284
- <SidebarContent>
285
- {Object.entries(tags).map(([key, value]: any) => (
286
- <SidebarGroup key={key} className="py-0">
287
- <SidebarGroupLabel>
288
- {t(`filter.tags.${key}`)}
289
- </SidebarGroupLabel>
290
- <SidebarGroupContent>
125
+ {tags.length > 0 && (
126
+ <div className="relative w-80">
127
+ <div className="sticky top-24">
128
+
129
+ <Card>
130
+ <SidebarHeader className="text-center font-bold">
131
+ {t("filter.categories")}
132
+ </SidebarHeader>
133
+ <SidebarContent>
134
+ <SidebarGroup className="py-0">
291
135
  <SidebarMenu>
292
- <SidebarMenuSub>
293
- {value.map(item => (
294
- <SidebarMenuSubItem key={item.shortId}>
295
- <SidebarMenuSubButton
296
- className="cursor-pointer"
297
- isActive={item.active}
298
- onClick={() => updateFilterParam(key, item)}
299
- >
300
- {item.label} ({item.hits}/{item.total})
301
- </SidebarMenuSubButton>
302
- </SidebarMenuSubItem>
303
- ))}
304
- </SidebarMenuSub>
136
+
137
+ {tags.map((tagItem) => (
138
+
139
+ <SidebarMenuSubItem key={tagItem.shortId}>
140
+ <SidebarMenuSubButton
141
+ className="cursor-pointer"
142
+ isActive={tagItem.active}
143
+ onClick={() => {
144
+ updateFilterParam(tagItem)
145
+ setLoading(true)
146
+ }}
147
+ >
148
+ {tagItem.label}
149
+ </SidebarMenuSubButton>
150
+ </SidebarMenuSubItem>
151
+ ))}
305
152
  </SidebarMenu>
306
- </SidebarGroupContent>
307
- </SidebarGroup>
308
- ))}
309
- </SidebarContent>
153
+ </SidebarGroup>
154
+ </SidebarContent>
155
+ </Card>
156
+ </div>
310
157
  </div>
311
158
  )}
312
-
313
- <div className="flex-1">
314
- <ResultList
315
- configs={configs}
316
- items={data.items}
317
- pagination={data.pageInfo}
318
- />
319
- </div>
320
159
  </div>
321
160
  </div>
322
161
  );
@@ -139,4 +139,18 @@ export const extractHtmlContent = (htmlString: string) => {
139
139
  metaTags,
140
140
  articleHtml,
141
141
  }
142
+ }
143
+
144
+ export const getTeaserInfo = (htmlString: string): {
145
+ imageSrc: string | null,
146
+ desc: string | null,
147
+ shortDesc: string | null
148
+ } => {
149
+ const $ = cheerio.load(htmlString)
150
+
151
+ return {
152
+ imageSrc: $('.teaserfig img').attr('src') || null,
153
+ desc: $('.teaser-p').text() || null,
154
+ shortDesc: $('.shortdesc').text() || null,
155
+ }
142
156
  }
@@ -1,12 +1,31 @@
1
- import { getInfoWithCache, getPrimaryInfo } from "./utils";
1
+ import { extractHtmlContent, getInfoWithCache, getPrimaryInfo } from "./utils";
2
2
  import { ArticleWrapper } from "./wrapper";
3
3
  import { PageWrapper } from "@c-rex/components/page-wrapper";
4
4
  import { DOCUMENTS_TYPE_AND_LINK } from "@c-rex/constants";
5
+ import { Metadata } from "next";
5
6
 
6
7
  const infoCache = new Map<string, Awaited<ReturnType<typeof getPrimaryInfo>>>();
7
8
 
9
+ export const generateMetadata = async (
10
+ { params }: { params: { id: string } }
11
+ ): Promise<Metadata> => {
12
+ const { htmlContent, title } = await getInfoWithCache(params.id, DOCUMENTS_TYPE_AND_LINK, infoCache);
13
+ const { metaTags } = extractHtmlContent(htmlContent)
14
+
15
+ const other: Record<string, string[]> = {}
16
+
17
+ for (const { name, content } of metaTags) {
18
+ if (Object.keys(other).includes(name)) {
19
+ other[name]?.push(content)
20
+ } else {
21
+ other[name] = [content]
22
+ }
23
+ }
24
+
25
+ return { ...other, title }
26
+ }
27
+
8
28
  export const DocumentsPageTemplate = async ({ params }: { params: { id: string } }) => {
9
- /*
10
29
  const {
11
30
  htmlContent,
12
31
  title,
@@ -15,11 +34,6 @@ export const DocumentsPageTemplate = async ({ params }: { params: { id: string }
15
34
  documentLang,
16
35
  packageId
17
36
  } = await getInfoWithCache(params.id, DOCUMENTS_TYPE_AND_LINK, infoCache);
18
- */
19
-
20
-
21
-
22
- const { htmlContent, title, availableVersions, packageId, articleLang, documentLang } = await getPrimaryInfo(params.id, DOCUMENTS_TYPE_AND_LINK);
23
37
 
24
38
  return (
25
39
  <PageWrapper title={title} pageType="DOC">
@@ -1,83 +1,92 @@
1
1
  import { DirectoryNodesService, InformationUnitsService, RenditionsService } from "@c-rex/services";
2
2
  import { DOCUMENTS_TYPE_AND_LINK, TOPICS_TYPE_AND_LINK } from "@c-rex/constants";
3
- import { DirectoryNodes, informationUnitsResponseItem, SidebarAvailableVersionsInterface } from "@c-rex/interfaces";
3
+ import { DirectoryNodes, informationUnitsItems, informationUnitsResponseItem, SidebarAvailableVersionsInterface } from "@c-rex/interfaces";
4
4
  import * as cheerio from 'cheerio'
5
5
  import { BLOG_TYPE_AND_LINK } from "@c-rex/constants";
6
6
 
7
- /*
8
- * Get primary info for a given information unit
9
- * @param id: string
10
- * @param type: string
11
- * @returns { htmlContent: string, title: string }
12
- */
13
- export const getPrimaryInfo = async (id: string, type: string): Promise<{
7
+
8
+ type ReturnType = {
14
9
  htmlContent: string,
15
10
  title: string,
16
11
  articleLang: string,
17
12
  packageId: string,
18
13
  documentLang: string,
19
14
  availableVersions: SidebarAvailableVersionsInterface[]
20
- }> => {
15
+ }
16
+
17
+ /*
18
+ * Get primary info for a given information unit
19
+ * @param id: string
20
+ * @param type: string
21
+ * @returns { htmlContent: string, title: string }
22
+ */
23
+ export const getPrimaryInfo = async (id: string, type: string): Promise<ReturnType> => {
21
24
  const renditionService = new RenditionsService();
22
25
  const informationService = new InformationUnitsService();
26
+ const directoryNodeService = new DirectoryNodesService();
23
27
  const informationUnitsItem = await informationService.getItem({ id });
24
28
 
25
29
  let title = informationUnitsItem.labels[0]?.value
26
- const versionOf = informationUnitsItem.versionOf.shortId
30
+
31
+ // const document: informationUnitsItems = null
27
32
  let documentLang = ""
33
+ const promiseList: any = []
28
34
  const articleLang = informationUnitsItem.languages[0];
29
35
 
30
- const promiseList: any = [
36
+ const [versions, rootNode] = await Promise.all([
31
37
  informationService.getList({
32
- restrict: [`versionOf.shortId=${versionOf}`],
38
+ restrict: [`versionOf.shortId=${informationUnitsItem.versionOf.shortId}`],
33
39
  fields: ["renditions", "class", "languages", "labels"],
34
40
  }),
35
- ]
41
+ getRootNode(informationUnitsItem.directoryNodes)
42
+ ])
43
+ const availableVersions = versions.items.map((item: informationUnitsResponseItem) => {
44
+ return {
45
+ shortId: item.shortId,
46
+ link: `/${type}/${item.shortId}`,
47
+ lang: item.language,
48
+ country: item.language.split("-")[1],
49
+ active: item.language === articleLang,
50
+ }
51
+ }).sort((a: SidebarAvailableVersionsInterface, b: SidebarAvailableVersionsInterface) => {
52
+ if (a.lang < b.lang) return -1;
53
+ if (a.lang > b.lang) return 1;
54
+ return 0;
55
+ }) as SidebarAvailableVersionsInterface[];
56
+
36
57
 
37
- const rootNode = await getRootNode(informationUnitsItem.directoryNodes)
38
58
 
39
59
  if (rootNode != null) {
40
60
  title = rootNode.informationUnits[0]?.labels[0]?.value;
41
61
  documentLang = rootNode.informationUnits[0]?.labels[0]?.language as string;
62
+
63
+
64
+ const infoId = rootNode.informationUnits[0]?.shortId as string;
65
+
66
+ document = await informationService.getItem({ id: infoId });
67
+
42
68
  }
43
69
 
44
70
  if ([TOPICS_TYPE_AND_LINK, BLOG_TYPE_AND_LINK].includes(type)) {
45
- promiseList.push(
46
- renditionService.getHTMLRendition({ renditions: informationUnitsItem.renditions })
47
- )
71
+ promiseList.push(renditionService.getHTMLRendition({ renditions: informationUnitsItem.renditions }))
48
72
 
49
73
  } else if (rootNode != null && type == DOCUMENTS_TYPE_AND_LINK) {
50
74
 
51
- const service = new DirectoryNodesService();
52
75
 
53
76
  const directoryId = rootNode.childNodes[0]?.shortId as string;
54
77
 
55
- const response = await service.getItem(directoryId);
78
+ const response = await directoryNodeService.getItem(directoryId);
56
79
 
57
80
  const infoId = response.informationUnits[0]?.shortId;
58
81
 
59
82
  const childInformationUnit = await informationService.getItem({ id: infoId as string });
60
83
 
61
- promiseList.push(
62
- renditionService.getHTMLRendition({ renditions: childInformationUnit.renditions })
63
- )
84
+ promiseList.push(renditionService.getHTMLRendition({ renditions: childInformationUnit.renditions }))
64
85
  }
65
86
 
66
- const [versions, htmlContent] = await Promise.all(promiseList)
87
+ const [htmlContent] = await Promise.all(promiseList)
88
+
67
89
 
68
- const availableVersions = versions.items.map((item: informationUnitsResponseItem) => {
69
- return {
70
- shortId: item.shortId,
71
- link: `/${type}/${item.shortId}`,
72
- lang: item.language,
73
- country: item.language.split("-")[1],
74
- active: item.language === articleLang,
75
- }
76
- }).sort((a: SidebarAvailableVersionsInterface, b: SidebarAvailableVersionsInterface) => {
77
- if (a.lang < b.lang) return -1;
78
- if (a.lang > b.lang) return 1;
79
- return 0;
80
- }) as SidebarAvailableVersionsInterface[];
81
90
 
82
91
  return {
83
92
  htmlContent,
@@ -9,10 +9,12 @@ import { RenderArticle } from "@c-rex/components/render-article";
9
9
  import { CloudDownload, Eye } from "lucide-react";
10
10
  import { informationUnitsItems, SidebarAvailableVersionsInterface, TreeOfContent } from "@c-rex/interfaces";
11
11
  import { DOCUMENTS_TYPE_AND_LINK, TOPICS_TYPE_AND_LINK } from "@c-rex/constants";
12
- import { call, generateBreadcrumbItems, generateTreeOfContent, getFileRenditions } from "@c-rex/utils";
12
+ import { call, formatDateToLocale, generateBreadcrumbItems, generateTreeOfContent, getFileRenditions } from "@c-rex/utils";
13
13
  import { DropdownMenu } from "@c-rex/components/dropdown-menu";
14
14
  import { FileRenditionType } from "@c-rex/types";
15
15
  import { useAppConfig } from "@c-rex/contexts/config-provider";
16
+ import { InfoCard } from "@c-rex/components/info-card";
17
+ import { useTranslations } from "next-intl";
16
18
 
17
19
  type Props = {
18
20
  htmlContent: string,
@@ -24,13 +26,43 @@ type Props = {
24
26
  availableVersions: SidebarAvailableVersionsInterface[],
25
27
  packageId: string,
26
28
  }
29
+ type DocumentsType = {
30
+ filesToDownload: {
31
+ format: string;
32
+ link: string;
33
+ }[],
34
+ filesToOpen: {
35
+ format: string,
36
+ link: string,
37
+ }[]
38
+ }
27
39
 
28
40
  const loadArticleData = async (id: string, type: string, title: string) => {
29
41
  const informationUnitsItem = await call<informationUnitsItems>('InformationUnitsService.getItem', { id: id });
30
42
  const { rootNode, result: treeOfContent } = await generateTreeOfContent(informationUnitsItem.directoryNodes);
31
-
32
43
  const articleLanguage = informationUnitsItem.languages[0]
33
44
 
45
+ const info: { label: string, value: string }[] = []
46
+
47
+ if (informationUnitsItem.created) {
48
+ info.push({
49
+ label: "createdAt",
50
+ value: formatDateToLocale(informationUnitsItem.created, articleLanguage)
51
+ })
52
+ }
53
+
54
+ if (informationUnitsItem.revision) {
55
+ info.push({
56
+ label: "revision",
57
+ value: informationUnitsItem.revision
58
+ })
59
+ }
60
+
61
+
62
+ // add title or label. title priority
63
+ //add language (use flag icon)
64
+
65
+
34
66
  let documents: {
35
67
  filesToDownload: FileRenditionType[];
36
68
  filesToOpen: FileRenditionType[];
@@ -67,26 +99,29 @@ const loadArticleData = async (id: string, type: string, title: string) => {
67
99
  treeOfContent,
68
100
  breadcrumbItems,
69
101
  documents,
70
- articleLanguage
102
+ articleLanguage,
103
+ info
71
104
  }
72
105
  };
73
106
 
74
- export const ArticleWrapper = ({ htmlContent, title, id, type, documentLang, articleLang, availableVersions, packageId }: Props) => {
75
- console.log(availableVersions)
107
+ export const ArticleWrapper = ({
108
+ htmlContent,
109
+ title,
110
+ id,
111
+ type,
112
+ documentLang,
113
+ articleLang,
114
+ availableVersions,
115
+ packageId
116
+ }: Props) => {
117
+ const t = useTranslations();
118
+
76
119
  const { setPackageID, setArticleLang } = useAppConfig()
77
120
  const [loading, setLoading] = useState<boolean>(true)
78
121
  const [breadcrumbItems, setBreadcrumbItems] = useState<TreeOfContent[]>([])
79
122
  const [treeOfContent, setTreeOfContent] = useState<TreeOfContent[]>([])
80
- const [documents, setDocuments] = useState<{
81
- filesToDownload: {
82
- format: string;
83
- link: string;
84
- }[],
85
- filesToOpen: {
86
- format: string,
87
- link: string,
88
- }[]
89
- }>({
123
+ const [articleInfo, setArticleInfo] = useState<any>([])
124
+ const [documents, setDocuments] = useState<DocumentsType>({
90
125
  filesToDownload: [],
91
126
  filesToOpen: [],
92
127
  })
@@ -100,11 +135,13 @@ export const ArticleWrapper = ({ htmlContent, title, id, type, documentLang, art
100
135
  treeOfContent,
101
136
  documents,
102
137
  breadcrumbItems,
138
+ info
103
139
  } = await loadArticleData(id, type, title)
104
140
 
105
141
  setTreeOfContent(treeOfContent)
106
142
  setDocuments(documents)
107
143
  setBreadcrumbItems(breadcrumbItems)
144
+ setArticleInfo(info)
108
145
  setLoading(false)
109
146
  }
110
147
 
@@ -113,8 +150,6 @@ export const ArticleWrapper = ({ htmlContent, title, id, type, documentLang, art
113
150
 
114
151
  return (
115
152
  <SidebarProvider>
116
- <title>{title}</title>
117
-
118
153
  <CheckArticleLangToast availableVersions={availableVersions} />
119
154
 
120
155
  <AppSidebar
@@ -124,27 +159,39 @@ export const ArticleWrapper = ({ htmlContent, title, id, type, documentLang, art
124
159
  loading={loading}
125
160
  />
126
161
  <SidebarInset>
127
- <div className="container flex flex-col">
128
- <header className="flex h-16 shrink-0 items-center justify-between">
129
- <Breadcrumb items={breadcrumbItems} loading={loading} lang={documentLang} />
130
-
131
- <div className="flex">
132
- {documents.filesToDownload.length > 0 && (
133
- <DropdownMenu
134
- items={documents.filesToDownload}
135
- icon={<CloudDownload />}
136
- />
137
- )}
138
- {documents.filesToOpen.length > 0 && (
139
- <DropdownMenu
140
- items={documents.filesToOpen}
141
- icon={<Eye />}
162
+ <div className="flex flex-row gap-4 p-4">
163
+ <div className="flex flex-1 flex-col">
164
+ <header className="flex h-12 shrink-0 items-center justify-between">
165
+ <Breadcrumb items={breadcrumbItems} loading={loading} lang={documentLang} />
166
+
167
+ <div className="flex">
168
+ {documents.filesToDownload.length > 0 && (
169
+ <DropdownMenu
170
+ items={documents.filesToDownload}
171
+ icon={<CloudDownload />}
172
+ />
173
+ )}
174
+ {documents.filesToOpen.length > 0 && (
175
+ <DropdownMenu
176
+ items={documents.filesToOpen}
177
+ icon={<Eye />}
178
+ />
179
+ )}
180
+ </div>
181
+ </header>
182
+ <RenderArticle htmlContent={htmlContent} contentLang={articleLang} />
183
+ </div>
184
+
185
+ <div className="w-60 relative">
186
+ <div className="sticky top-28 gap-4 flex flex-col">
187
+ {!loading && (
188
+ <InfoCard
189
+ title={t("about")}
190
+ items={articleInfo}
142
191
  />
143
192
  )}
144
193
  </div>
145
- </header>
146
-
147
- <RenderArticle htmlContent={htmlContent} contentLang={articleLang} />
194
+ </div>
148
195
  </div>
149
196
  </SidebarInset>
150
197
  </SidebarProvider>
@@ -1,8 +1,9 @@
1
- import { informationUnitsResponse } from "@c-rex/interfaces";
2
- import { InformationUnitsService } from "@c-rex/services";
3
1
  import { HomePage } from "./page";
4
2
  import { PageWrapper } from "@c-rex/components/page-wrapper";
5
3
  import { WildCardType } from "@c-rex/types";
4
+ import { InformationUnitsService } from "@c-rex/services";
5
+ import { informationUnitsResponse } from "@c-rex/interfaces";
6
+ import { SearchProvider } from "@c-rex/contexts/search";
6
7
 
7
8
  interface HomeProps {
8
9
  searchParams: {
@@ -52,8 +53,10 @@ export const HomeLayout = async ({ searchParams }: HomeProps) => {
52
53
  }
53
54
 
54
55
  return (
55
- <PageWrapper title="" pageType="HOME">
56
- <HomePage data={data} />
57
- </PageWrapper>
56
+ <SearchProvider>
57
+ <PageWrapper title="" pageType="HOME">
58
+ <HomePage data={data} />
59
+ </PageWrapper>
60
+ </SearchProvider>
58
61
  );
59
62
  };
@@ -1,12 +1,13 @@
1
1
  "use client";
2
2
 
3
3
  import React, { FC, useEffect, useState } from "react";
4
- import { Trash2, X } from "lucide-react"
4
+ import { ChevronDown, Trash2, X } from "lucide-react"
5
5
  import { useTranslations } from 'next-intl'
6
6
  import { parseAsBoolean, parseAsInteger, parseAsString, useQueryStates } from 'nuqs'
7
7
  import { informationUnitsResponse } from "@c-rex/interfaces";
8
8
  import { Button } from "@c-rex/ui/button";
9
9
  import { Badge } from "@c-rex/ui/badge";
10
+ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@c-rex/ui/collapsible";
10
11
  import {
11
12
  SidebarContent,
12
13
  SidebarGroup,
@@ -20,10 +21,9 @@ import {
20
21
  } from "@c-rex/ui/sidebar";
21
22
  import { ResultList } from "@c-rex/components/result-list";
22
23
  import { DialogFilter } from "@c-rex/components/dialog-filter";
23
- import { useAppConfig } from "@c-rex/contexts/config-provider";
24
24
  import { AutoComplete } from "@c-rex/components/autocomplete";
25
- import { OPERATOR_OPTIONS, WILD_CARD_OPTIONS } from "@c-rex/constants";
26
- import { Loading } from "@c-rex/components/loading";
25
+ import { OPERATOR_OPTIONS } from "@c-rex/constants";
26
+ import { useSearchContext } from "@c-rex/contexts/search";
27
27
 
28
28
  interface HomePageProps {
29
29
  data: informationUnitsResponse;
@@ -39,12 +39,11 @@ type filterModel = {
39
39
 
40
40
  export const HomePage: FC<HomePageProps> = ({ data }) => {
41
41
  const t = useTranslations();
42
- const { configs } = useAppConfig()
42
+ const { setLoading } = useSearchContext();
43
43
 
44
44
  const [tags, setTags] = useState<{ [key: string]: any[] }>(data.tags || {});
45
45
  const [filters, setFilters] = useState<filterModel[] | null>(null)
46
46
  const [disabled, setDisabled] = useState<boolean>(false)
47
- const [loading, setLoading] = useState<boolean>(true)
48
47
  const [params, setParams] = useQueryStates({
49
48
  language: parseAsString,
50
49
  page: parseAsInteger,
@@ -118,19 +117,16 @@ export const HomePage: FC<HomePageProps> = ({ data }) => {
118
117
  key: "operator",
119
118
  name: t("filter.operator"),
120
119
  value: `${params?.operator !== OPERATOR_OPTIONS.OR}`,
121
- default: OPERATOR_OPTIONS.OR,
122
- removable: params?.operator !== OPERATOR_OPTIONS.OR
120
+ removable: false
123
121
  }, {
124
122
  key: "like",
125
123
  name: t("filter.like"),
126
124
  value: `${params.like}`,
127
- default: false,
128
- removable: params?.like as boolean
125
+ removable: false
129
126
  }, {
130
127
  key: "wildcard",
131
128
  value: params.wildcard as string,
132
- default: WILD_CARD_OPTIONS.NONE,
133
- removable: params?.wildcard !== WILD_CARD_OPTIONS.NONE
129
+ removable: false,
134
130
  }]
135
131
 
136
132
  const languages = params.language?.split(",")
@@ -161,8 +157,15 @@ export const HomePage: FC<HomePageProps> = ({ data }) => {
161
157
  })
162
158
  }
163
159
 
164
- if (params.packages !== null && tags["packages"]) {
165
- const packageTag = tags["packages"]?.find(el => el.shortId === params.packages)
160
+ if (params.packages !== null) {
161
+ let packageTag = {
162
+ label: params.packages,
163
+ }
164
+
165
+ if (tags["packages"]) {
166
+ packageTag = tags["packages"]?.find(el => el.shortId === params.packages)
167
+ }
168
+
166
169
  filters.push({
167
170
  key: "packages",
168
171
  name: t("filter.tags.packages"),
@@ -220,8 +223,6 @@ export const HomePage: FC<HomePageProps> = ({ data }) => {
220
223
 
221
224
  return (
222
225
  <div className="container">
223
- {loading && <Loading opacity={true} />}
224
-
225
226
  <div className="grid grid-cols-12 gap-4 py-6">
226
227
  <div className="col-span-12 sm:col-span-10 md:col-span-10">
227
228
  <AutoComplete
@@ -233,7 +234,6 @@ export const HomePage: FC<HomePageProps> = ({ data }) => {
233
234
  <div className="col-span-12 sm:col-span-2 md:col-span-2">
234
235
  <div className="flex justify-end">
235
236
  <DialogFilter
236
- setLoading={setLoading}
237
237
  trigger={(
238
238
  <Button variant="default" disabled={disabled}>{t("filter.filters")}</Button>
239
239
  )}
@@ -279,33 +279,47 @@ export const HomePage: FC<HomePageProps> = ({ data }) => {
279
279
  <div className="flex flex-row gap-6 pb-6">
280
280
  {data.items.length > 0 && (
281
281
  <div className="w-80 bg-sidebar rounded-md border pb-4">
282
- <SidebarHeader className="text-center font-bold">
283
- Search Filters
282
+ <SidebarHeader className="!flex-row justify-center items-end font-bold">
283
+ {t("filter.filters")}
284
+ <span className="text-xs text-muted-foreground leading-5">
285
+ {data.pageInfo.totalItemCount} {t("results.results")}
286
+ </span>
284
287
  </SidebarHeader>
285
- <SidebarContent>
288
+ <SidebarContent className="!gap-0">
289
+
286
290
  {Object.entries(tags).map(([key, value]: any) => (
287
- <SidebarGroup key={key} className="py-0">
288
- <SidebarGroupLabel>
289
- {t(`filter.tags.${key}`)}
290
- </SidebarGroupLabel>
291
- <SidebarGroupContent>
292
- <SidebarMenu>
293
- <SidebarMenuSub>
294
- {value.map((item: any) => (
295
- <SidebarMenuSubItem key={item.shortId}>
296
- <SidebarMenuSubButton
297
- className="cursor-pointer"
298
- isActive={item.active}
299
- onClick={() => updateFilterParam(key, item)}
300
- >
301
- {item.label} ({item.hits}/{item.total})
302
- </SidebarMenuSubButton>
303
- </SidebarMenuSubItem>
304
- ))}
305
- </SidebarMenuSub>
306
- </SidebarMenu>
307
- </SidebarGroupContent>
308
- </SidebarGroup>
291
+
292
+ <Collapsible defaultOpen key={key} className="py-0 group/collapsible">
293
+ <SidebarGroup>
294
+
295
+ <SidebarGroupLabel asChild className="hover:bg-sidebar-accent text-sidebar-accent-foreground text-sm font-bold">
296
+ <CollapsibleTrigger className="!h-9">
297
+ {t(`filter.tags.${key}`)}
298
+ <ChevronDown className="ml-auto transition-transform group-data-[state=open]/collapsible:rotate-180" />
299
+ </CollapsibleTrigger>
300
+ </SidebarGroupLabel>
301
+
302
+ <CollapsibleContent>
303
+ <SidebarGroupContent>
304
+ <SidebarMenu>
305
+ <SidebarMenuSub>
306
+ {value.map((item: any) => (
307
+ <SidebarMenuSubItem key={item.shortId}>
308
+ <SidebarMenuSubButton
309
+ className="cursor-pointer"
310
+ isActive={item.active}
311
+ onClick={() => updateFilterParam(key, item)}
312
+ >
313
+ {item.label} ({item.hits}/{item.total})
314
+ </SidebarMenuSubButton>
315
+ </SidebarMenuSubItem>
316
+ ))}
317
+ </SidebarMenuSub>
318
+ </SidebarMenu>
319
+ </SidebarGroupContent>
320
+ </CollapsibleContent>
321
+ </SidebarGroup>
322
+ </Collapsible>
309
323
  ))}
310
324
  </SidebarContent>
311
325
  </div>
@@ -313,7 +327,6 @@ export const HomePage: FC<HomePageProps> = ({ data }) => {
313
327
 
314
328
  <div className="flex-1">
315
329
  <ResultList
316
- configs={configs}
317
330
  items={data.items}
318
331
  pagination={data.pageInfo}
319
332
  />