@greatapps/greatauth-ui 0.3.17 → 0.3.19

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": "@greatapps/greatauth-ui",
3
- "version": "0.3.17",
3
+ "version": "0.3.19",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -25,6 +25,7 @@ export function AppHeader({ config }: AppHeaderProps) {
25
25
  const pathname = usePathname();
26
26
  const segments = pathname.split("/").filter(Boolean);
27
27
 
28
+ const routeLabelsJson = JSON.stringify(config.routeLabels);
28
29
  const breadcrumbs = useMemo(() => {
29
30
  const items: { label: string; href: string }[] = [];
30
31
  for (let i = 0; i < segments.length; i++) {
@@ -35,7 +36,7 @@ export function AppHeader({ config }: AppHeaderProps) {
35
36
  }
36
37
  }
37
38
  return items;
38
- }, [segments, config.routeLabels]);
39
+ }, [segments, routeLabelsJson]); // eslint-disable-line react-hooks/exhaustive-deps
39
40
 
40
41
  const isLast = (i: number) => i === breadcrumbs.length - 1;
41
42
  const showEllipsis = breadcrumbs.length > 2;
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { useState, useEffect } from "react";
3
+ import { useState, useEffect, memo } from "react";
4
4
  import { usePathname, useRouter } from "next/navigation";
5
5
  import Link from "next/link";
6
6
  import { ChevronsUpDown, ChevronRight, LogOut } from "lucide-react";
@@ -50,7 +50,7 @@ function getUserInitials(name: string, email: string): string {
50
50
  return parts[0][0].toUpperCase();
51
51
  }
52
52
 
53
- function SimpleMenuItem({ item, pathname }: { item: MenuItem; pathname: string }) {
53
+ const SimpleMenuItem = memo(function SimpleMenuItem({ item, pathname }: { item: MenuItem; pathname: string }) {
54
54
  const isActive = item.isActive ?? pathname.startsWith(item.href);
55
55
  const Icon = item.icon;
56
56
 
@@ -69,9 +69,9 @@ function SimpleMenuItem({ item, pathname }: { item: MenuItem; pathname: string }
69
69
  </SidebarMenuButton>
70
70
  </SidebarMenuItem>
71
71
  );
72
- }
72
+ });
73
73
 
74
- function CollapsibleMenuItem({ item, pathname }: { item: MenuItem; pathname: string }) {
74
+ const CollapsibleMenuItem = memo(function CollapsibleMenuItem({ item, pathname }: { item: MenuItem; pathname: string }) {
75
75
  const Icon = item.icon;
76
76
  const isParentActive = pathname.startsWith(item.href);
77
77
  const isChildActive = item.children?.some((child) =>
@@ -82,8 +82,8 @@ function CollapsibleMenuItem({ item, pathname }: { item: MenuItem; pathname: str
82
82
  const [open, setOpen] = useState(isActive);
83
83
 
84
84
  useEffect(() => {
85
- if (isActive) setOpen(true);
86
- }, [isActive]);
85
+ if (open !== isActive) setOpen(isActive);
86
+ }, [isActive]); // eslint-disable-line react-hooks/exhaustive-deps
87
87
 
88
88
  return (
89
89
  <Collapsible open={open} onOpenChange={setOpen} className="group/collapsible">
@@ -113,7 +113,7 @@ function CollapsibleMenuItem({ item, pathname }: { item: MenuItem; pathname: str
113
113
  </SidebarMenuItem>
114
114
  </Collapsible>
115
115
  );
116
- }
116
+ });
117
117
 
118
118
  export function AppSidebar({ config }: AppSidebarProps) {
119
119
  const pathname = usePathname();
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { useMemo, useState } from "react";
3
+ import { useCallback, useMemo, useState } from "react";
4
4
  import type { ColumnDef } from "@tanstack/react-table";
5
5
  import type { GauthUser, GauthUserHookConfig } from "../../types/users";
6
6
  import { useUsers, useDeleteUser, useResetPassword } from "../../hooks/use-users";
@@ -165,10 +165,13 @@ export function UsersPage({ config, renderPhones }: UsersPageProps) {
165
165
  );
166
166
  }, [users, search]);
167
167
 
168
- const columns = useColumns(
169
- (u) => { setEditUser(u); setFormOpen(true); },
170
- (id) => setDeleteId(id),
171
- (u) => setResetUser(u),
168
+ const onEdit = useCallback((u: GauthUser) => { setEditUser(u); setFormOpen(true); }, []);
169
+ const onDelete = useCallback((id: number) => setDeleteId(id), []);
170
+ const onResetPassword = useCallback((u: GauthUser) => setResetUser(u), []);
171
+
172
+ const columns = useMemo(
173
+ () => useColumns(onEdit, onDelete, onResetPassword),
174
+ [onEdit, onDelete, onResetPassword],
172
175
  );
173
176
 
174
177
  function handleDelete() {
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
 
3
+ import { useMemo } from "react";
3
4
  import { useSession } from "../auth/auth-client";
4
5
 
5
6
  interface JwtClaimsConfig {
@@ -28,49 +29,53 @@ export function createUseAuth(config?: UseAuthConfig) {
28
29
  return function useAuth() {
29
30
  const { data: sessionData, isPending, error } = useSession();
30
31
 
31
- const session = sessionData?.session ?? null;
32
- const user = sessionData?.user ?? null;
32
+ const result = useMemo(() => {
33
+ const session = sessionData?.session ?? null;
34
+ const user = sessionData?.user ?? null;
33
35
 
34
- const sessionObj = session as Record<string, unknown> | null;
35
- const userObj = user as Record<string, unknown> | null;
36
+ const sessionObj = session as Record<string, unknown> | null;
37
+ const userObj = user as Record<string, unknown> | null;
36
38
 
37
- // Read from session first (per-login, isolated per app), fallback to user record (backward compat)
38
- const gauthToken = (sessionObj?.gauthToken ?? userObj?.gauthToken ?? null) as string | null;
39
- const idAccount = (sessionObj?.idAccount ?? userObj?.idAccount ?? null) as string | null;
40
- const gauthProfile = (sessionObj?.gauthProfile ?? userObj?.gauthProfile ?? null) as Record<string, unknown> | null;
39
+ // Read from session first (per-login, isolated per app), fallback to user record (backward compat)
40
+ const gauthToken = (sessionObj?.gauthToken ?? userObj?.gauthToken ?? null) as string | null;
41
+ const idAccount = (sessionObj?.idAccount ?? userObj?.idAccount ?? null) as string | null;
42
+ const gauthProfile = (sessionObj?.gauthProfile ?? userObj?.gauthProfile ?? null) as Record<string, unknown> | null;
41
43
 
42
- const result: Record<string, unknown> = {
43
- session,
44
- user,
45
- gauthToken,
46
- idAccount,
47
- gauthProfile,
48
- isLoading: isPending,
49
- isAuthenticated: !!session && !!user,
50
- error: error ?? null,
51
- };
44
+ const res: Record<string, unknown> = {
45
+ session,
46
+ user,
47
+ gauthToken,
48
+ idAccount,
49
+ gauthProfile,
50
+ isLoading: isPending,
51
+ isAuthenticated: !!session && !!user,
52
+ error: error ?? null,
53
+ };
52
54
 
53
- if (config?.jwtClaims && gauthToken) {
54
- const payload = decodeJwtPayload(gauthToken);
55
- if (payload) {
56
- for (const [jwtKey, fieldName] of Object.entries(config.jwtClaims)) {
57
- result[fieldName] = payload[jwtKey] ?? null;
55
+ if (config?.jwtClaims && gauthToken) {
56
+ const payload = decodeJwtPayload(gauthToken);
57
+ if (payload) {
58
+ for (const [jwtKey, fieldName] of Object.entries(config.jwtClaims)) {
59
+ res[fieldName] = payload[jwtKey] ?? null;
60
+ }
58
61
  }
59
62
  }
60
- }
61
63
 
62
- if (config?.accountIdOverride) {
63
- const overriddenId = config.accountIdOverride(user as Record<string, unknown> | null);
64
- if (overriddenId !== null) {
65
- result.idAccount = overriddenId;
64
+ if (config?.accountIdOverride) {
65
+ const overriddenId = config.accountIdOverride(user as Record<string, unknown> | null);
66
+ if (overriddenId !== null) {
67
+ res.idAccount = overriddenId;
68
+ }
66
69
  }
67
- }
68
70
 
69
- if (config?.extraUserFields && user) {
70
- for (const field of config.extraUserFields) {
71
- result[field] = (user as Record<string, unknown>)[field] ?? null;
71
+ if (config?.extraUserFields && user) {
72
+ for (const field of config.extraUserFields) {
73
+ res[field] = (user as Record<string, unknown>)[field] ?? null;
74
+ }
72
75
  }
73
- }
76
+
77
+ return res;
78
+ }, [sessionData, isPending, error]);
74
79
 
75
80
  return result;
76
81
  };
@@ -3,17 +3,19 @@
3
3
  import { useEffect, useState } from "react";
4
4
 
5
5
  const MOBILE_BREAKPOINT = 768;
6
+ const MOBILE_QUERY = `(max-width: ${MOBILE_BREAKPOINT - 1}px)`;
6
7
 
7
8
  export function useIsMobile() {
8
- const [isMobile, setIsMobile] = useState<boolean | undefined>(undefined);
9
+ const [isMobile, setIsMobile] = useState<boolean>(false);
9
10
 
10
11
  useEffect(() => {
11
- const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
12
+ if (typeof window === "undefined") return;
13
+ const mql = window.matchMedia(MOBILE_QUERY);
12
14
  const onChange = () => setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
13
15
  mql.addEventListener("change", onChange);
14
16
  setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
15
17
  return () => mql.removeEventListener("change", onChange);
16
18
  }, []);
17
19
 
18
- return !!isMobile;
20
+ return isMobile;
19
21
  }
@@ -29,7 +29,7 @@ async function request<T>(url: string, token: string, options?: RequestInit): Pr
29
29
 
30
30
  export function useUsers(config: GauthUserHookConfig, params?: Record<string, string>) {
31
31
  return useQuery({
32
- queryKey: ["greatauth", "users", config.accountId, params],
32
+ queryKey: ["greatauth", "users", config.accountId, JSON.stringify(params)],
33
33
  queryFn: () => {
34
34
  const qs = params ? "?" + new URLSearchParams(params).toString() : "";
35
35
  return request<GauthUser[]>(buildUrl(config, `users${qs}`), config.token!);