@djangocfg/ui-core 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.
- package/README.md +135 -0
- package/package.json +111 -0
- package/src/components/accordion.tsx +56 -0
- package/src/components/alert-dialog.tsx +142 -0
- package/src/components/alert.tsx +59 -0
- package/src/components/aspect-ratio.tsx +7 -0
- package/src/components/avatar.tsx +50 -0
- package/src/components/badge.tsx +36 -0
- package/src/components/button-group.tsx +85 -0
- package/src/components/button.tsx +111 -0
- package/src/components/calendar.tsx +213 -0
- package/src/components/card.tsx +76 -0
- package/src/components/carousel.tsx +261 -0
- package/src/components/chart.tsx +369 -0
- package/src/components/checkbox.tsx +29 -0
- package/src/components/collapsible.tsx +11 -0
- package/src/components/combobox.tsx +182 -0
- package/src/components/command.tsx +170 -0
- package/src/components/context-menu.tsx +200 -0
- package/src/components/copy.tsx +144 -0
- package/src/components/dialog.tsx +122 -0
- package/src/components/drawer.tsx +137 -0
- package/src/components/empty.tsx +104 -0
- package/src/components/field.tsx +244 -0
- package/src/components/form.tsx +178 -0
- package/src/components/hover-card.tsx +29 -0
- package/src/components/image-with-fallback.tsx +170 -0
- package/src/components/index.ts +86 -0
- package/src/components/input-group.tsx +170 -0
- package/src/components/input-otp.tsx +81 -0
- package/src/components/input.tsx +22 -0
- package/src/components/item.tsx +195 -0
- package/src/components/kbd.tsx +28 -0
- package/src/components/label.tsx +26 -0
- package/src/components/multi-select.tsx +222 -0
- package/src/components/og-image.tsx +47 -0
- package/src/components/popover.tsx +33 -0
- package/src/components/portal.tsx +106 -0
- package/src/components/preloader.tsx +250 -0
- package/src/components/progress.tsx +28 -0
- package/src/components/radio-group.tsx +43 -0
- package/src/components/resizable.tsx +111 -0
- package/src/components/scroll-area.tsx +102 -0
- package/src/components/section.tsx +58 -0
- package/src/components/select.tsx +158 -0
- package/src/components/separator.tsx +31 -0
- package/src/components/sheet.tsx +140 -0
- package/src/components/skeleton.tsx +15 -0
- package/src/components/slider.tsx +28 -0
- package/src/components/spinner.tsx +16 -0
- package/src/components/sticky.tsx +117 -0
- package/src/components/switch.tsx +29 -0
- package/src/components/table.tsx +120 -0
- package/src/components/tabs.tsx +238 -0
- package/src/components/textarea.tsx +22 -0
- package/src/components/toast.tsx +129 -0
- package/src/components/toaster.tsx +41 -0
- package/src/components/toggle-group.tsx +61 -0
- package/src/components/toggle.tsx +45 -0
- package/src/components/token-icon.tsx +156 -0
- package/src/components/tooltip-provider-safe.tsx +43 -0
- package/src/components/tooltip.tsx +32 -0
- package/src/hooks/index.ts +15 -0
- package/src/hooks/useCopy.ts +41 -0
- package/src/hooks/useCountdown.ts +73 -0
- package/src/hooks/useDebounce.ts +25 -0
- package/src/hooks/useDebouncedCallback.ts +58 -0
- package/src/hooks/useDebugTools.ts +52 -0
- package/src/hooks/useEventsBus.ts +53 -0
- package/src/hooks/useImageLoader.ts +95 -0
- package/src/hooks/useMediaQuery.ts +40 -0
- package/src/hooks/useMobile.tsx +22 -0
- package/src/hooks/useToast.ts +194 -0
- package/src/index.ts +14 -0
- package/src/lib/index.ts +2 -0
- package/src/lib/og-image.ts +151 -0
- package/src/lib/utils.ts +6 -0
- package/src/styles/base.css +20 -0
- package/src/styles/globals.css +12 -0
- package/src/styles/index.css +25 -0
- package/src/styles/sources.css +11 -0
- package/src/styles/theme/animations.css +65 -0
- package/src/styles/theme/dark.css +49 -0
- package/src/styles/theme/light.css +50 -0
- package/src/styles/theme/tokens.css +134 -0
- package/src/styles/theme.css +22 -0
- package/src/styles/utilities.css +187 -0
- package/src/types/index.ts +0 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { TooltipProvider as RadixTooltipProvider } from '@radix-ui/react-tooltip';
|
|
5
|
+
|
|
6
|
+
interface SafeTooltipProviderProps {
|
|
7
|
+
children: React.ReactNode;
|
|
8
|
+
delayDuration?: number;
|
|
9
|
+
skipDelayDuration?: number;
|
|
10
|
+
disableHoverableContent?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* SafeTooltipProvider - SSR-safe wrapper for Radix TooltipProvider
|
|
15
|
+
* Only renders on client-side to avoid hydration mismatches
|
|
16
|
+
*/
|
|
17
|
+
export function SafeTooltipProvider({
|
|
18
|
+
children,
|
|
19
|
+
delayDuration = 700,
|
|
20
|
+
skipDelayDuration = 300,
|
|
21
|
+
disableHoverableContent,
|
|
22
|
+
}: SafeTooltipProviderProps) {
|
|
23
|
+
const [mounted, setMounted] = React.useState(false);
|
|
24
|
+
|
|
25
|
+
React.useEffect(() => {
|
|
26
|
+
setMounted(true);
|
|
27
|
+
}, []);
|
|
28
|
+
|
|
29
|
+
if (!mounted) {
|
|
30
|
+
// During SSR, return children without TooltipProvider
|
|
31
|
+
return <>{children}</>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<RadixTooltipProvider
|
|
36
|
+
delayDuration={delayDuration}
|
|
37
|
+
skipDelayDuration={skipDelayDuration}
|
|
38
|
+
disableHoverableContent={disableHoverableContent}
|
|
39
|
+
>
|
|
40
|
+
{children}
|
|
41
|
+
</RadixTooltipProvider>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
|
|
5
|
+
|
|
6
|
+
import { cn } from "../lib/utils"
|
|
7
|
+
|
|
8
|
+
const TooltipProvider = TooltipPrimitive.Provider
|
|
9
|
+
|
|
10
|
+
const Tooltip = TooltipPrimitive.Root
|
|
11
|
+
|
|
12
|
+
const TooltipTrigger = TooltipPrimitive.Trigger
|
|
13
|
+
|
|
14
|
+
const TooltipContent = React.forwardRef<
|
|
15
|
+
React.ElementRef<typeof TooltipPrimitive.Content>,
|
|
16
|
+
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
|
17
|
+
>(({ className, sideOffset = 4, ...props }, ref) => (
|
|
18
|
+
<TooltipPrimitive.Portal>
|
|
19
|
+
<TooltipPrimitive.Content
|
|
20
|
+
ref={ref}
|
|
21
|
+
sideOffset={sideOffset}
|
|
22
|
+
className={cn(
|
|
23
|
+
"z-150 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
|
24
|
+
className
|
|
25
|
+
)}
|
|
26
|
+
{...props}
|
|
27
|
+
/>
|
|
28
|
+
</TooltipPrimitive.Portal>
|
|
29
|
+
))
|
|
30
|
+
TooltipContent.displayName = TooltipPrimitive.Content.displayName
|
|
31
|
+
|
|
32
|
+
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Hooks - Reusable React Hooks (No Next.js/Browser Storage dependencies)
|
|
3
|
+
// For use in Electron, Vite, CRA apps
|
|
4
|
+
// ============================================================================
|
|
5
|
+
|
|
6
|
+
export { useCountdown } from './useCountdown';
|
|
7
|
+
export { useDebouncedCallback } from './useDebouncedCallback';
|
|
8
|
+
export { useDebounce } from './useDebounce';
|
|
9
|
+
export { useDebugTools } from './useDebugTools';
|
|
10
|
+
export { useEventListener, events } from './useEventsBus';
|
|
11
|
+
export { useIsMobile } from './useMobile';
|
|
12
|
+
export { useMediaQuery } from './useMediaQuery';
|
|
13
|
+
export { useCopy } from './useCopy';
|
|
14
|
+
export { useImageLoader } from './useImageLoader';
|
|
15
|
+
export { useToast, toast } from './useToast';
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useCallback } from 'react';
|
|
4
|
+
|
|
5
|
+
import { useToast } from './useToast';
|
|
6
|
+
|
|
7
|
+
interface UseCopyOptions {
|
|
8
|
+
successMessage?: string;
|
|
9
|
+
errorMessage?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const useCopy = (options: UseCopyOptions = {}) => {
|
|
13
|
+
const { toast } = useToast();
|
|
14
|
+
|
|
15
|
+
const {
|
|
16
|
+
successMessage = "Copied to clipboard",
|
|
17
|
+
errorMessage = "Failed to copy to clipboard"
|
|
18
|
+
} = options;
|
|
19
|
+
|
|
20
|
+
const copyToClipboard = useCallback(async (text: string, customSuccessMessage?: string) => {
|
|
21
|
+
try {
|
|
22
|
+
await navigator.clipboard.writeText(text);
|
|
23
|
+
toast({
|
|
24
|
+
title: "Success!",
|
|
25
|
+
description: customSuccessMessage || successMessage,
|
|
26
|
+
variant: "default",
|
|
27
|
+
});
|
|
28
|
+
return true;
|
|
29
|
+
} catch (error) {
|
|
30
|
+
console.error('Failed to copy:', error);
|
|
31
|
+
toast({
|
|
32
|
+
title: "Error",
|
|
33
|
+
description: errorMessage,
|
|
34
|
+
variant: "destructive",
|
|
35
|
+
});
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}, [toast, successMessage, errorMessage]);
|
|
39
|
+
|
|
40
|
+
return { copyToClipboard };
|
|
41
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import moment from 'moment';
|
|
4
|
+
import { useEffect, useState } from 'react';
|
|
5
|
+
|
|
6
|
+
interface CountdownState {
|
|
7
|
+
days: number;
|
|
8
|
+
hours: number;
|
|
9
|
+
minutes: number;
|
|
10
|
+
seconds: number;
|
|
11
|
+
isExpired: boolean;
|
|
12
|
+
totalSeconds: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const useCountdown = (targetDate: string | null): CountdownState => {
|
|
16
|
+
const [countdown, setCountdown] = useState<CountdownState>({
|
|
17
|
+
days: 0,
|
|
18
|
+
hours: 0,
|
|
19
|
+
minutes: 0,
|
|
20
|
+
seconds: 0,
|
|
21
|
+
isExpired: false,
|
|
22
|
+
totalSeconds: 0,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (!targetDate) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const target = moment.utc(targetDate);
|
|
31
|
+
|
|
32
|
+
const updateCountdown = () => {
|
|
33
|
+
const now = moment.utc();
|
|
34
|
+
const diff = target.diff(now, 'seconds');
|
|
35
|
+
|
|
36
|
+
if (diff <= 0) {
|
|
37
|
+
setCountdown({
|
|
38
|
+
days: 0,
|
|
39
|
+
hours: 0,
|
|
40
|
+
minutes: 0,
|
|
41
|
+
seconds: 0,
|
|
42
|
+
isExpired: true,
|
|
43
|
+
totalSeconds: 0,
|
|
44
|
+
});
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const days = Math.floor(diff / (24 * 60 * 60));
|
|
49
|
+
const hours = Math.floor((diff % (24 * 60 * 60)) / (60 * 60));
|
|
50
|
+
const minutes = Math.floor((diff % (60 * 60)) / 60);
|
|
51
|
+
const seconds = diff % 60;
|
|
52
|
+
|
|
53
|
+
setCountdown({
|
|
54
|
+
days,
|
|
55
|
+
hours,
|
|
56
|
+
minutes,
|
|
57
|
+
seconds,
|
|
58
|
+
isExpired: false,
|
|
59
|
+
totalSeconds: diff,
|
|
60
|
+
});
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// Update immediately
|
|
64
|
+
updateCountdown();
|
|
65
|
+
|
|
66
|
+
// Update every second
|
|
67
|
+
const interval = setInterval(updateCountdown, 1000);
|
|
68
|
+
|
|
69
|
+
return () => clearInterval(interval);
|
|
70
|
+
}, [targetDate]);
|
|
71
|
+
|
|
72
|
+
return countdown;
|
|
73
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useDebounce hook
|
|
3
|
+
*
|
|
4
|
+
* Debounces a value by delaying its update until after a specified delay.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use client';
|
|
8
|
+
|
|
9
|
+
import { useState, useEffect } from 'react';
|
|
10
|
+
|
|
11
|
+
export function useDebounce<T>(value: T, delay: number = 300): T {
|
|
12
|
+
const [debouncedValue, setDebouncedValue] = useState<T>(value);
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
const handler = setTimeout(() => {
|
|
16
|
+
setDebouncedValue(value);
|
|
17
|
+
}, delay);
|
|
18
|
+
|
|
19
|
+
return () => {
|
|
20
|
+
clearTimeout(handler);
|
|
21
|
+
};
|
|
22
|
+
}, [value, delay]);
|
|
23
|
+
|
|
24
|
+
return debouncedValue;
|
|
25
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useRef } from 'react';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Creates a debounced version of a callback function.
|
|
7
|
+
*
|
|
8
|
+
* @param callback The function to debounce.
|
|
9
|
+
* @param delay The debounce delay in milliseconds.
|
|
10
|
+
* @returns A debounced callback function.
|
|
11
|
+
*/
|
|
12
|
+
export function useDebouncedCallback<T extends (...args: any[]) => any>(
|
|
13
|
+
callback: T,
|
|
14
|
+
delay: number
|
|
15
|
+
): (...args: Parameters<T>) => void {
|
|
16
|
+
const callbackRef = useRef(callback);
|
|
17
|
+
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
18
|
+
|
|
19
|
+
// Update ref when callback changes, but don't trigger effect
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
callbackRef.current = callback;
|
|
22
|
+
}, [callback]);
|
|
23
|
+
|
|
24
|
+
// Cleanup timeout on unmount
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
return () => {
|
|
27
|
+
if (timeoutRef.current) {
|
|
28
|
+
clearTimeout(timeoutRef.current);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
}, []);
|
|
32
|
+
|
|
33
|
+
const debouncedCallback = useCallback(
|
|
34
|
+
(...args: Parameters<T>) => {
|
|
35
|
+
if (timeoutRef.current) {
|
|
36
|
+
clearTimeout(timeoutRef.current);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
timeoutRef.current = setTimeout(() => {
|
|
40
|
+
callbackRef.current(...args);
|
|
41
|
+
}, delay);
|
|
42
|
+
},
|
|
43
|
+
[delay]
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
// Add a cancel method to the debounced function
|
|
47
|
+
// We attach it directly to the function object
|
|
48
|
+
(debouncedCallback as any).cancel = useCallback(() => {
|
|
49
|
+
if (timeoutRef.current) {
|
|
50
|
+
clearTimeout(timeoutRef.current);
|
|
51
|
+
timeoutRef.current = null;
|
|
52
|
+
}
|
|
53
|
+
}, []);
|
|
54
|
+
|
|
55
|
+
return debouncedCallback;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export default useDebouncedCallback;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useDebugValue } from 'react';
|
|
4
|
+
|
|
5
|
+
type DebugValue = Record<string, unknown> | unknown[] | null | undefined;
|
|
6
|
+
|
|
7
|
+
export function useDebugTools(values: DebugValue, prefix = '') {
|
|
8
|
+
if (values === null || values === undefined) {
|
|
9
|
+
useDebugValue(values, () => `${prefix}: ${values === null ? 'null' : 'undefined'}`);
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (Array.isArray(values)) {
|
|
14
|
+
values.forEach((value, index) => {
|
|
15
|
+
useDebugValue(value, (v) => {
|
|
16
|
+
const label = prefix ? `${prefix}[${index}]` : `[${index}]`;
|
|
17
|
+
return `${label}: ${formatValue(v)}`;
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (typeof values === 'object') {
|
|
24
|
+
for (const [key, value] of Object.entries(values)) {
|
|
25
|
+
useDebugValue(value, (v) => {
|
|
26
|
+
const label = prefix ? `${prefix}.${key}` : key;
|
|
27
|
+
return `${label}: ${formatValue(v)}`;
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Handle primitive values
|
|
34
|
+
useDebugValue(values, (v) => `${prefix}: ${formatValue(v)}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function formatValue(value: unknown): string {
|
|
38
|
+
if (value === null) return 'null';
|
|
39
|
+
if (value === undefined) return 'undefined';
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
if (typeof value === 'object') {
|
|
43
|
+
if (Array.isArray(value)) {
|
|
44
|
+
return `Array(${value.length})`;
|
|
45
|
+
}
|
|
46
|
+
return JSON.stringify(value);
|
|
47
|
+
}
|
|
48
|
+
return String(value);
|
|
49
|
+
} catch {
|
|
50
|
+
return '[Unserializable]';
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef } from 'react';
|
|
4
|
+
|
|
5
|
+
export type FormEvent<T extends string = string, P = any> = {
|
|
6
|
+
type: T;
|
|
7
|
+
payload?: P;
|
|
8
|
+
timestamp?: number;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
type EventListener<T extends FormEvent> = (event: T) => void;
|
|
12
|
+
|
|
13
|
+
class EventBus {
|
|
14
|
+
private listeners: Set<EventListener<any>> = new Set();
|
|
15
|
+
|
|
16
|
+
publish<T extends FormEvent>(event: T) {
|
|
17
|
+
this.listeners.forEach(listener => listener({
|
|
18
|
+
...event,
|
|
19
|
+
timestamp: event.timestamp || Date.now(),
|
|
20
|
+
}));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
subscribe<T extends FormEvent>(listener: EventListener<T>) {
|
|
24
|
+
this.listeners.add(listener);
|
|
25
|
+
return () => {
|
|
26
|
+
this.listeners.delete(listener);
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const events = new EventBus();
|
|
32
|
+
|
|
33
|
+
export function useEventListener<T extends string, P>(
|
|
34
|
+
eventType: T,
|
|
35
|
+
handler: (payload: P) => void
|
|
36
|
+
) {
|
|
37
|
+
const savedHandler = useRef(handler);
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
savedHandler.current = handler;
|
|
41
|
+
}, [handler]);
|
|
42
|
+
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
const listener = (event: FormEvent) => {
|
|
45
|
+
if (event.type === eventType) {
|
|
46
|
+
savedHandler.current(event.payload);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const unsubscribe = events.subscribe(listener);
|
|
51
|
+
return () => unsubscribe();
|
|
52
|
+
}, [eventType]);
|
|
53
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enhanced Image Loader Hook
|
|
3
|
+
*
|
|
4
|
+
* Hook to check if an image is loaded successfully with callback support
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use client';
|
|
8
|
+
|
|
9
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
10
|
+
|
|
11
|
+
export interface ImageLoaderState {
|
|
12
|
+
isLoading: boolean;
|
|
13
|
+
isLoaded: boolean;
|
|
14
|
+
hasError: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ImageLoaderCallbacks {
|
|
18
|
+
onLoadStart?: () => void;
|
|
19
|
+
onLoad?: (event: React.SyntheticEvent<HTMLImageElement>) => void;
|
|
20
|
+
onError?: (event: React.SyntheticEvent<HTMLImageElement>) => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const useImageLoader = (
|
|
24
|
+
src?: string,
|
|
25
|
+
callbacks?: ImageLoaderCallbacks
|
|
26
|
+
): ImageLoaderState => {
|
|
27
|
+
const [state, setState] = useState<ImageLoaderState>({
|
|
28
|
+
isLoading: false,
|
|
29
|
+
isLoaded: false,
|
|
30
|
+
hasError: false,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const handleLoad = useCallback((event: Event) => {
|
|
34
|
+
setState({
|
|
35
|
+
isLoading: false,
|
|
36
|
+
isLoaded: true,
|
|
37
|
+
hasError: false,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Call user callback if provided
|
|
41
|
+
if (callbacks?.onLoad) {
|
|
42
|
+
callbacks.onLoad(event as unknown as React.SyntheticEvent<HTMLImageElement>);
|
|
43
|
+
}
|
|
44
|
+
}, [callbacks?.onLoad]);
|
|
45
|
+
|
|
46
|
+
const handleError = useCallback((event: Event) => {
|
|
47
|
+
setState({
|
|
48
|
+
isLoading: false,
|
|
49
|
+
isLoaded: false,
|
|
50
|
+
hasError: true,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Call user callback if provided
|
|
54
|
+
if (callbacks?.onError) {
|
|
55
|
+
callbacks.onError(event as unknown as React.SyntheticEvent<HTMLImageElement>);
|
|
56
|
+
}
|
|
57
|
+
}, [callbacks?.onError]);
|
|
58
|
+
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
if (!src) {
|
|
61
|
+
setState({
|
|
62
|
+
isLoading: false,
|
|
63
|
+
isLoaded: false,
|
|
64
|
+
hasError: true,
|
|
65
|
+
});
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
setState({
|
|
70
|
+
isLoading: true,
|
|
71
|
+
isLoaded: false,
|
|
72
|
+
hasError: false,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Call load start callback
|
|
76
|
+
if (callbacks?.onLoadStart) {
|
|
77
|
+
callbacks.onLoadStart();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const img = new Image();
|
|
81
|
+
|
|
82
|
+
img.addEventListener('load', handleLoad);
|
|
83
|
+
img.addEventListener('error', handleError);
|
|
84
|
+
|
|
85
|
+
img.src = src;
|
|
86
|
+
|
|
87
|
+
// Cleanup function
|
|
88
|
+
return () => {
|
|
89
|
+
img.removeEventListener('load', handleLoad);
|
|
90
|
+
img.removeEventListener('error', handleError);
|
|
91
|
+
};
|
|
92
|
+
}, [src, handleLoad, handleError, callbacks?.onLoadStart]);
|
|
93
|
+
|
|
94
|
+
return state;
|
|
95
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from 'react';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Hook to check if a media query matches
|
|
7
|
+
*
|
|
8
|
+
* @param query - CSS media query string
|
|
9
|
+
* @returns boolean indicating if the query matches
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* const isMobile = useMediaQuery('(max-width: 768px)');
|
|
13
|
+
* const prefersDark = useMediaQuery('(prefers-color-scheme: dark)');
|
|
14
|
+
* const isLandscape = useMediaQuery('(orientation: landscape)');
|
|
15
|
+
*/
|
|
16
|
+
export function useMediaQuery(query: string): boolean {
|
|
17
|
+
const [matches, setMatches] = useState<boolean>(false);
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
const mediaQuery = window.matchMedia(query);
|
|
21
|
+
|
|
22
|
+
// Set initial value
|
|
23
|
+
setMatches(mediaQuery.matches);
|
|
24
|
+
|
|
25
|
+
// Create event listener
|
|
26
|
+
const handler = (event: MediaQueryListEvent) => {
|
|
27
|
+
setMatches(event.matches);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Add listener
|
|
31
|
+
mediaQuery.addEventListener('change', handler);
|
|
32
|
+
|
|
33
|
+
// Cleanup
|
|
34
|
+
return () => {
|
|
35
|
+
mediaQuery.removeEventListener('change', handler);
|
|
36
|
+
};
|
|
37
|
+
}, [query]);
|
|
38
|
+
|
|
39
|
+
return matches;
|
|
40
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
|
|
5
|
+
// Увеличил breakpoint до 1024px чтобы включить планшеты
|
|
6
|
+
const MOBILE_BREAKPOINT = 1024
|
|
7
|
+
|
|
8
|
+
export function useIsMobile() {
|
|
9
|
+
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
|
|
10
|
+
|
|
11
|
+
React.useEffect(() => {
|
|
12
|
+
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
|
|
13
|
+
const onChange = () => {
|
|
14
|
+
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
|
15
|
+
}
|
|
16
|
+
mql.addEventListener("change", onChange)
|
|
17
|
+
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
|
18
|
+
return () => mql.removeEventListener("change", onChange)
|
|
19
|
+
}, [])
|
|
20
|
+
|
|
21
|
+
return !!isMobile
|
|
22
|
+
}
|