mirror of
https://github.com/ramvignesh-b/pi-ku.git
synced 2026-05-04 08:56:52 +00:00
refactor: prevent re-renders by extracting components out
This commit is contained in:
@@ -23,7 +23,7 @@ export function DrawerSection({
|
|||||||
className={`join-item group flex flex-col transition-colors duration-3000 ease-in-out ${isOpen ? "bg-base-300/30" : ""}`}
|
className={`join-item group flex flex-col transition-colors duration-3000 ease-in-out ${isOpen ? "bg-base-300/30" : ""}`}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`transition-all duration-2000 ease-in-out bg-neutral/10 ${
|
className={`transition-all duration-1500 ease-in-out bg-neutral/10 ${
|
||||||
isOpen
|
isOpen
|
||||||
? "max-h-125 opacity-100 py-3 border-b border-base-content/5 overflow-visible"
|
? "max-h-125 opacity-100 py-3 border-b border-base-content/5 overflow-visible"
|
||||||
: "max-h-0 opacity-0 pointer-events-none"
|
: "max-h-0 opacity-0 pointer-events-none"
|
||||||
|
|||||||
@@ -8,22 +8,17 @@ import { PATHS } from "../config/routes";
|
|||||||
import { useAuth } from "../hooks/useAuth";
|
import { useAuth } from "../hooks/useAuth";
|
||||||
import { useLetters } from "../hooks/useLetters";
|
import { useLetters } from "../hooks/useLetters";
|
||||||
import {
|
import {
|
||||||
formateRelativeDateWithoutTime,
|
|
||||||
formatRelativeDate,
|
formatRelativeDate,
|
||||||
|
formatRelativeDateWithoutTime,
|
||||||
} from "../utils/dateFormat.ts";
|
} from "../utils/dateFormat.ts";
|
||||||
|
|
||||||
export default function Drawer() {
|
interface PasskeyModalProps {
|
||||||
const { user, logout, unlock } = useAuth();
|
onUnlock: (password: string) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
const [openSection, setOpenSection] = useState<string | null>(null);
|
function PasskeyModal({ onUnlock }: PasskeyModalProps) {
|
||||||
const navigate = useNavigate();
|
|
||||||
const { drafts, kept, sent, vault, loading, isAuthRequired } = useLetters();
|
|
||||||
|
|
||||||
if (!user) return null;
|
|
||||||
|
|
||||||
function PasskeyModal() {
|
|
||||||
return (
|
return (
|
||||||
<div className="modal modal-open bg-base-100/20 backdrop-blur-md">
|
<div className="modal modal-open bg-base-100/20 backdrop-blur-md z-100">
|
||||||
<div className="modal-box p-12 flex flex-col items-center">
|
<div className="modal-box p-12 flex flex-col items-center">
|
||||||
<LockKeyIcon
|
<LockKeyIcon
|
||||||
size={48}
|
size={48}
|
||||||
@@ -33,7 +28,7 @@ export default function Drawer() {
|
|||||||
Authentication Required
|
Authentication Required
|
||||||
</h3>
|
</h3>
|
||||||
<p className="py-4 font-sans">
|
<p className="py-4 font-sans">
|
||||||
We need your passkey to open your letters
|
We need you to re-enter your passkey to open your letters
|
||||||
</p>
|
</p>
|
||||||
<div className="divider w-1/2 mx-auto text-xs text-neutral-content/30 mt-0"></div>
|
<div className="divider w-1/2 mx-auto text-xs text-neutral-content/30 mt-0"></div>
|
||||||
<p className="text-xs text-neutral-content/30 font-mono italic">
|
<p className="text-xs text-neutral-content/30 font-mono italic">
|
||||||
@@ -47,7 +42,7 @@ export default function Drawer() {
|
|||||||
const formData = new FormData(e.currentTarget);
|
const formData = new FormData(e.currentTarget);
|
||||||
const password = formData.get("password") as string;
|
const password = formData.get("password") as string;
|
||||||
if (!password) return;
|
if (!password) return;
|
||||||
await unlock(password);
|
await onUnlock(password);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
@@ -68,6 +63,15 @@ export default function Drawer() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default function Drawer() {
|
||||||
|
const { user, logout, unlock } = useAuth();
|
||||||
|
|
||||||
|
const [openSection, setOpenSection] = useState<string | null>(null);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { drafts, kept, sent, vault, loading, isAuthRequired } = useLetters();
|
||||||
|
|
||||||
|
if (!user) return null;
|
||||||
|
|
||||||
const toggleSection = (id: string) =>
|
const toggleSection = (id: string) =>
|
||||||
setOpenSection(openSection === id ? null : id);
|
setOpenSection(openSection === id ? null : id);
|
||||||
|
|
||||||
@@ -75,8 +79,8 @@ export default function Drawer() {
|
|||||||
<div className="min-h-screen w-full bg-base-100 text-base-content flex flex-col items-center py-12 px-5 pb-32 font-serif transition-colors">
|
<div className="min-h-screen w-full bg-base-100 text-base-content flex flex-col items-center py-12 px-5 pb-32 font-serif transition-colors">
|
||||||
<div className="fixed inset-0 bg-[radial-gradient(circle_at_center,transparent_0%,rgba(0,0,0,0.5)_100%)] pointer-events-none z-0" />
|
<div className="fixed inset-0 bg-[radial-gradient(circle_at_center,transparent_0%,rgba(0,0,0,0.5)_100%)] pointer-events-none z-0" />
|
||||||
|
|
||||||
{isAuthRequired && <PasskeyModal />}
|
{isAuthRequired && <PasskeyModal onUnlock={unlock} />}
|
||||||
<header className="text-center mb-12 z-10 animate-in fade-in slide-in-from-top-4 duration-1000">
|
<header className="text-center mb-12 z-10 animate-in fade-in slide-in-from-top-4 duration-500">
|
||||||
<Logo />
|
<Logo />
|
||||||
<div className="font-sans text-xs tracking-[0.3em] uppercase text-base-content/40 mt-2">
|
<div className="font-sans text-xs tracking-[0.3em] uppercase text-base-content/40 mt-2">
|
||||||
Personal Archive
|
Personal Archive
|
||||||
@@ -94,7 +98,7 @@ export default function Drawer() {
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className="join join-vertical w-full max-w-120 bg-base-200 border border-base-content/10 shadow-2xl z-10 rounded-sm duration-1000 delay-200 min-h-64 flex flex-col">
|
<div className="join join-vertical w-full max-w-120 bg-base-200 border border-base-content/10 shadow-2xl z-10 rounded-sm duration-500 delay-200 min-h-64 flex flex-col">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="flex-1 flex flex-col items-center justify-center p-12 gap-4">
|
<div className="flex-1 flex flex-col items-center justify-center p-12 gap-4">
|
||||||
<span className="loading loading-ring loading-lg text-primary opacity-20"></span>
|
<span className="loading loading-ring loading-lg text-primary opacity-20"></span>
|
||||||
@@ -175,7 +179,7 @@ export default function Drawer() {
|
|||||||
id={letter.public_id}
|
id={letter.public_id}
|
||||||
preview={letter.metadata?.recipient || "Future Self"}
|
preview={letter.metadata?.recipient || "Future Self"}
|
||||||
timestamp={formatRelativeDate(letter.updated_at)}
|
timestamp={formatRelativeDate(letter.updated_at)}
|
||||||
unlock_at={formateRelativeDateWithoutTime(
|
unlock_at={formatRelativeDateWithoutTime(
|
||||||
letter.unlock_at || "",
|
letter.unlock_at || "",
|
||||||
)}
|
)}
|
||||||
isLocked={letter.unlock_at > new Date().toISOString()}
|
isLocked={letter.unlock_at > new Date().toISOString()}
|
||||||
@@ -188,20 +192,20 @@ export default function Drawer() {
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="group mt-15 z-10 bg-transparent border border-dashed border-base-content/10 px-8 py-4 text-base-content/40 italic cursor-pointer transition-all hover:border-primary/40 hover:text-base-content/60 hover:bg-primary/5 hover:-translate-y-0.5 flex items-center gap-2 focus-visible:ring-2 focus-visible:ring-primary/50 duration-1000"
|
className="group mt-15 z-10 bg-transparent border border-dashed border-base-content/10 px-8 py-4 text-base-content/40 italic cursor-pointer transition-all hover:border-primary/40 hover:text-base-content/60 hover:bg-primary/5 hover:-translate-y-0.5 flex items-center gap-2 focus-visible:ring-2 focus-visible:ring-primary/50 duration-500"
|
||||||
onClick={() => navigate(PATHS.write(""))}
|
onClick={() => navigate(PATHS.write(""))}
|
||||||
>
|
>
|
||||||
<FeatherIcon
|
<FeatherIcon
|
||||||
size={18}
|
size={18}
|
||||||
weight="duotone"
|
weight="duotone"
|
||||||
className="text-primary/30 transition-all duration-700 group-hover:text-primary"
|
className="text-primary/30 transition-all duration-300 group-hover:text-primary"
|
||||||
/>
|
/>
|
||||||
Write something{" "}
|
Write something{" "}
|
||||||
<span className="relative inline-flex">
|
<span className="relative inline-flex">
|
||||||
<span className="transition-opacity duration-1500 opacity-80 group-hover:opacity-0">
|
<span className="transition-opacity duration-500 opacity-80 group-hover:opacity-0">
|
||||||
. . . . . .
|
. . . . . .
|
||||||
</span>
|
</span>
|
||||||
<span className="absolute inset-0 text-primary transition-opacity duration-1000 opacity-0 group-hover:opacity-100">
|
<span className="absolute inset-0 text-primary transition-opacity duration-300 opacity-0 group-hover:opacity-100">
|
||||||
unsaid
|
unsaid
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
+270
-232
@@ -43,6 +43,241 @@ const toPlaceholderList = [
|
|||||||
"Something to bear...",
|
"Something to bear...",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
interface SealedModalProps {
|
||||||
|
sealedTargetId: string | null;
|
||||||
|
navigate: NavigateFunction;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SealedModal({ sealedTargetId, navigate }: SealedModalProps) {
|
||||||
|
if (!sealedTargetId) return null;
|
||||||
|
return (
|
||||||
|
<div className="modal modal-open modal-middle bg-base-100/20 backdrop-blur-md z-1000">
|
||||||
|
<div className="modal-box flex flex-col items-center text-center gap-6">
|
||||||
|
<LockIcon size={32} weight="duotone" className="text-primary mt-3" />
|
||||||
|
<h3 className="font-serif text-2xl">Your letter is sealed</h3>
|
||||||
|
<p className="text-base-content/60">
|
||||||
|
It's encrypted and always safe in your drawer.
|
||||||
|
</p>
|
||||||
|
<p className="text-base-content font-sans">
|
||||||
|
When you're ready,
|
||||||
|
<br />
|
||||||
|
you can{" "}
|
||||||
|
<span className="text-primary font-bold font-display">read</span> it,{" "}
|
||||||
|
<span className="text-accent font-bold font-display">send</span> it to
|
||||||
|
someone, or{" "}
|
||||||
|
<span className="text-error font-bold font-display">burn</span> it to
|
||||||
|
release
|
||||||
|
</p>
|
||||||
|
<div className="modal-action w-full justify-center gap-3 mt-4 mb-4">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-ghost btn-sm"
|
||||||
|
onClick={() => navigate(ROUTES.DRAWER)}
|
||||||
|
>
|
||||||
|
Keep it to myself
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-primary btn-sm"
|
||||||
|
onClick={() =>
|
||||||
|
navigate(PATHS.read(sealedTargetId), { replace: true })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
View letter
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ToolBarProps {
|
||||||
|
fileInputRef: React.RefObject<HTMLInputElement | null>;
|
||||||
|
sealBtnClicked: boolean;
|
||||||
|
setSealBtnClicked: (v: boolean) => void;
|
||||||
|
onSave: (status: "SEALED" | "DRAFT" | "VAULT", date?: Date) => Promise<void>;
|
||||||
|
setConfirmModal: (v: "VAULT" | "SEAL" | null) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ToolBar({
|
||||||
|
fileInputRef,
|
||||||
|
sealBtnClicked,
|
||||||
|
setSealBtnClicked,
|
||||||
|
onSave,
|
||||||
|
setConfirmModal,
|
||||||
|
}: ToolBarProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
id="writer-toolbar"
|
||||||
|
className="flex items-center justify-between mb-8 h-14 bg-base-100/50 backdrop-blur-md rounded-full border border-base-content/5 px-6"
|
||||||
|
>
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-ghost btn-sm group"
|
||||||
|
onClick={() => fileInputRef.current?.click()}
|
||||||
|
>
|
||||||
|
<ImageIcon size={18} weight="bold" />
|
||||||
|
<span className="hidden md:inline group-hover:inline transition-all duration-1000">
|
||||||
|
Add Image
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-ghost btn-sm text-[10px] group tracking-[0.2em] uppercase font-bold text-base-content/60 hover:text-base-content"
|
||||||
|
title="Store in your private drawer"
|
||||||
|
onClick={() => onSave("DRAFT")}
|
||||||
|
>
|
||||||
|
<TrayIcon size={18} weight="bold" />
|
||||||
|
<span className="hidden md:inline group-hover:inline transition-all duration-1000">
|
||||||
|
Draft
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="w-px h-4 bg-base-content/10 mx-2" />
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`btn btn-primary btn-sm rounded-full px-6 group ${sealBtnClicked ? "invisible" : "visible"}`}
|
||||||
|
onClick={() => setSealBtnClicked(true)}
|
||||||
|
>
|
||||||
|
<StampIcon
|
||||||
|
size={16}
|
||||||
|
weight="fill"
|
||||||
|
className="mr-1 group-hover:animate-bounce"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className={`hidden md:inline ${sealBtnClicked ? "inline" : ""} group-hover:inline transition-all duration-1000`}
|
||||||
|
>
|
||||||
|
Seal
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={`flex-col items-center gap-2 absolute right-0 z-100000 bg-primary/20 rounded-full p-8 -m-2 ${sealBtnClicked ? "" : "hidden"}`}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-accent btn-sm rounded-full px-6 group"
|
||||||
|
onClick={() => onSave("SEALED")}
|
||||||
|
>
|
||||||
|
<StampIcon
|
||||||
|
size={16}
|
||||||
|
weight="fill"
|
||||||
|
className="mr-1 group-hover:animate-bounce"
|
||||||
|
/>
|
||||||
|
<span className="transition-all duration-1000">Seal</span>
|
||||||
|
</button>
|
||||||
|
<div className="w-full divider text-neutral-content/60 mt-2 mb-2">
|
||||||
|
or
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-neutral btn-sm rounded-full px-6 group"
|
||||||
|
onClick={() => setConfirmModal("VAULT")}
|
||||||
|
>
|
||||||
|
<VaultIcon size={16} weight="fill" className="mr-1" />
|
||||||
|
<span className="transition-all duration-1000">Vault</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setSealBtnClicked(false)}
|
||||||
|
className={`bg-transparent cursor-pointer -mt-2 absolute z-1000001 right-0 text-primary ${sealBtnClicked ? "" : "hidden"}`}
|
||||||
|
>
|
||||||
|
<QuestionIcon weight="duotone" size={20} className={""} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function LetterHead() {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center mb-8 h-14">
|
||||||
|
<div className="badge badge-outline border-primary/20 bg-primary/5 text-primary gap-2 p-4 rounded-full">
|
||||||
|
<LockIcon size={14} weight="fill" />
|
||||||
|
<span className="text-[10px] uppercase tracking-widest font-bold">
|
||||||
|
Sealed & View Only
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface VaultConfirmProps {
|
||||||
|
onSave: (status: "SEALED" | "DRAFT" | "VAULT", date?: Date) => Promise<void>;
|
||||||
|
setConfirmModal: (v: "VAULT" | "SEAL" | null) => void;
|
||||||
|
setUnlockDate: (d: Date | null) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function VaultConfirm({
|
||||||
|
onSave,
|
||||||
|
setConfirmModal,
|
||||||
|
setUnlockDate,
|
||||||
|
}: VaultConfirmProps) {
|
||||||
|
return (
|
||||||
|
<div className={"modal modal-open bg-base-100/20 backdrop-blur-md"}>
|
||||||
|
<div className="modal-box p-12 flex flex-col items-center">
|
||||||
|
<VaultIcon
|
||||||
|
size={48}
|
||||||
|
className="text-primary mx-auto mb-8 animate-pulse"
|
||||||
|
/>
|
||||||
|
<h3 className="font-serif text-3xl">Vault this letter?</h3>
|
||||||
|
<p className="text-base-content/60 text-sm text-center mt-4">
|
||||||
|
Vaulting locks the letter permanently and will be{" "}
|
||||||
|
<span className={"font-bold text-primary"}>mailed</span> to you
|
||||||
|
automatically on the unlock date.
|
||||||
|
<br />
|
||||||
|
<span className={"underline"}>
|
||||||
|
You cannot edit or view the contents of the letter until then.
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<form
|
||||||
|
onSubmit={async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const formData = new FormData(e.currentTarget);
|
||||||
|
const unlockDateStr = formData.get("vault-date") as string;
|
||||||
|
const newUnlockDate = new Date(unlockDateStr);
|
||||||
|
setUnlockDate(newUnlockDate);
|
||||||
|
await onSave("VAULT", newUnlockDate);
|
||||||
|
setConfirmModal(null);
|
||||||
|
}}
|
||||||
|
id="vault-form"
|
||||||
|
>
|
||||||
|
<div className={"divider tracking-tightest font-display text-sm"}>
|
||||||
|
Set an unlock date
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
required
|
||||||
|
type="date"
|
||||||
|
className="input input-bordered w-full"
|
||||||
|
name="vault-date"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className="btn btn-primary mt-4"
|
||||||
|
type="submit"
|
||||||
|
form="vault-form"
|
||||||
|
>
|
||||||
|
Vault
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-ghost mt-4"
|
||||||
|
onClick={() => setConfirmModal(null)}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function Editor() {
|
export default function Editor() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const navigateRef = useRef<NavigateFunction>(navigate);
|
const navigateRef = useRef<NavigateFunction>(navigate);
|
||||||
@@ -76,26 +311,17 @@ export default function Editor() {
|
|||||||
|
|
||||||
const [recipient, setRecipient] = useState("");
|
const [recipient, setRecipient] = useState("");
|
||||||
const [unlockDate, setUnlockDate] = useState<Date | null>(null);
|
const [unlockDate, setUnlockDate] = useState<Date | null>(null);
|
||||||
|
const [placeholderIndex, setPlaceholderIndex] = useState(0);
|
||||||
|
|
||||||
const { masterKey } = useKeyStore();
|
const { masterKey } = useKeyStore();
|
||||||
|
|
||||||
const canvasRef = useRef<CanvasTools>(null);
|
const canvasRef = useRef<CanvasTools>(null);
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
const recipientInputRef = useRef<HTMLInputElement>(null);
|
|
||||||
|
|
||||||
|
// Placeholder rotation
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
if (recipientInputRef.current) {
|
setPlaceholderIndex((prev) => (prev + 1) % toPlaceholderList.length);
|
||||||
let currentOffset = parseInt(
|
|
||||||
recipientInputRef.current.dataset.offset || "0",
|
|
||||||
);
|
|
||||||
recipientInputRef.current.dataset.offset = (
|
|
||||||
(currentOffset + 1) %
|
|
||||||
toPlaceholderList.length
|
|
||||||
).toString();
|
|
||||||
recipientInputRef.current.placeholder =
|
|
||||||
toPlaceholderList[parseInt(recipientInputRef.current.dataset.offset)];
|
|
||||||
}
|
|
||||||
}, 4000);
|
}, 4000);
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
@@ -312,218 +538,6 @@ export default function Editor() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function SealedModal() {
|
|
||||||
if (!sealedTargetId) return null;
|
|
||||||
return (
|
|
||||||
<div className="modal modal-open modal-middle bg-base-100/20 backdrop-blur-md z-1000">
|
|
||||||
<div className="modal-box flex flex-col items-center text-center gap-6">
|
|
||||||
<LockIcon size={32} weight="duotone" className="text-primary mt-3" />
|
|
||||||
<h3 className="font-serif text-2xl">Your letter is sealed</h3>
|
|
||||||
<p className="text-base-content/60">
|
|
||||||
It's encrypted and always safe in your drawer.
|
|
||||||
</p>
|
|
||||||
<p className="text-base-content font-sans">
|
|
||||||
When you're ready,
|
|
||||||
<br />
|
|
||||||
you can{" "}
|
|
||||||
<span className="text-primary font-bold font-display">read</span>{" "}
|
|
||||||
it, <span className="text-accent font-bold font-display">send</span>{" "}
|
|
||||||
it to someone, or{" "}
|
|
||||||
<span className="text-error font-bold font-display">burn</span> it
|
|
||||||
to release
|
|
||||||
</p>
|
|
||||||
<div className="modal-action w-full justify-center gap-3 mt-4 mb-4">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-ghost btn-sm"
|
|
||||||
onClick={() => navigate(ROUTES.DRAWER)}
|
|
||||||
>
|
|
||||||
Keep it to myself
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-primary btn-sm"
|
|
||||||
onClick={() =>
|
|
||||||
navigate(PATHS.read(sealedTargetId), { replace: true })
|
|
||||||
}
|
|
||||||
>
|
|
||||||
View letter
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ToolBar() {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
id="writer-toolbar"
|
|
||||||
className="flex items-center justify-between mb-8 h-14 bg-base-100/50 backdrop-blur-md rounded-full border border-base-content/5 px-6"
|
|
||||||
>
|
|
||||||
<div className="flex gap-4">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-ghost btn-sm group"
|
|
||||||
onClick={() => fileInputRef.current?.click()}
|
|
||||||
>
|
|
||||||
<ImageIcon size={18} weight="bold" />
|
|
||||||
<span className="hidden md:inline group-hover:inline transition-all duration-1000">
|
|
||||||
Add Image
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
ref={fileInputRef}
|
|
||||||
onChange={handleImageUpload}
|
|
||||||
accept="image/*"
|
|
||||||
className="hidden"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-ghost btn-sm text-[10px] group tracking-[0.2em] uppercase font-bold text-base-content/60 hover:text-base-content"
|
|
||||||
title="Store in your private drawer"
|
|
||||||
onClick={() => handleSave("DRAFT")}
|
|
||||||
>
|
|
||||||
<TrayIcon size={18} weight="bold" />
|
|
||||||
<span className="hidden md:inline group-hover:inline transition-all duration-1000">
|
|
||||||
Draft
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div className="w-px h-4 bg-base-content/10 mx-2" />
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`btn btn-primary btn-sm rounded-full px-6 group ${sealBtnClicked ? "invisible" : "visible"}`}
|
|
||||||
onClick={() => setSealBtnClicked(true)}
|
|
||||||
>
|
|
||||||
<StampIcon
|
|
||||||
size={16}
|
|
||||||
weight="fill"
|
|
||||||
className="mr-1 group-hover:animate-bounce"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
className={`hidden md:inline ${sealBtnClicked ? "inline" : ""} group-hover:inline transition-all duration-1000`}
|
|
||||||
>
|
|
||||||
Seal
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className={`flex-col items-center gap-2 absolute right-0 z-100000 bg-primary/20 rounded-full p-8 -m-2 ${sealBtnClicked ? "" : "hidden"}`}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-accent btn-sm rounded-full px-6 group"
|
|
||||||
onClick={() => handleSave("SEALED")}
|
|
||||||
>
|
|
||||||
<StampIcon
|
|
||||||
size={16}
|
|
||||||
weight="fill"
|
|
||||||
className="mr-1 group-hover:animate-bounce"
|
|
||||||
/>
|
|
||||||
<span className="transition-all duration-1000">Seal</span>
|
|
||||||
</button>
|
|
||||||
<div className="w-full divider text-neutral-content/60 mt-2 mb-2">
|
|
||||||
or
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-neutral btn-sm rounded-full px-6 group"
|
|
||||||
onClick={() => setConfirmModal("VAULT")}
|
|
||||||
>
|
|
||||||
<VaultIcon size={16} weight="fill" className="mr-1" />
|
|
||||||
<span className="transition-all duration-1000">Vault</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={() => setSealBtnClicked(false)}
|
|
||||||
className={`bg-transparent cursor-pointer -mt-2 absolute z-1000001 right-0 text-primary ${sealBtnClicked ? "" : "hidden"}`}
|
|
||||||
>
|
|
||||||
<QuestionIcon weight="duotone" size={20} className={""} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function LetterHead() {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-center mb-8 h-14">
|
|
||||||
<div className="badge badge-outline border-primary/20 bg-primary/5 text-primary gap-2 p-4 rounded-full">
|
|
||||||
<LockIcon size={14} weight="fill" />
|
|
||||||
<span className="text-[10px] uppercase tracking-widest font-bold">
|
|
||||||
Sealed & View Only
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function VaultConfirm() {
|
|
||||||
return (
|
|
||||||
<div className={"modal modal-open bg-base-100/20 backdrop-blur-md"}>
|
|
||||||
<div className="modal-box p-12 flex flex-col items-center">
|
|
||||||
<VaultIcon
|
|
||||||
size={48}
|
|
||||||
className="text-primary mx-auto mb-8 animate-pulse"
|
|
||||||
/>
|
|
||||||
<h3 className="font-serif text-3xl">Vault this letter?</h3>
|
|
||||||
<p className="text-base-content/60 text-sm text-center mt-4">
|
|
||||||
Vaulting locks the letter permanently and will be{" "}
|
|
||||||
<span className={"font-bold text-primary"}>mailed</span> to you
|
|
||||||
automatically on the unlock date.
|
|
||||||
<br />
|
|
||||||
<span className={"underline"}>
|
|
||||||
You cannot edit or view the contents of the letter until then.
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
<form
|
|
||||||
onSubmit={async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const formData = new FormData(e.currentTarget);
|
|
||||||
const unlockDateStr = formData.get("vault-date") as string;
|
|
||||||
const newUnlockDate = new Date(unlockDateStr);
|
|
||||||
setUnlockDate(newUnlockDate);
|
|
||||||
await handleSave("VAULT", newUnlockDate);
|
|
||||||
setConfirmModal(null);
|
|
||||||
}}
|
|
||||||
id="vault-form"
|
|
||||||
>
|
|
||||||
<div className={"divider tracking-tightest font-display text-sm"}>
|
|
||||||
Set an unlock date
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
required
|
|
||||||
type="date"
|
|
||||||
className="input input-bordered w-full"
|
|
||||||
name="vault-date"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
className="btn btn-primary mt-4"
|
|
||||||
type="submit"
|
|
||||||
form="vault-form"
|
|
||||||
>
|
|
||||||
Vault
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type={"submit"}
|
|
||||||
className="btn btn-ghost mt-4"
|
|
||||||
onClick={() => setConfirmModal(null)}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Navbar
|
<Navbar
|
||||||
@@ -533,13 +547,13 @@ export default function Editor() {
|
|||||||
isSaveDatePulsing ? "animate-pulse" : ""
|
isSaveDatePulsing ? "animate-pulse" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<p className="text-sm text-neutral-content/30 flex-col justify-end leading-none text-right">
|
<div className="text-sm text-neutral-content/30 flex-col justify-end leading-none text-right">
|
||||||
<span className="text-[10px] uppercase tracking-widest font-bold">
|
<span className="text-[10px] uppercase tracking-widest font-bold">
|
||||||
Last Save
|
Last Save
|
||||||
</span>
|
</span>
|
||||||
<br />
|
<br />
|
||||||
<span className="italic">{lastSaved}</span>
|
<span className="italic">{lastSaved}</span>
|
||||||
</p>
|
</div>
|
||||||
<ClockIcon
|
<ClockIcon
|
||||||
size={16}
|
size={16}
|
||||||
weight="bold"
|
weight="bold"
|
||||||
@@ -631,8 +645,16 @@ export default function Editor() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{confirmModal === "VAULT" && <VaultConfirm />}
|
{confirmModal === "VAULT" && (
|
||||||
{sealedTargetId && <SealedModal />}
|
<VaultConfirm
|
||||||
|
onSave={handleSave}
|
||||||
|
setConfirmModal={setConfirmModal}
|
||||||
|
setUnlockDate={setUnlockDate}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{sealedTargetId && (
|
||||||
|
<SealedModal sealedTargetId={sealedTargetId} navigate={navigate} />
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="max-w-180 mx-auto px-1 md:px-0">
|
<div className="max-w-180 mx-auto px-1 md:px-0">
|
||||||
<div className="flex justify-between items-end mb-16 border-b border-base-content/5 pb-8 px-0">
|
<div className="flex justify-between items-end mb-16 border-b border-base-content/5 pb-8 px-0">
|
||||||
@@ -646,9 +668,7 @@ export default function Editor() {
|
|||||||
<input
|
<input
|
||||||
id="recipient"
|
id="recipient"
|
||||||
type="text"
|
type="text"
|
||||||
ref={recipientInputRef}
|
placeholder={toPlaceholderList[placeholderIndex]}
|
||||||
placeholder={toPlaceholderList[0]}
|
|
||||||
data-offset={"0"}
|
|
||||||
value={recipient}
|
value={recipient}
|
||||||
disabled={status !== "DRAFT"}
|
disabled={status !== "DRAFT"}
|
||||||
onChange={(e) => setRecipient(e.target.value)}
|
onChange={(e) => setRecipient(e.target.value)}
|
||||||
@@ -658,7 +678,25 @@ export default function Editor() {
|
|||||||
<DateDisplay />
|
<DateDisplay />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{status === "DRAFT" ? <ToolBar /> : <LetterHead />}
|
{status === "DRAFT" ? (
|
||||||
|
<ToolBar
|
||||||
|
fileInputRef={fileInputRef}
|
||||||
|
sealBtnClicked={sealBtnClicked}
|
||||||
|
setSealBtnClicked={setSealBtnClicked}
|
||||||
|
onSave={handleSave}
|
||||||
|
setConfirmModal={setConfirmModal}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<LetterHead />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
ref={fileInputRef}
|
||||||
|
onChange={handleImageUpload}
|
||||||
|
accept="image/*"
|
||||||
|
className="hidden"
|
||||||
|
/>
|
||||||
|
|
||||||
<ComposeCanvas ref={canvasRef} readOnly={status !== "DRAFT"} />
|
<ComposeCanvas ref={canvasRef} readOnly={status !== "DRAFT"} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export function formatRelativeDate(input: Date | string | number) {
|
|||||||
return dateTimeFormatter.format(date);
|
return dateTimeFormatter.format(date);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formateRelativeDateWithoutTime(input: Date | string | number) {
|
export function formatRelativeDateWithoutTime(input: Date | string | number) {
|
||||||
if (!input) return "";
|
if (!input) return "";
|
||||||
const date = new Date(input);
|
const date = new Date(input);
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|||||||
Reference in New Issue
Block a user