@tpzdsp/next-toolkit 1.0.1

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.
Files changed (61) hide show
  1. package/README.md +594 -0
  2. package/package.json +133 -0
  3. package/src/assets/fonts/gds-bold-w-v2.woff +0 -0
  4. package/src/assets/fonts/gds-bold-w2-v2.woff2 +0 -0
  5. package/src/assets/fonts/gds-light-w-v2.woff +0 -0
  6. package/src/assets/fonts/gds-light-w2-v2.woff2 +0 -0
  7. package/src/assets/images/defra-logo.svg +51 -0
  8. package/src/assets/images/ea-logo.svg +58 -0
  9. package/src/assets/images/ogl.svg +1 -0
  10. package/src/assets/styles/globals.css +68 -0
  11. package/src/assets/styles/index.ts +7 -0
  12. package/src/components/Button/Button.stories.tsx +41 -0
  13. package/src/components/Button/Button.test.tsx +55 -0
  14. package/src/components/Button/Button.tsx +44 -0
  15. package/src/components/Card/Card.stories.tsx +35 -0
  16. package/src/components/Card/Card.test.tsx +12 -0
  17. package/src/components/Card/Card.tsx +19 -0
  18. package/src/components/ErrorText/ErrorText.stories.tsx +34 -0
  19. package/src/components/ErrorText/ErrorText.test.tsx +35 -0
  20. package/src/components/ErrorText/ErrorText.tsx +17 -0
  21. package/src/components/Heading/Heading.stories.tsx +21 -0
  22. package/src/components/Heading/Heading.test.tsx +24 -0
  23. package/src/components/Heading/Heading.tsx +21 -0
  24. package/src/components/Hint/Hint.stories.tsx +40 -0
  25. package/src/components/Hint/Hint.test.tsx +35 -0
  26. package/src/components/Hint/Hint.tsx +13 -0
  27. package/src/components/Paragraph/Paragraph.stories.tsx +30 -0
  28. package/src/components/Paragraph/Paragraph.test.tsx +12 -0
  29. package/src/components/Paragraph/Paragraph.tsx +7 -0
  30. package/src/components/container/Container.tsx +38 -0
  31. package/src/components/dropdown/DropdownMenu.test.tsx +213 -0
  32. package/src/components/dropdown/DropdownMenu.tsx +106 -0
  33. package/src/components/dropdown/useDropdownMenu.ts +245 -0
  34. package/src/components/images/DefraLogo.tsx +64 -0
  35. package/src/components/images/EaLogo.tsx +81 -0
  36. package/src/components/images/OglLogo.tsx +18 -0
  37. package/src/components/index.ts +36 -0
  38. package/src/components/layout/footer/Copyright.tsx +14 -0
  39. package/src/components/layout/footer/Footer.tsx +26 -0
  40. package/src/components/layout/footer/Licence.tsx +19 -0
  41. package/src/components/layout/footer/MetaLinks.tsx +36 -0
  42. package/src/components/layout/header/Header.tsx +89 -0
  43. package/src/components/layout/header/HeaderAuthClient.tsx +32 -0
  44. package/src/components/layout/header/HeaderNavClient.tsx +64 -0
  45. package/src/components/link/ExternalLink.tsx +29 -0
  46. package/src/components/link/Link.tsx +26 -0
  47. package/src/components/link/NextLinkWrapper.tsx +66 -0
  48. package/src/components/theme/ThemeProvider.tsx +30 -0
  49. package/src/contexts/ThemeContext.tsx +72 -0
  50. package/src/contexts/index.ts +5 -0
  51. package/src/hooks/index.ts +6 -0
  52. package/src/hooks/useDebounce.ts +18 -0
  53. package/src/hooks/useLocalStorage.ts +57 -0
  54. package/src/index.ts +8 -0
  55. package/src/types.ts +99 -0
  56. package/src/utils/auth.ts +19 -0
  57. package/src/utils/constants.ts +3 -0
  58. package/src/utils/index.ts +4 -0
  59. package/src/utils/renderers.tsx +68 -0
  60. package/src/utils/utils.ts +63 -0
  61. package/src/vite-env.d.ts +17 -0
@@ -0,0 +1,89 @@
1
+ import type { ComponentType } from 'react';
2
+
3
+ import { FaHouse } from 'react-icons/fa6';
4
+
5
+ import { HeaderAuthClient } from './HeaderAuthClient';
6
+ import { HeaderNavClient } from './HeaderNavClient';
7
+ import type { Credentials, NavLink } from '../../../types';
8
+ import { DefraLogo } from '../../images/DefraLogo';
9
+ import { EaLogo } from '../../images/EaLogo';
10
+ import { ExternalLink } from '../../link/ExternalLink';
11
+ import { Link } from '../../link/Link';
12
+
13
+ type HeaderProps = {
14
+ credentials: Credentials | null;
15
+ dspUrl: string;
16
+ appName: string;
17
+ navLinks: NavLink[];
18
+ // headerAuthClientComponent: ComponentType<{ credentials: Credentials | null; hostname: string }>;
19
+ headerNavClientComponent?: ComponentType<{ navLinks: NavLink[] }>;
20
+ };
21
+
22
+ export const Header = ({
23
+ credentials,
24
+ dspUrl,
25
+ appName,
26
+ navLinks,
27
+ // headerAuthClientComponent,
28
+ headerNavClientComponent = HeaderNavClient,
29
+ }: HeaderProps) => {
30
+ const HeaderNav = headerNavClientComponent;
31
+
32
+ return (
33
+ <header className="bg-black w-full text-white border-b-2 border-brand">
34
+ <div
35
+ className="grid grid-rows-[min-content_min-content] grid-cols-2 md:grid-cols-3 gap-x-4
36
+ gap-y-2 md:gap-y-0 py-2 px-3 border-b border-gray-500"
37
+ >
38
+ <ExternalLink
39
+ href="https://www.gov.uk/government/organisations/department-for-environment-food-rural-affairs"
40
+ className="justify-center text-white visited:text-white active:text-black hover:text-white
41
+ text-base"
42
+ >
43
+ <span className="flex flex-col sm:flex-row sm:items-center gap-1 flex-1">
44
+ <DefraLogo className="w-[50px] h-[50px]" />
45
+
46
+ <span>Department for Environment Food &amp; Rural Affairs</span>
47
+ </span>
48
+ </ExternalLink>
49
+
50
+ <ExternalLink
51
+ href="/"
52
+ className="flex gap-1 text-base items-center text-white md:justify-self-center
53
+ justify-self-end h-min self-center visited:text-white active:text-black
54
+ hover:text-white"
55
+ >
56
+ <FaHouse className="shrink-0" />
57
+
58
+ <span>Data Services Platform</span>
59
+ </ExternalLink>
60
+
61
+ {dspUrl ? <HeaderAuthClient hostname={dspUrl} credentials={credentials} /> : <></>}
62
+ </div>
63
+
64
+ <div
65
+ className="w-full bg-white grid grid-cols-[1fr_min-content]
66
+ 2xs:grid-cols-[min-content_minmax(0,_1fr)_min-content] grid-rows-1 sm:grid-rows-none py-2
67
+ px-1 sm:px-3 items-center gap-y-4 gap-2"
68
+ >
69
+ <ExternalLink
70
+ href="https://www.gov.uk/government/organisations/environment-agency"
71
+ className="justify-self-start text-black hidden 2xs:block min-w-32"
72
+ >
73
+ <EaLogo className="text-nowrap overflow-hidden pl-2" />
74
+ </ExternalLink>
75
+
76
+ <Link
77
+ href="/"
78
+ className="text-center font-bold text-base sm:text-lg text-black visited:text-black
79
+ active:text-black hover:text-black"
80
+ >
81
+ {appName}
82
+ </Link>
83
+
84
+ {/* <HeaderNavClient navLinks={navLinks} /> */}
85
+ <HeaderNav navLinks={navLinks} />
86
+ </div>
87
+ </header>
88
+ );
89
+ };
@@ -0,0 +1,32 @@
1
+ 'use client';
2
+
3
+ import type { Credentials } from '../../../types';
4
+ import { Link } from '../../link/Link';
5
+
6
+ type HeaderAuthClientProps = {
7
+ credentials: Credentials | null;
8
+ hostname: string;
9
+ };
10
+
11
+ export const HeaderAuthClient = ({ hostname, credentials }: HeaderAuthClientProps) => {
12
+ console.log('HeaderAuthClient: ', { hostname, credentials });
13
+
14
+ return (
15
+ <span
16
+ className="flex gap-2 justify-self-start text-base col-span-2 row-start-2 col-start-1
17
+ md:row-start-1 md:col-start-3 h-min md:justify-self-end self-center items-center"
18
+ >
19
+ <span className="text-base border-r border-white pr-2 wrap break-anywhere sm:break-word">
20
+ <span>Welcome,</span> {credentials ? credentials.user.email : 'Guest'}
21
+ </span>
22
+
23
+ <Link
24
+ href={`${hostname}/${credentials ? 'api/logout' : 'login'}?redirect-uri=${encodeURIComponent(window?.location?.href)}`}
25
+ className="text-white visited:text-white active:text-black hover:text-white"
26
+ prefetch={false}
27
+ >
28
+ <span className="text-base">{credentials ? 'Logout' : 'Login'}</span>
29
+ </Link>
30
+ </span>
31
+ );
32
+ };
@@ -0,0 +1,64 @@
1
+ 'use client';
2
+
3
+ import type { NavLink } from '../../../types';
4
+ import { DropdownMenu, type DropdownMenuItem } from '../../dropdown/DropdownMenu';
5
+ import { ExternalLink } from '../../link/ExternalLink';
6
+ import { Link } from '../../link/Link';
7
+
8
+ type HeaderNavClientProps = {
9
+ navLinks: NavLink[];
10
+ };
11
+
12
+ const ExternalNavItem = ({ label, url, icon, ...props }: DropdownMenuItem<NavLink>) => (
13
+ <ExternalLink href={url} {...props}>
14
+ {icon ?? null}
15
+ <span>{label}</span>
16
+ </ExternalLink>
17
+ );
18
+
19
+ const InternalNavItem = ({ label, url, icon, ...props }: DropdownMenuItem<NavLink>) => (
20
+ <Link {...props} href={url}>
21
+ {icon ?? null}
22
+ <span>{label}</span>
23
+ </Link>
24
+ );
25
+
26
+ const NavItem = (props: DropdownMenuItem<NavLink>) => {
27
+ if (props.isExternal) {
28
+ return <ExternalNavItem {...props} />;
29
+ }
30
+
31
+ return <InternalNavItem {...props} />;
32
+ };
33
+
34
+ export const HeaderNavClient = ({ navLinks }: HeaderNavClientProps) => {
35
+ return (
36
+ <>
37
+ <nav className="block sm:hidden" aria-label="Small Screen Navigation">
38
+ <DropdownMenu
39
+ items={navLinks}
40
+ itemRenderer={NavItem}
41
+ itemClassName="text-black cursor-pointer px-2 py-1 gap-1 text-base flex items-center shrink-0"
42
+ />
43
+ </nav>
44
+
45
+ <nav
46
+ className="hidden sm:block col-span-full sm:col-span-1 sm:justify-self-end sm:gap-4 min-w-0
47
+ px-2 gap-2"
48
+ aria-label="Large Screen Navigation"
49
+ >
50
+ <ul className="flex flex-row gap-2 order-1 shrink-0">
51
+ {navLinks.map((link) => (
52
+ <li key={link.label} className="w-full py-1">
53
+ <NavItem
54
+ {...link}
55
+ className="text-black visited:text-black active:text-black hover:text-black gap-1
56
+ text-base flex items-center justify-center shrink-0 w-max"
57
+ />
58
+ </li>
59
+ ))}
60
+ </ul>
61
+ </nav>
62
+ </>
63
+ );
64
+ };
@@ -0,0 +1,29 @@
1
+ import type { ReactNode } from 'react';
2
+
3
+ import { twMerge } from 'tailwind-merge';
4
+
5
+ import type { ExtendProps } from '../../types';
6
+
7
+ type Props = {
8
+ children: ReactNode;
9
+ };
10
+
11
+ export type ExternalLinkProps = ExtendProps<'a', Props>;
12
+
13
+ export const ExternalLink = ({ href, className, children, ...props }: ExternalLinkProps) => (
14
+ <a
15
+ {...props}
16
+ className={twMerge(
17
+ `cursor-pointer text-link hover:decoration-[max(3px,_.1875rem,_.12em)] hover:text-link-hover
18
+ visited:text-link-visited focus:decoration-[max(3px,_.1875rem,_.12em)]
19
+ decoration-[max(1px,_.0625rem)] underline-offset-[0.1578em] underline outline-none
20
+ focus:text-focus-text focus:bg-focus inline-block`,
21
+ className,
22
+ )}
23
+ href={href}
24
+ rel="noopener noreferrer"
25
+ target="_blank"
26
+ >
27
+ {children}
28
+ </a>
29
+ );
@@ -0,0 +1,26 @@
1
+ import NextLink from 'next/link';
2
+ import { twMerge } from 'tailwind-merge';
3
+
4
+ import type { ExtendProps } from '../../types';
5
+
6
+ type Props = {
7
+ href: string | object;
8
+ };
9
+
10
+ export type LinkProps = ExtendProps<typeof NextLink, Props>;
11
+
12
+ export const Link = ({ href, className, children, ...props }: LinkProps) => (
13
+ <NextLink
14
+ {...props}
15
+ className={twMerge(
16
+ `cursor-pointer text-link hover:decoration-[max(3px,_.1875rem,_.12em)] hover:text-link-hover
17
+ visited:text-link-visited active:text-black focus:decoration-[max(3px,_.1875rem,_.12em)]
18
+ decoration-[max(1px,_.0625rem)] underline-offset-[0.1578em] underline outline-none
19
+ focus:text-focus-text focus:bg-focus inline-block`,
20
+ className,
21
+ )}
22
+ href={href}
23
+ >
24
+ <>{children}</>
25
+ </NextLink>
26
+ );
@@ -0,0 +1,66 @@
1
+ 'use client';
2
+
3
+ import type { ReactNode } from 'react';
4
+
5
+ import Link from 'next/link';
6
+
7
+ import { cn } from '../../utils';
8
+
9
+ export type NextLinkWrapperProps = {
10
+ href: string;
11
+ children: ReactNode;
12
+ className?: string;
13
+ target?: '_blank' | '_self' | '_parent' | '_top';
14
+ rel?: string;
15
+ prefetch?: boolean;
16
+ replace?: boolean;
17
+ scroll?: boolean;
18
+ shallow?: boolean;
19
+ };
20
+
21
+ export const NextLinkWrapper = ({
22
+ href,
23
+ children,
24
+ className,
25
+ target,
26
+ rel,
27
+ prefetch,
28
+ replace,
29
+ scroll,
30
+ shallow,
31
+ ...props
32
+ }: NextLinkWrapperProps) => {
33
+ // Handle external links
34
+ const isExternal =
35
+ href.startsWith('http') || href.startsWith('mailto:') || href.startsWith('tel:');
36
+
37
+ if (isExternal) {
38
+ return (
39
+ <a
40
+ href={href}
41
+ className={cn('text-blue-600 hover:text-blue-800 transition-colors', className)}
42
+ target={target ?? '_blank'}
43
+ rel={rel ?? 'noopener noreferrer'}
44
+ {...props}
45
+ >
46
+ {children}
47
+ </a>
48
+ );
49
+ }
50
+
51
+ return (
52
+ <Link
53
+ href={href}
54
+ className={cn('text-blue-600 hover:text-blue-800 transition-colors', className)}
55
+ prefetch={prefetch}
56
+ replace={replace}
57
+ scroll={scroll}
58
+ shallow={shallow}
59
+ target={target}
60
+ rel={rel}
61
+ {...props}
62
+ >
63
+ {children}
64
+ </Link>
65
+ );
66
+ };
@@ -0,0 +1,30 @@
1
+ 'use client';
2
+
3
+ import type { ReactNode } from 'react';
4
+
5
+ import { ThemeContext } from '../../contexts';
6
+ import { useLocalStorage } from '../../hooks';
7
+
8
+ export type ThemeProviderProps = {
9
+ children: ReactNode;
10
+ defaultTheme?: 'light' | 'dark';
11
+ };
12
+
13
+ export const ThemeProvider = ({ children, defaultTheme = 'light' }: ThemeProviderProps) => {
14
+ const [theme, setTheme] = useLocalStorage<'light' | 'dark'>('theme', defaultTheme);
15
+
16
+ const toggleTheme = () => {
17
+ setTheme(theme === 'light' ? 'dark' : 'light');
18
+ };
19
+
20
+ const value = {
21
+ theme,
22
+ toggleTheme,
23
+ };
24
+
25
+ return (
26
+ <ThemeContext.Provider value={value}>
27
+ <div className={theme === 'dark' ? 'dark' : ''}>{children}</div>
28
+ </ThemeContext.Provider>
29
+ );
30
+ };
@@ -0,0 +1,72 @@
1
+ 'use client'; // This is necessary for Next.js to treat this file as a client-side component
2
+
3
+ import { createContext, useContext, useState, useEffect } from 'react';
4
+ import type { ReactNode } from 'react';
5
+
6
+ import type { ThemeContextValue } from '../types';
7
+
8
+ export const ThemeContext = createContext<ThemeContextValue | undefined>(undefined);
9
+
10
+ type ThemeProviderProps = {
11
+ children: ReactNode;
12
+ defaultTheme?: 'light' | 'dark';
13
+ };
14
+
15
+ export const ThemeProvider = ({ children, defaultTheme = 'light' }: ThemeProviderProps) => {
16
+ const [theme, setTheme] = useState<'light' | 'dark'>(() => {
17
+ if (typeof window === 'undefined') {
18
+ return defaultTheme;
19
+ }
20
+
21
+ // Check localStorage first
22
+ const savedTheme = localStorage.getItem('theme') as 'light' | 'dark';
23
+
24
+ if (savedTheme) {
25
+ return savedTheme;
26
+ }
27
+
28
+ // Check system preference
29
+ if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
30
+ return 'dark';
31
+ }
32
+
33
+ return defaultTheme;
34
+ });
35
+
36
+ const toggleTheme = () => {
37
+ const newTheme = theme === 'light' ? 'dark' : 'light';
38
+
39
+ setTheme(newTheme);
40
+
41
+ if (typeof window !== 'undefined') {
42
+ localStorage.setItem('theme', newTheme);
43
+ }
44
+ };
45
+
46
+ useEffect(() => {
47
+ // Add theme class to document element
48
+ if (typeof window !== 'undefined') {
49
+ const root = window.document.documentElement;
50
+
51
+ root.classList.remove('light', 'dark');
52
+ root.classList.add(theme);
53
+ }
54
+ }, [theme]);
55
+
56
+ const value: ThemeContextValue = {
57
+ theme,
58
+ toggleTheme,
59
+ };
60
+
61
+ return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
62
+ };
63
+
64
+ export const useTheme = (): ThemeContextValue => {
65
+ const context = useContext(ThemeContext);
66
+
67
+ if (context === undefined) {
68
+ throw new Error('useTheme must be used within a ThemeProvider');
69
+ }
70
+
71
+ return context;
72
+ };
@@ -0,0 +1,5 @@
1
+ // Export all contexts and hooks
2
+ export { useTheme, ThemeContext } from './ThemeContext';
3
+
4
+ // Export context types
5
+ export type { ThemeContextValue } from '../types';
@@ -0,0 +1,6 @@
1
+ // Export all hooks
2
+ export { useLocalStorage } from './useLocalStorage';
3
+ export { useDebounce } from './useDebounce';
4
+
5
+ // Export hook types
6
+ export type { UseLocalStorageReturn } from './useLocalStorage';
@@ -0,0 +1,18 @@
1
+ import { useState, useEffect } from 'react';
2
+
3
+ export const useDebounce = <T>(value: T, delay: number): T => {
4
+ const [debouncedValue, setDebouncedValue] = useState<T>(value);
5
+
6
+ useEffect(() => {
7
+ const handler = setTimeout(() => {
8
+ setDebouncedValue(value);
9
+ }, delay);
10
+
11
+ // Cleanup function to clear the timeout if value changes
12
+ return () => {
13
+ clearTimeout(handler);
14
+ };
15
+ }, [value, delay]); // Re-run effect when value or delay changes
16
+
17
+ return debouncedValue;
18
+ };
@@ -0,0 +1,57 @@
1
+ import { useState, useCallback } from 'react';
2
+
3
+ export type UseLocalStorageReturn<T> = [T, (value: T | ((val: T) => T)) => void, () => void];
4
+
5
+ export const useLocalStorage = <T>(key: string, initialValue: T): UseLocalStorageReturn<T> => {
6
+ // State to store our value
7
+ const [storedValue, setStoredValue] = useState<T>(() => {
8
+ if (typeof window === 'undefined') {
9
+ return initialValue;
10
+ }
11
+
12
+ try {
13
+ const item = window.localStorage.getItem(key);
14
+
15
+ return item ? JSON.parse(item) : initialValue;
16
+ } catch (error) {
17
+ console.error(`Error reading localStorage key "${key}":`, error);
18
+
19
+ return initialValue;
20
+ }
21
+ });
22
+
23
+ // Return a wrapped version of useState's setter function that persists to localStorage
24
+ const setValue = useCallback(
25
+ (value: T | ((val: T) => T)) => {
26
+ try {
27
+ // Allow value to be a function so we have the same API as useState
28
+ const valueToStore = value instanceof Function ? value(storedValue) : value;
29
+
30
+ setStoredValue(valueToStore);
31
+
32
+ // Save to localStorage
33
+ if (typeof window !== 'undefined') {
34
+ window.localStorage.setItem(key, JSON.stringify(valueToStore));
35
+ }
36
+ } catch (error) {
37
+ console.error(`Error setting localStorage key "${key}":`, error);
38
+ }
39
+ },
40
+ [key, storedValue],
41
+ );
42
+
43
+ // Function to remove the item from localStorage
44
+ const removeValue = useCallback(() => {
45
+ try {
46
+ setStoredValue(initialValue);
47
+
48
+ if (typeof window !== 'undefined') {
49
+ window.localStorage.removeItem(key);
50
+ }
51
+ } catch (error) {
52
+ console.error(`Error removing localStorage key "${key}":`, error);
53
+ }
54
+ }, [key, initialValue]);
55
+
56
+ return [storedValue, setValue, removeValue];
57
+ };
package/src/index.ts ADDED
@@ -0,0 +1,8 @@
1
+ // Export all components.
2
+ export * from './components';
3
+
4
+ // Export all utilities (these can be used in both client and server)
5
+ export * from './utils';
6
+
7
+ // Export all types
8
+ export * from './types';
package/src/types.ts ADDED
@@ -0,0 +1,99 @@
1
+ // Common types used throughout the library and consuming applications
2
+ import type { ComponentProps, ReactNode } from 'react';
3
+
4
+ export type BaseProps = {
5
+ className?: string;
6
+ children?: ReactNode;
7
+ };
8
+
9
+ export type NextLinkProps = {
10
+ href: string;
11
+ children: ReactNode;
12
+ className?: string;
13
+ target?: '_blank' | '_self' | '_parent' | '_top';
14
+ rel?: string;
15
+ };
16
+
17
+ export type ThemeContextValue = {
18
+ theme: 'light' | 'dark';
19
+ toggleTheme: () => void;
20
+ };
21
+
22
+ // Component variant types
23
+ export type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost';
24
+ export type ButtonSize = 'sm' | 'md' | 'lg';
25
+ export type CardVariant = 'default' | 'elevated' | 'outlined';
26
+ export type ContainerSize = 'sm' | 'md' | 'lg' | 'xl' | 'full';
27
+
28
+ // Common utility types that apps might need
29
+ export type Variant = 'primary' | 'secondary' | 'success' | 'warning' | 'error' | 'info';
30
+ export type Size = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
31
+ export type Status = 'idle' | 'loading' | 'success' | 'error';
32
+ export type Theme = 'light' | 'dark' | 'system';
33
+
34
+ // Form related types
35
+ export type FormFieldProps = {
36
+ name: string;
37
+ label?: string;
38
+ error?: string;
39
+ required?: boolean;
40
+ disabled?: boolean;
41
+ className?: string;
42
+ };
43
+
44
+ // API related types
45
+ export type ApiResponse<T = unknown> = {
46
+ data: T;
47
+ success: boolean;
48
+ message?: string;
49
+ errors?: Record<string, string[]>;
50
+ };
51
+
52
+ export type PaginationMeta = {
53
+ page: number;
54
+ perPage: number;
55
+ total: number;
56
+ totalPages: number;
57
+ };
58
+
59
+ export type PaginatedResponse<T = unknown> = {
60
+ meta: PaginationMeta;
61
+ } & ApiResponse<T[]>;
62
+
63
+ // Event handler types
64
+ export type ClickHandler = (event: React.MouseEvent<HTMLElement>) => void;
65
+ export type ChangeHandler<T = HTMLInputElement> = (event: React.ChangeEvent<T>) => void;
66
+ export type SubmitHandler = (event: React.FormEvent<HTMLFormElement>) => void;
67
+
68
+ // Utility types for better developer experience
69
+ export type PropsWithRequired<T, K extends keyof T> = T & Required<Pick<T, K>>;
70
+ export type PropsWithOptional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
71
+
72
+ export type ExtendProps<
73
+ // `ComponentProps` internally constrains `Comp` to be `JSXElementConstructor<any>`,
74
+ // and since our type must have the same constraints to avoid errors, `any` is required here
75
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
76
+ Comp extends keyof React.JSX.IntrinsicElements | React.JSXElementConstructor<any>,
77
+ Props = object,
78
+ > = Props & Omit<ComponentProps<Comp>, keyof Props>;
79
+
80
+ // Define an interface for the decoded JWT payload
81
+ export type DecodedJWT = {
82
+ name: string;
83
+ email: string;
84
+ groupInfoIds: string[];
85
+ exp?: number; // Optional: JWT expiration timestamp
86
+ [key: string]: unknown; // Additional claims
87
+ };
88
+
89
+ export type Credentials = {
90
+ token: string;
91
+ user: DecodedJWT;
92
+ };
93
+
94
+ export type NavLink = {
95
+ label: string;
96
+ url: string;
97
+ isExternal: boolean;
98
+ icon?: ReactNode;
99
+ };
@@ -0,0 +1,19 @@
1
+ 'use server';
2
+
3
+ import { cookies } from 'next/headers';
4
+ import type { NextRequest } from 'next/server';
5
+
6
+ import { COOKIE_NAME } from './constants';
7
+ import { decodeAuthToken } from './utils';
8
+ import type { Credentials } from '../types';
9
+
10
+ export const getCredentials = async (source?: NextRequest | null): Promise<Credentials | null> => {
11
+ const cookieStore = source ? source?.cookies : await cookies();
12
+ const token = cookieStore.get(COOKIE_NAME)?.value;
13
+
14
+ if (!token) {
15
+ return null;
16
+ }
17
+
18
+ return decodeAuthToken(token);
19
+ };
@@ -0,0 +1,3 @@
1
+ export const COOKIE_NAME = 'auth0-jwt-test';
2
+
3
+ export const SUPPORT_URL = '/support';
@@ -0,0 +1,4 @@
1
+ export * from './utils';
2
+ export * from './auth';
3
+ export * from './constants';
4
+ export * from './renderers'; // This has testing-library dependencies, so probably need to add them as peer dependencies