@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@c-rex/components",
3
- "version": "0.1.5",
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": {
@@ -1,122 +1,160 @@
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, 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
- 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 { 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 debounceFetch = setTimeout(() => {
40
- if (query) {
41
- setLoading(true)
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
- }, [onSearch, query]);
103
+ }, [query]);
53
104
 
54
105
  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
- ) : (
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.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
- )}
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
- </CommandList>
117
- </PopoverContent>
118
- </Command>
119
- </Popover>
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 == undefined) {
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
  }
@@ -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
- startSelectedLanguages: string[];
20
- availableLanguages: any;
26
+ }
27
+ interface Languages {
28
+ lang: string;
29
+ value: string;
30
+ checked: boolean;
21
31
  }
22
32
 
23
- export const DialogFilter: FC<DialogFilterProps> = ({
24
- trigger,
25
- startSelectedLanguages,
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 [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 [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 onChange = (item: any) => {
48
- const newLang = languages.filter((lang: any) => lang.lang !== item.lang)
49
- setLanguages([...newLang, item].sort((a, b) => {
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
- <div className="grid grid-cols-2 items-center py-4">
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) => onChange({
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
- <DialogFooter>
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("applyFilters")}</Button>
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 React, { startTransition } from "react";
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 { availableLanguagesAndCountries, setContentLang, availableVersions } = useAppConfig()
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}
@@ -1,31 +1,19 @@
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;
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
- <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>
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
+ };
@@ -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
+ }