@djangocfg/ui-nextjs 1.4.45

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 (101) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +152 -0
  3. package/package.json +110 -0
  4. package/src/animations/AnimatedBackground.tsx +645 -0
  5. package/src/animations/index.ts +2 -0
  6. package/src/blocks/ArticleCard.tsx +94 -0
  7. package/src/blocks/ArticleList.tsx +95 -0
  8. package/src/blocks/CTASection.tsx +136 -0
  9. package/src/blocks/FeatureSection.tsx +104 -0
  10. package/src/blocks/Hero.tsx +102 -0
  11. package/src/blocks/NewsletterSection.tsx +119 -0
  12. package/src/blocks/StatsSection.tsx +103 -0
  13. package/src/blocks/SuperHero.tsx +328 -0
  14. package/src/blocks/TestimonialSection.tsx +122 -0
  15. package/src/blocks/index.ts +9 -0
  16. package/src/components/README.md +2018 -0
  17. package/src/components/breadcrumb-navigation.tsx +127 -0
  18. package/src/components/breadcrumb.tsx +132 -0
  19. package/src/components/button-download.tsx +275 -0
  20. package/src/components/dropdown-menu.tsx +219 -0
  21. package/src/components/index.ts +86 -0
  22. package/src/components/markdown/MarkdownMessage.tsx +338 -0
  23. package/src/components/markdown/index.ts +5 -0
  24. package/src/components/menubar.tsx +274 -0
  25. package/src/components/multi-select-pro/async.tsx +608 -0
  26. package/src/components/multi-select-pro/helpers.tsx +84 -0
  27. package/src/components/multi-select-pro/index.tsx +622 -0
  28. package/src/components/navigation-menu.tsx +153 -0
  29. package/src/components/pagination-static.tsx +348 -0
  30. package/src/components/pagination.tsx +138 -0
  31. package/src/components/phone-input.tsx +276 -0
  32. package/src/components/sidebar.tsx +866 -0
  33. package/src/components/sonner.tsx +31 -0
  34. package/src/components/ssr-pagination.tsx +237 -0
  35. package/src/hooks/index.ts +19 -0
  36. package/src/hooks/useCfgRouter.ts +153 -0
  37. package/src/hooks/useLocalStorage.ts +221 -0
  38. package/src/hooks/useQueryParams.ts +73 -0
  39. package/src/hooks/useSessionStorage.ts +188 -0
  40. package/src/hooks/useTheme.ts +57 -0
  41. package/src/index.ts +24 -0
  42. package/src/lib/index.ts +2 -0
  43. package/src/styles/index.css +2 -0
  44. package/src/theme/ForceTheme.tsx +115 -0
  45. package/src/theme/ThemeProvider.tsx +82 -0
  46. package/src/theme/ThemeToggle.tsx +52 -0
  47. package/src/theme/index.ts +3 -0
  48. package/src/tools/JsonForm/JsonSchemaForm.tsx +199 -0
  49. package/src/tools/JsonForm/examples/BotConfigExample.tsx +245 -0
  50. package/src/tools/JsonForm/examples/RealBotConfigExample.tsx +157 -0
  51. package/src/tools/JsonForm/index.ts +46 -0
  52. package/src/tools/JsonForm/templates/ArrayFieldItemTemplate.tsx +46 -0
  53. package/src/tools/JsonForm/templates/ArrayFieldTemplate.tsx +73 -0
  54. package/src/tools/JsonForm/templates/BaseInputTemplate.tsx +106 -0
  55. package/src/tools/JsonForm/templates/ErrorListTemplate.tsx +34 -0
  56. package/src/tools/JsonForm/templates/FieldTemplate.tsx +61 -0
  57. package/src/tools/JsonForm/templates/ObjectFieldTemplate.tsx +43 -0
  58. package/src/tools/JsonForm/templates/index.ts +12 -0
  59. package/src/tools/JsonForm/types.ts +83 -0
  60. package/src/tools/JsonForm/utils.ts +212 -0
  61. package/src/tools/JsonForm/widgets/CheckboxWidget.tsx +36 -0
  62. package/src/tools/JsonForm/widgets/NumberWidget.tsx +88 -0
  63. package/src/tools/JsonForm/widgets/SelectWidget.tsx +100 -0
  64. package/src/tools/JsonForm/widgets/SwitchWidget.tsx +34 -0
  65. package/src/tools/JsonForm/widgets/TextWidget.tsx +95 -0
  66. package/src/tools/JsonForm/widgets/index.ts +12 -0
  67. package/src/tools/JsonTree/index.tsx +252 -0
  68. package/src/tools/LottiePlayer/LottiePlayer.client.tsx +212 -0
  69. package/src/tools/LottiePlayer/index.tsx +54 -0
  70. package/src/tools/LottiePlayer/types.ts +108 -0
  71. package/src/tools/LottiePlayer/useLottie.ts +163 -0
  72. package/src/tools/Mermaid/Mermaid.client.tsx +341 -0
  73. package/src/tools/Mermaid/index.tsx +40 -0
  74. package/src/tools/OpenapiViewer/components/EndpointInfo.tsx +144 -0
  75. package/src/tools/OpenapiViewer/components/EndpointsLibrary.tsx +255 -0
  76. package/src/tools/OpenapiViewer/components/PlaygroundLayout.tsx +123 -0
  77. package/src/tools/OpenapiViewer/components/PlaygroundStepper.tsx +98 -0
  78. package/src/tools/OpenapiViewer/components/RequestBuilder.tsx +164 -0
  79. package/src/tools/OpenapiViewer/components/RequestParametersForm.tsx +253 -0
  80. package/src/tools/OpenapiViewer/components/ResponseViewer.tsx +169 -0
  81. package/src/tools/OpenapiViewer/components/VersionSelector.tsx +64 -0
  82. package/src/tools/OpenapiViewer/components/index.ts +14 -0
  83. package/src/tools/OpenapiViewer/constants.ts +39 -0
  84. package/src/tools/OpenapiViewer/context/PlaygroundContext.tsx +338 -0
  85. package/src/tools/OpenapiViewer/hooks/index.ts +8 -0
  86. package/src/tools/OpenapiViewer/hooks/useMobile.ts +10 -0
  87. package/src/tools/OpenapiViewer/hooks/useOpenApiSchema.ts +203 -0
  88. package/src/tools/OpenapiViewer/index.tsx +36 -0
  89. package/src/tools/OpenapiViewer/types.ts +152 -0
  90. package/src/tools/OpenapiViewer/utils/apiKeyManager.ts +149 -0
  91. package/src/tools/OpenapiViewer/utils/formatters.ts +71 -0
  92. package/src/tools/OpenapiViewer/utils/index.ts +9 -0
  93. package/src/tools/OpenapiViewer/utils/versionManager.ts +161 -0
  94. package/src/tools/PrettyCode/PrettyCode.client.tsx +217 -0
  95. package/src/tools/PrettyCode/index.tsx +43 -0
  96. package/src/tools/VideoPlayer/README.md +239 -0
  97. package/src/tools/VideoPlayer/VideoControls.tsx +138 -0
  98. package/src/tools/VideoPlayer/VideoPlayer.tsx +230 -0
  99. package/src/tools/VideoPlayer/index.ts +9 -0
  100. package/src/tools/VideoPlayer/types.ts +62 -0
  101. package/src/tools/index.ts +43 -0
@@ -0,0 +1,31 @@
1
+ "use client"
2
+
3
+ import { useTheme } from "next-themes"
4
+ import { Toaster as Sonner } from "sonner"
5
+
6
+ type ToasterProps = React.ComponentProps<typeof Sonner>
7
+
8
+ const Toaster = ({ ...props }: ToasterProps) => {
9
+ const { theme = "system" } = useTheme()
10
+
11
+ return (
12
+ <Sonner
13
+ theme={theme as ToasterProps["theme"]}
14
+ className="toaster group"
15
+ toastOptions={{
16
+ classNames: {
17
+ toast:
18
+ "group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
19
+ description: "group-[.toast]:text-muted-foreground",
20
+ actionButton:
21
+ "group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
22
+ cancelButton:
23
+ "group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
24
+ },
25
+ }}
26
+ {...props}
27
+ />
28
+ )
29
+ }
30
+
31
+ export { Toaster }
@@ -0,0 +1,237 @@
1
+ 'use client';
2
+
3
+ import React from 'react';
4
+ import { usePathname } from 'next/navigation';
5
+ import Link from 'next/link';
6
+ import { cn } from '@djangocfg/ui-core/lib';
7
+ import { useIsMobile } from '@djangocfg/ui-core/hooks';
8
+ import { useQueryParams } from '../hooks';
9
+ import {
10
+ Pagination,
11
+ PaginationContent,
12
+ PaginationEllipsis,
13
+ PaginationItem,
14
+ PaginationLink,
15
+ PaginationNext,
16
+ PaginationPrevious,
17
+ } from './pagination';
18
+
19
+ interface SSRPaginationProps {
20
+ currentPage: number;
21
+ totalPages: number;
22
+ totalItems: number;
23
+ itemsPerPage: number;
24
+ hasNextPage: boolean;
25
+ hasPreviousPage: boolean;
26
+ className?: string;
27
+ showInfo?: boolean;
28
+ maxVisiblePages?: number;
29
+ baseUrl?: string;
30
+ pathname?: string;
31
+ preserveQuery?: boolean;
32
+ prefetch?: boolean;
33
+ }
34
+
35
+ export const SSRPagination: React.FC<SSRPaginationProps> = ({
36
+ currentPage,
37
+ totalPages,
38
+ totalItems,
39
+ itemsPerPage,
40
+ hasNextPage,
41
+ hasPreviousPage,
42
+ className,
43
+ showInfo = true,
44
+ maxVisiblePages = 7,
45
+ baseUrl,
46
+ pathname: propPathname,
47
+ preserveQuery = true,
48
+ prefetch = true,
49
+ }) => {
50
+ const queryParams = useQueryParams();
51
+ const pathname = usePathname();
52
+ const isMobile = useIsMobile();
53
+
54
+ // Get current page from URL if available, otherwise use prop
55
+ const getCurrentPageFromUrl = (): number => {
56
+ const pageParam = queryParams.get('page');
57
+ if (pageParam) {
58
+ const pageNum = parseInt(pageParam, 10);
59
+ return isNaN(pageNum) ? 1 : pageNum;
60
+ }
61
+ return 1;
62
+ };
63
+
64
+ const actualCurrentPage = getCurrentPageFromUrl() || currentPage;
65
+
66
+ // Calculate actual navigation state based on current page from URL
67
+ const actualHasPreviousPage = actualCurrentPage > 1;
68
+
69
+ // Smart total pages calculation - if we're on a page higher than totalPages,
70
+ // extend totalPages to include current page + some extra
71
+ const smartTotalPages = Math.max(
72
+ totalPages,
73
+ actualCurrentPage + (hasNextPage ? 5 : 0)
74
+ );
75
+
76
+ const actualHasNextPage = hasNextPage;
77
+
78
+ // Generate URL for a specific page
79
+ const getPageUrl = (page: number): string => {
80
+ if (baseUrl) {
81
+ return `${baseUrl}?page=${page}`;
82
+ }
83
+
84
+ // Use current route with updated page parameter
85
+ const newSearchParams = preserveQuery
86
+ ? new URLSearchParams(queryParams.toString())
87
+ : new URLSearchParams();
88
+
89
+ newSearchParams.set('page', page.toString());
90
+
91
+ // Remove page=1 from URL to keep URLs clean
92
+ if (page === 1) {
93
+ newSearchParams.delete('page');
94
+ }
95
+
96
+ const queryString = newSearchParams.toString();
97
+ const basePath = propPathname || pathname || '';
98
+ return queryString ? `${basePath}?${queryString}` : basePath;
99
+ };
100
+
101
+ // Generate array of page numbers to display
102
+ const getVisiblePages = (): (number | 'ellipsis')[] => {
103
+ // On mobile, show fewer pages
104
+ const mobileMaxVisible = 3;
105
+ const effectiveMaxVisible = isMobile ? mobileMaxVisible : maxVisiblePages;
106
+
107
+ if (smartTotalPages <= effectiveMaxVisible) {
108
+ return Array.from({ length: smartTotalPages }, (_, i) => i + 1);
109
+ }
110
+
111
+ const pages: (number | 'ellipsis')[] = [];
112
+ const halfVisible = Math.floor(effectiveMaxVisible / 2);
113
+
114
+ if (isMobile) {
115
+ // Mobile: Show only current page and adjacent pages
116
+ if (actualCurrentPage > 1) {
117
+ pages.push(actualCurrentPage - 1);
118
+ }
119
+ pages.push(actualCurrentPage);
120
+ if (actualCurrentPage < smartTotalPages) {
121
+ pages.push(actualCurrentPage + 1);
122
+ }
123
+ } else {
124
+ // Desktop: Full pagination logic
125
+ // Always show first page
126
+ pages.push(1);
127
+
128
+ let start = Math.max(2, actualCurrentPage - halfVisible);
129
+ let end = Math.min(smartTotalPages - 1, actualCurrentPage + halfVisible);
130
+
131
+ // Adjust range if we're near the beginning or end
132
+ if (actualCurrentPage <= halfVisible + 1) {
133
+ end = Math.min(smartTotalPages - 1, effectiveMaxVisible - 1);
134
+ } else if (actualCurrentPage >= smartTotalPages - halfVisible) {
135
+ start = Math.max(2, smartTotalPages - effectiveMaxVisible + 2);
136
+ }
137
+
138
+ // Add ellipsis after first page if needed
139
+ if (start > 2) {
140
+ pages.push('ellipsis');
141
+ }
142
+
143
+ // Add middle pages
144
+ for (let i = start; i <= end; i++) {
145
+ pages.push(i);
146
+ }
147
+
148
+ // Add ellipsis before last page if needed
149
+ if (end < smartTotalPages - 1) {
150
+ pages.push('ellipsis');
151
+ }
152
+
153
+ // Always show last page (if more than 1 page)
154
+ if (smartTotalPages > 1) {
155
+ pages.push(smartTotalPages);
156
+ }
157
+ }
158
+
159
+ return pages;
160
+ };
161
+
162
+ // Don't render if there's only one page or no pages
163
+ if (smartTotalPages <= 1) {
164
+ return null;
165
+ }
166
+
167
+ const visiblePages = getVisiblePages();
168
+ const startItem = (actualCurrentPage - 1) * itemsPerPage + 1;
169
+ const endItem = Math.min(actualCurrentPage * itemsPerPage, totalItems);
170
+
171
+ return (
172
+ <div className={cn("space-y-4", className)}>
173
+ {/* Pagination Info */}
174
+ {showInfo && (
175
+ <div className="text-sm text-muted-foreground text-center">
176
+ {isMobile ? (
177
+ `Page ${actualCurrentPage} of ${smartTotalPages}`
178
+ ) : (
179
+ `Showing ${startItem.toLocaleString()} to ${endItem.toLocaleString()} of ${totalItems.toLocaleString()} results`
180
+ )}
181
+ </div>
182
+ )}
183
+
184
+ {/* Pagination Controls */}
185
+ <Pagination>
186
+ <PaginationContent>
187
+ {/* Previous Button */}
188
+ <PaginationItem>
189
+ {actualHasPreviousPage ? (
190
+ <Link href={getPageUrl(actualCurrentPage - 1)} prefetch={prefetch} passHref legacyBehavior>
191
+ <PaginationPrevious />
192
+ </Link>
193
+ ) : (
194
+ <PaginationPrevious
195
+ href="#"
196
+ className="pointer-events-none opacity-50"
197
+ onClick={(e) => e.preventDefault()}
198
+ />
199
+ )}
200
+ </PaginationItem>
201
+
202
+ {/* Page Numbers */}
203
+ {visiblePages.map((page, index) => (
204
+ <PaginationItem key={index}>
205
+ {page === 'ellipsis' ? (
206
+ <PaginationEllipsis />
207
+ ) : (
208
+ <Link href={getPageUrl(page)} prefetch={prefetch} passHref legacyBehavior>
209
+ <PaginationLink isActive={page === actualCurrentPage}>
210
+ {page}
211
+ </PaginationLink>
212
+ </Link>
213
+ )}
214
+ </PaginationItem>
215
+ ))}
216
+
217
+ {/* Next Button */}
218
+ <PaginationItem>
219
+ {hasNextPage ? (
220
+ <Link href={getPageUrl(actualCurrentPage + 1)} prefetch={prefetch} passHref legacyBehavior>
221
+ <PaginationNext />
222
+ </Link>
223
+ ) : (
224
+ <PaginationNext
225
+ href="#"
226
+ className="pointer-events-none opacity-50"
227
+ onClick={(e) => e.preventDefault()}
228
+ />
229
+ )}
230
+ </PaginationItem>
231
+ </PaginationContent>
232
+ </Pagination>
233
+ </div>
234
+ );
235
+ };
236
+
237
+ SSRPagination.displayName = 'SSRPagination';
@@ -0,0 +1,19 @@
1
+ // ============================================================================
2
+ // Re-export all hooks from @djangocfg/ui-core
3
+ // ============================================================================
4
+ export * from '@djangocfg/ui-core/hooks';
5
+
6
+ // ============================================================================
7
+ // Next.js/Browser specific hooks
8
+ // ============================================================================
9
+
10
+ // Storage hooks (browser localStorage/sessionStorage)
11
+ export { useLocalStorage } from './useLocalStorage';
12
+ export { useSessionStorage } from './useSessionStorage';
13
+
14
+ // Theme hook (uses next-themes)
15
+ export { useTheme } from './useTheme';
16
+
17
+ // Next.js router hooks
18
+ export { useQueryParams } from './useQueryParams';
19
+ export { useCfgRouter } from './useCfgRouter';
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Universal Router Hook with BasePath Support
3
+ *
4
+ * Wrapper around Next.js useRouter that automatically handles basePath
5
+ * for static builds served via iframe or subdirectory
6
+ *
7
+ * IMPORTANT: In Next.js 15 App Router, router.push() does NOT automatically
8
+ * handle basePath (unlike Pages Router). This is a breaking change in App Router.
9
+ *
10
+ * This hook ensures basePath is always included when navigating, especially
11
+ * important for static exports served via iframe where basePath is critical.
12
+ *
13
+ * @see https://nextjs.org/docs/app/building-your-application/upgrading/app-router-migration
14
+ */
15
+
16
+ 'use client';
17
+
18
+ import { useRouter as useNextRouter } from 'next/navigation';
19
+ import { useCallback, useMemo } from 'react';
20
+
21
+ /**
22
+ * Get base path from environment variable
23
+ */
24
+ function getBasePath(): string {
25
+ if (typeof process === 'undefined') {
26
+ return '';
27
+ }
28
+ return process.env.NEXT_PUBLIC_BASE_PATH || '';
29
+ }
30
+
31
+ /**
32
+ * Add base path to a route path
33
+ */
34
+ function withBasePath(path: string, basePath: string): string {
35
+ if (!basePath) {
36
+ return path;
37
+ }
38
+ // Ensure path starts with /
39
+ const normalizedPath = path.startsWith('/') ? path : `/${path}`;
40
+ // Remove trailing slash from basePath
41
+ const normalizedBasePath = basePath.replace(/\/$/, '');
42
+ return `${normalizedBasePath}${normalizedPath}`;
43
+ }
44
+
45
+ /**
46
+ * Router with basePath support
47
+ *
48
+ * Automatically adds basePath to all navigation methods when basePath is configured.
49
+ * In Next.js 15 App Router, router.push() doesn't handle basePath automatically,
50
+ * so this hook uses window.location to ensure basePath is always included.
51
+ *
52
+ * @example
53
+ * ```tsx
54
+ * const router = useCfgRouter();
55
+ *
56
+ * // With basePath='/cfg/admin':
57
+ * router.push('/dashboard'); // Client-side navigation to '/cfg/admin/dashboard'
58
+ * router.replace('/auth'); // Client-side replace with '/cfg/admin/auth'
59
+ * router.hardPush('/dashboard'); // Full page reload to '/cfg/admin/dashboard'
60
+ * router.hardReplace('/auth'); // Full page replace with '/cfg/admin/auth'
61
+ * ```
62
+ */
63
+ export function useCfgRouter() {
64
+ const router = useNextRouter();
65
+
66
+ // Get basePath and check if we're in static build mode
67
+ const basePath = useMemo(() => getBasePath(), []);
68
+ const isStaticBuild = useMemo(() => {
69
+ return typeof process !== 'undefined' && process.env.NEXT_PUBLIC_STATIC_BUILD === 'true';
70
+ }, []);
71
+
72
+ const push = useCallback((href: string, options?: { scroll?: boolean }) => {
73
+ if (basePath) {
74
+ // App Router doesn't handle basePath automatically, use window.location
75
+ window.location.href = withBasePath(href, basePath);
76
+ } else {
77
+ // No basePath configured, use standard router
78
+ router.push(href, options);
79
+ }
80
+ }, [router, basePath]);
81
+
82
+ const replace = useCallback((href: string, options?: { scroll?: boolean }) => {
83
+ if (basePath) {
84
+ // App Router doesn't handle basePath automatically, use window.location
85
+ window.location.replace(withBasePath(href, basePath));
86
+ } else {
87
+ // No basePath configured, use standard router
88
+ router.replace(href, options);
89
+ }
90
+ }, [router, basePath]);
91
+
92
+ /**
93
+ * Hard push - always uses window.location.href for full page reload
94
+ *
95
+ * Use this for auth redirects where React contexts need to reinitialize.
96
+ * Unlike push(), this ALWAYS triggers a full page reload, ensuring all
97
+ * contexts are reinitialized with fresh state.
98
+ *
99
+ * @example
100
+ * ```tsx
101
+ * // After successful login - contexts need to reload with new auth state
102
+ * router.hardPush('/dashboard');
103
+ * ```
104
+ */
105
+ const hardPush = useCallback((href: string) => {
106
+ window.location.href = withBasePath(href, basePath);
107
+ }, [basePath]);
108
+
109
+ /**
110
+ * Hard replace - always uses window.location.replace for full page reload
111
+ *
112
+ * Same as hardPush but replaces current history entry.
113
+ * Use for auth redirects where you don't want back button to return to login.
114
+ *
115
+ * @example
116
+ * ```tsx
117
+ * // After logout - replace so back button doesn't go to protected page
118
+ * router.hardReplace('/auth');
119
+ * ```
120
+ */
121
+ const hardReplace = useCallback((href: string) => {
122
+ window.location.replace(withBasePath(href, basePath));
123
+ }, [basePath]);
124
+
125
+ const prefetch = useCallback((href: string) => {
126
+ // Prefetch doesn't need basePath handling, Next.js handles it
127
+ router.prefetch(href);
128
+ }, [router]);
129
+
130
+ const back = useCallback(() => {
131
+ router.back();
132
+ }, [router]);
133
+
134
+ const forward = useCallback(() => {
135
+ router.forward();
136
+ }, [router]);
137
+
138
+ const refresh = useCallback(() => {
139
+ router.refresh();
140
+ }, [router]);
141
+
142
+ return {
143
+ push,
144
+ replace,
145
+ hardPush,
146
+ hardReplace,
147
+ prefetch,
148
+ back,
149
+ forward,
150
+ refresh,
151
+ };
152
+ }
153
+
@@ -0,0 +1,221 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useState, useRef } from 'react';
4
+
5
+ /**
6
+ * Simple localStorage hook with better error handling
7
+ *
8
+ * IMPORTANT: To prevent hydration mismatch, this hook:
9
+ * - Always returns initialValue on first render (same as SSR)
10
+ * - Reads from localStorage only after component mounts
11
+ *
12
+ * @param key - Storage key
13
+ * @param initialValue - Default value if key doesn't exist
14
+ * @returns [value, setValue, removeValue] - Current value, setter function, and remove function
15
+ */
16
+ export function useLocalStorage<T>(key: string, initialValue: T) {
17
+ // Always start with initialValue to match SSR
18
+ const [storedValue, setStoredValue] = useState<T>(initialValue);
19
+ const [isHydrated, setIsHydrated] = useState(false);
20
+ const isInitialized = useRef(false);
21
+
22
+ // Read from localStorage after mount (avoids hydration mismatch)
23
+ useEffect(() => {
24
+ if (isInitialized.current) return;
25
+ isInitialized.current = true;
26
+
27
+ try {
28
+ const item = window.localStorage.getItem(key);
29
+ if (item !== null) {
30
+ // Try to parse as JSON first, fallback to string
31
+ try {
32
+ setStoredValue(JSON.parse(item));
33
+ } catch {
34
+ // If JSON.parse fails, return as string
35
+ setStoredValue(item as T);
36
+ }
37
+ }
38
+ } catch (error) {
39
+ console.error(`Error reading localStorage key "${key}":`, error);
40
+ }
41
+
42
+ setIsHydrated(true);
43
+ }, [key]);
44
+
45
+ // Check data size and limit
46
+ const checkDataSize = (data: any): boolean => {
47
+ try {
48
+ const jsonString = JSON.stringify(data);
49
+ const sizeInBytes = new Blob([jsonString]).size;
50
+ const sizeInKB = sizeInBytes / 1024;
51
+
52
+ // Limit to 1MB per item
53
+ if (sizeInKB > 1024) {
54
+ console.warn(`Data size (${sizeInKB.toFixed(2)}KB) exceeds 1MB limit for key "${key}"`);
55
+ return false;
56
+ }
57
+
58
+ return true;
59
+ } catch (error) {
60
+ console.error(`Error checking data size for key "${key}":`, error);
61
+ return false;
62
+ }
63
+ };
64
+
65
+ // Clear old data when localStorage is full
66
+ const clearOldData = () => {
67
+ try {
68
+ const keys = Object.keys(localStorage).filter(key => key && typeof key === 'string');
69
+ // Remove oldest items if we have more than 50 items
70
+ if (keys.length > 50) {
71
+ const itemsToRemove = Math.ceil(keys.length * 0.2);
72
+ for (let i = 0; i < itemsToRemove; i++) {
73
+ try {
74
+ const key = keys[i];
75
+ if (key) {
76
+ localStorage.removeItem(key);
77
+ localStorage.removeItem(`${key}_timestamp`);
78
+ }
79
+ } catch {
80
+ // Ignore errors when removing items
81
+ }
82
+ }
83
+ }
84
+ } catch (error) {
85
+ console.error('Error clearing old localStorage data:', error);
86
+ }
87
+ };
88
+
89
+ // Force clear all data if quota is exceeded
90
+ const forceClearAll = () => {
91
+ try {
92
+ const keys = Object.keys(localStorage);
93
+ for (const key of keys) {
94
+ try {
95
+ localStorage.removeItem(key);
96
+ } catch {
97
+ // Ignore errors when removing items
98
+ }
99
+ }
100
+ } catch (error) {
101
+ console.error('Error force clearing localStorage:', error);
102
+ }
103
+ };
104
+
105
+ // Update localStorage when value changes
106
+ const setValue = (value: T | ((val: T) => T)) => {
107
+ try {
108
+ const valueToStore = value instanceof Function ? value(storedValue) : value;
109
+
110
+ // Check data size before attempting to save
111
+ if (!checkDataSize(valueToStore)) {
112
+ console.warn(`Data size too large for key "${key}", removing key`);
113
+ // Remove the key if data is too large
114
+ try {
115
+ window.localStorage.removeItem(key);
116
+ window.localStorage.removeItem(`${key}_timestamp`);
117
+ } catch {
118
+ // Ignore errors when removing
119
+ }
120
+ // Still update the state
121
+ setStoredValue(valueToStore);
122
+ return;
123
+ }
124
+
125
+ setStoredValue(valueToStore);
126
+
127
+ if (typeof window !== 'undefined') {
128
+ // Try to set the value
129
+ try {
130
+ // For strings, store directly without JSON.stringify
131
+ if (typeof valueToStore === 'string') {
132
+ window.localStorage.setItem(key, valueToStore);
133
+ } else {
134
+ window.localStorage.setItem(key, JSON.stringify(valueToStore));
135
+ }
136
+ // Add timestamp for cleanup
137
+ window.localStorage.setItem(`${key}_timestamp`, Date.now().toString());
138
+ } catch (storageError: any) {
139
+ // If quota exceeded, clear old data and try again
140
+ if (storageError.name === 'QuotaExceededError' ||
141
+ storageError.code === 22 ||
142
+ storageError.message?.includes('quota')) {
143
+ console.warn('localStorage quota exceeded, clearing old data...');
144
+ clearOldData();
145
+
146
+ // Try again after clearing
147
+ try {
148
+ // For strings, store directly without JSON.stringify
149
+ if (typeof valueToStore === 'string') {
150
+ window.localStorage.setItem(key, valueToStore);
151
+ } else {
152
+ window.localStorage.setItem(key, JSON.stringify(valueToStore));
153
+ }
154
+ window.localStorage.setItem(`${key}_timestamp`, Date.now().toString());
155
+ } catch (retryError) {
156
+ console.error(`Failed to set localStorage key "${key}" after clearing old data:`, retryError);
157
+ // If still fails, force clear all and try one more time
158
+ try {
159
+ forceClearAll();
160
+ // For strings, store directly without JSON.stringify
161
+ if (typeof valueToStore === 'string') {
162
+ window.localStorage.setItem(key, valueToStore);
163
+ } else {
164
+ window.localStorage.setItem(key, JSON.stringify(valueToStore));
165
+ }
166
+ window.localStorage.setItem(`${key}_timestamp`, Date.now().toString());
167
+ } catch (finalError) {
168
+ console.error(`Failed to set localStorage key "${key}" after force clearing:`, finalError);
169
+ // If still fails, just update the state without localStorage
170
+ setStoredValue(valueToStore);
171
+ }
172
+ }
173
+ } else {
174
+ throw storageError;
175
+ }
176
+ }
177
+ }
178
+ } catch (error) {
179
+ console.error(`Error setting localStorage key "${key}":`, error);
180
+ // Still update the state even if localStorage fails
181
+ const valueToStore = value instanceof Function ? value(storedValue) : value;
182
+ setStoredValue(valueToStore);
183
+ }
184
+ };
185
+
186
+ // Remove value from localStorage
187
+ const removeValue = () => {
188
+ try {
189
+ setStoredValue(initialValue);
190
+ if (typeof window !== 'undefined') {
191
+ try {
192
+ window.localStorage.removeItem(key);
193
+ window.localStorage.removeItem(`${key}_timestamp`);
194
+ } catch (removeError: any) {
195
+ // If removal fails due to quota, try to clear some data first
196
+ if (removeError.name === 'QuotaExceededError' ||
197
+ removeError.code === 22 ||
198
+ removeError.message?.includes('quota')) {
199
+ console.warn('localStorage quota exceeded during removal, clearing old data...');
200
+ clearOldData();
201
+
202
+ try {
203
+ window.localStorage.removeItem(key);
204
+ window.localStorage.removeItem(`${key}_timestamp`);
205
+ } catch (retryError) {
206
+ console.error(`Failed to remove localStorage key "${key}" after clearing:`, retryError);
207
+ // If still fails, force clear all
208
+ forceClearAll();
209
+ }
210
+ } else {
211
+ throw removeError;
212
+ }
213
+ }
214
+ }
215
+ } catch (error) {
216
+ console.error(`Error removing localStorage key "${key}":`, error);
217
+ }
218
+ };
219
+
220
+ return [storedValue, setValue, removeValue] as const;
221
+ }