@the_disreputable_doggo/password-gate 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 +36 -0
- package/dist/index.d.mts +20 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +125 -0
- package/dist/index.mjs +95 -0
- package/index.tsx +139 -0
- package/package.json +27 -0
- package/tsconfig.json +14 -0
package/README.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# @pierandpixel/password-gate
|
|
2
|
+
|
|
3
|
+
A React component that provides a full-screen password gate for early access or pre-launch sites.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @pierandpixel/password-gate
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
import PasswordGate from '@pierandpixel/password-gate';
|
|
15
|
+
|
|
16
|
+
function App() {
|
|
17
|
+
return (
|
|
18
|
+
<PasswordGate
|
|
19
|
+
title="Coming Soon"
|
|
20
|
+
subtitle="Enter your password"
|
|
21
|
+
datePrefix="Expected"
|
|
22
|
+
underConstruction="Building in progress"
|
|
23
|
+
placeholder="Password"
|
|
24
|
+
buttonText="Enter"
|
|
25
|
+
enabled={true}
|
|
26
|
+
password="secretpassword"
|
|
27
|
+
releaseDate="2027-01-01"
|
|
28
|
+
/>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Features
|
|
34
|
+
- Fully customizable text props for internationalization.
|
|
35
|
+
- Accepts configuration variables dynamically.
|
|
36
|
+
- Animated error state on incorrect password.
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
interface PasswordGateProps {
|
|
4
|
+
title: string;
|
|
5
|
+
subtitle: string;
|
|
6
|
+
datePrefix: string;
|
|
7
|
+
underConstruction: string;
|
|
8
|
+
placeholder: string;
|
|
9
|
+
buttonText: string;
|
|
10
|
+
enabled?: boolean;
|
|
11
|
+
password?: string;
|
|
12
|
+
releaseDate?: string;
|
|
13
|
+
onUnlock?: () => void;
|
|
14
|
+
showPasswordLabel?: string;
|
|
15
|
+
hidePasswordLabel?: string;
|
|
16
|
+
unlockBodyClass?: string;
|
|
17
|
+
}
|
|
18
|
+
declare function PasswordGate({ title, subtitle, datePrefix, underConstruction, placeholder, buttonText, enabled, password, releaseDate, onUnlock, showPasswordLabel, hidePasswordLabel, unlockBodyClass }: PasswordGateProps): React.JSX.Element;
|
|
19
|
+
|
|
20
|
+
export { type PasswordGateProps, PasswordGate as default };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
interface PasswordGateProps {
|
|
4
|
+
title: string;
|
|
5
|
+
subtitle: string;
|
|
6
|
+
datePrefix: string;
|
|
7
|
+
underConstruction: string;
|
|
8
|
+
placeholder: string;
|
|
9
|
+
buttonText: string;
|
|
10
|
+
enabled?: boolean;
|
|
11
|
+
password?: string;
|
|
12
|
+
releaseDate?: string;
|
|
13
|
+
onUnlock?: () => void;
|
|
14
|
+
showPasswordLabel?: string;
|
|
15
|
+
hidePasswordLabel?: string;
|
|
16
|
+
unlockBodyClass?: string;
|
|
17
|
+
}
|
|
18
|
+
declare function PasswordGate({ title, subtitle, datePrefix, underConstruction, placeholder, buttonText, enabled, password, releaseDate, onUnlock, showPasswordLabel, hidePasswordLabel, unlockBodyClass }: PasswordGateProps): React.JSX.Element;
|
|
19
|
+
|
|
20
|
+
export { type PasswordGateProps, PasswordGate as default };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
var __create = Object.create;
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
20
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
21
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
22
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
23
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
24
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
25
|
+
mod
|
|
26
|
+
));
|
|
27
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
28
|
+
|
|
29
|
+
// index.tsx
|
|
30
|
+
var index_exports = {};
|
|
31
|
+
__export(index_exports, {
|
|
32
|
+
default: () => PasswordGate
|
|
33
|
+
});
|
|
34
|
+
module.exports = __toCommonJS(index_exports);
|
|
35
|
+
var import_react = __toESM(require("react"));
|
|
36
|
+
function PasswordGate({
|
|
37
|
+
title,
|
|
38
|
+
subtitle,
|
|
39
|
+
datePrefix,
|
|
40
|
+
underConstruction,
|
|
41
|
+
placeholder,
|
|
42
|
+
buttonText,
|
|
43
|
+
enabled = true,
|
|
44
|
+
password = "",
|
|
45
|
+
releaseDate = "",
|
|
46
|
+
onUnlock,
|
|
47
|
+
showPasswordLabel = "Show password",
|
|
48
|
+
hidePasswordLabel = "Hide password",
|
|
49
|
+
unlockBodyClass = "locked"
|
|
50
|
+
}) {
|
|
51
|
+
const [passwordInput, setPasswordInput] = (0, import_react.useState)("");
|
|
52
|
+
const [showPassword, setShowPassword] = (0, import_react.useState)(false);
|
|
53
|
+
const [error, setError] = (0, import_react.useState)(false);
|
|
54
|
+
const [unlocked, setUnlocked] = (0, import_react.useState)(false);
|
|
55
|
+
const [shouldRender, setShouldRender] = (0, import_react.useState)(() => {
|
|
56
|
+
if (typeof window !== "undefined") {
|
|
57
|
+
return localStorage.getItem("site_unlocked") !== "true";
|
|
58
|
+
}
|
|
59
|
+
return true;
|
|
60
|
+
});
|
|
61
|
+
if (!enabled) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
if (!shouldRender) return null;
|
|
65
|
+
const handleSubmit = (e) => {
|
|
66
|
+
e.preventDefault();
|
|
67
|
+
if (passwordInput === password) {
|
|
68
|
+
setError(false);
|
|
69
|
+
setUnlocked(true);
|
|
70
|
+
localStorage.setItem("site_unlocked", "true");
|
|
71
|
+
if (unlockBodyClass && typeof document !== "undefined") {
|
|
72
|
+
document.documentElement.classList.remove(unlockBodyClass);
|
|
73
|
+
}
|
|
74
|
+
if (onUnlock) {
|
|
75
|
+
onUnlock();
|
|
76
|
+
}
|
|
77
|
+
setTimeout(() => {
|
|
78
|
+
setShouldRender(false);
|
|
79
|
+
}, 500);
|
|
80
|
+
} else {
|
|
81
|
+
setError(true);
|
|
82
|
+
setTimeout(() => setError(false), 500);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
return /* @__PURE__ */ import_react.default.createElement(
|
|
86
|
+
"div",
|
|
87
|
+
{
|
|
88
|
+
className: `fixed inset-0 z-50 flex items-center justify-center bg-base transition-opacity duration-500 ${unlocked ? "opacity-0 pointer-events-none" : "opacity-100"}`
|
|
89
|
+
},
|
|
90
|
+
/* @__PURE__ */ import_react.default.createElement("div", { className: "max-w-2xl px-6 text-center" }, /* @__PURE__ */ import_react.default.createElement("h1", { className: "text-4xl md:text-5xl font-bold text-heading mb-6" }, title), /* @__PURE__ */ import_react.default.createElement("p", { className: "text-lg text-body mb-8" }, subtitle), /* @__PURE__ */ import_react.default.createElement("div", { className: "bg-surface p-8 rounded-xl shadow-lg inline-block border border-accent-muted" }, /* @__PURE__ */ import_react.default.createElement("p", { className: "text-accent-muted font-bold mb-4" }, datePrefix, " ", releaseDate, " ", underConstruction), /* @__PURE__ */ import_react.default.createElement("form", { onSubmit: handleSubmit, className: "flex flex-col items-center gap-4" }, /* @__PURE__ */ import_react.default.createElement("div", { className: "relative w-full" }, /* @__PURE__ */ import_react.default.createElement(
|
|
91
|
+
"input",
|
|
92
|
+
{
|
|
93
|
+
type: showPassword ? "text" : "password",
|
|
94
|
+
placeholder,
|
|
95
|
+
value: passwordInput,
|
|
96
|
+
onChange: (e) => setPasswordInput(e.target.value),
|
|
97
|
+
className: `w-full px-4 py-2 pr-10 bg-base border rounded text-body focus:outline-none focus:ring-2 focus:ring-accent transition-transform ${error ? "border-red-500 animate-[shake_0.5s_ease-in-out]" : "border-accent-muted"}`
|
|
98
|
+
}
|
|
99
|
+
), /* @__PURE__ */ import_react.default.createElement(
|
|
100
|
+
"button",
|
|
101
|
+
{
|
|
102
|
+
type: "button",
|
|
103
|
+
onClick: () => setShowPassword(!showPassword),
|
|
104
|
+
className: "absolute inset-y-0 right-0 pr-3 flex items-center text-body hover:text-heading",
|
|
105
|
+
"aria-label": showPassword ? hidePasswordLabel : showPasswordLabel
|
|
106
|
+
},
|
|
107
|
+
showPassword ? /* @__PURE__ */ import_react.default.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", fill: "none", viewBox: "0 0 24 24", strokeWidth: 1.5, stroke: "currentColor", className: "w-5 h-5" }, /* @__PURE__ */ import_react.default.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M3.98 8.223A10.477 10.477 0 001.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.45 10.45 0 0112 4.5c4.756 0 8.773 3.162 10.065 7.498a10.523 10.523 0 01-4.293 5.774M6.228 6.228L3 3m3.228 3.228l3.65 3.65m7.894 7.894L21 21m-3.228-3.228l-3.65-3.65m0 0a3 3 0 10-4.243-4.243m4.242 4.242L9.88 9.88" })) : /* @__PURE__ */ import_react.default.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", fill: "none", viewBox: "0 0 24 24", strokeWidth: 1.5, stroke: "currentColor", className: "w-5 h-5" }, /* @__PURE__ */ import_react.default.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z" }), /* @__PURE__ */ import_react.default.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M15 12a3 3 0 11-6 0 3 3 0 016 0z" }))
|
|
108
|
+
)), /* @__PURE__ */ import_react.default.createElement(
|
|
109
|
+
"button",
|
|
110
|
+
{
|
|
111
|
+
type: "submit",
|
|
112
|
+
className: "px-6 py-2 bg-accent-muted text-white rounded font-semibold transition-colors"
|
|
113
|
+
},
|
|
114
|
+
buttonText
|
|
115
|
+
)))),
|
|
116
|
+
/* @__PURE__ */ import_react.default.createElement("style", null, `
|
|
117
|
+
@keyframes shake {
|
|
118
|
+
0%, 100% { transform: translateX(0); }
|
|
119
|
+
25% { transform: translateX(-5px); }
|
|
120
|
+
50% { transform: translateX(5px); }
|
|
121
|
+
75% { transform: translateX(-5px); }
|
|
122
|
+
}
|
|
123
|
+
`)
|
|
124
|
+
);
|
|
125
|
+
}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// index.tsx
|
|
2
|
+
import React, { useState } from "react";
|
|
3
|
+
function PasswordGate({
|
|
4
|
+
title,
|
|
5
|
+
subtitle,
|
|
6
|
+
datePrefix,
|
|
7
|
+
underConstruction,
|
|
8
|
+
placeholder,
|
|
9
|
+
buttonText,
|
|
10
|
+
enabled = true,
|
|
11
|
+
password = "",
|
|
12
|
+
releaseDate = "",
|
|
13
|
+
onUnlock,
|
|
14
|
+
showPasswordLabel = "Show password",
|
|
15
|
+
hidePasswordLabel = "Hide password",
|
|
16
|
+
unlockBodyClass = "locked"
|
|
17
|
+
}) {
|
|
18
|
+
const [passwordInput, setPasswordInput] = useState("");
|
|
19
|
+
const [showPassword, setShowPassword] = useState(false);
|
|
20
|
+
const [error, setError] = useState(false);
|
|
21
|
+
const [unlocked, setUnlocked] = useState(false);
|
|
22
|
+
const [shouldRender, setShouldRender] = useState(() => {
|
|
23
|
+
if (typeof window !== "undefined") {
|
|
24
|
+
return localStorage.getItem("site_unlocked") !== "true";
|
|
25
|
+
}
|
|
26
|
+
return true;
|
|
27
|
+
});
|
|
28
|
+
if (!enabled) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
if (!shouldRender) return null;
|
|
32
|
+
const handleSubmit = (e) => {
|
|
33
|
+
e.preventDefault();
|
|
34
|
+
if (passwordInput === password) {
|
|
35
|
+
setError(false);
|
|
36
|
+
setUnlocked(true);
|
|
37
|
+
localStorage.setItem("site_unlocked", "true");
|
|
38
|
+
if (unlockBodyClass && typeof document !== "undefined") {
|
|
39
|
+
document.documentElement.classList.remove(unlockBodyClass);
|
|
40
|
+
}
|
|
41
|
+
if (onUnlock) {
|
|
42
|
+
onUnlock();
|
|
43
|
+
}
|
|
44
|
+
setTimeout(() => {
|
|
45
|
+
setShouldRender(false);
|
|
46
|
+
}, 500);
|
|
47
|
+
} else {
|
|
48
|
+
setError(true);
|
|
49
|
+
setTimeout(() => setError(false), 500);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
return /* @__PURE__ */ React.createElement(
|
|
53
|
+
"div",
|
|
54
|
+
{
|
|
55
|
+
className: `fixed inset-0 z-50 flex items-center justify-center bg-base transition-opacity duration-500 ${unlocked ? "opacity-0 pointer-events-none" : "opacity-100"}`
|
|
56
|
+
},
|
|
57
|
+
/* @__PURE__ */ React.createElement("div", { className: "max-w-2xl px-6 text-center" }, /* @__PURE__ */ React.createElement("h1", { className: "text-4xl md:text-5xl font-bold text-heading mb-6" }, title), /* @__PURE__ */ React.createElement("p", { className: "text-lg text-body mb-8" }, subtitle), /* @__PURE__ */ React.createElement("div", { className: "bg-surface p-8 rounded-xl shadow-lg inline-block border border-accent-muted" }, /* @__PURE__ */ React.createElement("p", { className: "text-accent-muted font-bold mb-4" }, datePrefix, " ", releaseDate, " ", underConstruction), /* @__PURE__ */ React.createElement("form", { onSubmit: handleSubmit, className: "flex flex-col items-center gap-4" }, /* @__PURE__ */ React.createElement("div", { className: "relative w-full" }, /* @__PURE__ */ React.createElement(
|
|
58
|
+
"input",
|
|
59
|
+
{
|
|
60
|
+
type: showPassword ? "text" : "password",
|
|
61
|
+
placeholder,
|
|
62
|
+
value: passwordInput,
|
|
63
|
+
onChange: (e) => setPasswordInput(e.target.value),
|
|
64
|
+
className: `w-full px-4 py-2 pr-10 bg-base border rounded text-body focus:outline-none focus:ring-2 focus:ring-accent transition-transform ${error ? "border-red-500 animate-[shake_0.5s_ease-in-out]" : "border-accent-muted"}`
|
|
65
|
+
}
|
|
66
|
+
), /* @__PURE__ */ React.createElement(
|
|
67
|
+
"button",
|
|
68
|
+
{
|
|
69
|
+
type: "button",
|
|
70
|
+
onClick: () => setShowPassword(!showPassword),
|
|
71
|
+
className: "absolute inset-y-0 right-0 pr-3 flex items-center text-body hover:text-heading",
|
|
72
|
+
"aria-label": showPassword ? hidePasswordLabel : showPasswordLabel
|
|
73
|
+
},
|
|
74
|
+
showPassword ? /* @__PURE__ */ React.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", fill: "none", viewBox: "0 0 24 24", strokeWidth: 1.5, stroke: "currentColor", className: "w-5 h-5" }, /* @__PURE__ */ React.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M3.98 8.223A10.477 10.477 0 001.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.45 10.45 0 0112 4.5c4.756 0 8.773 3.162 10.065 7.498a10.523 10.523 0 01-4.293 5.774M6.228 6.228L3 3m3.228 3.228l3.65 3.65m7.894 7.894L21 21m-3.228-3.228l-3.65-3.65m0 0a3 3 0 10-4.243-4.243m4.242 4.242L9.88 9.88" })) : /* @__PURE__ */ React.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", fill: "none", viewBox: "0 0 24 24", strokeWidth: 1.5, stroke: "currentColor", className: "w-5 h-5" }, /* @__PURE__ */ React.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z" }), /* @__PURE__ */ React.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M15 12a3 3 0 11-6 0 3 3 0 016 0z" }))
|
|
75
|
+
)), /* @__PURE__ */ React.createElement(
|
|
76
|
+
"button",
|
|
77
|
+
{
|
|
78
|
+
type: "submit",
|
|
79
|
+
className: "px-6 py-2 bg-accent-muted text-white rounded font-semibold transition-colors"
|
|
80
|
+
},
|
|
81
|
+
buttonText
|
|
82
|
+
)))),
|
|
83
|
+
/* @__PURE__ */ React.createElement("style", null, `
|
|
84
|
+
@keyframes shake {
|
|
85
|
+
0%, 100% { transform: translateX(0); }
|
|
86
|
+
25% { transform: translateX(-5px); }
|
|
87
|
+
50% { transform: translateX(5px); }
|
|
88
|
+
75% { transform: translateX(-5px); }
|
|
89
|
+
}
|
|
90
|
+
`)
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
export {
|
|
94
|
+
PasswordGate as default
|
|
95
|
+
};
|
package/index.tsx
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface PasswordGateProps {
|
|
4
|
+
title: string;
|
|
5
|
+
subtitle: string;
|
|
6
|
+
datePrefix: string;
|
|
7
|
+
underConstruction: string;
|
|
8
|
+
placeholder: string;
|
|
9
|
+
buttonText: string;
|
|
10
|
+
enabled?: boolean;
|
|
11
|
+
password?: string;
|
|
12
|
+
releaseDate?: string;
|
|
13
|
+
onUnlock?: () => void;
|
|
14
|
+
showPasswordLabel?: string;
|
|
15
|
+
hidePasswordLabel?: string;
|
|
16
|
+
unlockBodyClass?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default function PasswordGate({
|
|
20
|
+
title,
|
|
21
|
+
subtitle,
|
|
22
|
+
datePrefix,
|
|
23
|
+
underConstruction,
|
|
24
|
+
placeholder,
|
|
25
|
+
buttonText,
|
|
26
|
+
enabled = true,
|
|
27
|
+
password = '',
|
|
28
|
+
releaseDate = '',
|
|
29
|
+
onUnlock,
|
|
30
|
+
showPasswordLabel = 'Show password',
|
|
31
|
+
hidePasswordLabel = 'Hide password',
|
|
32
|
+
unlockBodyClass = 'locked'
|
|
33
|
+
}: PasswordGateProps) {
|
|
34
|
+
const [passwordInput, setPasswordInput] = useState('');
|
|
35
|
+
const [showPassword, setShowPassword] = useState(false);
|
|
36
|
+
const [error, setError] = useState(false);
|
|
37
|
+
const [unlocked, setUnlocked] = useState(false);
|
|
38
|
+
const [shouldRender, setShouldRender] = useState(() => {
|
|
39
|
+
if (typeof window !== 'undefined') {
|
|
40
|
+
return localStorage.getItem('site_unlocked') !== 'true';
|
|
41
|
+
}
|
|
42
|
+
return true;
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// If password protection is disabled, we don't render anything
|
|
46
|
+
if (!enabled) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!shouldRender) return null;
|
|
51
|
+
|
|
52
|
+
const handleSubmit = (e: React.FormEvent) => {
|
|
53
|
+
e.preventDefault();
|
|
54
|
+
if (passwordInput === password) {
|
|
55
|
+
setError(false);
|
|
56
|
+
setUnlocked(true);
|
|
57
|
+
localStorage.setItem('site_unlocked', 'true');
|
|
58
|
+
|
|
59
|
+
if (unlockBodyClass && typeof document !== 'undefined') {
|
|
60
|
+
document.documentElement.classList.remove(unlockBodyClass);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (onUnlock) {
|
|
64
|
+
onUnlock();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Allow fade out animation to finish before removing from DOM
|
|
68
|
+
setTimeout(() => {
|
|
69
|
+
setShouldRender(false);
|
|
70
|
+
}, 500);
|
|
71
|
+
} else {
|
|
72
|
+
setError(true);
|
|
73
|
+
// Reset error after animation
|
|
74
|
+
setTimeout(() => setError(false), 500);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<div
|
|
80
|
+
className={`fixed inset-0 z-50 flex items-center justify-center bg-base transition-opacity duration-500 ${unlocked ? 'opacity-0 pointer-events-none' : 'opacity-100'}`}
|
|
81
|
+
>
|
|
82
|
+
<div className="max-w-2xl px-6 text-center">
|
|
83
|
+
<h1 className="text-4xl md:text-5xl font-bold text-heading mb-6">
|
|
84
|
+
{title}
|
|
85
|
+
</h1>
|
|
86
|
+
<p className="text-lg text-body mb-8">
|
|
87
|
+
{subtitle}
|
|
88
|
+
</p>
|
|
89
|
+
<div className="bg-surface p-8 rounded-xl shadow-lg inline-block border border-accent-muted">
|
|
90
|
+
<p className="text-accent-muted font-bold mb-4">
|
|
91
|
+
{datePrefix} {releaseDate} {underConstruction}
|
|
92
|
+
</p>
|
|
93
|
+
<form onSubmit={handleSubmit} className="flex flex-col items-center gap-4">
|
|
94
|
+
<div className="relative w-full">
|
|
95
|
+
<input
|
|
96
|
+
type={showPassword ? "text" : "password"}
|
|
97
|
+
placeholder={placeholder}
|
|
98
|
+
value={passwordInput}
|
|
99
|
+
onChange={(e) => setPasswordInput(e.target.value)}
|
|
100
|
+
className={`w-full px-4 py-2 pr-10 bg-base border rounded text-body focus:outline-none focus:ring-2 focus:ring-accent transition-transform ${error ? 'border-red-500 animate-[shake_0.5s_ease-in-out]' : 'border-accent-muted'}`}
|
|
101
|
+
/>
|
|
102
|
+
<button
|
|
103
|
+
type="button"
|
|
104
|
+
onClick={() => setShowPassword(!showPassword)}
|
|
105
|
+
className="absolute inset-y-0 right-0 pr-3 flex items-center text-body hover:text-heading"
|
|
106
|
+
aria-label={showPassword ? hidePasswordLabel : showPasswordLabel}
|
|
107
|
+
>
|
|
108
|
+
{showPassword ? (
|
|
109
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-5 h-5">
|
|
110
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M3.98 8.223A10.477 10.477 0 001.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.45 10.45 0 0112 4.5c4.756 0 8.773 3.162 10.065 7.498a10.523 10.523 0 01-4.293 5.774M6.228 6.228L3 3m3.228 3.228l3.65 3.65m7.894 7.894L21 21m-3.228-3.228l-3.65-3.65m0 0a3 3 0 10-4.243-4.243m4.242 4.242L9.88 9.88" />
|
|
111
|
+
</svg>
|
|
112
|
+
) : (
|
|
113
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-5 h-5">
|
|
114
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z" />
|
|
115
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
116
|
+
</svg>
|
|
117
|
+
)}
|
|
118
|
+
</button>
|
|
119
|
+
</div>
|
|
120
|
+
<button
|
|
121
|
+
type="submit"
|
|
122
|
+
className="px-6 py-2 bg-accent-muted text-white rounded font-semibold transition-colors"
|
|
123
|
+
>
|
|
124
|
+
{buttonText}
|
|
125
|
+
</button>
|
|
126
|
+
</form>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
<style>{`
|
|
130
|
+
@keyframes shake {
|
|
131
|
+
0%, 100% { transform: translateX(0); }
|
|
132
|
+
25% { transform: translateX(-5px); }
|
|
133
|
+
50% { transform: translateX(5px); }
|
|
134
|
+
75% { transform: translateX(-5px); }
|
|
135
|
+
}
|
|
136
|
+
`}</style>
|
|
137
|
+
</div>
|
|
138
|
+
);
|
|
139
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@the_disreputable_doggo/password-gate",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Password Gate component for Pier & Pixel templates",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.mjs",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsup index.tsx --format cjs,esm --dts"
|
|
10
|
+
},
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git@github.com:HadleySwello/password-gate.git"
|
|
14
|
+
},
|
|
15
|
+
"peerDependencies": {
|
|
16
|
+
"react": "^19.0.0",
|
|
17
|
+
"react-dom": "^19.0.0"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@types/react": "^19.0.0",
|
|
21
|
+
"@types/react-dom": "^19.0.0",
|
|
22
|
+
"tsup": "^8.0.0",
|
|
23
|
+
"typescript": "^5.0.0",
|
|
24
|
+
"react": "^19.2.7",
|
|
25
|
+
"react-dom": "^19.2.7"
|
|
26
|
+
}
|
|
27
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ESNext",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "node",
|
|
6
|
+
"jsx": "react",
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"forceConsistentCasingInFileNames": true,
|
|
10
|
+
"declaration": true,
|
|
11
|
+
"emitDeclarationOnly": true
|
|
12
|
+
},
|
|
13
|
+
"include": ["index.tsx"]
|
|
14
|
+
}
|