mirror of
https://github.com/hanzo-apps/dataroom.git
synced 2026-10-30 14:47:36 +00:00
Four localStorage keys still spelled upstream's brand — papermark.email,
papermark.name, last_papermark_login, papermark-all-links-collapsed. The first
two hold a visitor's email and name and are what makes an access form
prefill on a return visit, so renaming them naively signs every returning
visitor out of their own convenience.
lib/webstorage.ts already owned web storage, so the rename lives there:
- StorageKey names each key once. Eight call sites used to spell the string
themselves, one of them via a local const, which is why the set had
drifted into three different naming shapes.
- LEGACY_KEYS maps each new key to the name it used to have. getItem falls
back to the old key, copies the value forward and drops the old entry.
Migrating on read means no boot hook to register and no ordering to get
wrong; a key migrates the first time something asks for it, and only
then. setItem and removeItem clear the legacy entry too, so a value
cannot come back from the dead.
Six of those call sites were reaching for window.localStorage directly and
so skipped the wrapper's try/catch. They now go through it, which is what it
is for — a viewer opening a link in a third-party context with storage
blocked was throwing rather than degrading.
Co-authored-by: Hanzo Dev <[email protected]>
161 lines
4.8 KiB
TypeScript
161 lines
4.8 KiB
TypeScript
import {
|
|
Dispatch,
|
|
SetStateAction,
|
|
useEffect,
|
|
useRef,
|
|
useState,
|
|
type CSSProperties,
|
|
} from "react";
|
|
|
|
import { Brand, DataroomBrand } from "@prisma/client";
|
|
import { useDebouncedCallback } from "use-debounce";
|
|
|
|
import { cn } from "@/lib/utils";
|
|
import { validateEmail } from "@/lib/utils/validate-email";
|
|
import { StorageKey, localStorage } from "@/lib/webstorage";
|
|
|
|
import { DEFAULT_ACCESS_FORM_TYPE } from ".";
|
|
import { useAccessFormTheme } from "./access-form-theme";
|
|
|
|
export default function EmailSection({
|
|
data,
|
|
setData,
|
|
brand,
|
|
disableEditEmail,
|
|
useCustomAccessForm,
|
|
onValidationChange,
|
|
}: {
|
|
data: DEFAULT_ACCESS_FORM_TYPE;
|
|
setData: Dispatch<SetStateAction<DEFAULT_ACCESS_FORM_TYPE>>;
|
|
brand?: Partial<Brand> | Partial<DataroomBrand> | null;
|
|
disableEditEmail?: boolean;
|
|
useCustomAccessForm?: boolean;
|
|
onValidationChange: (isValid: boolean) => void;
|
|
}) {
|
|
const { email } = data;
|
|
const theme = useAccessFormTheme();
|
|
const [emailError, setEmailError] = useState<string | null>(null);
|
|
const [isDirty, setIsDirty] = useState(false);
|
|
|
|
useEffect(() => {
|
|
// Load email from localStorage when the component mounts
|
|
const storedEmail = localStorage.getItem(StorageKey.viewerEmail);
|
|
if (storedEmail) {
|
|
setData((prevData) => ({
|
|
...prevData,
|
|
email: storedEmail.toLowerCase(),
|
|
}));
|
|
}
|
|
}, [setData]);
|
|
|
|
const handleInvalid = (e: React.InvalidEvent<HTMLInputElement>) => {
|
|
e.preventDefault(); // Prevent default browser validation popup
|
|
setEmailError("Please enter a valid email address");
|
|
};
|
|
|
|
const debouncedValidation = useDebouncedCallback(
|
|
(value: string) => {
|
|
const isValid = !value || validateEmail(value);
|
|
if (isDirty && value && !isValid) {
|
|
setEmailError("Please enter a valid email address");
|
|
} else {
|
|
setEmailError(null);
|
|
}
|
|
// Notify parent component about validation status
|
|
onValidationChange?.(isValid);
|
|
},
|
|
500, // 500ms delay
|
|
);
|
|
|
|
const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const newEmail = e.target.value.toLowerCase();
|
|
setEmailError(null); // Clear error when typing
|
|
|
|
debouncedValidation(newEmail);
|
|
|
|
// Update the state
|
|
setData({ ...data, email: newEmail });
|
|
// Store in localStorage
|
|
localStorage.setItem(StorageKey.viewerEmail, newEmail);
|
|
|
|
// Optional: Clear error if input becomes valid
|
|
if (e.target.validity.valid) {
|
|
setEmailError(null);
|
|
}
|
|
};
|
|
|
|
const handleBlur = (e: React.FocusEvent<HTMLInputElement>) => {
|
|
setIsDirty(true);
|
|
const value = e.target.value;
|
|
const isValid = !value || validateEmail(value);
|
|
if (value && !isValid) {
|
|
setEmailError("Please enter a valid email address");
|
|
}
|
|
onValidationChange?.(isValid);
|
|
};
|
|
|
|
const handleFocus = () => {
|
|
// Optionally clear error when user focuses the input to type again
|
|
setEmailError(null);
|
|
};
|
|
|
|
return (
|
|
<div className="relative space-y-2">
|
|
<label
|
|
htmlFor="email"
|
|
className="block text-sm font-medium leading-6 text-white"
|
|
style={{ color: theme.textColor }}
|
|
>
|
|
Email address
|
|
</label>
|
|
<input
|
|
name="email"
|
|
id="email"
|
|
type="email"
|
|
autoCorrect="off"
|
|
autoComplete="email"
|
|
autoFocus
|
|
required
|
|
translate="no"
|
|
className={cn(
|
|
"notranslate flex w-full cursor-text rounded-md border-0 bg-black py-1.5 text-gray-500 shadow-sm ring-1 ring-inset ring-gray-600 placeholder:text-[var(--access-placeholder)] focus:ring-2 focus:ring-inset focus:ring-[var(--access-input-focus)] sm:text-sm sm:leading-6",
|
|
emailError && isDirty && "ring-red-500",
|
|
)}
|
|
style={{
|
|
backgroundColor: theme.controlBgColor,
|
|
borderColor: theme.controlBorderColor,
|
|
"--access-placeholder": theme.controlPlaceholderColor,
|
|
"--access-input-focus": theme.controlBorderStrongColor,
|
|
color: disableEditEmail
|
|
? theme.subtleTextColor
|
|
: theme.textColor,
|
|
} as CSSProperties}
|
|
value={email || ""}
|
|
placeholder="Enter email"
|
|
onChange={handleEmailChange}
|
|
onInvalid={handleInvalid}
|
|
onBlur={handleBlur}
|
|
onFocus={handleFocus}
|
|
disabled={disableEditEmail}
|
|
data-1p-ignore
|
|
aria-invalid={emailError ? "true" : "false"}
|
|
aria-describedby={emailError ? "email-error" : undefined}
|
|
/>
|
|
{emailError && (
|
|
<p
|
|
id="email-error"
|
|
className="mt-1 text-sm text-red-500"
|
|
style={{ color: theme.textColor }}
|
|
>
|
|
{emailError}
|
|
</p>
|
|
)}
|
|
<p className="text-sm" style={{ color: theme.subtleTextColor }}>
|
|
{useCustomAccessForm
|
|
? "This data will be shared with the content provider."
|
|
: "This data will be shared with the sender."}
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|