@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@c-rex/components",
3
- "version": "0.1.6",
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": {
@@ -1,122 +1,138 @@
1
- import React from "react";
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
- Command,
7
- CommandEmpty,
8
- CommandGroup,
9
- CommandItem,
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
- onSelect: (value: string) => void;
21
- onSearch: (value: string) => Promise<string[]>;
11
+ embedded: boolean
12
+ searchByPackage: boolean
22
13
  };
23
14
 
24
- export const AutoComplete = ({
25
- initialValue,
26
- onSearch,
27
- onSelect,
28
- }: Props) => {
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 debounceFetch = setTimeout(() => {
40
- if (query) {
41
- setLoading(true)
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
- }, [onSearch, query]);
81
+ }, [query]);
53
82
 
54
83
  return (
55
- <div className="flex items-center">
56
- <Popover open={open} onOpenChange={setOpen}>
57
- <Command shouldFilter={!loading} >
58
- <PopoverAnchor asChild>
59
- <CommandPrimitive.Input
60
- asChild
61
- value={query}
62
- onValueChange={setQuery}
63
- onKeyDown={(e) => setOpen(e.key !== "Escape")}
64
- onMouseDown={() => setOpen(false)}
65
- onFocus={() => setOpen(true)}
66
- >
67
- <Input value={query} />
68
- </CommandPrimitive.Input>
69
- </PopoverAnchor>
70
-
71
- <PopoverContent
72
- asChild
73
- onOpenAutoFocus={(e) => e.preventDefault()}
74
- onInteractOutside={(e) => {
75
- if (
76
- e.target instanceof Element &&
77
- e.target.hasAttribute("cmdk-input")
78
- ) {
79
- e.preventDefault();
80
- }
81
- }}
82
- className="w-[--radix-popover-trigger-width] p-0"
83
- >
84
- <CommandList
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.length > 0 ? (
96
- <CommandGroup>
97
- {suggestions.map((option) => (
98
- <CommandItem
99
- key={option}
100
- value={option}
101
- onMouseDown={(e) => e.preventDefault()}
102
- onSelect={(inputValue) => {
103
- setOpen(false);
104
- onSelect(inputValue);
105
- }}
106
- >
107
- {option}
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
- </CommandList>
117
- </PopoverContent>
118
- </Command>
119
- </Popover>
120
- </div>
132
+ </>
133
+ )}
134
+ </ul>
135
+ )}
136
+ </div >
121
137
  );
122
138
  }
@@ -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
- startSelectedLanguages: string[];
20
- availableLanguages: any;
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
- trigger,
25
- startSelectedLanguages,
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 [languages, setLanguages] = useState(availableLanguages.map((item: any) => {
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 onChange = (item: any) => {
48
- const newLang = languages.filter((lang: any) => lang.lang !== item.lang)
49
- setLanguages([...newLang, item].sort((a, b) => {
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
- const selectedLanguages = languages.filter((item: any) => item.checked).map((item: any) => item.value).join(',')
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
- <div className="grid grid-cols-2 items-center py-4">
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) => onChange({
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
- <DialogFooter>
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("applyFilters")}</Button>
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
  }
@@ -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
+ };
@@ -1,31 +1,20 @@
1
- import React, { FC } from "react";
1
+ import { FC } from "react";
2
2
  import Link from "next/link";
3
3
  import Image from "next/image";
4
- import { SignOut, SignInBtn } from "./sign-in-out-btns";
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
- <DropdownMenu>
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
+ };
@@ -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 title={title} />
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
+ }