@zuzjs/ui 0.9.7 → 0.9.82
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/dist/cjs/comps/Calendar/index.d.ts +3 -0
- package/dist/cjs/comps/Calendar/index.js +109 -0
- package/dist/cjs/comps/Calendar/types.d.ts +6 -0
- package/dist/cjs/comps/Calendar/types.js +1 -0
- package/dist/cjs/comps/Crumb/index.d.ts +1 -1
- package/dist/cjs/comps/DatePicker/index.d.ts +11 -0
- package/dist/cjs/comps/DatePicker/index.js +73 -0
- package/dist/cjs/comps/DatePicker/types.d.ts +6 -0
- package/dist/cjs/comps/DatePicker/types.js +1 -0
- package/dist/cjs/comps/Icon/index.d.ts +2 -0
- package/dist/cjs/comps/Icon/index.js +5 -2
- package/dist/cjs/comps/List/index.d.ts +1 -1
- package/dist/cjs/comps/Select/optionItem.d.ts +2 -1
- package/dist/cjs/comps/Select/optionItem.js +3 -2
- package/dist/cjs/comps/Select/types.d.ts +2 -0
- package/dist/cjs/comps/Table/index.js +2 -2
- package/dist/cjs/comps/Table/row.js +2 -2
- package/dist/cjs/comps/Table/types.d.ts +2 -0
- package/dist/cjs/comps/TextArea/commands.d.ts +12 -0
- package/dist/cjs/comps/TextArea/commands.js +16 -0
- package/dist/cjs/comps/TextArea/index.d.ts +13 -6
- package/dist/cjs/comps/TextArea/index.js +88 -6
- package/dist/cjs/comps/TextArea/types.d.ts +21 -0
- package/dist/cjs/comps/TextArea/types.js +1 -0
- package/dist/cjs/comps/index.d.ts +5 -1
- package/dist/cjs/comps/index.js +3 -0
- package/dist/cjs/comps/svgicons.d.ts +1 -0
- package/dist/cjs/comps/svgicons.js +1 -0
- package/dist/cjs/funs/index.d.ts +5 -0
- package/dist/cjs/funs/index.js +42 -0
- package/dist/cjs/funs/stylesheet.js +1 -0
- package/dist/cjs/hooks/index.d.ts +2 -0
- package/dist/cjs/hooks/index.js +2 -0
- package/dist/cjs/hooks/useCommandActions.d.ts +29 -0
- package/dist/cjs/hooks/useCommandActions.js +104 -0
- package/dist/cjs/hooks/usePosition.d.ts +2 -1
- package/dist/cjs/hooks/usePushNotifications.d.ts +45 -0
- package/dist/cjs/hooks/usePushNotifications.js +166 -0
- package/dist/cjs/hooks/useSlider.d.ts +1 -1
- package/dist/css/styles.css +1 -1
- package/dist/esm/comps/Calendar/index.d.ts +3 -0
- package/dist/esm/comps/Calendar/index.js +109 -0
- package/dist/esm/comps/Calendar/types.d.ts +6 -0
- package/dist/esm/comps/Calendar/types.js +1 -0
- package/dist/esm/comps/Crumb/index.d.ts +1 -1
- package/dist/esm/comps/DatePicker/index.d.ts +11 -0
- package/dist/esm/comps/DatePicker/index.js +73 -0
- package/dist/esm/comps/DatePicker/types.d.ts +6 -0
- package/dist/esm/comps/DatePicker/types.js +1 -0
- package/dist/esm/comps/Icon/index.d.ts +2 -0
- package/dist/esm/comps/Icon/index.js +5 -2
- package/dist/esm/comps/List/index.d.ts +1 -1
- package/dist/esm/comps/Select/optionItem.d.ts +2 -1
- package/dist/esm/comps/Select/optionItem.js +3 -2
- package/dist/esm/comps/Select/types.d.ts +2 -0
- package/dist/esm/comps/Table/index.js +2 -2
- package/dist/esm/comps/Table/row.js +2 -2
- package/dist/esm/comps/Table/types.d.ts +2 -0
- package/dist/esm/comps/TextArea/commands.d.ts +12 -0
- package/dist/esm/comps/TextArea/commands.js +16 -0
- package/dist/esm/comps/TextArea/index.d.ts +13 -6
- package/dist/esm/comps/TextArea/index.js +88 -6
- package/dist/esm/comps/TextArea/types.d.ts +21 -0
- package/dist/esm/comps/TextArea/types.js +1 -0
- package/dist/esm/comps/index.d.ts +5 -1
- package/dist/esm/comps/index.js +3 -0
- package/dist/esm/comps/svgicons.d.ts +1 -0
- package/dist/esm/comps/svgicons.js +1 -0
- package/dist/esm/funs/index.d.ts +5 -0
- package/dist/esm/funs/index.js +42 -0
- package/dist/esm/funs/stylesheet.js +1 -0
- package/dist/esm/hooks/index.d.ts +2 -0
- package/dist/esm/hooks/index.js +2 -0
- package/dist/esm/hooks/useCommandActions.d.ts +29 -0
- package/dist/esm/hooks/useCommandActions.js +104 -0
- package/dist/esm/hooks/usePosition.d.ts +2 -1
- package/dist/esm/hooks/usePushNotifications.d.ts +45 -0
- package/dist/esm/hooks/usePushNotifications.js +166 -0
- package/dist/esm/hooks/useSlider.d.ts +1 -1
- package/dist/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -1
package/dist/cjs/funs/index.js
CHANGED
|
@@ -502,3 +502,45 @@ export const checkPasswordStrength = (password) => {
|
|
|
502
502
|
suggestion: suggestions
|
|
503
503
|
};
|
|
504
504
|
};
|
|
505
|
+
export const getCaretCoordinates = (element, position) => {
|
|
506
|
+
const div = document.createElement('div');
|
|
507
|
+
const style = window.getComputedStyle(element);
|
|
508
|
+
const properties = [
|
|
509
|
+
'fontFamily', 'fontSize', 'fontWeight', 'letterSpacing',
|
|
510
|
+
'lineHeight', 'paddingLeft', 'paddingTop', 'borderLeftWidth',
|
|
511
|
+
'borderTopWidth', 'boxSizing', 'width', 'height', 'overflow'
|
|
512
|
+
];
|
|
513
|
+
// Copy styles to temporary div
|
|
514
|
+
properties.forEach(prop => {
|
|
515
|
+
div.style.setProperty(prop, style.getPropertyValue(prop));
|
|
516
|
+
});
|
|
517
|
+
div.style.position = 'absolute';
|
|
518
|
+
div.style.visibility = 'hidden';
|
|
519
|
+
div.style.whiteSpace = 'pre-wrap';
|
|
520
|
+
div.style.wordWrap = 'break-word';
|
|
521
|
+
div.textContent = element.value.substring(0, position);
|
|
522
|
+
// Append a span to measure caret position
|
|
523
|
+
const span = document.createElement('span');
|
|
524
|
+
span.textContent = element.value.substring(position) || ' ';
|
|
525
|
+
div.appendChild(span);
|
|
526
|
+
document.body.appendChild(div);
|
|
527
|
+
// Get coordinates
|
|
528
|
+
const rect = span.getBoundingClientRect();
|
|
529
|
+
const textareaRect = element.getBoundingClientRect();
|
|
530
|
+
const coordinates = {
|
|
531
|
+
top: rect.top - textareaRect.top + element.scrollTop,
|
|
532
|
+
left: rect.left - textareaRect.left + element.scrollLeft,
|
|
533
|
+
};
|
|
534
|
+
document.body.removeChild(div);
|
|
535
|
+
return coordinates;
|
|
536
|
+
};
|
|
537
|
+
export const urlBase64ToUint8Array = (base64String) => {
|
|
538
|
+
const padding = '='.repeat((4 - base64String.length % 4) % 4);
|
|
539
|
+
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
|
|
540
|
+
const rawData = window.atob(base64);
|
|
541
|
+
const outputArray = new Uint8Array(rawData.length);
|
|
542
|
+
for (let i = 0; i < rawData.length; ++i) {
|
|
543
|
+
outputArray[i] = rawData.charCodeAt(i);
|
|
544
|
+
}
|
|
545
|
+
return outputArray;
|
|
546
|
+
};
|
|
@@ -2,6 +2,7 @@ export { default as useAnchorPosition } from './useAnchorPosition';
|
|
|
2
2
|
export { default as useBase } from './useBase';
|
|
3
3
|
export { default as useCalendar } from './useCalendar';
|
|
4
4
|
export { useColorScheme } from './useColorScheme';
|
|
5
|
+
export { default as useCommandActions, type Command, type CommandActionProps } from './useCommandActions';
|
|
5
6
|
export { default as useContextMenu } from './useContextMenu';
|
|
6
7
|
export { default as useDB, type IDBOptions, type IDBSchema } from './useDB';
|
|
7
8
|
export { default as useDebounce } from './useDebounce';
|
|
@@ -28,6 +29,7 @@ export { default as useSheet } from './useSheet';
|
|
|
28
29
|
export { default as useShortcuts } from './useShortcuts';
|
|
29
30
|
export { default as useNetworkStatus } from './useNetworkStatus';
|
|
30
31
|
export { default as usePosition } from './usePosition';
|
|
32
|
+
export { default as usePushNotifications, type PushNotificationsOptions, type PushNotificationsResult, type PushSubscriptionMeta } from './usePushNotifications';
|
|
31
33
|
export { default as useResizeObserver } from './useResizeObserver';
|
|
32
34
|
export { default as useSlider } from './useSlider';
|
|
33
35
|
export { default as useToast } from './useToast';
|
package/dist/cjs/hooks/index.js
CHANGED
|
@@ -2,6 +2,7 @@ export { default as useAnchorPosition } from './useAnchorPosition';
|
|
|
2
2
|
export { default as useBase } from './useBase';
|
|
3
3
|
export { default as useCalendar } from './useCalendar';
|
|
4
4
|
export { useColorScheme } from './useColorScheme';
|
|
5
|
+
export { default as useCommandActions } from './useCommandActions';
|
|
5
6
|
export { default as useContextMenu } from './useContextMenu';
|
|
6
7
|
export { default as useDB } from './useDB';
|
|
7
8
|
export { default as useDebounce } from './useDebounce';
|
|
@@ -29,6 +30,7 @@ export { default as useSheet } from './useSheet';
|
|
|
29
30
|
export { default as useShortcuts } from './useShortcuts';
|
|
30
31
|
export { default as useNetworkStatus } from './useNetworkStatus';
|
|
31
32
|
export { default as usePosition } from './usePosition';
|
|
33
|
+
export { default as usePushNotifications } from './usePushNotifications';
|
|
32
34
|
export { default as useResizeObserver } from './useResizeObserver';
|
|
33
35
|
export { default as useSlider } from './useSlider';
|
|
34
36
|
export { default as useToast } from './useToast';
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { RefObject } from "react";
|
|
2
|
+
export type Command = {
|
|
3
|
+
label: string;
|
|
4
|
+
value: string;
|
|
5
|
+
icon?: string;
|
|
6
|
+
type?: 'command' | 'submenu' | 'action';
|
|
7
|
+
subCommands?: Command[];
|
|
8
|
+
action?: React.ReactNode | ((props: {
|
|
9
|
+
onSelect: (value: string) => void;
|
|
10
|
+
}) => React.ReactNode);
|
|
11
|
+
};
|
|
12
|
+
export type CommandActionProps = {
|
|
13
|
+
command?: string;
|
|
14
|
+
commands?: Command[];
|
|
15
|
+
cmd?: (value: string, textarea: HTMLTextAreaElement | HTMLInputElement) => void;
|
|
16
|
+
ref: RefObject<HTMLTextAreaElement | HTMLInputElement | null>;
|
|
17
|
+
};
|
|
18
|
+
declare const useCommandActions: ({ command, commands, cmd, ref, }: CommandActionProps) => {
|
|
19
|
+
showDropdown: boolean;
|
|
20
|
+
dropdownPosition: {
|
|
21
|
+
top: number;
|
|
22
|
+
left: number;
|
|
23
|
+
};
|
|
24
|
+
handleKeyDown: (event: React.KeyboardEvent<HTMLTextAreaElement | HTMLInputElement>) => void;
|
|
25
|
+
handleInput: (event: React.FormEvent<HTMLTextAreaElement | HTMLInputElement>) => void;
|
|
26
|
+
handleCommandSelect: (value: string) => void;
|
|
27
|
+
parentRef: RefObject<HTMLDivElement | null>;
|
|
28
|
+
};
|
|
29
|
+
export default useCommandActions;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { useRef } from "react";
|
|
4
|
+
const useCommandActions = ({ command = '/', commands = [], cmd, ref, }) => {
|
|
5
|
+
const [commandStart, setCommandStart] = useState(-1);
|
|
6
|
+
const [showDropdown, setShowDropdown] = useState(false);
|
|
7
|
+
const [dropdownPosition, setDropdownPosition] = useState({ top: 0, left: 0 });
|
|
8
|
+
const parentRef = useRef(null);
|
|
9
|
+
const handleInput = (event) => {
|
|
10
|
+
const textarea = ref.current;
|
|
11
|
+
if (textarea && showDropdown) {
|
|
12
|
+
const { value } = textarea;
|
|
13
|
+
if (commandStart < 0 || commandStart >= value.length || value[commandStart] !== command) {
|
|
14
|
+
setShowDropdown(false);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
const handleKeyDown = (event) => {
|
|
19
|
+
const textarea = ref.current;
|
|
20
|
+
if (!textarea)
|
|
21
|
+
return;
|
|
22
|
+
const { selectionStart } = textarea;
|
|
23
|
+
if (event.key === command) {
|
|
24
|
+
setCommandStart(selectionStart || -1);
|
|
25
|
+
const caretPos = getCaretCoordinates(textarea, selectionStart || -1);
|
|
26
|
+
setDropdownPosition({
|
|
27
|
+
top: caretPos.top + 20,
|
|
28
|
+
left: caretPos.left,
|
|
29
|
+
});
|
|
30
|
+
setShowDropdown(true);
|
|
31
|
+
}
|
|
32
|
+
else if (event.key === 'Escape') {
|
|
33
|
+
event.preventDefault();
|
|
34
|
+
event.stopPropagation();
|
|
35
|
+
setShowDropdown(false);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
const handleCommandSelect = (value) => {
|
|
39
|
+
const textarea = ref.current;
|
|
40
|
+
if (!textarea)
|
|
41
|
+
return;
|
|
42
|
+
if (cmd) {
|
|
43
|
+
cmd(value, textarea);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
const { value: currentValue, selectionStart } = textarea;
|
|
47
|
+
const newValue = currentValue.slice(0, commandStart) +
|
|
48
|
+
value +
|
|
49
|
+
currentValue.slice(selectionStart || -1);
|
|
50
|
+
textarea.value = newValue;
|
|
51
|
+
textarea.setSelectionRange(commandStart + value.length, commandStart + value.length);
|
|
52
|
+
const event = new Event('input', { bubbles: true });
|
|
53
|
+
textarea.dispatchEvent(event);
|
|
54
|
+
}
|
|
55
|
+
setShowDropdown(false);
|
|
56
|
+
textarea.focus();
|
|
57
|
+
};
|
|
58
|
+
const getCaretCoordinates = (element, position) => {
|
|
59
|
+
const canvas = document.createElement('canvas');
|
|
60
|
+
const context = canvas.getContext('2d');
|
|
61
|
+
if (!context) {
|
|
62
|
+
return { top: 0, left: 0 };
|
|
63
|
+
}
|
|
64
|
+
const style = window.getComputedStyle(element);
|
|
65
|
+
context.font = `${style.fontSize} ${style.fontFamily}`;
|
|
66
|
+
const text = element.value.substring(0, position);
|
|
67
|
+
const lines = text.split('\n');
|
|
68
|
+
const lastLine = lines[lines.length - 1];
|
|
69
|
+
const left = context.measureText(lastLine).width;
|
|
70
|
+
const lineHeight = parseFloat(style.lineHeight) || parseFloat(style.fontSize) * 1.2;
|
|
71
|
+
const top = (lines.length - 1) * lineHeight;
|
|
72
|
+
const paddingTop = parseFloat(style.paddingTop) || 0;
|
|
73
|
+
const paddingLeft = parseFloat(style.paddingLeft) || 0;
|
|
74
|
+
const scrollTop = element.scrollTop;
|
|
75
|
+
canvas.remove();
|
|
76
|
+
return {
|
|
77
|
+
top: top - scrollTop + paddingTop,
|
|
78
|
+
left: left + paddingLeft,
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
const handleClickOutside = (event) => {
|
|
83
|
+
if (ref.current &&
|
|
84
|
+
parentRef.current &&
|
|
85
|
+
!ref.current.contains(event.target) &&
|
|
86
|
+
!parentRef.current.contains(event.target)) {
|
|
87
|
+
setShowDropdown(false);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
91
|
+
return () => {
|
|
92
|
+
document.removeEventListener('mousedown', handleClickOutside);
|
|
93
|
+
};
|
|
94
|
+
}, [ref]);
|
|
95
|
+
return {
|
|
96
|
+
showDropdown,
|
|
97
|
+
dropdownPosition,
|
|
98
|
+
handleKeyDown,
|
|
99
|
+
handleInput,
|
|
100
|
+
handleCommandSelect,
|
|
101
|
+
parentRef,
|
|
102
|
+
};
|
|
103
|
+
};
|
|
104
|
+
export default useCommandActions;
|
|
@@ -5,7 +5,8 @@ interface PositionOptions {
|
|
|
5
5
|
container?: HTMLElement | null;
|
|
6
6
|
triggerRef?: React.RefObject<HTMLElement>;
|
|
7
7
|
}
|
|
8
|
-
export declare const usePosition: (ref: React.RefObject<HTMLElement>,
|
|
8
|
+
export declare const usePosition: (ref: React.RefObject<HTMLElement>, // The element to be positioned
|
|
9
|
+
options?: PositionOptions) => {
|
|
9
10
|
postion: "fixed" | "absolute" | null;
|
|
10
11
|
reposition: () => void;
|
|
11
12
|
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export type PushSubscriptionMeta = {
|
|
2
|
+
endpoint: string;
|
|
3
|
+
keys: {
|
|
4
|
+
p256dh: string;
|
|
5
|
+
auth: string;
|
|
6
|
+
};
|
|
7
|
+
};
|
|
8
|
+
export type PushNotificationsOptions = {
|
|
9
|
+
/**
|
|
10
|
+
* VAPID public key (required for subscription)
|
|
11
|
+
*/
|
|
12
|
+
vapidPublicKey: string;
|
|
13
|
+
/**
|
|
14
|
+
* Path to your service worker file
|
|
15
|
+
* @default '/sw.js'
|
|
16
|
+
*/
|
|
17
|
+
serviceWorkerPath?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Auto-request permission on mount
|
|
20
|
+
* @default false
|
|
21
|
+
*/
|
|
22
|
+
requestPermissionOnMount?: boolean;
|
|
23
|
+
};
|
|
24
|
+
export type PushNotificationsResult = {
|
|
25
|
+
/** Current permission state */
|
|
26
|
+
permission: NotificationPermission;
|
|
27
|
+
/** Push subscription object (or null if not subscribed) */
|
|
28
|
+
subscription: PushSubscription | null;
|
|
29
|
+
/** JSON representation of subscription (easy to send to backend) */
|
|
30
|
+
subscriptionMeta: PushSubscriptionMeta | null;
|
|
31
|
+
/** Is push supported in this browser? */
|
|
32
|
+
isSupported: boolean;
|
|
33
|
+
/** Request permission and subscribe */
|
|
34
|
+
subscribe: () => Promise<PushSubscription | null>;
|
|
35
|
+
/** Unsubscribe from push */
|
|
36
|
+
unsubscribe: () => Promise<boolean>;
|
|
37
|
+
/** Request notification permission only */
|
|
38
|
+
requestPermission: () => Promise<NotificationPermission>;
|
|
39
|
+
/** Error state */
|
|
40
|
+
error: string | null;
|
|
41
|
+
/** Loading state */
|
|
42
|
+
isLoading: boolean;
|
|
43
|
+
};
|
|
44
|
+
declare const usePushNotifications: (options: PushNotificationsOptions) => PushNotificationsResult;
|
|
45
|
+
export default usePushNotifications;
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from "react";
|
|
2
|
+
import { urlBase64ToUint8Array } from "../funs";
|
|
3
|
+
const usePushNotifications = (options) => {
|
|
4
|
+
const { vapidPublicKey, serviceWorkerPath = '/sw.js', requestPermissionOnMount = false, } = options;
|
|
5
|
+
const [permission, setPermission] = useState('default');
|
|
6
|
+
const [subscription, setSubscription] = useState(null);
|
|
7
|
+
const [subscriptionMeta, setSubscriptionMeta] = useState(null);
|
|
8
|
+
const [error, setError] = useState(null);
|
|
9
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
10
|
+
const [registration, setRegistration] = useState(null);
|
|
11
|
+
const [isSupported, setIsSupported] = useState(false);
|
|
12
|
+
// Register service worker
|
|
13
|
+
const registerServiceWorker = useCallback(async () => {
|
|
14
|
+
if (!isSupported)
|
|
15
|
+
return null;
|
|
16
|
+
try {
|
|
17
|
+
const reg = await navigator.serviceWorker.register(serviceWorkerPath, {
|
|
18
|
+
scope: '/',
|
|
19
|
+
updateViaCache: 'all',
|
|
20
|
+
});
|
|
21
|
+
setRegistration(reg);
|
|
22
|
+
return reg;
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
console.error('Service Worker registration failed:', err);
|
|
26
|
+
setError('Failed to register service worker');
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}, [serviceWorkerPath, isSupported]);
|
|
30
|
+
// Get current subscription
|
|
31
|
+
const getCurrentSubscription = useCallback(async (reg) => {
|
|
32
|
+
try {
|
|
33
|
+
const sub = await reg.pushManager.getSubscription();
|
|
34
|
+
setSubscription(sub);
|
|
35
|
+
if (sub) {
|
|
36
|
+
setSubscriptionMeta(sub.toJSON());
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
setSubscriptionMeta(null);
|
|
40
|
+
}
|
|
41
|
+
return sub;
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
console.error('Failed to get subscription:', err);
|
|
45
|
+
setError('Failed to get subscription');
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}, []);
|
|
49
|
+
// Subscribe to push
|
|
50
|
+
const subscribe = useCallback(async () => {
|
|
51
|
+
if (!isSupported || !registration) {
|
|
52
|
+
setError('Push not supported or service worker not registered');
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
setIsLoading(true);
|
|
56
|
+
setError(null);
|
|
57
|
+
try {
|
|
58
|
+
const permissionResult = await Notification.requestPermission();
|
|
59
|
+
setPermission(permissionResult);
|
|
60
|
+
if (permissionResult !== 'granted') {
|
|
61
|
+
setError('Permission not granted for notifications');
|
|
62
|
+
setIsLoading(false);
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
const applicationServerKey = urlBase64ToUint8Array(vapidPublicKey);
|
|
66
|
+
const pushSubscription = await registration.pushManager.subscribe({
|
|
67
|
+
userVisibleOnly: true,
|
|
68
|
+
applicationServerKey,
|
|
69
|
+
});
|
|
70
|
+
setSubscription(pushSubscription);
|
|
71
|
+
setSubscriptionMeta(pushSubscription.toJSON());
|
|
72
|
+
setIsLoading(false);
|
|
73
|
+
return pushSubscription;
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
console.error('Subscription failed:', err);
|
|
77
|
+
setError(err.message || 'Failed to subscribe');
|
|
78
|
+
setIsLoading(false);
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}, [isSupported, registration, vapidPublicKey]);
|
|
82
|
+
// Unsubscribe
|
|
83
|
+
const unsubscribe = useCallback(async () => {
|
|
84
|
+
if (!subscription)
|
|
85
|
+
return true;
|
|
86
|
+
try {
|
|
87
|
+
const result = await subscription.unsubscribe();
|
|
88
|
+
if (result) {
|
|
89
|
+
setSubscription(null);
|
|
90
|
+
setSubscriptionMeta(null);
|
|
91
|
+
}
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
console.error('Failed to unsubscribe:', err);
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}, [subscription]);
|
|
99
|
+
// Request permission only
|
|
100
|
+
const requestPermission = useCallback(async () => {
|
|
101
|
+
const result = await Notification.requestPermission();
|
|
102
|
+
setPermission(result);
|
|
103
|
+
return result;
|
|
104
|
+
}, []);
|
|
105
|
+
// Initialize
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
const _isSupported = 'PushManager' in window && 'serviceWorker' in navigator;
|
|
108
|
+
setIsSupported(_isSupported);
|
|
109
|
+
if (!_isSupported) {
|
|
110
|
+
setError('Push notifications not supported in this browser');
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
let isMounted = true;
|
|
114
|
+
const init = async () => {
|
|
115
|
+
const reg = await registerServiceWorker();
|
|
116
|
+
if (!reg || !isMounted)
|
|
117
|
+
return;
|
|
118
|
+
setPermission(Notification.permission);
|
|
119
|
+
await getCurrentSubscription(reg);
|
|
120
|
+
if (requestPermissionOnMount && Notification.permission === 'default') {
|
|
121
|
+
await requestPermission();
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
init();
|
|
125
|
+
return () => {
|
|
126
|
+
isMounted = false;
|
|
127
|
+
};
|
|
128
|
+
}, [
|
|
129
|
+
isSupported,
|
|
130
|
+
registerServiceWorker,
|
|
131
|
+
getCurrentSubscription,
|
|
132
|
+
requestPermissionOnMount,
|
|
133
|
+
requestPermission,
|
|
134
|
+
]);
|
|
135
|
+
// Listen for permission changes
|
|
136
|
+
useEffect(() => {
|
|
137
|
+
if (!('permissions' in navigator))
|
|
138
|
+
return;
|
|
139
|
+
let revoked = false;
|
|
140
|
+
const checkPermission = async () => {
|
|
141
|
+
if (revoked)
|
|
142
|
+
return;
|
|
143
|
+
const perm = await navigator.permissions.query({ name: 'notifications' });
|
|
144
|
+
perm.onchange = () => {
|
|
145
|
+
setPermission(Notification.permission);
|
|
146
|
+
if (Notification.permission === 'denied') {
|
|
147
|
+
revoked = true;
|
|
148
|
+
unsubscribe();
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
};
|
|
152
|
+
checkPermission();
|
|
153
|
+
}, [unsubscribe]);
|
|
154
|
+
return {
|
|
155
|
+
permission,
|
|
156
|
+
subscription,
|
|
157
|
+
subscriptionMeta,
|
|
158
|
+
isSupported,
|
|
159
|
+
subscribe,
|
|
160
|
+
unsubscribe,
|
|
161
|
+
requestPermission,
|
|
162
|
+
error,
|
|
163
|
+
isLoading,
|
|
164
|
+
};
|
|
165
|
+
};
|
|
166
|
+
export default usePushNotifications;
|