next-unsaved-changes-guard 1.0.0

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 ADDED
@@ -0,0 +1,133 @@
1
+ # Next Unsaved Changes Guard
2
+
3
+ A lightweight, customizable React component for Next.js 13+ (App Router) that prevents users from accidentally leaving a page with unsaved changes.
4
+
5
+ ## Features
6
+
7
+ - 🛡️ **Route Guard**: Intercepts navigation attempts when determining that a form is dirty.
8
+ - 🔄 **Browser Event Handling**: Warns users when closing the tab or refreshing the window.
9
+ - 🎨 **Fully Customizable**: Override default styles easily using stable CSS class names.
10
+ - 🧩 **Next.js App Router Support**: Built specifically for `next/navigation`.
11
+
12
+ ## Compatibility
13
+
14
+ | Package | Version |
15
+ | ------- | ------------------------------- |
16
+ | Next.js | `13.0.0` or higher (App Router) |
17
+ | React | `18.0.0` or higher |
18
+
19
+ > [!NOTE]
20
+ > This package utilizes `next/navigation` which is exclusive to the **Next.js App Router**. It is **not** compatible with the legacy Pages Router (`pages/` directory).
21
+
22
+ ## Installation
23
+
24
+ ```bash
25
+ npm install next-unsaved-changes-guard
26
+ # or
27
+ yarn add next-unsaved-changes-guard
28
+ # or
29
+ pnpm add next-unsaved-changes-guard
30
+ ```
31
+
32
+ ## Usage
33
+
34
+ Import the component and wrap your form or page content. Pass the `formDirty` state to control when the guard should be active.
35
+
36
+ ```tsx
37
+ "use client";
38
+
39
+ import { useEffect, useRef, useState } from "react";
40
+ import { NextUnsavedChangesGuard } from "next-unsaved-changes-guard";
41
+
42
+ export default function MyFormPage() {
43
+ const [isDirty, setIsDirty] = useState(false);
44
+ const [errors, setErrors] = useState({});
45
+ const submitBtnRef = useRef<HTMLButtonElement>(null);
46
+
47
+ // Example save function
48
+ const handleSave = async () => {
49
+ // Perform API call or form submission
50
+ console.log("Saving data...");
51
+ await new Promise((resolve) => setTimeout(resolve, 1000));
52
+ };
53
+
54
+ return (
55
+ <NextUnsavedChangesGuard
56
+ formDirty={isDirty}
57
+ formErrors={errors}
58
+ saveData={() => submitBtnRef.current?.click()}
59
+ >
60
+ <form
61
+ onChange={() => setIsDirty(true)}
62
+ onSubmit={(e) => {
63
+ e.preventDefault();
64
+ handleSave();
65
+ }}
66
+ >
67
+ <h1>Edit Profile</h1>
68
+ <input type="text" placeholder="Name" />
69
+
70
+ {/* Hidden submit button triggered by the Guard's "Save" action */}
71
+ <button ref={submitBtnRef} type="submit" style={{ display: "none" }} />
72
+ </form>
73
+ </NextUnsavedChangesGuard>
74
+ );
75
+ }
76
+ ```
77
+
78
+ ### Props
79
+
80
+ | Prop | Type | Default | Description |
81
+ | ------------ | --------------------- | ------------ | ---------------------------------------------------------------------------------------------------------- |
82
+ | `children` | `ReactNode` | **Required** | The content to be rendered (usually your page or form). |
83
+ | `formDirty` | `boolean` | **Required** | If `true`, navigation attempts will trigger the confirmation dialog. |
84
+ | `saveData` | `() => Promise<void>` | **Required** | Function called when the user clicks "Save" in the dialog. |
85
+ | `formErrors` | `Record<string, any>` | `{}` | If provided and not empty, the dialog will prevent navigation on "Save" attempt until errors are resolved. |
86
+
87
+ ## Styling
88
+
89
+ The component injects default styles, but every element has a stable CSS class name that you can target in your global CSS or CSS modules to override the look and feel.
90
+
91
+ ### CSS Classes
92
+
93
+ | Class Name | Description |
94
+ | ---------------------- | -------------------------------------------------- |
95
+ | `.nucg-ad-overlay` | The semi-transparent backdrop covering the screen. |
96
+ | `.nucg-ad-dialog` | The main modal container. |
97
+ | `.nucg-ad-title` | The modal header title ("Unsaved Changes"). |
98
+ | `.nucg-ad-description` | The warning message text. |
99
+ | `.nucg-ad-footer` | Container for the action buttons. |
100
+ | `.nucg-ad-btn` | Base style shared by all buttons. |
101
+ | `.nucg-ad-cancel` | The "Stay" button (Secondary). |
102
+ | `.nucg-ad-danger` | The "Discard" button (Destructive). |
103
+ | `.nucg-ad-primary` | The "Save" button (Primary). |
104
+
105
+ ### Example Customization
106
+
107
+ Add this to your `globals.css` to match your brand colors:
108
+
109
+ ```css
110
+ /* Override the Save button color */
111
+ .nucg-ad-primary {
112
+ background-color: #3b82f6 !important; /* Tailwind Blue-500 */
113
+ }
114
+ .nucg-ad-primary:hover {
115
+ background-color: #2563eb !important; /* Tailwind Blue-600 */
116
+ }
117
+
118
+ /* Customize the modal radius and shadow */
119
+ .nucg-ad-dialog {
120
+ border-radius: 16px !important;
121
+ box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1) !important;
122
+ }
123
+
124
+ /* Change the backdrop color */
125
+ .nucg-ad-overlay {
126
+ background: rgba(0, 0, 0, 0.7) !important;
127
+ backdrop-filter: blur(4px);
128
+ }
129
+ ```
130
+
131
+ ## License
132
+
133
+ MIT © Shibu Dhara
package/dist/index.cjs ADDED
@@ -0,0 +1,245 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.tsx
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ NextUnsavedChangesGuard: () => NextUnsavedChangesGuard
24
+ });
25
+ module.exports = __toCommonJS(index_exports);
26
+
27
+ // src/NextUnsavedChangesGuard.tsx
28
+ var import_react = require("react");
29
+ var import_navigation = require("next/navigation");
30
+ var import_jsx_runtime = require("react/jsx-runtime");
31
+ var alertDialogCSS = `
32
+ .nucg-ad-overlay {
33
+ position: fixed;
34
+ inset: 0;
35
+ background: rgba(0,0,0,0.45);
36
+ backdrop-filter: blur(2px);
37
+ display: flex;
38
+ align-items: center;
39
+ justify-content: center;
40
+ z-index: 50;
41
+ animation: ad-fade-in .15s ease-out;
42
+ }
43
+
44
+ .nucg-ad-dialog {
45
+ width: 100%;
46
+ max-width: 420px;
47
+ background: #ffffff;
48
+ border-radius: 12px;
49
+ padding: 24px;
50
+ box-shadow:
51
+ 0 10px 30px rgba(0,0,0,0.15),
52
+ 0 1px 2px rgba(0,0,0,0.1);
53
+ animation: ad-scale-in .15s ease-out;
54
+ }
55
+
56
+ .nucg-ad-title {
57
+ font-size: 1.125rem;
58
+ font-weight: 600;
59
+ color: #111827;
60
+ margin-bottom: 6px;
61
+ }
62
+
63
+ .nucg-ad-description {
64
+ font-size: 0.95rem;
65
+ color: #6b7280;
66
+ line-height: 1.45;
67
+ }
68
+
69
+ .nucg-ad-footer {
70
+ display: flex;
71
+ justify-content: flex-end;
72
+ gap: 10px;
73
+ margin-top: 20px;
74
+ }
75
+
76
+ .nucg-ad-btn {
77
+ padding: 8px 14px;
78
+ border-radius: 8px;
79
+ font-size: 0.875rem;
80
+ border: none;
81
+ cursor: pointer;
82
+ transition: background .15s ease, transform .05s ease;
83
+ }
84
+
85
+ .nucg-ad-btn:active {
86
+ transform: scale(0.97);
87
+ }
88
+
89
+ .nucg-ad-cancel {
90
+ background: #f3f4f6;
91
+ color: #111827;
92
+ }
93
+ .nucg-ad-cancel:hover {
94
+ background: #e5e7eb;
95
+ }
96
+
97
+ .nucg-ad-danger {
98
+ background: #fee2e2;
99
+ color: #b91c1c;
100
+ }
101
+ .nucg-ad-danger:hover {
102
+ background: #fecaca;
103
+ }
104
+
105
+ .nucg-ad-primary {
106
+ background: #111827;
107
+ color: #ffffff;
108
+ }
109
+ .nucg-ad-primary:hover {
110
+ background: #1f2937;
111
+ }
112
+
113
+ @keyframes nucg-ad-fade-in {
114
+ from { opacity: 0; }
115
+ to { opacity: 1; }
116
+ }
117
+
118
+ @keyframes nucg-ad-scale-in {
119
+ from { transform: scale(.96); opacity: 0; }
120
+ to { transform: scale(1); opacity: 1; }
121
+ }
122
+ `;
123
+ var NextUnsavedChangesGuard = ({
124
+ children,
125
+ formDirty,
126
+ saveData,
127
+ formErrors = {}
128
+ }) => {
129
+ const [isDirty, setDirty] = (0, import_react.useState)(formDirty);
130
+ const [isDialogOpen, setIsDialogOpen] = (0, import_react.useState)(false);
131
+ const [nextRoute, setNextRoute] = (0, import_react.useState)(null);
132
+ const router = (0, import_navigation.useRouter)();
133
+ const isRouteChanging = (0, import_react.useRef)(false);
134
+ (0, import_react.useEffect)(() => {
135
+ setDirty(formDirty);
136
+ }, [formDirty]);
137
+ (0, import_react.useEffect)(() => {
138
+ const handleWindowClose = (e) => {
139
+ if (!isDirty) return;
140
+ e.preventDefault();
141
+ e.returnValue = "You have unsaved changes. Are you sure you want to leave?";
142
+ };
143
+ window.addEventListener("beforeunload", handleWindowClose);
144
+ return () => {
145
+ window.removeEventListener("beforeunload", handleWindowClose);
146
+ };
147
+ }, [isDirty]);
148
+ (0, import_react.useEffect)(() => {
149
+ const originalPush = router.push;
150
+ router.push = (url) => {
151
+ if (isDirty && !isRouteChanging.current) {
152
+ setNextRoute(url);
153
+ setIsDialogOpen(true);
154
+ return Promise.resolve();
155
+ }
156
+ isRouteChanging.current = false;
157
+ return originalPush(url);
158
+ };
159
+ return () => {
160
+ router.push = originalPush;
161
+ };
162
+ }, [isDirty, router]);
163
+ const handleSave = async () => {
164
+ if (Object.keys(formErrors).length === 0) {
165
+ try {
166
+ await saveData();
167
+ setDirty(false);
168
+ setIsDialogOpen(false);
169
+ if (nextRoute) {
170
+ isRouteChanging.current = true;
171
+ router.push(nextRoute);
172
+ }
173
+ } catch (error) {
174
+ console.log("Error saving changes");
175
+ }
176
+ } else {
177
+ setIsDialogOpen(false);
178
+ console.log("Form contains errors");
179
+ }
180
+ };
181
+ const handleDiscard = () => {
182
+ setDirty(false);
183
+ setIsDialogOpen(false);
184
+ if (nextRoute) {
185
+ isRouteChanging.current = true;
186
+ router.push(nextRoute);
187
+ }
188
+ };
189
+ const handleStay = () => {
190
+ setIsDialogOpen(false);
191
+ setNextRoute(null);
192
+ };
193
+ function injectAlertDialogStyles() {
194
+ if (typeof document === "undefined") return;
195
+ if (!document.getElementById("alert-dialog-styles")) {
196
+ const style = document.createElement("style");
197
+ style.id = "alert-dialog-styles";
198
+ style.textContent = alertDialogCSS;
199
+ document.head.appendChild(style);
200
+ }
201
+ }
202
+ (0, import_react.useEffect)(() => {
203
+ injectAlertDialogStyles();
204
+ }, []);
205
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
206
+ children,
207
+ isDialogOpen && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "nucg-ad-overlay", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "nucg-ad-dialog", role: "alertdialog", "aria-modal": "true", children: [
208
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
209
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { className: "nucg-ad-title", children: "Unsaved Changes" }),
210
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "nucg-ad-description", children: "You have unsaved changes. Do you want to save them before leaving?" })
211
+ ] }),
212
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "nucg-ad-footer", children: [
213
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
214
+ "button",
215
+ {
216
+ className: "nucg-ad-btn nucg-ad-cancel",
217
+ onClick: handleStay,
218
+ children: "Stay"
219
+ }
220
+ ),
221
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
222
+ "button",
223
+ {
224
+ className: "nucg-ad-btn nucg-ad-danger",
225
+ onClick: handleDiscard,
226
+ children: "Discard"
227
+ }
228
+ ),
229
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
230
+ "button",
231
+ {
232
+ className: "nucg-ad-btn nucg-ad-primary",
233
+ onClick: handleSave,
234
+ children: "Save"
235
+ }
236
+ )
237
+ ] })
238
+ ] }) })
239
+ ] });
240
+ };
241
+ // Annotate the CommonJS export names for ESM import in node:
242
+ 0 && (module.exports = {
243
+ NextUnsavedChangesGuard
244
+ });
245
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.tsx","../src/NextUnsavedChangesGuard.tsx"],"sourcesContent":["export { NextUnsavedChangesGuard } from \"./NextUnsavedChangesGuard\";\n","\"use client\";\n\nimport { useState, useEffect, useRef } from \"react\";\nimport { useRouter } from \"next/navigation\";\n\nconst alertDialogCSS = `\n .nucg-ad-overlay {\n position: fixed;\n inset: 0;\n background: rgba(0,0,0,0.45);\n backdrop-filter: blur(2px);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 50;\n animation: ad-fade-in .15s ease-out;\n }\n\n .nucg-ad-dialog {\n width: 100%;\n max-width: 420px;\n background: #ffffff;\n border-radius: 12px;\n padding: 24px;\n box-shadow:\n 0 10px 30px rgba(0,0,0,0.15),\n 0 1px 2px rgba(0,0,0,0.1);\n animation: ad-scale-in .15s ease-out;\n }\n\n .nucg-ad-title {\n font-size: 1.125rem;\n font-weight: 600;\n color: #111827;\n margin-bottom: 6px;\n }\n\n .nucg-ad-description {\n font-size: 0.95rem;\n color: #6b7280;\n line-height: 1.45;\n }\n\n .nucg-ad-footer {\n display: flex;\n justify-content: flex-end;\n gap: 10px;\n margin-top: 20px;\n }\n\n .nucg-ad-btn {\n padding: 8px 14px;\n border-radius: 8px;\n font-size: 0.875rem;\n border: none;\n cursor: pointer;\n transition: background .15s ease, transform .05s ease;\n }\n\n .nucg-ad-btn:active {\n transform: scale(0.97);\n }\n\n .nucg-ad-cancel {\n background: #f3f4f6;\n color: #111827;\n }\n .nucg-ad-cancel:hover {\n background: #e5e7eb;\n }\n\n .nucg-ad-danger {\n background: #fee2e2;\n color: #b91c1c;\n }\n .nucg-ad-danger:hover {\n background: #fecaca;\n }\n\n .nucg-ad-primary {\n background: #111827;\n color: #ffffff;\n }\n .nucg-ad-primary:hover {\n background: #1f2937;\n }\n\n @keyframes nucg-ad-fade-in {\n from { opacity: 0; }\n to { opacity: 1; }\n }\n\n @keyframes nucg-ad-scale-in {\n from { transform: scale(.96); opacity: 0; }\n to { transform: scale(1); opacity: 1; }\n }\n`;\n\ninterface UnsavedChangesGuardProps {\n children: React.ReactNode;\n formDirty: boolean;\n saveData: () => Promise<void>;\n formErrors?: Record<string, any>;\n}\n\nexport const NextUnsavedChangesGuard = ({\n children,\n formDirty,\n saveData,\n formErrors = {},\n}: UnsavedChangesGuardProps) => {\n const [isDirty, setDirty] = useState(formDirty);\n const [isDialogOpen, setIsDialogOpen] = useState(false);\n const [nextRoute, setNextRoute] = useState<string | null>(null);\n const router = useRouter();\n const isRouteChanging = useRef(false);\n\n // Sync isDirty with formDirty prop\n useEffect(() => {\n setDirty(formDirty);\n }, [formDirty]);\n\n // Handle browser close/refresh\n useEffect(() => {\n const handleWindowClose = (e: BeforeUnloadEvent) => {\n if (!isDirty) return;\n e.preventDefault();\n e.returnValue =\n \"You have unsaved changes. Are you sure you want to leave?\";\n };\n\n window.addEventListener(\"beforeunload\", handleWindowClose);\n return () => {\n window.removeEventListener(\"beforeunload\", handleWindowClose);\n };\n }, [isDirty]);\n\n // Intercept route changes\n useEffect(() => {\n const originalPush = router.push;\n\n router.push = (url: string) => {\n if (isDirty && !isRouteChanging.current) {\n setNextRoute(url);\n setIsDialogOpen(true);\n return Promise.resolve(); // Prevent navigation without throwing\n }\n isRouteChanging.current = false;\n return originalPush(url);\n };\n\n return () => {\n router.push = originalPush;\n };\n }, [isDirty, router]);\n\n // Handle \"Save\" action\n const handleSave = async () => {\n if (Object.keys(formErrors).length === 0) {\n try {\n await saveData();\n setDirty(false);\n setIsDialogOpen(false);\n if (nextRoute) {\n isRouteChanging.current = true;\n router.push(nextRoute);\n }\n } catch (error) {\n console.log(\"Error saving changes\");\n }\n } else {\n setIsDialogOpen(false);\n console.log(\"Form contains errors\");\n }\n };\n\n // Handle \"Discard\" action\n const handleDiscard = () => {\n setDirty(false);\n setIsDialogOpen(false);\n if (nextRoute) {\n isRouteChanging.current = true;\n router.push(nextRoute);\n }\n };\n\n // Handle \"Stay\" action\n const handleStay = () => {\n setIsDialogOpen(false);\n setNextRoute(null);\n };\n\n function injectAlertDialogStyles() {\n if (typeof document === \"undefined\") return;\n\n if (!document.getElementById(\"alert-dialog-styles\")) {\n const style = document.createElement(\"style\");\n style.id = \"alert-dialog-styles\";\n style.textContent = alertDialogCSS;\n document.head.appendChild(style);\n }\n }\n\n useEffect(() => {\n injectAlertDialogStyles();\n }, []);\n\n return (\n <>\n {children}\n {isDialogOpen && (\n <div className=\"nucg-ad-overlay\">\n <div className=\"nucg-ad-dialog\" role=\"alertdialog\" aria-modal=\"true\">\n <div>\n <h2 className=\"nucg-ad-title\">Unsaved Changes</h2>\n <p className=\"nucg-ad-description\">\n You have unsaved changes. Do you want to save them before\n leaving?\n </p>\n </div>\n\n <div className=\"nucg-ad-footer\">\n <button\n className=\"nucg-ad-btn nucg-ad-cancel\"\n onClick={handleStay}\n >\n Stay\n </button>\n\n <button\n className=\"nucg-ad-btn nucg-ad-danger\"\n onClick={handleDiscard}\n >\n Discard\n </button>\n\n <button\n className=\"nucg-ad-btn nucg-ad-primary\"\n onClick={handleSave}\n >\n Save\n </button>\n </div>\n </div>\n </div>\n )}\n </>\n );\n};\n"],"mappings":"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","names":[]}
@@ -0,0 +1,11 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+
3
+ interface UnsavedChangesGuardProps {
4
+ children: React.ReactNode;
5
+ formDirty: boolean;
6
+ saveData: () => Promise<void>;
7
+ formErrors?: Record<string, any>;
8
+ }
9
+ declare const NextUnsavedChangesGuard: ({ children, formDirty, saveData, formErrors, }: UnsavedChangesGuardProps) => react_jsx_runtime.JSX.Element;
10
+
11
+ export { NextUnsavedChangesGuard };
@@ -0,0 +1,11 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+
3
+ interface UnsavedChangesGuardProps {
4
+ children: React.ReactNode;
5
+ formDirty: boolean;
6
+ saveData: () => Promise<void>;
7
+ formErrors?: Record<string, any>;
8
+ }
9
+ declare const NextUnsavedChangesGuard: ({ children, formDirty, saveData, formErrors, }: UnsavedChangesGuardProps) => react_jsx_runtime.JSX.Element;
10
+
11
+ export { NextUnsavedChangesGuard };
package/dist/index.js ADDED
@@ -0,0 +1,218 @@
1
+ // src/NextUnsavedChangesGuard.tsx
2
+ import { useState, useEffect, useRef } from "react";
3
+ import { useRouter } from "next/navigation";
4
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
5
+ var alertDialogCSS = `
6
+ .nucg-ad-overlay {
7
+ position: fixed;
8
+ inset: 0;
9
+ background: rgba(0,0,0,0.45);
10
+ backdrop-filter: blur(2px);
11
+ display: flex;
12
+ align-items: center;
13
+ justify-content: center;
14
+ z-index: 50;
15
+ animation: ad-fade-in .15s ease-out;
16
+ }
17
+
18
+ .nucg-ad-dialog {
19
+ width: 100%;
20
+ max-width: 420px;
21
+ background: #ffffff;
22
+ border-radius: 12px;
23
+ padding: 24px;
24
+ box-shadow:
25
+ 0 10px 30px rgba(0,0,0,0.15),
26
+ 0 1px 2px rgba(0,0,0,0.1);
27
+ animation: ad-scale-in .15s ease-out;
28
+ }
29
+
30
+ .nucg-ad-title {
31
+ font-size: 1.125rem;
32
+ font-weight: 600;
33
+ color: #111827;
34
+ margin-bottom: 6px;
35
+ }
36
+
37
+ .nucg-ad-description {
38
+ font-size: 0.95rem;
39
+ color: #6b7280;
40
+ line-height: 1.45;
41
+ }
42
+
43
+ .nucg-ad-footer {
44
+ display: flex;
45
+ justify-content: flex-end;
46
+ gap: 10px;
47
+ margin-top: 20px;
48
+ }
49
+
50
+ .nucg-ad-btn {
51
+ padding: 8px 14px;
52
+ border-radius: 8px;
53
+ font-size: 0.875rem;
54
+ border: none;
55
+ cursor: pointer;
56
+ transition: background .15s ease, transform .05s ease;
57
+ }
58
+
59
+ .nucg-ad-btn:active {
60
+ transform: scale(0.97);
61
+ }
62
+
63
+ .nucg-ad-cancel {
64
+ background: #f3f4f6;
65
+ color: #111827;
66
+ }
67
+ .nucg-ad-cancel:hover {
68
+ background: #e5e7eb;
69
+ }
70
+
71
+ .nucg-ad-danger {
72
+ background: #fee2e2;
73
+ color: #b91c1c;
74
+ }
75
+ .nucg-ad-danger:hover {
76
+ background: #fecaca;
77
+ }
78
+
79
+ .nucg-ad-primary {
80
+ background: #111827;
81
+ color: #ffffff;
82
+ }
83
+ .nucg-ad-primary:hover {
84
+ background: #1f2937;
85
+ }
86
+
87
+ @keyframes nucg-ad-fade-in {
88
+ from { opacity: 0; }
89
+ to { opacity: 1; }
90
+ }
91
+
92
+ @keyframes nucg-ad-scale-in {
93
+ from { transform: scale(.96); opacity: 0; }
94
+ to { transform: scale(1); opacity: 1; }
95
+ }
96
+ `;
97
+ var NextUnsavedChangesGuard = ({
98
+ children,
99
+ formDirty,
100
+ saveData,
101
+ formErrors = {}
102
+ }) => {
103
+ const [isDirty, setDirty] = useState(formDirty);
104
+ const [isDialogOpen, setIsDialogOpen] = useState(false);
105
+ const [nextRoute, setNextRoute] = useState(null);
106
+ const router = useRouter();
107
+ const isRouteChanging = useRef(false);
108
+ useEffect(() => {
109
+ setDirty(formDirty);
110
+ }, [formDirty]);
111
+ useEffect(() => {
112
+ const handleWindowClose = (e) => {
113
+ if (!isDirty) return;
114
+ e.preventDefault();
115
+ e.returnValue = "You have unsaved changes. Are you sure you want to leave?";
116
+ };
117
+ window.addEventListener("beforeunload", handleWindowClose);
118
+ return () => {
119
+ window.removeEventListener("beforeunload", handleWindowClose);
120
+ };
121
+ }, [isDirty]);
122
+ useEffect(() => {
123
+ const originalPush = router.push;
124
+ router.push = (url) => {
125
+ if (isDirty && !isRouteChanging.current) {
126
+ setNextRoute(url);
127
+ setIsDialogOpen(true);
128
+ return Promise.resolve();
129
+ }
130
+ isRouteChanging.current = false;
131
+ return originalPush(url);
132
+ };
133
+ return () => {
134
+ router.push = originalPush;
135
+ };
136
+ }, [isDirty, router]);
137
+ const handleSave = async () => {
138
+ if (Object.keys(formErrors).length === 0) {
139
+ try {
140
+ await saveData();
141
+ setDirty(false);
142
+ setIsDialogOpen(false);
143
+ if (nextRoute) {
144
+ isRouteChanging.current = true;
145
+ router.push(nextRoute);
146
+ }
147
+ } catch (error) {
148
+ console.log("Error saving changes");
149
+ }
150
+ } else {
151
+ setIsDialogOpen(false);
152
+ console.log("Form contains errors");
153
+ }
154
+ };
155
+ const handleDiscard = () => {
156
+ setDirty(false);
157
+ setIsDialogOpen(false);
158
+ if (nextRoute) {
159
+ isRouteChanging.current = true;
160
+ router.push(nextRoute);
161
+ }
162
+ };
163
+ const handleStay = () => {
164
+ setIsDialogOpen(false);
165
+ setNextRoute(null);
166
+ };
167
+ function injectAlertDialogStyles() {
168
+ if (typeof document === "undefined") return;
169
+ if (!document.getElementById("alert-dialog-styles")) {
170
+ const style = document.createElement("style");
171
+ style.id = "alert-dialog-styles";
172
+ style.textContent = alertDialogCSS;
173
+ document.head.appendChild(style);
174
+ }
175
+ }
176
+ useEffect(() => {
177
+ injectAlertDialogStyles();
178
+ }, []);
179
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
180
+ children,
181
+ isDialogOpen && /* @__PURE__ */ jsx("div", { className: "nucg-ad-overlay", children: /* @__PURE__ */ jsxs("div", { className: "nucg-ad-dialog", role: "alertdialog", "aria-modal": "true", children: [
182
+ /* @__PURE__ */ jsxs("div", { children: [
183
+ /* @__PURE__ */ jsx("h2", { className: "nucg-ad-title", children: "Unsaved Changes" }),
184
+ /* @__PURE__ */ jsx("p", { className: "nucg-ad-description", children: "You have unsaved changes. Do you want to save them before leaving?" })
185
+ ] }),
186
+ /* @__PURE__ */ jsxs("div", { className: "nucg-ad-footer", children: [
187
+ /* @__PURE__ */ jsx(
188
+ "button",
189
+ {
190
+ className: "nucg-ad-btn nucg-ad-cancel",
191
+ onClick: handleStay,
192
+ children: "Stay"
193
+ }
194
+ ),
195
+ /* @__PURE__ */ jsx(
196
+ "button",
197
+ {
198
+ className: "nucg-ad-btn nucg-ad-danger",
199
+ onClick: handleDiscard,
200
+ children: "Discard"
201
+ }
202
+ ),
203
+ /* @__PURE__ */ jsx(
204
+ "button",
205
+ {
206
+ className: "nucg-ad-btn nucg-ad-primary",
207
+ onClick: handleSave,
208
+ children: "Save"
209
+ }
210
+ )
211
+ ] })
212
+ ] }) })
213
+ ] });
214
+ };
215
+ export {
216
+ NextUnsavedChangesGuard
217
+ };
218
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/NextUnsavedChangesGuard.tsx"],"sourcesContent":["\"use client\";\n\nimport { useState, useEffect, useRef } from \"react\";\nimport { useRouter } from \"next/navigation\";\n\nconst alertDialogCSS = `\n .nucg-ad-overlay {\n position: fixed;\n inset: 0;\n background: rgba(0,0,0,0.45);\n backdrop-filter: blur(2px);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 50;\n animation: ad-fade-in .15s ease-out;\n }\n\n .nucg-ad-dialog {\n width: 100%;\n max-width: 420px;\n background: #ffffff;\n border-radius: 12px;\n padding: 24px;\n box-shadow:\n 0 10px 30px rgba(0,0,0,0.15),\n 0 1px 2px rgba(0,0,0,0.1);\n animation: ad-scale-in .15s ease-out;\n }\n\n .nucg-ad-title {\n font-size: 1.125rem;\n font-weight: 600;\n color: #111827;\n margin-bottom: 6px;\n }\n\n .nucg-ad-description {\n font-size: 0.95rem;\n color: #6b7280;\n line-height: 1.45;\n }\n\n .nucg-ad-footer {\n display: flex;\n justify-content: flex-end;\n gap: 10px;\n margin-top: 20px;\n }\n\n .nucg-ad-btn {\n padding: 8px 14px;\n border-radius: 8px;\n font-size: 0.875rem;\n border: none;\n cursor: pointer;\n transition: background .15s ease, transform .05s ease;\n }\n\n .nucg-ad-btn:active {\n transform: scale(0.97);\n }\n\n .nucg-ad-cancel {\n background: #f3f4f6;\n color: #111827;\n }\n .nucg-ad-cancel:hover {\n background: #e5e7eb;\n }\n\n .nucg-ad-danger {\n background: #fee2e2;\n color: #b91c1c;\n }\n .nucg-ad-danger:hover {\n background: #fecaca;\n }\n\n .nucg-ad-primary {\n background: #111827;\n color: #ffffff;\n }\n .nucg-ad-primary:hover {\n background: #1f2937;\n }\n\n @keyframes nucg-ad-fade-in {\n from { opacity: 0; }\n to { opacity: 1; }\n }\n\n @keyframes nucg-ad-scale-in {\n from { transform: scale(.96); opacity: 0; }\n to { transform: scale(1); opacity: 1; }\n }\n`;\n\ninterface UnsavedChangesGuardProps {\n children: React.ReactNode;\n formDirty: boolean;\n saveData: () => Promise<void>;\n formErrors?: Record<string, any>;\n}\n\nexport const NextUnsavedChangesGuard = ({\n children,\n formDirty,\n saveData,\n formErrors = {},\n}: UnsavedChangesGuardProps) => {\n const [isDirty, setDirty] = useState(formDirty);\n const [isDialogOpen, setIsDialogOpen] = useState(false);\n const [nextRoute, setNextRoute] = useState<string | null>(null);\n const router = useRouter();\n const isRouteChanging = useRef(false);\n\n // Sync isDirty with formDirty prop\n useEffect(() => {\n setDirty(formDirty);\n }, [formDirty]);\n\n // Handle browser close/refresh\n useEffect(() => {\n const handleWindowClose = (e: BeforeUnloadEvent) => {\n if (!isDirty) return;\n e.preventDefault();\n e.returnValue =\n \"You have unsaved changes. Are you sure you want to leave?\";\n };\n\n window.addEventListener(\"beforeunload\", handleWindowClose);\n return () => {\n window.removeEventListener(\"beforeunload\", handleWindowClose);\n };\n }, [isDirty]);\n\n // Intercept route changes\n useEffect(() => {\n const originalPush = router.push;\n\n router.push = (url: string) => {\n if (isDirty && !isRouteChanging.current) {\n setNextRoute(url);\n setIsDialogOpen(true);\n return Promise.resolve(); // Prevent navigation without throwing\n }\n isRouteChanging.current = false;\n return originalPush(url);\n };\n\n return () => {\n router.push = originalPush;\n };\n }, [isDirty, router]);\n\n // Handle \"Save\" action\n const handleSave = async () => {\n if (Object.keys(formErrors).length === 0) {\n try {\n await saveData();\n setDirty(false);\n setIsDialogOpen(false);\n if (nextRoute) {\n isRouteChanging.current = true;\n router.push(nextRoute);\n }\n } catch (error) {\n console.log(\"Error saving changes\");\n }\n } else {\n setIsDialogOpen(false);\n console.log(\"Form contains errors\");\n }\n };\n\n // Handle \"Discard\" action\n const handleDiscard = () => {\n setDirty(false);\n setIsDialogOpen(false);\n if (nextRoute) {\n isRouteChanging.current = true;\n router.push(nextRoute);\n }\n };\n\n // Handle \"Stay\" action\n const handleStay = () => {\n setIsDialogOpen(false);\n setNextRoute(null);\n };\n\n function injectAlertDialogStyles() {\n if (typeof document === \"undefined\") return;\n\n if (!document.getElementById(\"alert-dialog-styles\")) {\n const style = document.createElement(\"style\");\n style.id = \"alert-dialog-styles\";\n style.textContent = alertDialogCSS;\n document.head.appendChild(style);\n }\n }\n\n useEffect(() => {\n injectAlertDialogStyles();\n }, []);\n\n return (\n <>\n {children}\n {isDialogOpen && (\n <div className=\"nucg-ad-overlay\">\n <div className=\"nucg-ad-dialog\" role=\"alertdialog\" aria-modal=\"true\">\n <div>\n <h2 className=\"nucg-ad-title\">Unsaved Changes</h2>\n <p className=\"nucg-ad-description\">\n You have unsaved changes. Do you want to save them before\n leaving?\n </p>\n </div>\n\n <div className=\"nucg-ad-footer\">\n <button\n className=\"nucg-ad-btn nucg-ad-cancel\"\n onClick={handleStay}\n >\n Stay\n </button>\n\n <button\n className=\"nucg-ad-btn nucg-ad-danger\"\n onClick={handleDiscard}\n >\n Discard\n </button>\n\n <button\n className=\"nucg-ad-btn nucg-ad-primary\"\n onClick={handleSave}\n >\n Save\n </button>\n </div>\n </div>\n </div>\n )}\n </>\n );\n};\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "next-unsaved-changes-guard",
3
+ "version": "1.0.0",
4
+ "description": "A lightweight, customizable React component for Next.js 13+ (App Router) that prevents users from accidentally leaving a page with unsaved changes.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/shibu2000/npm-packages.git",
10
+ "directory": "next-unsaved-changes-guard"
11
+ },
12
+ "homepage": "https://github.com/shibu2000/npm-packages/tree/main/next-unsaved-changes-guard#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/shibu2000/npm-packages/issues"
15
+ },
16
+ "author": {
17
+ "name": "Shibu Dhara",
18
+ "email": "shibudhara147@gmail.com",
19
+ "url": "https://github.com/shibu2000"
20
+ },
21
+ "exports": {
22
+ ".": {
23
+ "types": "./dist/index.d.ts",
24
+ "import": "./dist/index.js",
25
+ "require": "./dist/index.cjs"
26
+ }
27
+ },
28
+ "files": [
29
+ "dist",
30
+ "README.md"
31
+ ],
32
+ "scripts": {
33
+ "build": "tsup",
34
+ "prepublishOnly": "npm run build"
35
+ },
36
+ "peerDependencies": {
37
+ "next": ">=13",
38
+ "react": ">=18"
39
+ },
40
+ "devDependencies": {
41
+ "@types/react": "^19",
42
+ "next": "^16.1.1",
43
+ "tsup": "^8.5.1",
44
+ "typescript": "^5.9.3"
45
+ },
46
+ "keywords": [
47
+ "nextjs",
48
+ "react",
49
+ "unsaved-changes",
50
+ "form-guard",
51
+ "route-guard",
52
+ "navigation-guard",
53
+ "prevent-navigation",
54
+ "beforeunload",
55
+ "nextjs-app-router",
56
+ "next-navigation",
57
+ "app-router",
58
+ "form-dirty",
59
+ "data-loss-prevention",
60
+ "modal",
61
+ "dialog",
62
+ "typescript"
63
+ ]
64
+ }