@c-rex/components 0.1.22 → 0.1.24
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 +84 -64
- package/src/article/article-action-bar.tsx +89 -0
- package/src/article/article-content.tsx +55 -0
- package/src/autocomplete.tsx +55 -50
- package/src/breadcrumb.tsx +3 -1
- package/src/directoryNodes/tree-of-content.tsx +49 -0
- package/src/{bookmark-button.tsx → favorites/bookmark-button.tsx} +12 -3
- package/src/{favorite-button.tsx → favorites/favorite-button.tsx} +1 -1
- package/src/generated/client-components.tsx +1350 -0
- package/src/generated/create-client-request.tsx +105 -0
- package/src/generated/create-server-request.tsx +61 -0
- package/src/generated/create-suggestions-request.tsx +56 -0
- package/src/generated/server-components.tsx +1056 -0
- package/src/generated/suggestions.tsx +299 -0
- package/src/info/info-table.tsx +127 -60
- package/src/info/shared.tsx +2 -8
- package/src/navbar/language-switcher/shared.tsx +1 -1
- package/src/navbar/navbar.tsx +1 -1
- package/src/{stories → navbar/stories}/navbar.stories.tsx +1 -1
- package/src/page-wrapper.tsx +1 -1
- package/src/renditions/file-download.tsx +84 -0
- package/src/renditions/html.tsx +55 -0
- package/src/renditions/image/container.tsx +52 -0
- package/src/renditions/image/rendition.tsx +61 -0
- package/src/{dialog-filter.tsx → results/dialog-filter.tsx} +22 -23
- package/src/results/filter-navbar.tsx +241 -0
- package/src/results/filter-sidebar/index.tsx +125 -0
- package/src/results/filter-sidebar/utils.ts +164 -0
- package/src/{pagination.tsx → results/pagination.tsx} +12 -10
- package/src/results/result-container.tsx +70 -0
- package/src/{stories/blog-view.stories.tsx → results/stories/cards.stories.tsx} +1 -1
- package/src/{stories/table-view.stories.tsx → results/stories/table.stories.tsx} +1 -1
- package/src/results/table-with-images.tsx +140 -0
- package/src/{result-view → results}/table.tsx +1 -2
- package/src/results/utils.ts +67 -0
- package/src/{navbar/search-input.tsx → search-input.tsx} +9 -6
- package/src/share-button.tsx +49 -0
- package/src/stores/search-settings-store.ts +1 -1
- package/src/blur-image.tsx +0 -23
- package/src/info/info-card.tsx +0 -44
- package/src/left-sidebar.tsx +0 -90
- package/src/result-list.tsx +0 -43
- package/src/result-view/table-with-images.tsx +0 -199
- package/src/right-sidebar.tsx +0 -70
- package/src/search-modal.tsx +0 -140
- package/src/stories/blur-image.stories.tsx +0 -51
- package/src/stories/sidebar.stories.tsx +0 -94
- /package/src/{file-icon.tsx → icons/file-icon.tsx} +0 -0
- /package/src/{flag.tsx → icons/flag-icon.tsx} +0 -0
- /package/src/{loading.tsx → icons/loading.tsx} +0 -0
- /package/src/{result-view/blog.tsx → results/cards.tsx} +0 -0
- /package/src/{empty.tsx → results/empty.tsx} +0 -0
- /package/src/{stories → results/stories}/empty.stories.tsx +0 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { FC } from "react";
|
|
2
|
+
import * as cheerio from "cheerio"
|
|
3
|
+
import { FragmentsGetAll } from "../generated/server-components";
|
|
4
|
+
|
|
5
|
+
interface HtmlRenditionProps {
|
|
6
|
+
htmlFormats?: string[]
|
|
7
|
+
shortId: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const HtmlRendition: FC<HtmlRenditionProps> = ({
|
|
11
|
+
shortId,
|
|
12
|
+
htmlFormats = ["application/xhtml+xml", "application/html", "text/html"]
|
|
13
|
+
}) => {
|
|
14
|
+
const empty = <div>No rendition available</div>;
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<FragmentsGetAll
|
|
18
|
+
queryParams={{
|
|
19
|
+
Fields: ["titles", "renditions"],
|
|
20
|
+
Embed: ["renditions"],
|
|
21
|
+
PageSize: 1,
|
|
22
|
+
Links: true,
|
|
23
|
+
Restrict: [
|
|
24
|
+
`informationUnits=${shortId}`,
|
|
25
|
+
...htmlFormats.map(format => `renditions.format=${format}`)
|
|
26
|
+
],
|
|
27
|
+
|
|
28
|
+
}}
|
|
29
|
+
render={async (data, error) => {
|
|
30
|
+
if (error) {
|
|
31
|
+
return <div>Error loading content</div>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const renditions = data?.items?.[0]?.renditions;
|
|
35
|
+
|
|
36
|
+
if (renditions == null || renditions.length == 0) return empty;
|
|
37
|
+
if (renditions.length == 0 || renditions[0] == undefined || renditions[0].links == undefined) return empty;
|
|
38
|
+
|
|
39
|
+
const filteredLinks = renditions[0].links.filter((item) => item.rel == "view");
|
|
40
|
+
|
|
41
|
+
if (filteredLinks.length == 0 || filteredLinks[0] == undefined || filteredLinks[0].href == undefined) return empty;
|
|
42
|
+
|
|
43
|
+
const url = filteredLinks[0].href;
|
|
44
|
+
const html = await fetch(url).then(res => res.text());
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
const $ = cheerio.load(html)
|
|
48
|
+
|
|
49
|
+
const articleHtml = $("body").html() || ""
|
|
50
|
+
|
|
51
|
+
return <div dangerouslySetInnerHTML={{ __html: articleHtml }} />;
|
|
52
|
+
}}
|
|
53
|
+
/>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { FC } from "react";
|
|
4
|
+
import { FragmentsGetAllClient } from "../../generated/client-components";
|
|
5
|
+
import { ImageRendition } from "./rendition";
|
|
6
|
+
import { Skeleton } from "@c-rex/ui/skeleton";
|
|
7
|
+
|
|
8
|
+
interface ImageContainerProps {
|
|
9
|
+
itemShortId: string;
|
|
10
|
+
imageRestrictions?: string[];
|
|
11
|
+
imageFormats?: string[];
|
|
12
|
+
emptyImageStyle?: string;
|
|
13
|
+
imageStyle?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const ImageRenditionContainer: FC<ImageContainerProps> = ({
|
|
17
|
+
itemShortId,
|
|
18
|
+
imageRestrictions,
|
|
19
|
+
emptyImageStyle,
|
|
20
|
+
imageStyle,
|
|
21
|
+
imageFormats = ["image/svg+xml", "image/gif", "image/png", "image/jpg"],
|
|
22
|
+
}) => {
|
|
23
|
+
const newRestrictions = [
|
|
24
|
+
`informationUnits=${itemShortId}`,
|
|
25
|
+
//...imageFormats.map(format => `renditions.format=${format}`)
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<FragmentsGetAllClient
|
|
30
|
+
queryParams={{
|
|
31
|
+
Fields: ["titles", "renditions"],
|
|
32
|
+
Embed: ["renditions"],
|
|
33
|
+
PageSize: 1,
|
|
34
|
+
Links: true,
|
|
35
|
+
Restrict: newRestrictions,
|
|
36
|
+
}}
|
|
37
|
+
>
|
|
38
|
+
{({ data, isLoading }) => (
|
|
39
|
+
isLoading ? (
|
|
40
|
+
<Skeleton className="w-full h-full" />
|
|
41
|
+
) : (
|
|
42
|
+
<ImageRendition
|
|
43
|
+
items={data?.items as any}
|
|
44
|
+
formats={imageFormats}
|
|
45
|
+
emptyImageStyle={emptyImageStyle}
|
|
46
|
+
imageStyle={imageStyle}
|
|
47
|
+
/>
|
|
48
|
+
)
|
|
49
|
+
)}
|
|
50
|
+
</FragmentsGetAllClient>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { FC } from "react";
|
|
4
|
+
import { ImageOff } from "lucide-react";
|
|
5
|
+
import { DocumentModel, ExternalProductGraphicModel, FragmentModel, InformationUnitModel, PackageModel, RenditionModel, TopicModel } from "@c-rex/interfaces";
|
|
6
|
+
import { cn } from "@c-rex/utils";
|
|
7
|
+
interface ImageRenditionProps {
|
|
8
|
+
items: (
|
|
9
|
+
DocumentModel |
|
|
10
|
+
ExternalProductGraphicModel |
|
|
11
|
+
FragmentModel |
|
|
12
|
+
InformationUnitModel |
|
|
13
|
+
PackageModel |
|
|
14
|
+
TopicModel
|
|
15
|
+
)[];
|
|
16
|
+
emptyImageStyle?: string;
|
|
17
|
+
imageStyle?: string;
|
|
18
|
+
formats: string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const ImageRendition: FC<ImageRenditionProps> = ({ items, formats, emptyImageStyle, imageStyle }) => {
|
|
22
|
+
if (!items || items.length === 0) return <ImageOff className={cn(emptyImageStyle, "text-muted")} />;
|
|
23
|
+
|
|
24
|
+
const item = items[0];
|
|
25
|
+
if (!item || item == undefined) return <ImageOff className={cn(emptyImageStyle, "text-muted")} />;
|
|
26
|
+
|
|
27
|
+
const wanted = formats.map((f) => f.toLowerCase());
|
|
28
|
+
let rendition = null
|
|
29
|
+
const renditions = items[0]?.renditions as RenditionModel[] || [];
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
for (const fmt of wanted) {
|
|
33
|
+
|
|
34
|
+
const found = renditions.find((r) => r.format?.toLowerCase() === fmt);
|
|
35
|
+
|
|
36
|
+
if (found) {
|
|
37
|
+
rendition = found;
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (rendition == null) return <ImageOff className={cn(emptyImageStyle, "text-muted")} />;
|
|
43
|
+
|
|
44
|
+
const src =
|
|
45
|
+
rendition.links?.find((l) => l.rel === "view")?.href ??
|
|
46
|
+
rendition.links?.find((l) => l.rel === "download")?.href ??
|
|
47
|
+
rendition.links?.find((l) => l.rel === "resource")?.href ??
|
|
48
|
+
rendition.source ??
|
|
49
|
+
"";
|
|
50
|
+
|
|
51
|
+
if (!src) return <ImageOff className={cn(emptyImageStyle, "text-muted")} />;
|
|
52
|
+
|
|
53
|
+
const alt =
|
|
54
|
+
item.labels?.[0]?.value ??
|
|
55
|
+
item.titles?.[0]?.value ??
|
|
56
|
+
"Document image";
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<img src={src} alt={alt} loading="lazy" className={cn(imageStyle, "w-full")} />
|
|
60
|
+
);
|
|
61
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
1
3
|
import React, { FC, useEffect, useState } from "react"
|
|
2
4
|
import { Button } from "@c-rex/ui/button"
|
|
3
5
|
import {
|
|
@@ -17,9 +19,9 @@ import { useTranslations } from "next-intl"
|
|
|
17
19
|
import { LanguageAndCountries } from "@c-rex/interfaces"
|
|
18
20
|
import { WILD_CARD_OPTIONS, OPERATOR_OPTIONS } from "@c-rex/constants"
|
|
19
21
|
import { Switch } from "@c-rex/ui/switch"
|
|
20
|
-
import { useSearchSettingsStore } from "
|
|
22
|
+
import { useSearchSettingsStore } from "../stores/search-settings-store"
|
|
21
23
|
import { OperatorType, WildCardType } from "@c-rex/types"
|
|
22
|
-
import { useLanguageStore } from "
|
|
24
|
+
import { useLanguageStore } from "../stores/language-store"
|
|
23
25
|
|
|
24
26
|
interface DialogFilterProps {
|
|
25
27
|
trigger: React.ReactNode;
|
|
@@ -32,30 +34,31 @@ interface Languages {
|
|
|
32
34
|
|
|
33
35
|
export const DialogFilter: FC<DialogFilterProps> = ({ trigger }) => {
|
|
34
36
|
const t = useTranslations("filter");
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
const savedOperator = useSearchSettingsStore((state) => state.operator)
|
|
38
|
-
const savedWildcard = useSearchSettingsStore((state) => state.wildcard)
|
|
39
|
-
const savedLanguages = useLanguageStore(state => state.contentLang);
|
|
37
|
+
const contentLang = useLanguageStore(state => state.contentLang);
|
|
38
|
+
const searchLanguages = useSearchSettingsStore(state => state.language);
|
|
40
39
|
const availableLanguagesAndCountries = useLanguageStore(state => state.availableLanguages);
|
|
41
40
|
|
|
42
|
-
const [like, setLike] = useState<boolean>(
|
|
43
|
-
const [operator, setOperator] = useState<string>(
|
|
44
|
-
const [wildcard, setWildcard] = useState<string>(
|
|
41
|
+
const [like, setLike] = useState<boolean>(useSearchSettingsStore.getState().like);
|
|
42
|
+
const [operator, setOperator] = useState<string>(useSearchSettingsStore.getState().operator);
|
|
43
|
+
const [wildcard, setWildcard] = useState<string>(useSearchSettingsStore.getState().wildcard);
|
|
44
|
+
const [languages, setLanguages] = useState<Languages[]>([]);
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const checked =
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
const aux: Languages[] = availableLanguagesAndCountries.map((item: LanguageAndCountries) => {
|
|
48
|
+
const checked = searchLanguages.length === 0
|
|
49
|
+
? item.value === contentLang
|
|
50
|
+
: searchLanguages.includes(item.value);
|
|
49
51
|
|
|
50
52
|
return {
|
|
51
53
|
lang: item.lang,
|
|
52
54
|
value: item.value,
|
|
53
|
-
checked,
|
|
55
|
+
checked: checked,
|
|
54
56
|
}
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
setLanguages(aux);
|
|
60
|
+
}, [availableLanguagesAndCountries, contentLang]);
|
|
57
61
|
|
|
58
|
-
const [languages, setLanguages] = useState<Languages[]>(generateLanguagesList());
|
|
59
62
|
const [params, setParams] = useQueryStates({
|
|
60
63
|
search: parseAsString,
|
|
61
64
|
language: parseAsString,
|
|
@@ -99,10 +102,6 @@ export const DialogFilter: FC<DialogFilterProps> = ({ trigger }) => {
|
|
|
99
102
|
});
|
|
100
103
|
}
|
|
101
104
|
|
|
102
|
-
useEffect(() => {
|
|
103
|
-
setLanguages(generateLanguagesList())
|
|
104
|
-
}, [availableLanguagesAndCountries])
|
|
105
|
-
|
|
106
105
|
return (
|
|
107
106
|
<Dialog>
|
|
108
107
|
<DialogTrigger asChild>
|
|
@@ -118,13 +117,13 @@ export const DialogFilter: FC<DialogFilterProps> = ({ trigger }) => {
|
|
|
118
117
|
{t("languages")}:
|
|
119
118
|
</Label>
|
|
120
119
|
<div className="flex items-center">
|
|
121
|
-
{languages?.map((item
|
|
120
|
+
{languages?.map((item, index) => {
|
|
122
121
|
return (
|
|
123
122
|
<div key={index} className="mr-4">
|
|
124
123
|
<Checkbox
|
|
125
124
|
id={`available-languages${item.lang}`}
|
|
126
125
|
className="mr-2"
|
|
127
|
-
{
|
|
126
|
+
checked={item.checked}
|
|
128
127
|
onCheckedChange={(checked: boolean) => onChangeCheckbox({
|
|
129
128
|
...item,
|
|
130
129
|
checked: checked,
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
import { FC, useMemo } from "react";
|
|
3
|
+
import { useTranslations } from "next-intl";
|
|
4
|
+
import { parseAsBoolean, parseAsInteger, parseAsString, useQueryStates } from "nuqs";
|
|
5
|
+
import { Button } from "@c-rex/ui/button";
|
|
6
|
+
import { Funnel, X } from "lucide-react";
|
|
7
|
+
import { Badge } from "@c-rex/ui/badge";
|
|
8
|
+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@c-rex/ui/tooltip";
|
|
9
|
+
import { OPERATOR_OPTIONS } from "@c-rex/constants";
|
|
10
|
+
import { Tags } from "@c-rex/interfaces";
|
|
11
|
+
import { memoizeFilteredTags } from "./filter-sidebar/utils";
|
|
12
|
+
|
|
13
|
+
type filterModel = {
|
|
14
|
+
key: string
|
|
15
|
+
name?: string
|
|
16
|
+
value: string
|
|
17
|
+
default?: string | boolean | null
|
|
18
|
+
removable: boolean
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type FilterNavbarProps = {
|
|
22
|
+
tags?: Tags
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const FilterNavbar: FC<FilterNavbarProps> = ({ tags }) => {
|
|
26
|
+
const t = useTranslations()
|
|
27
|
+
const [params, setParams] = useQueryStates({
|
|
28
|
+
language: parseAsString,
|
|
29
|
+
page: parseAsInteger,
|
|
30
|
+
wildcard: parseAsString,
|
|
31
|
+
operator: parseAsString,
|
|
32
|
+
packages: parseAsString,
|
|
33
|
+
filter: parseAsString,
|
|
34
|
+
like: parseAsBoolean,
|
|
35
|
+
search: {
|
|
36
|
+
defaultValue: "",
|
|
37
|
+
parse(value) {
|
|
38
|
+
return value
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
}, {
|
|
42
|
+
history: 'push',
|
|
43
|
+
shallow: false,
|
|
44
|
+
});
|
|
45
|
+
const isMobile = false
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
const filteredTags = useMemo(() => {
|
|
49
|
+
return memoizeFilteredTags(tags, params.filter, params.packages);
|
|
50
|
+
}, [tags, params.filter, params.packages]);
|
|
51
|
+
|
|
52
|
+
const filters = useMemo(() => {
|
|
53
|
+
const filters: filterModel[] = [{
|
|
54
|
+
key: "operator",
|
|
55
|
+
name: t("filter.operator"),
|
|
56
|
+
value: `${params?.operator !== OPERATOR_OPTIONS.OR}`,
|
|
57
|
+
removable: false
|
|
58
|
+
}, {
|
|
59
|
+
key: "like",
|
|
60
|
+
name: t("filter.like"),
|
|
61
|
+
value: `${params.like}`,
|
|
62
|
+
removable: false
|
|
63
|
+
}, {
|
|
64
|
+
key: "wildcard",
|
|
65
|
+
name: t("filter.wildcard"),
|
|
66
|
+
value: params.wildcard as string,
|
|
67
|
+
removable: false,
|
|
68
|
+
}]
|
|
69
|
+
|
|
70
|
+
if (params.language !== null) {
|
|
71
|
+
const languages = params.language.split(",")
|
|
72
|
+
languages.forEach((item) => {
|
|
73
|
+
const aux = languages.filter(langItem => langItem !== item)
|
|
74
|
+
filters.push({
|
|
75
|
+
key: "language",
|
|
76
|
+
name: t(`languages`),
|
|
77
|
+
value: item.toUpperCase(),
|
|
78
|
+
removable: languages.length > 1,
|
|
79
|
+
default: aux.join(",")
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (params.filter !== null) {
|
|
85
|
+
const splittedParam = params.filter.split(",")
|
|
86
|
+
|
|
87
|
+
splittedParam.forEach((item, index) => {
|
|
88
|
+
const aux = item.split(".shortId=")
|
|
89
|
+
const name = aux[0] as string
|
|
90
|
+
const shortId = aux[1] as string
|
|
91
|
+
|
|
92
|
+
const defaultValue = [...splittedParam]
|
|
93
|
+
defaultValue.splice(index, 1)
|
|
94
|
+
|
|
95
|
+
if (!Object.keys(filteredTags).includes(name) || !filteredTags[name]) return;
|
|
96
|
+
|
|
97
|
+
const tag = filteredTags[name].find(el => el.shortId === shortId)
|
|
98
|
+
if (!tag) return;
|
|
99
|
+
|
|
100
|
+
const value = defaultValue.length == 0 ? null : defaultValue.join(",")
|
|
101
|
+
|
|
102
|
+
filters.push({
|
|
103
|
+
key: "filter",
|
|
104
|
+
name: t(`filter.tags.${name}`),
|
|
105
|
+
value: tag.label,
|
|
106
|
+
removable: true,
|
|
107
|
+
default: value
|
|
108
|
+
})
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (params.packages !== null) {
|
|
113
|
+
let label = params.packages
|
|
114
|
+
|
|
115
|
+
if (filteredTags["packages"]) {
|
|
116
|
+
const aux = filteredTags["packages"].find(el => el.shortId === params.packages)
|
|
117
|
+
|
|
118
|
+
if (aux) {
|
|
119
|
+
label = aux.label
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
filters.push({
|
|
124
|
+
key: "packages",
|
|
125
|
+
name: t("filter.tags.packages"),
|
|
126
|
+
value: label,
|
|
127
|
+
removable: true,
|
|
128
|
+
default: null
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
Object.keys(params)
|
|
133
|
+
.filter(item => !["page", "search", "language", "operator", "like", "wildcard", "filter", "packages"].includes(item))
|
|
134
|
+
.forEach(item => {
|
|
135
|
+
if (params[item as keyof typeof params] === null || params[item as keyof typeof params] === undefined) return;
|
|
136
|
+
const value = params[item as keyof typeof params] as string
|
|
137
|
+
filters.push({ key: item, value: value, removable: true, default: null })
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
return filters
|
|
141
|
+
}, [filteredTags, params]);
|
|
142
|
+
|
|
143
|
+
if (filters == null || filters.length === 0) {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
<div className="pb-4 flex justify-between">
|
|
149
|
+
|
|
150
|
+
<div className="flex flex-wrap gap-2">
|
|
151
|
+
<Button
|
|
152
|
+
size="sm"
|
|
153
|
+
variant="secondary"
|
|
154
|
+
//onClick={() => setOpen(true)}
|
|
155
|
+
className="md:hidden"
|
|
156
|
+
>
|
|
157
|
+
<Funnel className="h-2" />
|
|
158
|
+
</Button>
|
|
159
|
+
|
|
160
|
+
{filters.length > 0 && (
|
|
161
|
+
<>
|
|
162
|
+
{filters.slice(0, 1).map((item) => (
|
|
163
|
+
<Badge
|
|
164
|
+
key={`${item.key}-${item?.value}`}
|
|
165
|
+
variant="outline"
|
|
166
|
+
className="h-8"
|
|
167
|
+
>
|
|
168
|
+
{item?.name ? item.name : item.key}: {item.value}
|
|
169
|
+
|
|
170
|
+
{item.removable && (
|
|
171
|
+
<Button size="xs" variant="ghost" onClick={() => {
|
|
172
|
+
setParams({ [item.key]: item?.default })
|
|
173
|
+
}}>
|
|
174
|
+
<X className="h-2" />
|
|
175
|
+
</Button>
|
|
176
|
+
)}
|
|
177
|
+
</Badge>
|
|
178
|
+
))}
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
{isMobile ? (
|
|
182
|
+
<TooltipProvider>
|
|
183
|
+
<Tooltip delayDuration={100}>
|
|
184
|
+
<TooltipTrigger>
|
|
185
|
+
<Badge
|
|
186
|
+
key={`grouped-filters`}
|
|
187
|
+
variant="outline"
|
|
188
|
+
className="h-8"
|
|
189
|
+
>
|
|
190
|
+
+{filters.length - 1} {t("filter.filters")}
|
|
191
|
+
</Badge>
|
|
192
|
+
|
|
193
|
+
</TooltipTrigger>
|
|
194
|
+
|
|
195
|
+
<TooltipContent>
|
|
196
|
+
{filters.slice(1).map((item) => {
|
|
197
|
+
const label = item?.name ? item.name : item.key
|
|
198
|
+
const returnString = `${label}: ${item.value}`;
|
|
199
|
+
return <div className="capitalize" key={returnString}>{returnString}</div>;
|
|
200
|
+
})}
|
|
201
|
+
</TooltipContent>
|
|
202
|
+
</Tooltip>
|
|
203
|
+
</TooltipProvider>
|
|
204
|
+
) : (
|
|
205
|
+
<>
|
|
206
|
+
{filters.slice(1).map((item) => (
|
|
207
|
+
<Badge
|
|
208
|
+
key={`${item.key}-${item?.value}`}
|
|
209
|
+
variant="outline"
|
|
210
|
+
className="h-8"
|
|
211
|
+
>
|
|
212
|
+
{item?.name ? item.name : item.key}: {item.value}
|
|
213
|
+
|
|
214
|
+
{item.removable && (
|
|
215
|
+
<Button size="xs" variant="ghost" onClick={() => {
|
|
216
|
+
setParams({ [item.key]: item?.default })
|
|
217
|
+
}}>
|
|
218
|
+
<X className="h-2" />
|
|
219
|
+
</Button>
|
|
220
|
+
)}
|
|
221
|
+
</Badge>
|
|
222
|
+
))}
|
|
223
|
+
</>
|
|
224
|
+
)}
|
|
225
|
+
</>
|
|
226
|
+
)}
|
|
227
|
+
</div>
|
|
228
|
+
|
|
229
|
+
<Button
|
|
230
|
+
size="sm"
|
|
231
|
+
variant="secondary"
|
|
232
|
+
onClick={() => {
|
|
233
|
+
setParams({ filter: null, packages: null })
|
|
234
|
+
}}
|
|
235
|
+
>
|
|
236
|
+
{t("reset")}
|
|
237
|
+
</Button>
|
|
238
|
+
|
|
239
|
+
</div>
|
|
240
|
+
);
|
|
241
|
+
};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { FC, useMemo } from "react";
|
|
4
|
+
import { useTranslations } from 'next-intl'
|
|
5
|
+
import { Check, ChevronDown } from "lucide-react"
|
|
6
|
+
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@c-rex/ui/collapsible";
|
|
7
|
+
import {
|
|
8
|
+
SidebarContent,
|
|
9
|
+
SidebarGroup,
|
|
10
|
+
SidebarGroupContent,
|
|
11
|
+
SidebarGroupLabel,
|
|
12
|
+
SidebarHeader,
|
|
13
|
+
SidebarMenu,
|
|
14
|
+
SidebarMenuSub,
|
|
15
|
+
SidebarMenuSubButton,
|
|
16
|
+
SidebarMenuSubItem
|
|
17
|
+
} from "@c-rex/ui/sidebar";
|
|
18
|
+
import { parseAsString, useQueryStates } from "nuqs";
|
|
19
|
+
import { memoizeFilteredTags, removeFilterItem, updateFilterParam } from "./utils";
|
|
20
|
+
import { FilterItem, Tags } from "@c-rex/interfaces";
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
export interface FilterSidebarProps {
|
|
24
|
+
tags?: Tags
|
|
25
|
+
totalItemCount?: number
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
//TODO; check layout on mobile
|
|
30
|
+
export const FilterSidebar: FC<FilterSidebarProps> = ({ tags, totalItemCount }) => {
|
|
31
|
+
const t = useTranslations();
|
|
32
|
+
const [params, setParams] = useQueryStates({
|
|
33
|
+
packages: parseAsString,
|
|
34
|
+
filter: parseAsString,
|
|
35
|
+
}, {
|
|
36
|
+
history: 'push',
|
|
37
|
+
shallow: false,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const filteredTags = useMemo(() => {
|
|
41
|
+
return memoizeFilteredTags(tags, params.filter, params.packages);
|
|
42
|
+
}, [tags, params.filter, params.packages]);
|
|
43
|
+
|
|
44
|
+
const onClickHandler = (key: string, item: FilterItem) => {
|
|
45
|
+
if (item.active) {
|
|
46
|
+
setParams(removeFilterItem(key, item, params.filter))
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
setParams(updateFilterParam(key, item, params.filter))
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const content = (
|
|
54
|
+
<SidebarContent className="!gap-0" suppressHydrationWarning>
|
|
55
|
+
|
|
56
|
+
{Object.entries(filteredTags).map(([key, value]) => (
|
|
57
|
+
|
|
58
|
+
<Collapsible defaultOpen key={key} className="py-0 group/collapsible">
|
|
59
|
+
<SidebarGroup>
|
|
60
|
+
|
|
61
|
+
<SidebarGroupLabel asChild className="hover:bg-sidebar-accent text-sidebar-accent-foreground text-sm font-bold">
|
|
62
|
+
<CollapsibleTrigger className="!h-9">
|
|
63
|
+
{t(`filter.tags.${key}`)}
|
|
64
|
+
<ChevronDown className="ml-auto transition-transform group-data-[state=open]/collapsible:rotate-180" />
|
|
65
|
+
</CollapsibleTrigger>
|
|
66
|
+
</SidebarGroupLabel>
|
|
67
|
+
|
|
68
|
+
<CollapsibleContent>
|
|
69
|
+
<SidebarGroupContent>
|
|
70
|
+
<SidebarMenu>
|
|
71
|
+
<SidebarMenuSub>
|
|
72
|
+
{value.map((item) => (
|
|
73
|
+
<SidebarMenuSubItem key={item.shortId}>
|
|
74
|
+
<SidebarMenuSubButton
|
|
75
|
+
className="cursor-pointer"
|
|
76
|
+
isActive={item.active}
|
|
77
|
+
onClick={() => onClickHandler(key, item)}
|
|
78
|
+
>
|
|
79
|
+
{item.label} ({item.hits}/{item.total})
|
|
80
|
+
{item.active && <Check className="ml-2" />}
|
|
81
|
+
</SidebarMenuSubButton>
|
|
82
|
+
</SidebarMenuSubItem>
|
|
83
|
+
))}
|
|
84
|
+
</SidebarMenuSub>
|
|
85
|
+
</SidebarMenu>
|
|
86
|
+
</SidebarGroupContent>
|
|
87
|
+
</CollapsibleContent>
|
|
88
|
+
</SidebarGroup>
|
|
89
|
+
</Collapsible>
|
|
90
|
+
))}
|
|
91
|
+
</SidebarContent>
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
/*
|
|
95
|
+
if (isMobile) {
|
|
96
|
+
return (
|
|
97
|
+
|
|
98
|
+
<SheetContent
|
|
99
|
+
side="left"
|
|
100
|
+
className="!pt-6 !px-2 w-[400px] overflow-y-auto"
|
|
101
|
+
>
|
|
102
|
+
<SheetHeader className="justify-center items-end font-bold">
|
|
103
|
+
{t("filter.filters")}
|
|
104
|
+
<span className="text-xs text-muted-foreground leading-5">
|
|
105
|
+
{totalItemCount} {t("results.results")}
|
|
106
|
+
</span>
|
|
107
|
+
</SheetHeader>
|
|
108
|
+
{content}
|
|
109
|
+
</SheetContent>
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
*/
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<div className="w-60 lg:w-80 bg-sidebar rounded-md border pb-4">
|
|
116
|
+
<SidebarHeader className="justify-center items-end font-bold">
|
|
117
|
+
{t("filter.filters")}
|
|
118
|
+
<span className="text-xs text-muted-foreground leading-5">
|
|
119
|
+
{totalItemCount} {t("results.results")}
|
|
120
|
+
</span>
|
|
121
|
+
</SidebarHeader>
|
|
122
|
+
{content}
|
|
123
|
+
</div>
|
|
124
|
+
);
|
|
125
|
+
};
|