@c-rex/components 0.1.6 → 0.1.8
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 +9 -1
- package/src/autocomplete.tsx +113 -97
- package/src/dialog-filter.tsx +110 -21
- package/src/loading.tsx +12 -0
- package/src/navbar/navbar.tsx +11 -63
- package/src/navbar/search-input.tsx +45 -0
- package/src/navbar/settings.tsx +57 -0
- package/src/navbar/user-menu.tsx +59 -0
- package/src/page-wrapper.tsx +10 -2
- package/src/search-modal.tsx +144 -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.8",
|
|
4
4
|
"files": [
|
|
5
5
|
"src"
|
|
6
6
|
],
|
|
@@ -72,6 +72,14 @@
|
|
|
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"
|
|
79
|
+
},
|
|
80
|
+
"./loading": {
|
|
81
|
+
"types": "./src/loading.tsx",
|
|
82
|
+
"import": "./src/loading.tsx"
|
|
75
83
|
}
|
|
76
84
|
},
|
|
77
85
|
"scripts": {
|
package/src/autocomplete.tsx
CHANGED
|
@@ -1,122 +1,138 @@
|
|
|
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 } from "@c-rex/utils";
|
|
4
|
+
import { 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 { articleLang, 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: articleLang || contentLang });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const onSelect = (value: string) => {
|
|
30
|
+
const params: QueryParams[] = [
|
|
31
|
+
{ key: "search", value: value },
|
|
32
|
+
{ key: "operator", value: "OR" },
|
|
33
|
+
{ key: "page", value: "1" },
|
|
34
|
+
{ key: "language", value: articleLang || contentLang as string },
|
|
35
|
+
{ key: "wildcard", value: WILD_CARD_OPTIONS.BOTH },
|
|
36
|
+
{ key: "like", value: "false" },
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
if (searchByPackage && packageID !== null) {
|
|
40
|
+
params.push({ key: "packages", value: packageID })
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const aux = generateQueryParams(params)
|
|
44
|
+
|
|
45
|
+
window.location.href = `${window.location.origin}/?${aux}`
|
|
46
|
+
};
|
|
47
|
+
|
|
33
48
|
|
|
34
49
|
useEffect(() => {
|
|
35
50
|
setQuery(initialValue);
|
|
36
51
|
}, [initialValue]);
|
|
37
52
|
|
|
38
53
|
useEffect(() => {
|
|
39
|
-
const
|
|
40
|
-
if (
|
|
41
|
-
|
|
42
|
-
onSearch(query).then(suggestions => {
|
|
43
|
-
setSuggestions(suggestions)
|
|
44
|
-
setLoading(false)
|
|
45
|
-
});
|
|
46
|
-
} else {
|
|
47
|
-
setSuggestions([]);
|
|
54
|
+
const handleClickOutside = (e: MouseEvent) => {
|
|
55
|
+
if (!containerRef.current?.contains(e.target as Node)) {
|
|
56
|
+
setOpen(false);
|
|
48
57
|
}
|
|
58
|
+
};
|
|
59
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
60
|
+
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
61
|
+
}, []);
|
|
62
|
+
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
if (query.length < 2) {
|
|
65
|
+
setSuggestions([]);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const debounceFetch = setTimeout(() => {
|
|
70
|
+
setLoading(true)
|
|
71
|
+
onSearch(query).then(suggestions => {
|
|
72
|
+
setSuggestions(suggestions)
|
|
73
|
+
setLoading(false)
|
|
74
|
+
}).catch(() => {
|
|
75
|
+
setLoading(false)
|
|
76
|
+
setSuggestions([])
|
|
77
|
+
});
|
|
49
78
|
}, 300);
|
|
50
79
|
|
|
51
80
|
return () => clearTimeout(debounceFetch);
|
|
52
|
-
}, [
|
|
81
|
+
}, [query]);
|
|
53
82
|
|
|
54
83
|
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
|
-
) : (
|
|
84
|
+
<div className="relative" ref={containerRef}>
|
|
85
|
+
<Input
|
|
86
|
+
variant={embedded ? "embedded" : undefined}
|
|
87
|
+
type="text"
|
|
88
|
+
placeholder={t("search")}
|
|
89
|
+
value={query}
|
|
90
|
+
onChange={(e) => {
|
|
91
|
+
setQuery(e.target.value);
|
|
92
|
+
setOpen(true);
|
|
93
|
+
}}
|
|
94
|
+
onKeyDown={(e) => {
|
|
95
|
+
if (e.key === "Enter") {
|
|
96
|
+
e.preventDefault();
|
|
97
|
+
onSelect(query);
|
|
98
|
+
setOpen(false)
|
|
99
|
+
}
|
|
100
|
+
}}
|
|
101
|
+
/>
|
|
102
|
+
|
|
103
|
+
{open && (
|
|
104
|
+
<ul className="absolute z-10 w-full bg-white border border-gray-300 rounded-lg shadow-lg max-h-60 overflow-y-auto">
|
|
105
|
+
{loading ? (
|
|
106
|
+
<li>
|
|
107
|
+
<div className="flex items-center justify-center py-4">
|
|
108
|
+
<div className="animate-spin rounded-full h-6 w-6 border-2 border-gray-300 border-t-gray-950" />
|
|
109
|
+
</div>
|
|
110
|
+
</li>
|
|
111
|
+
) : (
|
|
112
|
+
<>
|
|
113
|
+
{suggestions.length > 0 ? (
|
|
94
114
|
<>
|
|
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
|
-
)}
|
|
115
|
+
{suggestions.map((option, index) => (
|
|
116
|
+
<li
|
|
117
|
+
key={index}
|
|
118
|
+
className="px-4 py-2 hover:bg-accent cursor-pointer text-sm"
|
|
119
|
+
onClick={() => {
|
|
120
|
+
setQuery(option);
|
|
121
|
+
setOpen(false);
|
|
122
|
+
onSelect(`"${option}"`);
|
|
123
|
+
}}
|
|
124
|
+
>
|
|
125
|
+
{option}
|
|
126
|
+
</li>
|
|
127
|
+
))}
|
|
114
128
|
</>
|
|
129
|
+
) : (
|
|
130
|
+
<li className="px-4 py-2">No suggestions.</li>
|
|
115
131
|
)}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
</
|
|
119
|
-
|
|
120
|
-
</div>
|
|
132
|
+
</>
|
|
133
|
+
)}
|
|
134
|
+
</ul>
|
|
135
|
+
)}
|
|
136
|
+
</div >
|
|
121
137
|
);
|
|
122
138
|
}
|
package/src/dialog-filter.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { FC, useState } from "react"
|
|
1
|
+
import React, { FC, use, useEffect, useState } from "react"
|
|
2
2
|
import { Button } from "@c-rex/ui/button"
|
|
3
3
|
import {
|
|
4
4
|
Dialog,
|
|
@@ -9,34 +9,50 @@ 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"
|
|
12
13
|
import { Checkbox } from "@c-rex/ui/checkbox"
|
|
13
14
|
import { Label } from "@c-rex/ui/label"
|
|
14
|
-
import { parseAsInteger, parseAsString, useQueryStates } from "nuqs"
|
|
15
|
+
import { parseAsBoolean, parseAsInteger, parseAsString, useQueryStates } from "nuqs"
|
|
15
16
|
import { useTranslations } from "next-intl"
|
|
17
|
+
import { useAppConfig } from "@c-rex/contexts/config-provider"
|
|
18
|
+
import { LanguageAndCountries } from "@c-rex/interfaces"
|
|
19
|
+
import { WILD_CARD_OPTIONS, OPERATOR_OPTIONS } from "@c-rex/constants"
|
|
20
|
+
import { Switch } from "@c-rex/ui/switch"
|
|
16
21
|
|
|
17
22
|
interface DialogFilterProps {
|
|
18
23
|
trigger: React.ReactNode;
|
|
19
|
-
|
|
20
|
-
|
|
24
|
+
setLoading: (loading: boolean) => void;
|
|
25
|
+
}
|
|
26
|
+
interface Languages {
|
|
27
|
+
lang: string;
|
|
28
|
+
value: string;
|
|
29
|
+
checked: boolean;
|
|
21
30
|
}
|
|
22
31
|
|
|
23
|
-
export const DialogFilter: FC<DialogFilterProps> = ({
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
availableLanguages,
|
|
27
|
-
}) => {
|
|
28
|
-
const t = useTranslations();
|
|
29
|
-
|
|
32
|
+
export const DialogFilter: FC<DialogFilterProps> = ({ trigger, setLoading }) => {
|
|
33
|
+
const t = useTranslations("filter");
|
|
34
|
+
const { availableLanguagesAndCountries } = useAppConfig()
|
|
30
35
|
const [params, setParams] = useQueryStates({
|
|
31
36
|
language: parseAsString,
|
|
32
37
|
page: parseAsInteger,
|
|
38
|
+
wildcard: parseAsString,
|
|
39
|
+
operator: parseAsString,
|
|
40
|
+
filter: 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 [disabled, setDisabled] = useState<boolean>(false)
|
|
53
|
+
const [languages, setLanguages] = useState<Languages[]>(availableLanguagesAndCountries.map((item: LanguageAndCountries) => {
|
|
39
54
|
const checked = startSelectedLanguages.includes(item.value)
|
|
55
|
+
|
|
40
56
|
return {
|
|
41
57
|
lang: item.lang,
|
|
42
58
|
value: item.value,
|
|
@@ -44,22 +60,51 @@ export const DialogFilter: FC<DialogFilterProps> = ({
|
|
|
44
60
|
}
|
|
45
61
|
}));
|
|
46
62
|
|
|
47
|
-
const
|
|
48
|
-
const
|
|
49
|
-
|
|
63
|
+
const onChangeCheckbox = (item: Languages) => {
|
|
64
|
+
const newLangList = languages.filter((lang) => lang.lang !== item.lang)
|
|
65
|
+
|
|
66
|
+
setLanguages([...newLangList, item].sort((a, b) => {
|
|
50
67
|
return a.value.localeCompare(b.value)
|
|
51
68
|
}))
|
|
52
69
|
}
|
|
53
70
|
|
|
54
71
|
const apply = () => {
|
|
55
|
-
|
|
72
|
+
setLoading(true);
|
|
73
|
+
|
|
74
|
+
const selectedLanguages = languages
|
|
75
|
+
.filter((item: any) => item.checked)
|
|
76
|
+
.map((item: any) => item.value)
|
|
77
|
+
.join(',')
|
|
56
78
|
|
|
57
79
|
setParams({
|
|
58
80
|
page: 1,
|
|
59
81
|
language: selectedLanguages,
|
|
82
|
+
operator: operator,
|
|
83
|
+
wildcard: wildcard,
|
|
84
|
+
like: like,
|
|
85
|
+
filter: null,
|
|
60
86
|
});
|
|
61
87
|
}
|
|
62
88
|
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
setOperator(params.operator || OPERATOR_OPTIONS.OR);
|
|
91
|
+
setLike(params.like || false);
|
|
92
|
+
setWildcard(params.wildcard || "");
|
|
93
|
+
setLanguages(availableLanguagesAndCountries.map((item: LanguageAndCountries) => {
|
|
94
|
+
const checked = startSelectedLanguages.includes(item.value)
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
lang: item.lang,
|
|
98
|
+
value: item.value,
|
|
99
|
+
checked,
|
|
100
|
+
}
|
|
101
|
+
}));
|
|
102
|
+
}, [params])
|
|
103
|
+
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
setDisabled(languages.every((item) => !item.checked));
|
|
106
|
+
}, [languages])
|
|
107
|
+
|
|
63
108
|
return (
|
|
64
109
|
<Dialog>
|
|
65
110
|
<DialogTrigger asChild>
|
|
@@ -69,7 +114,8 @@ export const DialogFilter: FC<DialogFilterProps> = ({
|
|
|
69
114
|
<DialogHeader>
|
|
70
115
|
<DialogTitle>{t("filters")}</DialogTitle>
|
|
71
116
|
</DialogHeader>
|
|
72
|
-
|
|
117
|
+
|
|
118
|
+
<div className="grid grid-cols-2 items-center pt-2">
|
|
73
119
|
<Label className="text-right">
|
|
74
120
|
{t("languages")}:
|
|
75
121
|
</Label>
|
|
@@ -81,7 +127,7 @@ export const DialogFilter: FC<DialogFilterProps> = ({
|
|
|
81
127
|
id={`available-languages${item.lang}`}
|
|
82
128
|
className="mr-2"
|
|
83
129
|
{...({ checked: item.checked })}
|
|
84
|
-
onCheckedChange={(checked: boolean) =>
|
|
130
|
+
onCheckedChange={(checked: boolean) => onChangeCheckbox({
|
|
85
131
|
...item,
|
|
86
132
|
checked: checked,
|
|
87
133
|
})}
|
|
@@ -96,14 +142,57 @@ export const DialogFilter: FC<DialogFilterProps> = ({
|
|
|
96
142
|
)
|
|
97
143
|
})}
|
|
98
144
|
</div>
|
|
145
|
+
</div>
|
|
99
146
|
|
|
147
|
+
<div className="grid grid-cols-2 items-center pt-2">
|
|
148
|
+
<Label className="text-right">
|
|
149
|
+
{t("wildcard")}:
|
|
150
|
+
</Label>
|
|
151
|
+
<div className="flex items-center">
|
|
152
|
+
<Select onValueChange={setWildcard} defaultValue={wildcard}>
|
|
153
|
+
<SelectTrigger className="w-[180px]">
|
|
154
|
+
<SelectValue placeholder="" />
|
|
155
|
+
</SelectTrigger>
|
|
156
|
+
<SelectContent>
|
|
157
|
+
<SelectGroup>
|
|
158
|
+
{Object.keys(WILD_CARD_OPTIONS).map((item: string) => {
|
|
159
|
+
return (
|
|
160
|
+
<SelectItem key={item} value={item}>
|
|
161
|
+
{item}
|
|
162
|
+
</SelectItem>
|
|
163
|
+
)
|
|
164
|
+
})}
|
|
165
|
+
</SelectGroup>
|
|
166
|
+
</SelectContent>
|
|
167
|
+
</Select>
|
|
168
|
+
</div>
|
|
100
169
|
</div>
|
|
101
|
-
|
|
170
|
+
|
|
171
|
+
<div className="grid grid-cols-2 items-center pt-2">
|
|
172
|
+
<Label htmlFor="operator" className="text-right">
|
|
173
|
+
{t("operator")}:
|
|
174
|
+
</Label>
|
|
175
|
+
|
|
176
|
+
<Switch
|
|
177
|
+
id="operator"
|
|
178
|
+
onCheckedChange={(value) => setOperator(value ? OPERATOR_OPTIONS.AND : OPERATOR_OPTIONS.OR)}
|
|
179
|
+
checked={operator == OPERATOR_OPTIONS.AND}
|
|
180
|
+
/>
|
|
181
|
+
</div>
|
|
182
|
+
|
|
183
|
+
<div className="grid grid-cols-2 items-center pt-2">
|
|
184
|
+
<Label htmlFor="like" className="text-right">
|
|
185
|
+
{t("like")}:
|
|
186
|
+
</Label>
|
|
187
|
+
<Switch id="like" onCheckedChange={(value) => setLike(value)} checked={like} />
|
|
188
|
+
</div>
|
|
189
|
+
|
|
190
|
+
<DialogFooter className="pt-2">
|
|
102
191
|
<DialogClose asChild>
|
|
103
|
-
<Button onClick={apply}>{t("
|
|
192
|
+
<Button onClick={apply} disabled={disabled}>{t("apply")}</Button>
|
|
104
193
|
</DialogClose>
|
|
105
194
|
</DialogFooter>
|
|
106
195
|
</DialogContent>
|
|
107
|
-
</Dialog>
|
|
196
|
+
</Dialog >
|
|
108
197
|
)
|
|
109
198
|
}
|
package/src/loading.tsx
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import React, { FC } from "react";
|
|
2
|
+
|
|
3
|
+
type LoadingProps = {
|
|
4
|
+
opacity: boolean;
|
|
5
|
+
}
|
|
6
|
+
export const Loading: FC<LoadingProps> = ({ opacity }) => {
|
|
7
|
+
return (
|
|
8
|
+
<div className={`fixed inset-0 z-[100000] w-full h-full flex justify-center items-center bg-white ${opacity ? "opacity-50" : ""}`}>
|
|
9
|
+
<div className="animate-spin rounded-full h-12 w-12 border-2 border-gray-300 border-t-gray-950"></div>
|
|
10
|
+
</div>
|
|
11
|
+
);
|
|
12
|
+
};
|
package/src/navbar/navbar.tsx
CHANGED
|
@@ -1,31 +1,20 @@
|
|
|
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;
|
|
14
|
+
showPkgFilter: boolean;
|
|
25
15
|
}
|
|
26
16
|
|
|
27
|
-
export const NavBar: FC<NavBarProps> = async ({ title }) => {
|
|
28
|
-
const t = await getTranslations();
|
|
17
|
+
export const NavBar: FC<NavBarProps> = async ({ title, showInput, showPkgFilter }) => {
|
|
29
18
|
const configs = await getConfigs();
|
|
30
19
|
|
|
31
20
|
let session: any;
|
|
@@ -54,20 +43,12 @@ export const NavBar: FC<NavBarProps> = async ({ title }) => {
|
|
|
54
43
|
</div>
|
|
55
44
|
|
|
56
45
|
<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
|
-
*/}
|
|
46
|
+
<SearchInput showInput={showInput} showPkgFilter={showPkgFilter} />
|
|
63
47
|
|
|
64
48
|
{configs.OIDC.user.enabled && (
|
|
65
49
|
<>
|
|
66
50
|
{session ? (
|
|
67
|
-
|
|
68
|
-
<span>{t("user.welcome", { userName: session.user?.name as string })}</span>
|
|
69
|
-
<SignOut />
|
|
70
|
-
</>
|
|
51
|
+
<UserMenu session={session} />
|
|
71
52
|
) : (
|
|
72
53
|
<SignInBtn />
|
|
73
54
|
)}
|
|
@@ -75,40 +56,7 @@ export const NavBar: FC<NavBarProps> = async ({ title }) => {
|
|
|
75
56
|
)}
|
|
76
57
|
|
|
77
58
|
{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>
|
|
59
|
+
<SettingsMenu />
|
|
112
60
|
)}
|
|
113
61
|
</div>
|
|
114
62
|
</div>
|
|
@@ -0,0 +1,45 @@
|
|
|
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
|
+
showPkgFilter: boolean
|
|
11
|
+
}
|
|
12
|
+
export const SearchInput: FC<Props> = ({ showInput, showPkgFilter }) => {
|
|
13
|
+
const [checked, setChecked] = useState<boolean>(true);
|
|
14
|
+
|
|
15
|
+
if (!showInput) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<div className="flex items-center px-3 border rounded-full h-8">
|
|
21
|
+
<Search className="h-4 w-4 shrink-0 opacity-50" />
|
|
22
|
+
|
|
23
|
+
<AutoComplete
|
|
24
|
+
initialValue=""
|
|
25
|
+
embedded={true}
|
|
26
|
+
searchByPackage={checked}
|
|
27
|
+
/>
|
|
28
|
+
|
|
29
|
+
{showPkgFilter && <>
|
|
30
|
+
<TooltipProvider>
|
|
31
|
+
<Tooltip delayDuration={100}>
|
|
32
|
+
<TooltipTrigger onClick={() => setChecked(!checked)}>
|
|
33
|
+
{checked ? <FileCheck className="opacity-50" /> : <FileX className="opacity-50" />}
|
|
34
|
+
</TooltipTrigger>
|
|
35
|
+
|
|
36
|
+
<TooltipContent>
|
|
37
|
+
If checked will search only in this document
|
|
38
|
+
</TooltipContent>
|
|
39
|
+
</Tooltip>
|
|
40
|
+
</TooltipProvider>
|
|
41
|
+
</>}
|
|
42
|
+
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
};
|
|
@@ -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,20 @@ import { NavBar } from './navbar/navbar';
|
|
|
4
4
|
type Props = {
|
|
5
5
|
children: React.ReactNode;
|
|
6
6
|
title: string;
|
|
7
|
+
pageType: "BLOG" | "DOC" | "HOME"
|
|
7
8
|
}
|
|
8
9
|
|
|
9
|
-
export const PageWrapper = ({ children, title }: Props) => {
|
|
10
|
+
export const PageWrapper = ({ children, title, pageType }: Props) => {
|
|
11
|
+
const showInput = pageType === "DOC" || pageType === "BLOG"
|
|
12
|
+
const showPkgFilter = pageType === "DOC"
|
|
13
|
+
|
|
10
14
|
return (
|
|
11
15
|
<>
|
|
12
|
-
<NavBar
|
|
16
|
+
<NavBar
|
|
17
|
+
title={title}
|
|
18
|
+
showInput={showInput}
|
|
19
|
+
showPkgFilter={showPkgFilter}
|
|
20
|
+
/>
|
|
13
21
|
{children}
|
|
14
22
|
</>
|
|
15
23
|
);
|
|
@@ -0,0 +1,144 @@
|
|
|
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
|
+
packages: 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
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<Dialog>
|
|
88
|
+
<DialogTrigger asChild>
|
|
89
|
+
{trigger}
|
|
90
|
+
</DialogTrigger>
|
|
91
|
+
<DialogContent>
|
|
92
|
+
<DialogHeader>
|
|
93
|
+
<DialogTitle>Search</DialogTitle>
|
|
94
|
+
</DialogHeader>
|
|
95
|
+
<Input value={value} autoFocus onChange={(e) => setValue(e.target.value)} />
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
{loading ? (
|
|
100
|
+
<div className="flex items-center justify-center py-4">
|
|
101
|
+
<div className="animate-spin rounded-full h-6 w-6 border-2 border-gray-300 border-t-gray-950" />
|
|
102
|
+
</div>
|
|
103
|
+
) : (
|
|
104
|
+
<div>
|
|
105
|
+
{suggestions.map((option) => (
|
|
106
|
+
<Toggle
|
|
107
|
+
key={option}
|
|
108
|
+
value={option}
|
|
109
|
+
className="m-0"
|
|
110
|
+
onClick={(inputValue) => {
|
|
111
|
+
setValue(`"${inputValue}"`);
|
|
112
|
+
}}
|
|
113
|
+
>
|
|
114
|
+
{option}
|
|
115
|
+
</Toggle>
|
|
116
|
+
))}
|
|
117
|
+
</div>
|
|
118
|
+
)}
|
|
119
|
+
|
|
120
|
+
<Label className="hover:bg-accent/50 flex items-start gap-3 rounded-lg border p-3">
|
|
121
|
+
<Checkbox
|
|
122
|
+
id="toggle-2"
|
|
123
|
+
defaultChecked
|
|
124
|
+
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"
|
|
125
|
+
/>
|
|
126
|
+
<div className="font-normal">
|
|
127
|
+
<span className="text-sm leading-none font-medium">
|
|
128
|
+
Search only on this document
|
|
129
|
+
</span>
|
|
130
|
+
<p className="text-muted-foreground text-sm">
|
|
131
|
+
You can enable or disable notifications at any time.
|
|
132
|
+
</p>
|
|
133
|
+
</div>
|
|
134
|
+
</Label>
|
|
135
|
+
|
|
136
|
+
<DialogFooter className="pt-2">
|
|
137
|
+
<DialogClose asChild>
|
|
138
|
+
<Button onClick={apply}>{t("apply")}</Button>
|
|
139
|
+
</DialogClose>
|
|
140
|
+
</DialogFooter>
|
|
141
|
+
</DialogContent>
|
|
142
|
+
</Dialog >
|
|
143
|
+
)
|
|
144
|
+
}
|