@c-rex/components 0.1.5 → 0.1.7
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 +5 -1
- package/src/autocomplete.tsx +135 -97
- package/src/check-article-lang.tsx +1 -10
- package/src/dialog-filter.tsx +82 -19
- package/src/navbar/language-switcher/content-language-switch.tsx +15 -2
- package/src/navbar/navbar.tsx +10 -63
- package/src/navbar/search-input.tsx +42 -0
- package/src/navbar/settings.tsx +57 -0
- package/src/navbar/user-menu.tsx +59 -0
- package/src/page-wrapper.tsx +3 -2
- package/src/search-modal.tsx +145 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@c-rex/components",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"files": [
|
|
5
5
|
"src"
|
|
6
6
|
],
|
|
@@ -72,6 +72,10 @@
|
|
|
72
72
|
"./page-wrapper": {
|
|
73
73
|
"types": "./src/page-wrapper.tsx",
|
|
74
74
|
"import": "./src/page-wrapper.tsx"
|
|
75
|
+
},
|
|
76
|
+
"./search-modal": {
|
|
77
|
+
"types": "./src/search-modal.tsx",
|
|
78
|
+
"import": "./src/search-modal.tsx"
|
|
75
79
|
}
|
|
76
80
|
},
|
|
77
81
|
"scripts": {
|
package/src/autocomplete.tsx
CHANGED
|
@@ -1,122 +1,160 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { Command as CommandPrimitive } from "cmdk";
|
|
3
|
-
import { useEffect, useState } from "react";
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
4
2
|
import { Input } from "@c-rex/ui/input";
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
CommandList
|
|
11
|
-
} from "@c-rex/ui/command";
|
|
12
|
-
import {
|
|
13
|
-
Popover,
|
|
14
|
-
PopoverAnchor,
|
|
15
|
-
PopoverContent
|
|
16
|
-
} from "@c-rex/ui/popover";
|
|
3
|
+
import { call, generateQueryParams, getFromCookieString } from "@c-rex/utils";
|
|
4
|
+
import { CONTENT_LANG_KEY, WILD_CARD_OPTIONS } from "@c-rex/constants";
|
|
5
|
+
import { useTranslations } from "next-intl";
|
|
6
|
+
import { useAppConfig } from "@c-rex/contexts/config-provider";
|
|
7
|
+
import { QueryParams } from "@c-rex/types";
|
|
17
8
|
|
|
18
9
|
type Props = {
|
|
19
10
|
initialValue: string;
|
|
20
|
-
|
|
21
|
-
|
|
11
|
+
embedded: boolean
|
|
12
|
+
searchByPackage: boolean
|
|
22
13
|
};
|
|
23
14
|
|
|
24
|
-
export const AutoComplete = ({
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const [query, setQuery] = useState(initialValue);
|
|
30
|
-
const [suggestions, setSuggestions] = useState<string[]>([]);
|
|
15
|
+
export const AutoComplete = ({ initialValue, embedded, searchByPackage }: Props) => {
|
|
16
|
+
const t = useTranslations();
|
|
17
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
18
|
+
const { contentLang, packageID } = useAppConfig()
|
|
19
|
+
|
|
31
20
|
const [open, setOpen] = useState(false);
|
|
21
|
+
const [query, setQuery] = useState(initialValue);
|
|
32
22
|
const [loading, setLoading] = useState(false);
|
|
23
|
+
const [suggestions, setSuggestions] = useState<string[]>([]);
|
|
24
|
+
|
|
25
|
+
const onSearch = (value: string): Promise<string[]> => {
|
|
26
|
+
return call<string[]>("InformationUnitsService.getSuggestions", { query: value, language: contentLang });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const onSelect = (value: string) => {
|
|
30
|
+
const cookie = getFromCookieString(document.cookie, CONTENT_LANG_KEY)
|
|
31
|
+
const params: QueryParams[] = [
|
|
32
|
+
{
|
|
33
|
+
key: "search",
|
|
34
|
+
value: value
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
key: "operator",
|
|
38
|
+
value: "OR"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
key: "page",
|
|
42
|
+
value: "1"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
key: "language",
|
|
46
|
+
value: cookie
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
key: "wildcard",
|
|
50
|
+
value: WILD_CARD_OPTIONS.BOTH
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
key: "like",
|
|
54
|
+
value: "false"
|
|
55
|
+
},
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
if (searchByPackage && packageID !== null) {
|
|
59
|
+
params.push({
|
|
60
|
+
key: "package",
|
|
61
|
+
value: packageID
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const aux = generateQueryParams(params)
|
|
66
|
+
|
|
67
|
+
window.location.href = `${window.location.origin}/?${aux}`
|
|
68
|
+
};
|
|
69
|
+
|
|
33
70
|
|
|
34
71
|
useEffect(() => {
|
|
35
72
|
setQuery(initialValue);
|
|
36
73
|
}, [initialValue]);
|
|
37
74
|
|
|
38
75
|
useEffect(() => {
|
|
39
|
-
const
|
|
40
|
-
if (
|
|
41
|
-
|
|
42
|
-
onSearch(query).then(suggestions => {
|
|
43
|
-
setSuggestions(suggestions)
|
|
44
|
-
setLoading(false)
|
|
45
|
-
});
|
|
46
|
-
} else {
|
|
47
|
-
setSuggestions([]);
|
|
76
|
+
const handleClickOutside = (e: MouseEvent) => {
|
|
77
|
+
if (!containerRef.current?.contains(e.target as Node)) {
|
|
78
|
+
setOpen(false);
|
|
48
79
|
}
|
|
80
|
+
};
|
|
81
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
82
|
+
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
83
|
+
}, []);
|
|
84
|
+
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
if (query.length < 2) {
|
|
87
|
+
setSuggestions([]);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const debounceFetch = setTimeout(() => {
|
|
92
|
+
setLoading(true)
|
|
93
|
+
onSearch(query).then(suggestions => {
|
|
94
|
+
setSuggestions(suggestions)
|
|
95
|
+
setLoading(false)
|
|
96
|
+
}).catch(() => {
|
|
97
|
+
setLoading(false)
|
|
98
|
+
setSuggestions([])
|
|
99
|
+
});
|
|
49
100
|
}, 300);
|
|
50
101
|
|
|
51
102
|
return () => clearTimeout(debounceFetch);
|
|
52
|
-
}, [
|
|
103
|
+
}, [query]);
|
|
53
104
|
|
|
54
105
|
return (
|
|
55
|
-
<div className="
|
|
56
|
-
<
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
key={query}
|
|
86
|
-
>
|
|
87
|
-
{loading ? (
|
|
88
|
-
<CommandPrimitive.Loading>
|
|
89
|
-
<div className="flex items-center justify-center py-4">
|
|
90
|
-
<div className="animate-spin rounded-full h-6 w-6 border-2 border-gray-300 border-t-gray-950" />
|
|
91
|
-
</div>
|
|
92
|
-
</CommandPrimitive.Loading>
|
|
93
|
-
) : (
|
|
106
|
+
<div className="relative" ref={containerRef}>
|
|
107
|
+
<Input
|
|
108
|
+
variant={embedded ? "embedded" : undefined}
|
|
109
|
+
type="text"
|
|
110
|
+
placeholder={t("search")}
|
|
111
|
+
value={query}
|
|
112
|
+
onChange={(e) => {
|
|
113
|
+
setQuery(e.target.value);
|
|
114
|
+
setOpen(true);
|
|
115
|
+
}}
|
|
116
|
+
onKeyDown={(e) => {
|
|
117
|
+
if (e.key === "Enter") {
|
|
118
|
+
e.preventDefault();
|
|
119
|
+
onSelect(query);
|
|
120
|
+
setOpen(false)
|
|
121
|
+
}
|
|
122
|
+
}}
|
|
123
|
+
/>
|
|
124
|
+
|
|
125
|
+
{open && (
|
|
126
|
+
<ul className="absolute z-10 w-full bg-white border border-gray-300 rounded-lg shadow-lg max-h-60 overflow-y-auto">
|
|
127
|
+
{loading ? (
|
|
128
|
+
<li>
|
|
129
|
+
<div className="flex items-center justify-center py-4">
|
|
130
|
+
<div className="animate-spin rounded-full h-6 w-6 border-2 border-gray-300 border-t-gray-950" />
|
|
131
|
+
</div>
|
|
132
|
+
</li>
|
|
133
|
+
) : (
|
|
134
|
+
<>
|
|
135
|
+
{suggestions.length > 0 ? (
|
|
94
136
|
<>
|
|
95
|
-
{suggestions.
|
|
96
|
-
<
|
|
97
|
-
{
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
</CommandItem>
|
|
109
|
-
))}
|
|
110
|
-
</CommandGroup>
|
|
111
|
-
) : (
|
|
112
|
-
<CommandEmpty>No suggestions.</CommandEmpty>
|
|
113
|
-
)}
|
|
137
|
+
{suggestions.map((option, index) => (
|
|
138
|
+
<li
|
|
139
|
+
key={index}
|
|
140
|
+
className="px-4 py-2 hover:bg-accent cursor-pointer text-sm"
|
|
141
|
+
onClick={() => {
|
|
142
|
+
setQuery(option);
|
|
143
|
+
setOpen(false);
|
|
144
|
+
onSelect(`"${option}"`);
|
|
145
|
+
}}
|
|
146
|
+
>
|
|
147
|
+
{option}
|
|
148
|
+
</li>
|
|
149
|
+
))}
|
|
114
150
|
</>
|
|
151
|
+
) : (
|
|
152
|
+
<li className="px-4 py-2">No suggestions.</li>
|
|
115
153
|
)}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
</
|
|
119
|
-
|
|
120
|
-
</div>
|
|
154
|
+
</>
|
|
155
|
+
)}
|
|
156
|
+
</ul>
|
|
157
|
+
)}
|
|
158
|
+
</div >
|
|
121
159
|
);
|
|
122
160
|
}
|
|
@@ -21,9 +21,7 @@ export const CheckArticleLangToast: FC<Props> = ({ availableVersions }) => {
|
|
|
21
21
|
if (activeArticle == undefined || activeArticle.lang == contentLang) return
|
|
22
22
|
|
|
23
23
|
const articleAvailable = availableVersions.find((item) => item.lang === contentLang)
|
|
24
|
-
if (articleAvailable
|
|
25
|
-
articleNotAvailableToast()
|
|
26
|
-
} else {
|
|
24
|
+
if (articleAvailable != undefined) {
|
|
27
25
|
articleAvailableInToast(articleAvailable.lang, articleAvailable.link)
|
|
28
26
|
}
|
|
29
27
|
}, [])
|
|
@@ -39,12 +37,5 @@ export const CheckArticleLangToast: FC<Props> = ({ availableVersions }) => {
|
|
|
39
37
|
})
|
|
40
38
|
}
|
|
41
39
|
|
|
42
|
-
const articleNotAvailableToast = () => {
|
|
43
|
-
toast(t('sorry'), {
|
|
44
|
-
description: t('toast.articleNotAvailable'),
|
|
45
|
-
duration: 10000,
|
|
46
|
-
})
|
|
47
|
-
}
|
|
48
|
-
|
|
49
40
|
return null
|
|
50
41
|
}
|
package/src/dialog-filter.tsx
CHANGED
|
@@ -9,34 +9,49 @@ import {
|
|
|
9
9
|
DialogTitle,
|
|
10
10
|
DialogTrigger,
|
|
11
11
|
} from "@c-rex/ui/dialog"
|
|
12
|
+
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@c-rex/ui/select"
|
|
13
|
+
import { ToggleGroup, ToggleGroupItem } from "@c-rex/ui/toggle-group"
|
|
14
|
+
import { RadioGroup, RadioGroupItem } from "@c-rex/ui/radio-group"
|
|
12
15
|
import { Checkbox } from "@c-rex/ui/checkbox"
|
|
13
16
|
import { Label } from "@c-rex/ui/label"
|
|
14
|
-
import { parseAsInteger, parseAsString, useQueryStates } from "nuqs"
|
|
17
|
+
import { parseAsBoolean, parseAsInteger, parseAsString, useQueryStates } from "nuqs"
|
|
15
18
|
import { useTranslations } from "next-intl"
|
|
19
|
+
import { useAppConfig } from "@c-rex/contexts/config-provider"
|
|
20
|
+
import { LanguageAndCountries } from "@c-rex/interfaces"
|
|
21
|
+
import { WILD_CARD_OPTIONS, OPERATOR_OPTIONS } from "@c-rex/constants"
|
|
22
|
+
import { Switch } from "@c-rex/ui/switch"
|
|
16
23
|
|
|
17
24
|
interface DialogFilterProps {
|
|
18
25
|
trigger: React.ReactNode;
|
|
19
|
-
|
|
20
|
-
|
|
26
|
+
}
|
|
27
|
+
interface Languages {
|
|
28
|
+
lang: string;
|
|
29
|
+
value: string;
|
|
30
|
+
checked: boolean;
|
|
21
31
|
}
|
|
22
32
|
|
|
23
|
-
export const DialogFilter: FC<DialogFilterProps> = ({
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
availableLanguages,
|
|
27
|
-
}) => {
|
|
28
|
-
const t = useTranslations();
|
|
29
|
-
|
|
33
|
+
export const DialogFilter: FC<DialogFilterProps> = ({ trigger }) => {
|
|
34
|
+
const t = useTranslations("filter");
|
|
35
|
+
const { availableLanguagesAndCountries } = useAppConfig()
|
|
30
36
|
const [params, setParams] = useQueryStates({
|
|
31
37
|
language: parseAsString,
|
|
32
38
|
page: parseAsInteger,
|
|
39
|
+
wildcard: parseAsString,
|
|
40
|
+
operator: parseAsString,
|
|
41
|
+
like: parseAsBoolean,
|
|
33
42
|
}, {
|
|
34
43
|
history: 'push',
|
|
35
44
|
shallow: false,
|
|
36
45
|
});
|
|
37
46
|
|
|
38
|
-
const
|
|
47
|
+
const startSelectedLanguages = params.language?.split(',') || []
|
|
48
|
+
|
|
49
|
+
const [operator, setOperator] = useState<string>(params.operator || OPERATOR_OPTIONS.OR);
|
|
50
|
+
const [like, setLike] = useState<boolean>(params.like || false);
|
|
51
|
+
const [wildcard, setWildcard] = useState<string>(params.wildcard || "");
|
|
52
|
+
const [languages, setLanguages] = useState<Languages[]>(availableLanguagesAndCountries.map((item: LanguageAndCountries) => {
|
|
39
53
|
const checked = startSelectedLanguages.includes(item.value)
|
|
54
|
+
|
|
40
55
|
return {
|
|
41
56
|
lang: item.lang,
|
|
42
57
|
value: item.value,
|
|
@@ -44,9 +59,10 @@ export const DialogFilter: FC<DialogFilterProps> = ({
|
|
|
44
59
|
}
|
|
45
60
|
}));
|
|
46
61
|
|
|
47
|
-
const
|
|
48
|
-
const
|
|
49
|
-
|
|
62
|
+
const onChangeCheckbox = (item: Languages) => {
|
|
63
|
+
const newLangList = languages.filter((lang) => lang.lang !== item.lang)
|
|
64
|
+
|
|
65
|
+
setLanguages([...newLangList, item].sort((a, b) => {
|
|
50
66
|
return a.value.localeCompare(b.value)
|
|
51
67
|
}))
|
|
52
68
|
}
|
|
@@ -57,6 +73,9 @@ export const DialogFilter: FC<DialogFilterProps> = ({
|
|
|
57
73
|
setParams({
|
|
58
74
|
page: 1,
|
|
59
75
|
language: selectedLanguages,
|
|
76
|
+
operator: operator,
|
|
77
|
+
wildcard: wildcard,
|
|
78
|
+
like: like,
|
|
60
79
|
});
|
|
61
80
|
}
|
|
62
81
|
|
|
@@ -69,7 +88,8 @@ export const DialogFilter: FC<DialogFilterProps> = ({
|
|
|
69
88
|
<DialogHeader>
|
|
70
89
|
<DialogTitle>{t("filters")}</DialogTitle>
|
|
71
90
|
</DialogHeader>
|
|
72
|
-
|
|
91
|
+
|
|
92
|
+
<div className="grid grid-cols-2 items-center pt-2">
|
|
73
93
|
<Label className="text-right">
|
|
74
94
|
{t("languages")}:
|
|
75
95
|
</Label>
|
|
@@ -81,7 +101,7 @@ export const DialogFilter: FC<DialogFilterProps> = ({
|
|
|
81
101
|
id={`available-languages${item.lang}`}
|
|
82
102
|
className="mr-2"
|
|
83
103
|
{...({ checked: item.checked })}
|
|
84
|
-
onCheckedChange={(checked: boolean) =>
|
|
104
|
+
onCheckedChange={(checked: boolean) => onChangeCheckbox({
|
|
85
105
|
...item,
|
|
86
106
|
checked: checked,
|
|
87
107
|
})}
|
|
@@ -96,14 +116,57 @@ export const DialogFilter: FC<DialogFilterProps> = ({
|
|
|
96
116
|
)
|
|
97
117
|
})}
|
|
98
118
|
</div>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
<div className="grid grid-cols-2 items-center pt-2">
|
|
122
|
+
<Label className="text-right">
|
|
123
|
+
{t("wildcard")}:
|
|
124
|
+
</Label>
|
|
125
|
+
<div className="flex items-center">
|
|
126
|
+
<Select onValueChange={setWildcard} defaultValue={wildcard}>
|
|
127
|
+
<SelectTrigger className="w-[180px]">
|
|
128
|
+
<SelectValue placeholder="" />
|
|
129
|
+
</SelectTrigger>
|
|
130
|
+
<SelectContent>
|
|
131
|
+
<SelectGroup>
|
|
132
|
+
{Object.keys(WILD_CARD_OPTIONS).map((item: string) => {
|
|
133
|
+
return (
|
|
134
|
+
<SelectItem key={item} value={item}>
|
|
135
|
+
{item}
|
|
136
|
+
</SelectItem>
|
|
137
|
+
)
|
|
138
|
+
})}
|
|
139
|
+
</SelectGroup>
|
|
140
|
+
</SelectContent>
|
|
141
|
+
</Select>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
<div className="grid grid-cols-2 items-center pt-2">
|
|
146
|
+
<Label className="text-right">
|
|
147
|
+
{t("operator")}:
|
|
148
|
+
</Label>
|
|
99
149
|
|
|
150
|
+
<Switch
|
|
151
|
+
id="like"
|
|
152
|
+
onCheckedChange={(value) => setOperator(value ? OPERATOR_OPTIONS.AND : OPERATOR_OPTIONS.OR)}
|
|
153
|
+
checked={operator == OPERATOR_OPTIONS.AND}
|
|
154
|
+
/>
|
|
100
155
|
</div>
|
|
101
|
-
|
|
156
|
+
|
|
157
|
+
<div className="grid grid-cols-2 items-center pt-2">
|
|
158
|
+
<Label htmlFor="like" className="text-right">
|
|
159
|
+
{t("like")}:
|
|
160
|
+
</Label>
|
|
161
|
+
<Switch id="like" onCheckedChange={(value) => setLike(value)} checked={like} />
|
|
162
|
+
</div>
|
|
163
|
+
|
|
164
|
+
<DialogFooter className="pt-2">
|
|
102
165
|
<DialogClose asChild>
|
|
103
|
-
<Button onClick={apply}>{t("
|
|
166
|
+
<Button onClick={apply}>{t("apply")}</Button>
|
|
104
167
|
</DialogClose>
|
|
105
168
|
</DialogFooter>
|
|
106
169
|
</DialogContent>
|
|
107
|
-
</Dialog>
|
|
170
|
+
</Dialog >
|
|
108
171
|
)
|
|
109
172
|
}
|
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import { startTransition } from "react";
|
|
4
4
|
import { SharedLanguageSwitch } from "./shared";
|
|
5
5
|
import { getFromCookieString, setCookie } from "@c-rex/utils";
|
|
6
6
|
import { CONTENT_LANG_KEY } from "@c-rex/constants";
|
|
7
7
|
import { useQueryState } from "nuqs"
|
|
8
8
|
import { useAppConfig } from "@c-rex/contexts/config-provider";
|
|
9
|
+
import { useTranslations } from "next-intl";
|
|
10
|
+
import { toast } from "sonner"
|
|
9
11
|
|
|
10
12
|
export const ContentLanguageSwitch = () => {
|
|
11
|
-
const
|
|
13
|
+
const t = useTranslations();
|
|
12
14
|
const contentLang = getFromCookieString(document.cookie, CONTENT_LANG_KEY)
|
|
15
|
+
const { availableLanguagesAndCountries, setContentLang, availableVersions } = useAppConfig()
|
|
16
|
+
|
|
13
17
|
const [queryLanguage, setContentLanguage] = useQueryState('language', {
|
|
14
18
|
history: 'push',
|
|
15
19
|
shallow: false,
|
|
@@ -28,11 +32,20 @@ export const ContentLanguageSwitch = () => {
|
|
|
28
32
|
|
|
29
33
|
if (filteredList.length > 0 && filteredList[0]) {
|
|
30
34
|
window.location.href = filteredList[0].link as string;
|
|
35
|
+
} else {
|
|
36
|
+
articleNotAvailableToast()
|
|
31
37
|
}
|
|
32
38
|
}
|
|
33
39
|
});
|
|
34
40
|
};
|
|
35
41
|
|
|
42
|
+
const articleNotAvailableToast = () => {
|
|
43
|
+
toast(t('sorry'), {
|
|
44
|
+
description: t('toast.articleNotAvailable'),
|
|
45
|
+
duration: 10000,
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
|
|
36
49
|
return (
|
|
37
50
|
<SharedLanguageSwitch
|
|
38
51
|
availableLanguagesAndCountries={availableLanguagesAndCountries}
|
package/src/navbar/navbar.tsx
CHANGED
|
@@ -1,31 +1,19 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { FC } from "react";
|
|
2
2
|
import Link from "next/link";
|
|
3
3
|
import Image from "next/image";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
DropdownMenu,
|
|
7
|
-
DropdownMenuContent,
|
|
8
|
-
DropdownMenuLabel,
|
|
9
|
-
DropdownMenuPortal,
|
|
10
|
-
DropdownMenuSeparator,
|
|
11
|
-
DropdownMenuSub,
|
|
12
|
-
DropdownMenuSubContent,
|
|
13
|
-
DropdownMenuSubTrigger,
|
|
14
|
-
DropdownMenuTrigger,
|
|
15
|
-
} from "@c-rex/ui/dropdown-menu"
|
|
16
|
-
import { Settings } from "lucide-react";
|
|
17
|
-
import { ContentLanguageSwitch } from "./language-switcher/content-language-switch";
|
|
18
|
-
import { getTranslations } from "next-intl/server";
|
|
19
|
-
import { UILanguageSwitch } from "./language-switcher/ui-language-switch";
|
|
4
|
+
import { SignInBtn } from "./sign-in-out-btns";
|
|
20
5
|
import { getServerSession } from "next-auth";
|
|
21
6
|
import { getConfigs } from "@c-rex/utils/next-cookies";
|
|
7
|
+
import { SettingsMenu } from "./settings";
|
|
8
|
+
import { UserMenu } from "./user-menu";
|
|
9
|
+
import { SearchInput } from "./search-input";
|
|
22
10
|
|
|
23
11
|
interface NavBarProps {
|
|
24
12
|
title: string;
|
|
13
|
+
showInput: boolean;
|
|
25
14
|
}
|
|
26
15
|
|
|
27
|
-
export const NavBar: FC<NavBarProps> = async ({ title }) => {
|
|
28
|
-
const t = await getTranslations();
|
|
16
|
+
export const NavBar: FC<NavBarProps> = async ({ title, showInput }) => {
|
|
29
17
|
const configs = await getConfigs();
|
|
30
18
|
|
|
31
19
|
let session: any;
|
|
@@ -54,20 +42,12 @@ export const NavBar: FC<NavBarProps> = async ({ title }) => {
|
|
|
54
42
|
</div>
|
|
55
43
|
|
|
56
44
|
<div className="flex items-center space-x-3">
|
|
57
|
-
{
|
|
58
|
-
<div className="flex items-center px-3 border rounded-full h-8">
|
|
59
|
-
<Search className="h-4 w-4 shrink-0 opacity-50" />
|
|
60
|
-
<Input variant="embedded" placeholder="Search" />
|
|
61
|
-
</div>
|
|
62
|
-
*/}
|
|
45
|
+
<SearchInput showInput={showInput} />
|
|
63
46
|
|
|
64
47
|
{configs.OIDC.user.enabled && (
|
|
65
48
|
<>
|
|
66
49
|
{session ? (
|
|
67
|
-
|
|
68
|
-
<span>{t("user.welcome", { userName: session.user?.name as string })}</span>
|
|
69
|
-
<SignOut />
|
|
70
|
-
</>
|
|
50
|
+
<UserMenu session={session} />
|
|
71
51
|
) : (
|
|
72
52
|
<SignInBtn />
|
|
73
53
|
)}
|
|
@@ -75,40 +55,7 @@ export const NavBar: FC<NavBarProps> = async ({ title }) => {
|
|
|
75
55
|
)}
|
|
76
56
|
|
|
77
57
|
{configs.languageSwitcher.enabled && (
|
|
78
|
-
<
|
|
79
|
-
<DropdownMenuTrigger>
|
|
80
|
-
<Settings />
|
|
81
|
-
</DropdownMenuTrigger>
|
|
82
|
-
<DropdownMenuContent align="start" sideOffset={20} alignOffset={20}>
|
|
83
|
-
<DropdownMenuLabel>{t("accountSettings.accountSettings")}</DropdownMenuLabel>
|
|
84
|
-
<DropdownMenuSeparator />
|
|
85
|
-
|
|
86
|
-
<DropdownMenuSub>
|
|
87
|
-
<DropdownMenuSubTrigger>
|
|
88
|
-
<span>{t("accountSettings.contentLanguage")}</span>
|
|
89
|
-
</DropdownMenuSubTrigger>
|
|
90
|
-
|
|
91
|
-
<DropdownMenuPortal>
|
|
92
|
-
<DropdownMenuSubContent>
|
|
93
|
-
<ContentLanguageSwitch />
|
|
94
|
-
</DropdownMenuSubContent>
|
|
95
|
-
</DropdownMenuPortal>
|
|
96
|
-
</DropdownMenuSub>
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
<DropdownMenuSub>
|
|
100
|
-
<DropdownMenuSubTrigger>
|
|
101
|
-
<span>{t("accountSettings.uiLanguage")}</span>
|
|
102
|
-
</DropdownMenuSubTrigger>
|
|
103
|
-
|
|
104
|
-
<DropdownMenuPortal>
|
|
105
|
-
<DropdownMenuSubContent>
|
|
106
|
-
<UILanguageSwitch />
|
|
107
|
-
</DropdownMenuSubContent>
|
|
108
|
-
</DropdownMenuPortal>
|
|
109
|
-
</DropdownMenuSub>
|
|
110
|
-
</DropdownMenuContent>
|
|
111
|
-
</DropdownMenu>
|
|
58
|
+
<SettingsMenu />
|
|
112
59
|
)}
|
|
113
60
|
</div>
|
|
114
61
|
</div>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import React, { FC, useState } from "react";
|
|
4
|
+
import { FileCheck, FileX, Search } from "lucide-react";
|
|
5
|
+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@c-rex/ui/tooltip";
|
|
6
|
+
import { AutoComplete } from "../autocomplete";
|
|
7
|
+
|
|
8
|
+
type Props = {
|
|
9
|
+
showInput: boolean
|
|
10
|
+
}
|
|
11
|
+
export const SearchInput: FC<Props> = ({ showInput }) => {
|
|
12
|
+
const [checked, setChecked] = useState<boolean>(false);
|
|
13
|
+
|
|
14
|
+
if (!showInput) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div className="flex items-center px-3 border rounded-full h-8">
|
|
20
|
+
<Search className="h-4 w-4 shrink-0 opacity-50" />
|
|
21
|
+
|
|
22
|
+
<AutoComplete
|
|
23
|
+
initialValue=""
|
|
24
|
+
embedded={true}
|
|
25
|
+
searchByPackage={checked}
|
|
26
|
+
/>
|
|
27
|
+
|
|
28
|
+
<TooltipProvider>
|
|
29
|
+
<Tooltip delayDuration={100}>
|
|
30
|
+
<TooltipTrigger onClick={() => setChecked(!checked)}>
|
|
31
|
+
{checked ? <FileCheck className="opacity-50" /> : <FileX className="opacity-50" />}
|
|
32
|
+
</TooltipTrigger>
|
|
33
|
+
|
|
34
|
+
<TooltipContent>
|
|
35
|
+
If checked will search only in this document
|
|
36
|
+
</TooltipContent>
|
|
37
|
+
</Tooltip>
|
|
38
|
+
</TooltipProvider>
|
|
39
|
+
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DropdownMenu,
|
|
3
|
+
DropdownMenuContent,
|
|
4
|
+
DropdownMenuLabel,
|
|
5
|
+
DropdownMenuPortal,
|
|
6
|
+
DropdownMenuSeparator,
|
|
7
|
+
DropdownMenuSub,
|
|
8
|
+
DropdownMenuSubContent,
|
|
9
|
+
DropdownMenuSubTrigger,
|
|
10
|
+
DropdownMenuTrigger,
|
|
11
|
+
} from "@c-rex/ui/dropdown-menu"
|
|
12
|
+
import { Settings } from "lucide-react";
|
|
13
|
+
import { useTranslations } from "next-intl";
|
|
14
|
+
import { FC } from 'react';
|
|
15
|
+
import { ContentLanguageSwitch } from "./language-switcher/content-language-switch";
|
|
16
|
+
import { UILanguageSwitch } from "./language-switcher/ui-language-switch";
|
|
17
|
+
|
|
18
|
+
export const SettingsMenu: FC = () => {
|
|
19
|
+
const t = useTranslations();
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<DropdownMenu>
|
|
23
|
+
<DropdownMenuTrigger>
|
|
24
|
+
<Settings />
|
|
25
|
+
</DropdownMenuTrigger>
|
|
26
|
+
<DropdownMenuContent align="end" sideOffset={10} alignOffset={0}>
|
|
27
|
+
<DropdownMenuLabel>{t("accountSettings.accountSettings")}</DropdownMenuLabel>
|
|
28
|
+
<DropdownMenuSeparator />
|
|
29
|
+
|
|
30
|
+
<DropdownMenuSub>
|
|
31
|
+
<DropdownMenuSubTrigger>
|
|
32
|
+
<span>{t("accountSettings.contentLanguage")}</span>
|
|
33
|
+
</DropdownMenuSubTrigger>
|
|
34
|
+
|
|
35
|
+
<DropdownMenuPortal>
|
|
36
|
+
<DropdownMenuSubContent>
|
|
37
|
+
<ContentLanguageSwitch />
|
|
38
|
+
</DropdownMenuSubContent>
|
|
39
|
+
</DropdownMenuPortal>
|
|
40
|
+
</DropdownMenuSub>
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
<DropdownMenuSub>
|
|
44
|
+
<DropdownMenuSubTrigger>
|
|
45
|
+
<span>{t("accountSettings.uiLanguage")}</span>
|
|
46
|
+
</DropdownMenuSubTrigger>
|
|
47
|
+
|
|
48
|
+
<DropdownMenuPortal>
|
|
49
|
+
<DropdownMenuSubContent>
|
|
50
|
+
<UILanguageSwitch />
|
|
51
|
+
</DropdownMenuSubContent>
|
|
52
|
+
</DropdownMenuPortal>
|
|
53
|
+
</DropdownMenuSub>
|
|
54
|
+
</DropdownMenuContent>
|
|
55
|
+
</DropdownMenu>
|
|
56
|
+
);
|
|
57
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
import { FC } from 'react';
|
|
3
|
+
import {
|
|
4
|
+
DropdownMenu,
|
|
5
|
+
DropdownMenuContent,
|
|
6
|
+
DropdownMenuGroup,
|
|
7
|
+
DropdownMenuItem,
|
|
8
|
+
DropdownMenuLabel,
|
|
9
|
+
DropdownMenuSeparator,
|
|
10
|
+
DropdownMenuTrigger,
|
|
11
|
+
} from "@c-rex/ui/dropdown-menu"
|
|
12
|
+
import { CircleUser } from "lucide-react";
|
|
13
|
+
import { useTranslations } from "next-intl";
|
|
14
|
+
import { useAppConfig } from "@c-rex/contexts/config-provider";
|
|
15
|
+
import { signOut } from "next-auth/react";
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
interface Props {
|
|
19
|
+
session: any
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const UserMenu: FC<Props> = ({ session }) => {
|
|
23
|
+
const t = useTranslations();
|
|
24
|
+
const { configs } = useAppConfig();
|
|
25
|
+
|
|
26
|
+
const endpoint = configs.OIDC.user.issuer.split("oidc/")[0];
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<DropdownMenu>
|
|
30
|
+
<DropdownMenuTrigger>
|
|
31
|
+
<CircleUser />
|
|
32
|
+
</DropdownMenuTrigger>
|
|
33
|
+
<DropdownMenuContent align="end" sideOffset={10} alignOffset={0}>
|
|
34
|
+
<DropdownMenuLabel>
|
|
35
|
+
{t("user.welcome", { userName: session.user.name.split(" ")[0] })}
|
|
36
|
+
</DropdownMenuLabel>
|
|
37
|
+
<DropdownMenuLabel className='font-normal text-gray-600'>{session.user.email}</DropdownMenuLabel>
|
|
38
|
+
<DropdownMenuSeparator />
|
|
39
|
+
<DropdownMenuGroup>
|
|
40
|
+
<DropdownMenuItem>
|
|
41
|
+
<a href={`${endpoint}oidc/Profile`} target="_blank" rel="noreferrer">
|
|
42
|
+
Profile settings
|
|
43
|
+
</a>
|
|
44
|
+
</DropdownMenuItem>
|
|
45
|
+
|
|
46
|
+
<DropdownMenuItem>
|
|
47
|
+
<a href={`${endpoint}oidc/Profile/ChangePassword`} target="_blank" rel="noreferrer">
|
|
48
|
+
Change password
|
|
49
|
+
</a>
|
|
50
|
+
</DropdownMenuItem>
|
|
51
|
+
|
|
52
|
+
<DropdownMenuItem onClick={() => signOut()} className='text-red-500 focus:text-red-500'>
|
|
53
|
+
{t("user.signOut")}
|
|
54
|
+
</DropdownMenuItem>
|
|
55
|
+
</DropdownMenuGroup>
|
|
56
|
+
</DropdownMenuContent>
|
|
57
|
+
</DropdownMenu>
|
|
58
|
+
);
|
|
59
|
+
};
|
package/src/page-wrapper.tsx
CHANGED
|
@@ -4,12 +4,13 @@ import { NavBar } from './navbar/navbar';
|
|
|
4
4
|
type Props = {
|
|
5
5
|
children: React.ReactNode;
|
|
6
6
|
title: string;
|
|
7
|
+
showInput: boolean;
|
|
7
8
|
}
|
|
8
9
|
|
|
9
|
-
export const PageWrapper = ({ children, title }: Props) => {
|
|
10
|
+
export const PageWrapper = ({ children, title, showInput }: Props) => {
|
|
10
11
|
return (
|
|
11
12
|
<>
|
|
12
|
-
<NavBar title={title} />
|
|
13
|
+
<NavBar title={title} showInput={showInput} />
|
|
13
14
|
{children}
|
|
14
15
|
</>
|
|
15
16
|
);
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import React, { FC, useEffect, useState } from "react"
|
|
4
|
+
import { Button } from "@c-rex/ui/button"
|
|
5
|
+
import {
|
|
6
|
+
Dialog,
|
|
7
|
+
DialogClose,
|
|
8
|
+
DialogContent,
|
|
9
|
+
DialogFooter,
|
|
10
|
+
DialogHeader,
|
|
11
|
+
DialogTitle,
|
|
12
|
+
DialogTrigger,
|
|
13
|
+
} from "@c-rex/ui/dialog"
|
|
14
|
+
import { parseAsBoolean, parseAsInteger, parseAsString, useQueryStates } from "nuqs"
|
|
15
|
+
import { useTranslations } from "next-intl"
|
|
16
|
+
import { CONTENT_LANG_KEY, WILD_CARD_OPTIONS } from "@c-rex/constants"
|
|
17
|
+
import { call, getFromCookieString } from "@c-rex/utils"
|
|
18
|
+
import { Input } from "@c-rex/ui/input"
|
|
19
|
+
import { useAppConfig } from "@c-rex/contexts/config-provider"
|
|
20
|
+
import { Label } from "@c-rex/ui/label"
|
|
21
|
+
import { Checkbox } from "@c-rex/ui/checkbox"
|
|
22
|
+
import { Toggle } from "@c-rex/ui/toggle"
|
|
23
|
+
|
|
24
|
+
interface SearchModalProps {
|
|
25
|
+
trigger: React.ReactNode;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const SearchModal: FC<SearchModalProps> = ({ trigger }) => {
|
|
29
|
+
const t = useTranslations("filter");
|
|
30
|
+
const { contentLang } = useAppConfig()
|
|
31
|
+
|
|
32
|
+
const [suggestions, setSuggestions] = useState<string[]>([]);
|
|
33
|
+
const [value, setValue] = useState<string>("");
|
|
34
|
+
const [loading, setLoading] = useState<boolean>(false);
|
|
35
|
+
const [params, setParams] = useQueryStates({
|
|
36
|
+
language: parseAsString,
|
|
37
|
+
page: parseAsInteger,
|
|
38
|
+
wildcard: parseAsString,
|
|
39
|
+
operator: parseAsString,
|
|
40
|
+
search: parseAsString,
|
|
41
|
+
package: parseAsString,
|
|
42
|
+
tags: parseAsBoolean,
|
|
43
|
+
like: parseAsBoolean,
|
|
44
|
+
}, {
|
|
45
|
+
history: 'push',
|
|
46
|
+
shallow: false,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
const onSearch = (value: string): Promise<string[]> => {
|
|
51
|
+
return call<string[]>("InformationUnitsService.getSuggestions", { query: value, language: contentLang });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
const debounceFetch = setTimeout(() => {
|
|
57
|
+
if (value) {
|
|
58
|
+
setLoading(true)
|
|
59
|
+
onSearch(value).then(suggestions => {
|
|
60
|
+
setSuggestions(suggestions)
|
|
61
|
+
setLoading(false)
|
|
62
|
+
});
|
|
63
|
+
} else {
|
|
64
|
+
setSuggestions([]);
|
|
65
|
+
}
|
|
66
|
+
}, 300);
|
|
67
|
+
|
|
68
|
+
return () => clearTimeout(debounceFetch);
|
|
69
|
+
}, [value]);
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
const apply = () => {
|
|
73
|
+
const cookie = getFromCookieString(document.cookie, CONTENT_LANG_KEY)
|
|
74
|
+
|
|
75
|
+
setParams({
|
|
76
|
+
search: value,
|
|
77
|
+
operator: "OR",
|
|
78
|
+
page: 1,
|
|
79
|
+
language: cookie,
|
|
80
|
+
wildcard: WILD_CARD_OPTIONS.BOTH,
|
|
81
|
+
tags: false,
|
|
82
|
+
like: false,
|
|
83
|
+
package: ""
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<Dialog>
|
|
89
|
+
<DialogTrigger asChild>
|
|
90
|
+
{trigger}
|
|
91
|
+
</DialogTrigger>
|
|
92
|
+
<DialogContent>
|
|
93
|
+
<DialogHeader>
|
|
94
|
+
<DialogTitle>Search</DialogTitle>
|
|
95
|
+
</DialogHeader>
|
|
96
|
+
<Input value={value} autoFocus onChange={(e) => setValue(e.target.value)} />
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
{loading ? (
|
|
101
|
+
<div className="flex items-center justify-center py-4">
|
|
102
|
+
<div className="animate-spin rounded-full h-6 w-6 border-2 border-gray-300 border-t-gray-950" />
|
|
103
|
+
</div>
|
|
104
|
+
) : (
|
|
105
|
+
<div>
|
|
106
|
+
{suggestions.map((option) => (
|
|
107
|
+
<Toggle
|
|
108
|
+
key={option}
|
|
109
|
+
value={option}
|
|
110
|
+
className="m-0"
|
|
111
|
+
onClick={(inputValue) => {
|
|
112
|
+
setValue(`"${inputValue}"`);
|
|
113
|
+
}}
|
|
114
|
+
>
|
|
115
|
+
{option}
|
|
116
|
+
</Toggle>
|
|
117
|
+
))}
|
|
118
|
+
</div>
|
|
119
|
+
)}
|
|
120
|
+
|
|
121
|
+
<Label className="hover:bg-accent/50 flex items-start gap-3 rounded-lg border p-3">
|
|
122
|
+
<Checkbox
|
|
123
|
+
id="toggle-2"
|
|
124
|
+
defaultChecked
|
|
125
|
+
className="data-[state=checked]:border-blue-600 data-[state=checked]:bg-blue-600 data-[state=checked]:text-white dark:data-[state=checked]:border-blue-700 dark:data-[state=checked]:bg-blue-700"
|
|
126
|
+
/>
|
|
127
|
+
<div className="font-normal">
|
|
128
|
+
<span className="text-sm leading-none font-medium">
|
|
129
|
+
Search only on this document
|
|
130
|
+
</span>
|
|
131
|
+
<p className="text-muted-foreground text-sm">
|
|
132
|
+
You can enable or disable notifications at any time.
|
|
133
|
+
</p>
|
|
134
|
+
</div>
|
|
135
|
+
</Label>
|
|
136
|
+
|
|
137
|
+
<DialogFooter className="pt-2">
|
|
138
|
+
<DialogClose asChild>
|
|
139
|
+
<Button onClick={apply}>{t("apply")}</Button>
|
|
140
|
+
</DialogClose>
|
|
141
|
+
</DialogFooter>
|
|
142
|
+
</DialogContent>
|
|
143
|
+
</Dialog >
|
|
144
|
+
)
|
|
145
|
+
}
|